在微服务架构中,一个业务操作往往涉及多个服务的数据库修改(如下单→扣库存→减余额)。分布式事务的核心是保证这些跨服务操作的原子性:要么全部成功,要么全部失败。Seata 作为阿里开源的分布式事务框架,提供了简单易用的解决方案。本文将从部署到完整业务代码,带你掌握基于 Seata 的分布式事务实现。

一、核心概念与原理

1. Seata 核心角色

  • TC(Transaction Coordinator):事务协调者(独立服务),负责协调全局事务的提交或回滚。
  • TM(Transaction Manager):事务管理器(在发起全局事务的服务中),负责开启和结束全局事务。
  • RM(Resource Manager):资源管理器(在参与事务的每个服务中),负责分支事务的提交/回滚。

2. 事务模式

本文重点讲解两种常用模式:

  • AT 模式:无侵入式,基于 undo log 实现自动回滚(推荐生产使用,性能好)。
  • XA 模式:基于数据库原生 XA 协议,强一致性但性能较低(适合对一致性要求极高的场景)。

二、部署 Seata TC 服务

TC 是 Seata 的核心协调服务,必须先部署。

1. 下载 Seata 服务器

Seata 官网 下载 1.6.1 版本,解压后目录结构如下:

seata-server-1.6.1/
├── bin/          # 启动脚本
├── conf/         # 配置文件
├── lib/          # 依赖包

2. 配置 TC 服务

修改 conf/application.yml,配置注册中心(Nacos)和存储模式:

server:
  port: 8091  # TC 服务端口

spring:
  application:
    name: seata-server

# 注册中心配置(使用 Nacos)
registry:
  type: nacos
  nacos:
    server-addr: 127.0.0.1:8848  # Nacos 地址
    group: SEATA_GROUP  # 分组(默认即可)
    application: seata-server  # TC 在 Nacos 中的服务名

# 配置中心(从 Nacos 读取配置)
config:
  type: nacos
  nacos:
    server-addr: 127.0.0.1:8848
    group: SEATA_GROUP
    dataId: seataServer.properties  # 配置文件名

# 存储模式(file 适合测试,生产用 db)
store:
  mode: file

3. 在 Nacos 中添加 Seata 配置

登录 Nacos 控制台(http://localhost:8848,账号密码 nacos/nacos),创建配置:

  • Data ID:seataServer.properties
  • Group:SEATA_GROUP
  • 配置内容:

    # 事务组映射(微服务需与之一致)
    service.vgroupMapping.my_test_tx_group=default
    # TC 服务地址(由注册中心自动发现)
    service.default.grouplist=127.0.0.1:8091
    metrics.enabled=false  # 关闭监控,减少干扰

4. 启动 TC 服务

  • Windows:双击 bin/seata-server.bat
  • Linux/Mac:执行 sh bin/seata-server.sh

启动成功后,在 Nacos 服务列表中能看到 seata-server,说明 TC 部署完成。

三、环境准备与数据库设计

1. 微服务架构

我们将实现一个经典的分布式事务场景:

  • 订单服务(order-service):创建订单
  • 库存服务(storage-service):扣减商品库存
  • 账户服务(account-service):扣减用户余额

业务流程:用户下单 → 订单服务创建订单 → 调用库存服务扣库存 → 调用账户服务扣余额 → 全部成功则提交,任一失败则回滚。

2. 数据库设计

为三个服务分别创建数据库,并添加 undo_log 表(AT 模式必需)。

(1)订单库(order_db)

-- 订单表
CREATE TABLE `order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `count` int NOT NULL COMMENT '购买数量',
  `money` decimal(10,2) NOT NULL COMMENT '总金额',
  `status` int NOT NULL COMMENT '订单状态:0-创建中,1-已完成',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- AT 模式回滚日志表
CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(2)库存库(storage_db)

-- 库存表
CREATE TABLE `storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `total` int NOT NULL COMMENT '总库存',
  `used` int NOT NULL COMMENT '已使用库存',
  `residue` int NOT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 初始化数据(商品1库存100)
INSERT INTO `storage` VALUES (1, 1, 100, 0, 100);

-- 回滚日志表(同上,复制 order_db 的 undo_log 表结构)

(3)账户库(account_db)

-- 账户表
CREATE TABLE `account` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `total` decimal(10,2) NOT NULL COMMENT '总余额',
  `used` decimal(10,2) NOT NULL COMMENT '已使用余额',
  `residue` decimal(10,2) NOT NULL COMMENT '剩余余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 初始化数据(用户1余额1000)
INSERT INTO `account` VALUES (1, 1, 1000, 0, 1000);

-- 回滚日志表(同上)

四、微服务集成 Seata

1. 统一依赖(所有服务)

pom.xml 中添加依赖:

<dependencies>
    <!-- Spring Boot 基础 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <!-- Seata 核心依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.7.RELEASE</version>
    </dependency>
    
    <!-- Nacos 服务发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <!-- OpenFeign(服务间调用) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- MyBatis(数据库操作) -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2. 订单服务(order-service)

(1)配置文件(application.yml)

server:
  port: 8081

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  # Nacos 地址
    alibaba:
      seata:
        tx-service-group: my_test_tx_group  # 事务组名(与 TC 一致)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false
    username: root
    password: 123456  # 替换为你的数据库密码

# MyBatis 配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.order.entity

# Seata 配置
seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  tx-service-group: my_test_tx_group  # 与上面一致

(2)实体类(Order.java)

package com.example.order.entity;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status; // 0-创建中,1-已完成
}

