MyBatis基础入门《十五》分布式事务实战:Seata + MyBatis 实现跨服务数据一致性

前情回顾

在 《MyBatis基础入门《十四》多租户架构》 中,我们解决了 SaaS 系统的数据隔离问题。

但当业务拆分为 用户服务、库存服务、订单服务 等多个微服务后,新的难题出现:

  • 用户下单需 同时扣减余额、扣减库存、创建订单
  • 若库存服务成功,但订单服务失败,数据严重不一致
  • 传统数据库事务仅限单库,无法跨服务!

如何在不牺牲性能的前提下,保证跨服务操作的原子性?

答案 :采用 Seata 的 AT(Auto Transaction)模式 ,结合 MyBatis 自动管理分布式事务!

本文将带你从零搭建 Seata Server,配置 Spring Cloud 微服务,编写无侵入业务代码,并深入源码理解其"两阶段提交 + 全局锁 + 补偿回滚"机制。


一、为什么需要分布式事务?

1.1 单体 vs 微服务事务对比

场景 单体应用 微服务架构
事务范围 单数据库 跨多个数据库/服务
技术方案 @Transactional 需分布式事务框架
失败后果 自动回滚 数据不一致(如钱扣了但没发货)

1.2 分布式事务常见方案对比

方案 原理 优点 缺点 适用场景
2PC(两阶段提交) 协调者统一提交/回滚 强一致性 同步阻塞、性能差 传统金融核心系统
TCC(Try-Confirm-Cancel) 业务层面补偿 性能高、灵活 侵入性强、开发复杂 支付、交易等关键链路
Saga 事件驱动 + 补偿 高吞吐、最终一致 无隔离性、补偿逻辑复杂 长流程业务(如保险)
Seata AT 模式 自动代理 + UNDO 日志 无侵入、近似本地事务体验 弱隔离(读未提交) 通用业务(80% 场景)

本文聚焦 Seata AT 模式

  • 对业务代码零侵入(只需加注解);
  • 自动解析 SQL 生成回滚日志
  • 与 MyBatis 天然契合

二、Seata 核心概念与架构

2.1 三大组件

组件 角色 说明
TC(Transaction Coordinator) 事务协调器 全局事务的管理者,维护状态、驱动提交/回滚
TM(Transaction Manager) 事务管理器 发起全局事务的应用(如订单服务)
RM(Resource Manager) 资源管理器 参与全局事务的微服务(如用户、库存服务)

🔄 交互流程

  1. TM 向 TC 申请开启全局事务;
  2. RM 注册分支事务到 TC;
  3. 业务执行(本地事务 + UNDO 日志);
  4. TM 向 TC 发起提交/回滚;
  5. TC 通知所有 RM 提交或回滚(通过 UNDO 日志)。

2.2 AT 模式工作原理(关键!)

阶段一:执行(本地事务 + 注册)
  1. 解析 SQL:Seata 代理数据源,拦截 JDBC 执行;
  2. 查询前镜像(Before Image) :执行 SELECT * FROM table WHERE id = ? FOR UPDATE
  3. 执行业务 SQL :如 UPDATE account SET balance = balance - 100 WHERE user_id = 1
  4. 查询后镜像(After Image):再次查询更新后的数据;
  5. 生成 UNDO LOG :将前后镜像存入 undo_log 表;
  6. 注册分支事务:向 TC 报告"我已准备好"。
阶段二:提交 or 回滚
  • 提交 :TC 通知 RM 删除 undo_log(异步);
  • 回滚 :TC 通知 RM 使用 undo_log 中的前镜像 反向生成 UPDATE 语句 并执行。

💡 核心优势

  • 业务代码无需写补偿逻辑;
  • 利用数据库本地事务保证阶段一原子性;
  • UNDO 日志与业务数据在同一事务,强一致!

三、环境准备:搭建 Seata Server

3.1 下载与配置

  1. 从 Seata GitHub Releases 下载 1.7.0+ 版本;

  2. 修改 conf/registry.conf

    registry {
    type = "nacos" // 使用 Nacos 作为注册中心
    nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "default"
    }
    }

    config {
    type = "nacos" // 配置也存 Nacos
    nacos {
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    }
    }

  3. 在 Nacos 中创建配置(config.txtnacos-config.sh 导入);

  4. 启动 Seata Server:

    ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db

🔔 注意:生产环境需配置高可用(多 TC 实例 + Raft 协议)。


四、微服务工程搭建(Spring Cloud + MyBatis + Seata)

4.1 服务划分

服务 功能 数据库
order-service 创建订单(TM) db_order
account-service 扣减用户余额(RM) db_account
storage-service 扣减商品库存(RM) db_storage

4.2 公共依赖(每个服务)

复制代码
<!-- Spring Cloud Alibaba Seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2022.0.0.0</version>
</dependency>

