最新网站发布,thinkphp开源cms系统,网站如何做流量,阿里云部署网站前情回顾#xff1a;
在 《MyBatis基础入门《十四》多租户架构》 中#xff0c;我们解决了 SaaS 系统的数据隔离问题。
但当业务拆分为 用户服务、库存服务、订单服务 等多个微服务后#xff0c;新的难题出现#xff1a;用户下单需 同时扣减余额、扣减库存、创建订单#…前情回顾在 《MyBatis基础入门《十四》多租户架构》 中我们解决了 SaaS 系统的数据隔离问题。但当业务拆分为用户服务、库存服务、订单服务等多个微服务后新的难题出现用户下单需同时扣减余额、扣减库存、创建订单若库存服务成功但订单服务失败数据严重不一致传统数据库事务仅限单库无法跨服务如何在不牺牲性能的前提下保证跨服务操作的原子性答案采用Seata 的 ATAuto Transaction模式结合 MyBatis 自动管理分布式事务本文将带你从零搭建 Seata Server配置 Spring Cloud 微服务编写无侵入业务代码并深入源码理解其“两阶段提交 全局锁 补偿回滚”机制。一、为什么需要分布式事务1.1 单体 vs 微服务事务对比场景单体应用微服务架构事务范围单数据库跨多个数据库/服务技术方案Transactional需分布式事务框架失败后果自动回滚数据不一致如钱扣了但没发货1.2 分布式事务常见方案对比方案原理优点缺点适用场景2PC两阶段提交协调者统一提交/回滚强一致性同步阻塞、性能差传统金融核心系统TCCTry-Confirm-Cancel业务层面补偿性能高、灵活侵入性强、开发复杂支付、交易等关键链路Saga事件驱动 补偿高吞吐、最终一致无隔离性、补偿逻辑复杂长流程业务如保险Seata AT 模式自动代理 UNDO 日志无侵入、近似本地事务体验弱隔离读未提交通用业务80% 场景✅本文聚焦 Seata AT 模式对业务代码零侵入只需加注解自动解析 SQL 生成回滚日志与 MyBatis 天然契合二、Seata 核心概念与架构2.1 三大组件组件角色说明TCTransaction Coordinator事务协调器全局事务的管理者维护状态、驱动提交/回滚TMTransaction Manager事务管理器发起全局事务的应用如订单服务RMResource Manager资源管理器参与全局事务的微服务如用户、库存服务交互流程TM 向 TC 申请开启全局事务RM 注册分支事务到 TC业务执行本地事务 UNDO 日志TM 向 TC 发起提交/回滚TC 通知所有 RM 提交或回滚通过 UNDO 日志。2.2 AT 模式工作原理关键阶段一执行本地事务 注册解析 SQLSeata 代理数据源拦截 JDBC 执行查询前镜像Before Image执行SELECT * FROM table WHERE id ? FOR UPDATE执行业务 SQL如UPDATE account SET balance balance - 100 WHERE user_id 1查询后镜像After Image再次查询更新后的数据生成 UNDO LOG将前后镜像存入undo_log表注册分支事务向 TC 报告“我已准备好”。阶段二提交 or 回滚提交TC 通知 RM 删除undo_log异步回滚TC 通知 RM 使用undo_log中的前镜像反向生成 UPDATE 语句并执行。核心优势业务代码无需写补偿逻辑利用数据库本地事务保证阶段一原子性UNDO 日志与业务数据在同一事务强一致三、环境准备搭建 Seata Server3.1 下载与配置从 Seata GitHub Releases 下载 1.7.0 版本修改conf/registry.confregistry { 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 } }在 Nacos 中创建配置config.txt→nacos-config.sh导入启动 Seata Server./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db注意生产环境需配置高可用多 TC 实例 Raft 协议。四、微服务工程搭建Spring Cloud MyBatis Seata4.1 服务划分服务功能数据库order-service创建订单TMdb_orderaccount-service扣减用户余额RMdb_accountstorage-service扣减商品库存RMdb_storage4.2 公共依赖每个服务!-- Spring Cloud Alibaba Seata -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId version2022.0.0.0/version /dependency !-- MyBatis Plus简化 CRUD -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL -- dependency groupIdmysql/groupId artifactIdmysql-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.yml5.1 order-serviceTMserver: port: 8081 spring: application: name: order-service datasource: url: jdbc:mysql://localhost:3306/db_order?useSSLfalse 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:88485.2 account-service / storage-serviceRM配置类似仅spring.application.name和datasource.url不同。关键点tx-service-group必须与 Seata Server 配置匹配Seata Starter 会自动将DataSource包装为DataSourceProxy拦截 SQL。六、业务代码实现零侵入6.1 Entity 与 MapperMyBatis 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 BaseMapperAccount { 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 入口方法加GlobalTransactionalRM 服务无需任何 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); }✅ 确保全局事务 IDXID在服务间传递八、深度解析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 QueryWrapperAccount().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 核 8GMySQL 5.7单事务耗时增加15~25msTPS 从 1200 降至 800可接受。十二、与其他方案对比AT vs TCC维度Seata ATTCC代码侵入无仅注解高需实现 Try/Confirm/Cancel开发效率★★★★★★★☆☆☆性能中高无镜像查询隔离性弱需额外处理强业务控制适用场景通用 CRUD高并发核心链路✅建议80% 业务用AT 模式支付、红包等用TCC 模式。十三、总结Seata MyBatis 最佳实践表结构每个 RM 数据库必须有undo_log表数据源确保被DataSourceProxy代理Seata Starter 自动完成事务入口仅 TM 服务加GlobalTransactional服务调用透传 XIDFeign/RestTemplate 拦截器隔离性关键查询加FOR UPDATE或依赖全局锁监控集成 Seata 控制台观察事务状态。✨核心价值开发体验接近本地事务自动处理回滚无需人工补偿与 MyBatis 生态无缝融合