(3)Mapper 接口与 XML

// OrderMapper.java
package com.example.order.mapper;

import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {
    // 创建订单
    void create(Order order);
    // 更新订单状态
    void updateStatus(@Param("id") Long id, @Param("status") Integer status);
}
<!-- resources/mapper/OrderMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.order.mapper.OrderMapper">
    <insert id="create">
        INSERT INTO `order` (user_id, product_id, count, money, status)
        VALUES (#{userId}, #{productId}, #{count}, #{money}, 0)
    </insert>
    
    <update id="updateStatus">
        UPDATE `order` SET status = #{status} WHERE id = #{id}
    </update>
</mapper>

(4)Feign 客户端(调用其他服务)

// StorageFeignClient.java
package com.example.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "storage-service") // 目标服务名
public interface StorageFeignClient {
    // 扣减库存
    @PostMapping("/storage/decrease")
    String decrease(
            @RequestParam("productId") Long productId,
            @RequestParam("count") Integer count
    );
}

// AccountFeignClient.java
package com.example.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(name = "account-service")
public interface AccountFeignClient {
    // 扣减余额
    @PostMapping("/account/decrease")
    String decrease(
            @RequestParam("userId") Long userId,
            @RequestParam("money") BigDecimal money
    );
}

(5)Service 层(核心业务逻辑)

package com.example.order.service;