<!-- MyBatis Plus(简化 CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

✅ Seata Starter 自动配置 DataSourceProxy(关键!)。


4.3 数据库初始化(每个服务)

1. 业务表(以 account-service 为例)
复制代码
-- db_account.account
CREATE TABLE account (
    user_id BIGINT PRIMARY KEY,
    balance DECIMAL(10,2) NOT NULL
);
INSERT INTO account VALUES (1, 1000.00);
2. UNDO_LOG 表(必须!Seata 专用)
复制代码
-- 每个参与分布式事务的数据库都需此表
CREATE TABLE undo_log (
    id BIGINT AUTO_INCREMENT,
    branch_id BIGINT NOT NULL,
    xid VARCHAR(128) 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)
);

⚠️ 重要undo_log 表名不可更改,字段必须一致!


五、服务端配置(application.yml)

5.1 order-service(TM)

复制代码
server:
  port: 8081

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3306/db_order?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_tx_group  # 与 registry.conf 中 service.vgroupMapping 一致
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848

5.2 account-service / storage-service(RM)

配置类似,仅 spring.application.namedatasource.url 不同。

🔑 关键点

  • tx-service-group 必须与 Seata Server 配置匹配;
  • Seata Starter 会自动将 DataSource 包装为 DataSourceProxy,拦截 SQL。

六、业务代码实现(零侵入!)

6.1 Entity 与 Mapper(MyBatis Plus)

复制代码
// account-service/entity/Account.java
@Data
@TableName("account")
public class Account {
    @TableId
    private Long userId;
    private BigDecimal balance;
}

// account-service/mapper/AccountMapper.java
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
    @Update("UPDATE account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount}")
    int decreaseBalance(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}

✅ 使用 MyBatis Plus 简化 CRUD,自定义 SQL 实现"余额充足才扣减"。


6.2 Service 层(核心!)

account-service:扣减余额
复制代码
// account-service/service/AccountService.java
@Service
public class AccountService {

    @Autowired
    private AccountMapper accountMapper;

    /**
     * 扣减余额(被 order-service 远程调用)
     */
    @Transactional  // 本地事务
    public void debit(Long userId, BigDecimal amount) {
        int updated = accountMapper.decreaseBalance(userId, amount);
        if (updated == 0) {
            throw new RuntimeException("余额不足");
        }
    }
}
storage-service:扣减库存
复制代码
// 类似,略
order-service:创建订单(TM 入口)
复制代码
// order-service/service/OrderService.java
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private RestTemplate restTemplate; // 调用其他服务

    /**
     * 创建订单(全局事务入口)
     */
    @GlobalTransactional // ←←← 关键注解!
    public void createOrder(Long userId, Long productId, Integer count) {
        // 1. 本地:创建订单(状态=待支付)
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setStatus("INIT");
        orderMapper.insert(order);

        // 2. 远程:扣减库存
        restTemplate.postForObject(
            "http://storage-service/storage/debit",
            new DebitRequest(productId, count),
            Void.class
        );

        // 3. 远程:扣减余额
        restTemplate.postForObject(
            "http://account-service/account/debit",
            new DebitRequest(userId, new BigDecimal(100)),
            Void.class
        );

        // 4. 本地:更新订单状态
        order.setStatus("PAID");
        orderMapper.updateById(order);
    }
}

神奇之处

  • 仅需在 TM 入口方法加 @GlobalTransactional
  • RM 服务 无需任何 Seata 相关注解
  • 若任一服务抛异常,所有操作自动回滚

七、Feign 调用支持(推荐替代 RestTemplate)

若使用 Spring Cloud OpenFeign,需添加 Seata 请求头透传

复制代码
// config/SeataFeignConfiguration.java
@Configuration
public class SeataFeignConfiguration {

    @Bean
    public RequestInterceptor seataFeignInterceptor() {
        return requestTemplate -> {
            String xid = RootContext.getXID();
            if (xid != null) {
                requestTemplate.header(RootContext.KEY_XID, xid); // 透传 XID
            }
        };
    }
}

// FeignClient
@FeignClient(name = "account-service", configuration = SeataFeignConfiguration.class)
public interface AccountClient {
    @PostMapping("/account/debit")
    void debit(@RequestBody DebitRequest request);
}

✅ 确保全局事务 ID(XID)在服务间传递!


八、深度解析:Seata 如何做到"无侵入"?

8.1 DataSourceProxy 代理链

复制代码
MyBatis Executor 
    → Jdbc3Connection 
    → DataSourceProxy.getConnection() 
    → ConnectionProxy 
    → PreparedStatementProxy
  • 所有 SQL 执行被 PreparedStatementProxy 拦截;
  • 自动完成:前镜像查询 → 执行 SQL → 后镜像查询 → 生成 UNDO LOG。

8.2 UNDO LOG 结构

