Mastering Distributed Transactions with Seata in Spring Cloud Microservices

This article explains why distributed transactions are essential in microservice architectures, introduces Alibaba's open‑source Seata solution, compares its AT, TCC and Saga modes, shows step‑by‑step Docker deployment, provides full Spring Cloud code examples, and lists common pitfalls and debugging tips.

Coder Trainee
Coder Trainee
Coder Trainee
Mastering Distributed Transactions with Seata in Spring Cloud Microservices

In a typical e‑commerce order flow—creating an order, deducting inventory, adding loyalty points, and initiating payment—each step runs in a separate microservice, making it hard to guarantee data consistency when one operation fails. The article starts with this scenario to illustrate the need for distributed transactions.

The author reminds readers of the CAP theorem, noting that in a distributed system partition tolerance (P) is mandatory, so a trade‑off between consistency (C) and availability (A) must be made.

Seata Overview

Seata (an open‑source project from Alibaba) provides three core components:

TC (Transaction Coordinator) – a standalone Seata Server that coordinates global transactions.

TM (Transaction Manager) – embedded in each business service (e.g., the order service) to start, commit, or roll back a global transaction.

RM (Resource Manager) – also embedded in each service to manage branch transactions on the underlying database.

Transaction Modes Comparison

Seata supports three execution modes, each with distinct principles, advantages, and drawbacks:

AT (Automatic Transaction) – uses an undo_log table to record before‑image data and automatically rolls back on failure. It is non‑intrusive and works for most scenarios, but requires the undo_log table.

TCC (Try‑Confirm‑Cancel) – requires developers to write explicit try, confirm, and cancel methods. It offers high performance and fine‑grained control, but adds implementation complexity and is suited for high‑concurrency use cases.

Saga – models a long‑running transaction as a state machine of compensating actions. It handles long‑duration processes but needs explicit compensation logic and is more complex to design.

Deployment

The article provides a ready‑to‑use Docker‑Compose file to launch a Seata Server (image seataio/seata-server:1.7.0) with ports 8091 (transaction coordination) and 7091 (console). It also shows how to create the Seata database and the required undo_log table for AT mode.

# docker-compose.yml
version: '3'
services:
  seata-server:
    image: seataio/seata-server:1.7.0
    container_name: seata-server
    ports:
      - "8091:8091"   # transaction coordinator
      - "7091:7091"   # console
    environment:
      - SEATA_IP=127.0.0.1
      - SEATA_PORT=8091
      - STORE_MODE=file
    volumes:
      - ./seata/config:/seata-server/resources

SQL to create the undo_log table (required when store.mode=db is used) is also included.

AT Mode Implementation

Dependencies are added via Maven’s dependencyManagement and the spring-cloud-starter-alibaba-seata starter. Configuration in application.yml sets the transaction group name, enables Seata, and points to Nacos for service discovery.

# application.yml
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: blog_tx_group
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: blog_tx_group
  config:
    type: nacos
    nacos:
      server-addr: ${NACOS_HOST:localhost}:8848
      namespace: seata
      group: SEATA_GROUP
      data-id: seata-server.properties
  registry:
    type: nacos
    nacos:
      server-addr: ${NACOS_HOST:localhost}:8848
      namespace: seata
      group: SEATA_GROUP

Business code is annotated with @GlobalTransactional. The example OrderService.createOrder method creates an order, calls remote stock and point services, and relies on Seata to automatically roll back all steps if any call fails.

@Service
@Slf4j
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockFeignClient stockClient;
    @Autowired
    private PointFeignClient pointClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderCreateRequest request) {
        // 1. create order locally
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setAmount(request.getAmount());
        order.setStatus(0); // pending payment
        orderMapper.insert(order);

        // 2. deduct stock (remote call)
        Result stockResult = stockClient.deductStock(request.getProductId(), request.getQuantity());
        if (!stockResult.isSuccess()) {
            throw new BusinessException("扣减库存失败");
        }

        // 3. add points (remote call)
        Result pointResult = pointClient.addPoints(request.getUserId(), request.getAmount() * 10);
        if (!pointResult.isSuccess()) {
            throw new BusinessException("增加积分失败");
        }
        // If any step fails, Seata rolls back the whole transaction
    }
}

