ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringCloudAlibaba Seata分布式事务 AT模式

SpringCloudAlibaba Seata分布式事务 AT模式 还有一篇文章是SpringCloudAlibaba Seata分布式事务那篇是14年总结的等找到工作抽时间整合到这块儿这篇文章的内容都DeepSeek提供的不得不说它的总结非常清楚明白所以这次面试的话可以利用DeepSeek重新整理一篇概念核心角色TC (Transaction Coordinator) - 事务协调器独立部署的调度中心负责维护全局事务和分支事务的状态驱动提交或回滚。TM (Transaction Manager) - 事务管理器嵌入在应用中的“事务发起者”用GlobalTransactional定义全局事务边界。RM (Resource Manager) - 资源管理器管理每个分支事务即本地数据库操作负责向TC注册、上报状态并执行提交/回滚指令。其中TC为单独部署的Server服务端TM和RM为嵌入到应用中的 Client 客户端TC就是事务管理器RM就是每个数据库连接或者说本地事务TM就是事务的入口或者说事务的发起方当然它本身也是一个RM执行流程TM请求TC开启一个全局事务TC会生成一个XID作为该全局事务的编号XID会在微服务的调用链路中传播保证将多个微服务的子事务关联在一起RM请求TC将本地事务注册为全局事务的分支事务通过全局事务的XID进行关联TM请求TC告诉XID对应的全局事务是进行提交还是回滚TC驱动RM将XID对应的自己的本地事务进行提交还是回滚每个微服务都需要有一个undo_log表用于自动补偿例如一个事务已经提交了但这时发生了异常此时该事务已经无法回滚了。在提交事务之前会查询一下要执行事务涉及到的表数据并记录在undo_log表中如果发生了上面的情况则将数据自动改回去而不需要我们敲代码来执行这个操作。二阶段提交精髓在于“一阶段提交二阶段补偿”第一阶段PrepareTM向TC申请开启全局事务生成全局唯一的XID并传播到各个微服务。RM执行业务SQL同时Seata的数据源代理会拦截SQL记录数据修改前后的镜像before-image和after-image作为回滚日志undo_log保存在当前数据库中。在本地事务提交前RM会向TC申请一个全局锁针对修改的数据行。拿到全局锁后本地事务提交业务数据更新生效本地锁被释放。这个“一阶段就提交并释放本地锁”是AT模式高性能的关键。第二阶段Commit / Rollback全局提交如果所有分支都成功TC通知各RM异步删除undo_log并释放全局锁非常快。全局回滚如果任一分支失败TC通知所有RM进行回滚。RM根据XID找到对应的undo_log用before-image的旧数据生成反向SQL如补偿更新将数据恢复然后删除日志并释放全局锁。关键机制 Undo Log与全局锁Undo Log回滚日志这是AT模式实现自动回滚的核心。它在每个参与事务的数据库中必须创建一张undo_log表记录数据更新前后的快照。通过解析这些快照Seata能在需要时自动生成补偿SQL对业务代码零侵入。隔离性实现Seata AT模式的隔离性基于“全局锁”实现写隔离核心是“先抢全局锁再提交本地事务”。不同全局事务要修改同一行数据时必须先获得该行的全局锁。这有效防止了脏写但引入全局锁竞争也是其性能瓶颈之一。读隔离默认是读未提交因为一阶段已提交。如果需要“读已提交”的强隔离需在查询SQL中使用SELECT … FOR UPDATE此时Seata会代理该语句去申请全局锁从而读到已提交的数据测试seata:enabled:trueapplication-id:awu-springboot-code-1# 事务分组名称你可以随便起但必须和下面 mapping 的 key 对应tx-service-group:default_tx_groupregistry:type:nacosnacos:server-addr:192.168.239.3:8848username:nacospassword:nacos# 关键桥梁告诉客户端你的 default_tx_group 去找 Nacos 里名为 default 的集群Seata 2.0 服务端默认就是注册到 defaultservice:vgroup-mapping:default_tx_group:defaultpackagecom.awu.springboot.code1.controller;importcom.awu.springboot.code1.feign.OrderFeignClient;importio.seata.spring.annotation.GlobalTransactional;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(/distributed-tx-test)publicclassDistributedTxTestController{AutowiredprivateOrderFeignClientorderFeignClient;// ⚠️ 全局事务注解加在发起入口GetMapping(/user-place-order)GlobalTransactionalpublicStringuserPlaceOrder(){System.out.println( 开始Code1用户下单入口 );// 模拟 用户下单主表入库System.out.println(【成功】Code1 用户下单记录写入数据库成功);System.out.println(--- Code1 准备调用 Code2 生成订单 ---);returnorderFeignClient.createOrder();}}packagecom.awu.springboot.code2.controller;importcom.awu.springboot.code2.feign.StockFeignClient;importcom.awu.springboot.code2.mapper.User;importcom.awu.springboot.code2.mapper.UserMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(/distributed-tx-test)publicclassDistributedTxTestController{AutowiredprivateStockFeignClientstockFeignClient;AutowiredUserMapperuserMapper;GetMapping(/create-order)publicStringcreateOrder(){System.out.println( 1. 进入 Code2订单服务 );// 模拟 生成订单写入数据库System.out.println(【成功】Code2 生成订单成功(假装写入了订单表));User张三newUser(1L,张三,55);userMapper.updateById(张三);System.out.println( 1. 进入 Code2订单服务 );System.out.println(当前 XID: io.seata.core.context.RootContext.getXID());System.out.println(--- Code2 准备调用 Code3 扣减库存 ---);returnstockFeignClient.decreaseStock();}}packagecom.awu.springboot.code3.controller;importcom.alibaba.fastjson.JSON;importcom.awu.springboot.code3.mapper.User;importcom.awu.springboot.code3.mapper.UserMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(/distributed-tx-test)publicclassDistributedTxTestController{// Autowired// private StockService stockService; // 你的库存ServiceAutowiredUserMapperuserMapper;GetMapping(/decrease-stock)publicStringdecreaseStock(){System.out.println( 2. 进入 Code3扣减库存服务 );// 模拟 扣减数据库库存System.out.println(【成功】Code3 扣减库存成功(假装从数据库扣了 1 件));// 这里测试脏读会读取到code2服务的脏数据UseruseruserMapper.selectById(1L);System.out.println(JSON.toJSONString(user));// 这里模拟一个异常用于测试全局回滚// User lisi new User(2L, 李四, 22);// userMapper.updateById(lisi);// ⚠️ 重点制造全局回滚异常// System.out.println(--- Code3 检测到异常情况准备回滚 ---);// throw new RuntimeException(【模拟报错】Code3 库存扣减服务发生故障触发全局回滚);return扣减成功;}}问题同一个全局事务内的脏读问题code1远程调用code2code2修改了当前行数据code2远程调用code3code3读取了当前行数据他们在同一个全局事务内那么code3是读到了code2修改前的还是修改后的数据呢是修改后的数据因为同一个事务内修改后的数据是可见的Seata 的设计是同一个全局事务内的分支事务对该事务自己持有的锁是“可重入”的。Code2和Code3属于同一个全局事务Code2加的锁Code3可以直接通过不会阻塞等待。如果Code2和Code3在同一个全局事务中Code3读到Code2的未提交数据是符合 Seata 设计预期的。这并不算真正的“脏读”而是同一事务内的数据可见性。解决方案先调用code3后调用code2不同全局事务的脏读处理for update GlobalLock Transactional这可以解决不同事务之间的脏读问题它会去获取全局锁我们可以设置重试时间让它直到获取锁成功为止才读到数据这样就避免了脏读的问题。seata:client:rm:lock:# 全局锁重试间隔毫秒默认 10msretry-interval:10# 全局锁重试次数默认 30 次retry-times:300000# 如果设为 true则重试次数无限retry-policy-branch-rollback-on-conflict:falseenabled:trueapplication-id:awu-springboot-code-3# 事务分组名称你可以随便起但必须和下面 mapping 的 key 对应tx-service-group:default_tx_groupregistry:type:nacosnacos:server-addr:192.168.239.3:8848username:nacospassword:nacos# 关键桥梁告诉客户端你的 default_tx_group 去找 Nacos 里名为 default 的集群Seata 2.0 服务端默认就是注册到 defaultservice:vgroup-mapping:default_tx_group:defaultGetMapping(“/dirty-read”)public String dirtyRead() {User user userService.selectByIdForUpdateDirtyRead(1L);log.info(“测试不同事务”);return JSON.toJSONString(user);}OverrideGlobalLockTransactionalpublic User selectByIdForUpdateDirtyRead(Long id) {// 测试用for update避免脏数据这里seata会去拿全局锁User user userMapper.selectByIdForUpdate(1L);System.out.println(JSON.toJSONString(user));return user;}Select(“SELECT * FROM user WHERE id #{id} FOR UPDATE”)User selectByIdForUpdate(Long id);AT 模式脏写问题 ⭐非常常问二阶段提交 / 回滚消息丢失怎么办幂等怎么保证全局锁超时时间在绝望的时候朱重八不止一次地祈求上天从道教地太上老君到佛教的如来佛祖只要他能知道的名字都祈求到了祈祷的唯一内容只是希望与父母在一起生活下去有口饭吃。明朝那些事儿当年明月
返回列表