import com.alibaba.fastjson.JSONObject;
import com.example.order.entity.Order;
import com.example.order.feign.AccountFeignClient;
import com.example.order.feign.StorageFeignClient;
import com.example.order.mapper.OrderMapper;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StorageFeignClient storageFeignClient;
    @Autowired
    private AccountFeignClient accountFeignClient;

    /**
     * 创建订单(全局事务入口)
     * @GlobalTransactional:标记为全局事务,name 为事务名(唯一)
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public String createOrder(Order order) {
        log.info("全局事务ID:{}", RootContext.getXID()); // 打印全局事务ID

        // 1. 创建订单(本地事务)
        log.info("开始创建订单...");
        orderMapper.create(order);

        // 2. 扣减库存(调用库存服务)
        log.info("开始扣减库存...");
        String storageResult = storageFeignClient.decrease(order.getProductId(), order.getCount());
        if (!"扣减库存成功".equals(storageResult)) {
            throw new RuntimeException("扣减库存失败:" + storageResult);
        }

        // 3. 扣减余额(调用账户服务)
        log.info("开始扣减余额...");
        String accountResult = accountFeignClient.decrease(order.getUserId(), order.getMoney());
        if (!"扣减余额成功".equals(accountResult)) {
            throw new RuntimeException("扣减余额失败:" + accountResult);
        }

        // 4. 更新订单状态为“已完成”
        log.info("更新订单状态...");
        orderMapper.updateStatus(order.getId(), 1);

        // 模拟异常(测试回滚时打开注释)
        // int i = 1 / 0;

        return "订单创建成功,ID:" + order.getId();
    }
}

(6)Controller 层

package com.example.order.controller;

import com.example.order.entity.Order;
import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/order/create")
    public String createOrder(@RequestBody Order order) {
        return orderService.createOrder(order);
    }
}

(7)启动类

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients // 开启 Feign 客户端
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

3. 库存服务(storage-service)

(1)配置文件(application.yml)

server:
  port: 8082

spring:
  application:
    name: storage-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/storage_db?useSSL=false
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.storage.entity

seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  tx-service-group: my_test_tx_group

(2)实体类(Storage.java)

package com.example.storage.entity;

import lombok.Data;

@Data
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;
    private Integer used;
    private Integer residue;
}

(3)Mapper 接口与 XML

// StorageMapper.java
package com.example.storage.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageMapper {
    // 扣减库存
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
<!-- resources/mapper/StorageMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.storage.mapper.StorageMapper">
    <update id="decrease">
        UPDATE storage
        SET used = used + #{count}, residue = residue - #{count}
        WHERE product_id = #{productId}
    </update>
</mapper>

(4)Service 层

package com.example.storage.service;

import com.example.storage.mapper.StorageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class StorageService {

    @Autowired
    private StorageMapper storageMapper;

    // 扣减库存(本地事务)
    @Transactional
    public void decrease(Long productId, Integer count) {
        log.info("扣减库存:productId={}, count={}", productId, count);
        storageMapper.decrease(productId, count);
    }
}

(5)Controller 层

package com.example.storage.controller;

import com.example.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    @PostMapping("/storage/decrease")
    public String decrease(
            @RequestParam("productId") Long productId,
            @RequestParam("count") Integer count
    ) {
        storageService.decrease(productId, count);
        return "扣减库存成功";
    }
}

(6)启动类

package com.example.storage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StorageServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageServiceApplication.class, args);
    }
}

4. 账户服务(account-service)

(1)配置文件(application.yml)

server:
  port: 8083

spring:
  application:
    name: account-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/account_db?useSSL=false
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.account.entity

seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  tx-service-group: my_test_tx_group

(2)实体类(Account.java)

package com.example.account.entity;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Account {
    private Long id;
    private Long userId;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
}

(3)Mapper 接口与 XML

// AccountMapper.java
package com.example.account.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface AccountMapper {
    // 扣减余额
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
<!-- resources/mapper/AccountMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.account.mapper.AccountMapper">
    <update id="decrease">
        UPDATE account
        SET used = used + #{money}, residue = residue - #{money}
        WHERE user_id = #{userId}
    </update>
</mapper>

(4)Service 层

package com.example.account.service;

import com.example.account.mapper.AccountMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
@Slf4j
public class AccountService {

    @Autowired
    private AccountMapper accountMapper;

    // 扣减余额(本地事务)
    @Transactional
    public void decrease(Long userId, BigDecimal money) {
        log.info("扣减余额:userId={}, money={}", userId, money);
        accountMapper.decrease(userId, money);
    }
}

(5)Controller 层

package com.example.account.controller;

import com.example.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PostMapping("/account/decrease")
    public String decrease(
            @RequestParam("userId") Long userId,
            @RequestParam("money") BigDecimal money
    ) {
        accountService.decrease(userId, money);
        return "扣减余额成功";
    }
}

(6)启动类

package com.example.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccountServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountServiceApplication.class, args);
    }
}

五、AT 模式实战

AT 模式是 Seata 的默认模式,无需额外配置,直接使用即可。

1. 测试正常流程

  1. 启动 Nacos、Seata TC 服务。
  2. 启动订单服务、库存服务、账户服务。
  3. 发送请求创建订单:

    curl -X POST http://localhost:8081/order/create \
    -H "Content-Type: application/json" \
    -d '{"userId":1,"productId":1,"count":10,"money":100.00}'
  4. 检查数据库:

    • 订单表新增记录,状态为 1(已完成)。
    • 库存表:used=10,residue=90。
    • 账户表:used=100,residue=900。
      → 所有操作成功提交。

2. 测试回滚流程

  1. 在订单服务的 createOrder 方法中打开异常注释:int i = 1 / 0;
  2. 再次发送请求,会触发异常。
  3. 检查数据库:

    • 订单表无新增记录(或状态仍为 0,但最终会回滚)。
    • 库存和账户数据未变化。
      → 所有操作成功回滚。

六、XA 模式实战

XA 模式基于数据库原生 XA 协议,需修改配置开启。

1. 切换为 XA 模式

在三个服务的 application.yml 中添加:

seata:
  data-source-proxy-mode: XA  # 默认为 AT,改为 XA

2. 测试 XA 模式

重复 AT 模式的测试步骤,结果一致。区别在于:

  • XA 模式下,事务执行过程中会锁定数据(如扣库存时其他事务无法修改该记录)。
  • 性能略低于 AT 模式(因资源锁定时间更长)。

七、AT 与 XA 模式对比

特性AT 模式XA 模式
侵入性无(自动生成 undo log)无(依赖数据库原生支持)
性能高(异步提交,无锁等待)低(锁定资源直到全局提交)
一致性最终一致性强一致性
适用场景大多数微服务场景金融核心交易等强一致性场景

八、常见问题解决

  1. 事务不回滚?

    • 检查 @GlobalTransactional 是否添加在订单服务的 createOrder 方法上。
    • 确保所有服务的 tx-service-group 与 TC 配置一致(my_test_tx_group)。
    • 检查 undo_log 表是否创建(AT 模式必需)。
  2. 服务间调用失败?

    • 检查 Nacos 服务列表,确认三个服务和 Seata 均已注册。
    • 检查 Feign 客户端的服务名是否正确(storage-serviceaccount-service)。
  3. XA 模式下数据库死锁?

    • 尽量缩短事务执行时间,减少资源锁定时间。
    • 避免多个事务同时修改同一批数据。

九、总结

本文通过完整的代码示例,讲解了基于 Seata 的分布式事务方案:

  1. 部署 Seata TC 服务作为事务协调者。
  2. 实现三个微服务(订单、库存、账户),通过 Feign 实现服务间调用。
  3. 在订单服务中用 @GlobalTransactional 标记全局事务入口。
  4. 分别测试了 AT 模式(默认,高性能)和 XA 模式(强一致性)。

通过 Seata,我们可以在微服务架构中轻松保证跨服务操作的原子性,无需手动编写复杂的回滚逻辑。实际项目中,建议优先选择 AT 模式,仅在强一致性场景下使用 XA 模式。


本文作者:
文章标签:Java指南微服务Spring Cloud服务治理
文章标题:Java 基于 Seata 的分布式事务入门指南:从部署到完整实战
本文地址:https://www.ducky.vip/archives/seata.html
版权说明:若无注明,本文皆 iDuckie's Blog 原创,转载请保留文章出处。
最后修改:2024 年 04 月 15 日
如果觉得我的文章对你有用,请随意赞赏