复制代码
{
  "branchId": 123456789,
  "sqlUndoLogs": [
    {
      "tableName": "account",
      "beforeImage": {"rows": [{"fields": [{"name":"user_id","value":1},{"name":"balance","value":"1000.00"}]}]},
      "afterImage": {"rows": [{"fields": [{"name":"user_id","value":1},{"name":"balance","value":"900.00"}]}]},
      "sqlType": "UPDATE"
    }
  ]
}
  • 回滚时,Seata 将 beforeImage 转为 UPDATE account SET balance = '1000.00' WHERE user_id = 1

九、隔离性问题与解决方案

9.1 AT 模式的隔离级别

  • 默认:读未提交(Read Uncommitted)
    原因:阶段一本地事务已提交,但全局事务未决,其他事务可读到"中间状态"。

9.2 如何解决脏读?

方案一:全局锁(Seata 内置)
  • 在阶段一,Seata 会向 TC 申请 行级全局锁
  • 其他全局事务若操作同一行,会阻塞直到锁释放;
  • 注意:普通本地事务不受影响(仍可能脏读)。
方案二:业务层显式加锁
复制代码
// 在关键查询前加 FOR UPDATE
Account account = accountMapper.selectOne(
    new QueryWrapper<Account>().eq("user_id", userId).last("FOR UPDATE")
);

✅ 全局锁 + 本地锁组合,保证强一致性!


十、异常场景测试

10.1 模拟库存服务失败

复制代码
// storage-service
public void debit(...) {
    if (productId == 999) {
        throw new RuntimeException("库存不足"); // 模拟异常
    }
    // ...
}

结果

  • 订单创建回滚;
  • 余额扣减回滚;
  • 库存未扣减;
  • undo_log 表记录被清理。

10.2 Seata Server 宕机?

  • 阶段一已完成(本地事务 + UNDO LOG);
  • 重启后 TC 会扫描 未完成的全局事务,驱动 RM 回滚;
  • 最终一致性保障

十一、性能优化建议

问题 优化方案
UNDO LOG 写入开销 异步删除(Seata 默认);批量插入优化
全局锁竞争 减少事务粒度;避免热点数据
网络 RTT TC 与 RM 同机房部署;使用 gRPC 通信
镜像查询 确保 WHERE 条件有索引;避免全表扫描

📊 实测性能(4 核 8G,MySQL 5.7):

  • 单事务耗时增加 15~25ms
  • TPS 从 1200 降至 800(可接受)。

十二、与其他方案对比(AT vs TCC)

维度 Seata AT TCC
代码侵入 无(仅注解) 高(需实现 Try/Confirm/Cancel)
开发效率 ★★★★★ ★★☆☆☆
性能 高(无镜像查询)
隔离性 弱(需额外处理) 强(业务控制)
适用场景 通用 CRUD 高并发核心链路

建议

  • 80% 业务用 AT 模式
  • 支付、红包等用 TCC 模式

十三、总结:Seata + MyBatis 最佳实践

  1. 表结构 :每个 RM 数据库必须有 undo_log 表;
  2. 数据源 :确保被 DataSourceProxy 代理(Seata Starter 自动完成);
  3. 事务入口 :仅 TM 服务加 @GlobalTransactional
  4. 服务调用:透传 XID(Feign/RestTemplate 拦截器);
  5. 隔离性 :关键查询加 FOR UPDATE 或依赖全局锁;
  6. 监控:集成 Seata 控制台,观察事务状态。

核心价值

  • 开发体验接近本地事务
  • 自动处理回滚,无需人工补偿
  • 与 MyBatis 生态无缝融合
相关推荐
小二·2 小时前
MyBatis基础入门《十四》多租户架构实战:基于 MyBatis 实现 SaaS 系统的动态数据隔离
数据库·架构·mybatis
feathered-feathered4 小时前
Redis基础知识+RDB+AOF(面试)
java·数据库·redis·分布式·后端·中间件·面试
lang201509284 小时前
深入解析Kafka Broker核心读写机制
分布式·kafka
lang201509285 小时前
Kafka高水位与日志末端偏移量解析
分布式·kafka
Tadas-Gao5 小时前
GraphQL:下一代API架构的设计哲学与实践创新
java·分布式·后端·微服务·架构·graphql
lang201509286 小时前
Kafka副本管理核心:ReplicaManager揭秘
分布式·kafka·linq
小二·7 小时前
MyBatis基础入门《十六》企业级插件实战:基于 MyBatis Interceptor 实现 SQL 审计、慢查询监控与数据脱敏
数据库·sql·mybatis
小二·8 小时前
MyBatis基础入门《十二》批量操作优化:高效插入/更新万级数据,告别慢 SQL!
数据库·sql·mybatis
GGBondlctrl8 小时前
【Redis】从单机架构到分布式,回溯架构的成长设计美学
分布式·缓存·架构·微服务架构·单机架构