news 2026/4/4 12:44:53

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 = "" } }
  1. 在 Nacos 中创建配置(config.txtnacos-config.sh导入);
  2. 启动 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 默认);批量插入优化
全局锁竞争减少事务粒度;避免热点数据
网络 RTTTC 与 RM 同机房部署;使用 gRPC 通信
镜像查询确保 WHERE 条件有索引;避免全表扫描

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

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

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

维度Seata ATTCC
代码侵入无(仅注解)高(需实现 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 生态无缝融合
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/4 12:19:38

Java开发者必看:用Seed-Coder-8B-Base提升Spring项目编码速度

Java开发者必看&#xff1a;用Seed-Coder-8B-Base提升Spring项目编码速度 在现代企业级开发中&#xff0c;Java 依然是构建高可用、可扩展后端服务的首选语言。尤其是在 Spring Boot 和 Spring Cloud 构成的微服务生态下&#xff0c;项目的迭代速度直接决定了产品上线节奏。然而…

作者头像 李华
网站建设 2026/4/2 23:45:52

夸克网盘下载提速 -在线免费解析

今天教大家一招能解决夸克网盘限制的在线工具。这个工具也是完全免费使用的。下面让大家看看我用这个工具的下载速度咋样。地址获取&#xff1a;放在这里了&#xff0c;可以直接获取 这个速度还是不错的把。对于平常不怎么下载的用户还是很友好的。下面开始今天的教学 输入我给…

作者头像 李华
网站建设 2026/4/3 4:42:16

Markdown语法高亮插件辅助编写Qwen3-VL-30B提示词工程

利用 Markdown 语法高亮构建高效 Qwen3-VL-30B 提示工程体系 在多模态 AI 快速演进的今天&#xff0c;如何让大模型“准确理解”我们的意图&#xff0c;已成为决定系统成败的关键。尤其是在视觉语言任务中——比如从一张财报图表中提取关键数据、分析医疗影像中的异常区域&…

作者头像 李华
网站建设 2026/4/3 4:37:40

AutoGPT如何实现跨语言任务执行?翻译协调机制

AutoGPT如何实现跨语言任务执行&#xff1f;翻译协调机制 在当今全球信息高度互联的背景下&#xff0c;一个中文用户想要了解最新的AI伦理研究&#xff0c;却不得不面对绝大多数前沿论文都以英文发表的现实。手动复制、翻译、整理不仅效率低下&#xff0c;还容易因术语不一致导…

作者头像 李华
网站建设 2026/4/2 11:26:42

AutoGPT与Supabase后端即服务集成教程

AutoGPT与Supabase后端即服务集成实践 在AI代理系统日益复杂的今天&#xff0c;一个核心挑战摆在开发者面前&#xff1a;如何让像AutoGPT这样的自主智能体不仅“能想”&#xff0c;还能“记得住、管得好、看得清”&#xff1f;我们见过太多实验性项目因程序中断而前功尽弃&…

作者头像 李华