TCC Mode Implementation

The TCC workflow is illustrated with a three‑phase table (Try, Confirm, Cancel). An interface StockTccAction defines the three methods, and the implementation handles resource reservation, confirmation, and rollback.

@LocalTCC
public interface StockTccAction {
    @TwoPhaseBusinessAction(name = "deductStock", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryDeduct(@BusinessActionContextParameter(paramName = "productId") Long productId,
                     @BusinessActionContextParameter(paramName = "quantity") Integer quantity);
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

Implementation logs each phase, checks inventory, freezes stock in tryDeduct, permanently deducts in confirm, and releases the freeze in cancel. The service then invokes the TCC action inside a global transaction.

@Service
@Slf4j
public class StockTccActionImpl implements StockTccAction {
    @Autowired
    private StockMapper stockMapper;

    @Override
    public boolean tryDeduct(Long productId, Integer quantity) {
        log.info("TCC Try: 扣减库存 {}, 数量 {}", productId, quantity);
        Stock stock = stockMapper.selectByProductId(productId);
        if (stock.getAvailable() < quantity) {
            throw new BusinessException("库存不足");
        }
        int result = stockMapper.freezeStock(productId, quantity);
        return result > 0;
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        Long productId = Long.parseLong(context.getActionContext("productId").toString());
        Integer quantity = Integer.parseInt(context.getActionContext("quantity").toString());
        log.info("TCC Confirm: 确认扣减库存 {}, 数量 {}", productId, quantity);
        stockMapper.confirmDeduct(productId, quantity);
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        Long productId = Long.parseLong(context.getActionContext("productId").toString());
        Integer quantity = Integer.parseInt(context.getActionContext("quantity").toString());
        log.info("TCC Cancel: 回滚扣减库存 {}, 数量 {}", productId, quantity);
        stockMapper.unfreezeStock(productId, quantity);
        return true;
    }
}

Real‑World Blog System Example

The article walks through a concrete use case where publishing an article must also update a statistics service. It shows the YAML configuration for the article service, the @GlobalTransactional method that saves the article and calls the statistics service, and the same rollback guarantees.

@Service
@Slf4j
public class ArticlePublishService {
    @Autowired
    private ArticleMapper articleMapper;
    @Autowired
    private StatisticsFeignClient statisticsClient;

    @GlobalTransactional(name = "publish-article", rollbackFor = Exception.class)
    public Long publishArticle(ArticlePublishRequest request, Long userId) {
        // 1. save article
        Article article = new Article();
        article.setTitle(request.getTitle());
        article.setContent(request.getContent());
        article.setUserId(userId);
        article.setStatus(1);
        articleMapper.insert(article);

        // 2. update statistics (remote call)
        Result result = statisticsClient.updateStatistics(userId, article.getContent().length());
        if (!result.isSuccess()) {
            throw new BusinessException("更新统计失败");
        }
        return article.getId();
    }
}

Common Pitfalls and Debugging Tips

Missing undo_log table – leads to "Table 'xxx.undo_log' doesn't exist" errors; create the table in every business database.

Transaction not rolling back – ensure the method is annotated with @GlobalTransactional and that caught exceptions are re‑thrown.

Empty TCC rollback – when Try is never executed, Cancel should detect this and return success without performing any rollback logic.

Next Episode Preview

The author teases the upcoming ninth part of the series, which will cover containerized deployment, Docker image building, Kubernetes orchestration, and CI/CD pipelines.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MySQLSpring Clouddistributed-transactiontccsagaSeataDocker ComposeAT Mode
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.