多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Seata AT模式分布式事务实战:订单库存一致性方案与性能优化

Seata AT模式分布式事务实战:订单库存一致性方案与性能优化 2. 从痛点出发为什么订单库存场景需要分布式事务我这两年处理过不少分布式事务相关的故障印象最深的一次是线上促活动态调整库存后订单表和库存表数据对不上财务对账出了问题最后靠人工补单才收场。事后复盘问题根源就是订单服务和库存服务跨了两个数据库本地事务管不住全局一致性。所谓分布式事务本质上是解决“多个独立数据源之间的数据一致性”问题。拿最常见的电商下单流程来说用户下单要扣库存、写订单、可能还要加积分这三个操作分别落在订单库、库存库、积分库。如果扣库存成功但订单创建失败库存就被白扣了如果订单创建成功但扣库存失败超卖就发生了。传统方案里要么用TCC手工补偿要么靠消息对账但代码侵入都太重。Seata在这种背景下是比较实用的一套方案尤其AT模式对业务代码的侵入比TCC小很多。我最早接触Seata是在一个订单中台项目里当时面临的技术选型就是阿里的Seata和开源的TCC框架二选一。对比之后我选了Seata后面在十几个生产环境里落地也踩了不少坑今天把整套优化和落地经验整理出来。这套内容适合谁看如果你负责的业务涉及跨库操作比如订单库存、账户余额、积分流水这类场景如果你正在选型分布式事务方案或者已经引入Seata但性能不理想、时常出现数据不一致报警那这篇内容能帮你省不少排查时间。3. Seata AT模式核心机制拆解3.1 AT模式到底做了什么AT模式全称是Automatic Transaction核心思想是“业务无侵入”。业务代码里只需要加一个GlobalTransactional注解Seata就会接管整个调用的全局事务。它的工作原理可以分三步理解第一事务发起方TM开启全局事务生成全局事务IDXID通过Dubbo或Spring Cloud的调用链传递下去。第二每个参与事务的分支事务RM在自己的本地库中执行SQL同时Seata会拦截SQL生成前后镜像before image和after image并写入一张undo_log表。这个undo_log表就是回滚的依据。第三全局事务提交时所有分支都执行成功TM通知RM异步删除undo_log记录如果任何一步失败RM根据undo_log里的前后镜像生成反向SQL把数据恢复到事务开始前的状态。这套机制带了一个很重要的概念叫全局锁。AT模式里RM在执行本地SQL时会对涉及的主键记录加全局锁锁信息记录在Seata服务端的global_table和lock_table表中。目的是防止两个全局事务同时修改同一条记录保证写冲突可控。我刚开始接触AT模式时直觉上担心它性能不行理由是每笔SQL都要做镜像、写undo_log还要和TC事务协调器通信。后来压测下来发现只要参数调对AT模式对业务RT的影响能控制在10%以内这在绝大多数业务场景里是可以接受的。3.2 AT模式和TCC、消息队列对比AT模式能火起来不是因为性能最好而是因为它在“代码侵入”和“一致性保障”之间找到了一个相对平衡的位置。TCCTry-Confirm-Cancel模式要求业务方自己实现三个接口比如库存服务要写Try扣减预留库存、Confirm确认扣减、Cancel回滚预留库存。这套逻辑写起来非常痛苦而且幂等、悬挂、空回滚这三个问题都得自己处理开发成本很高。消息队列方案适合最终一致性场景比如下单成功后发一条MQ消息去异步扣库存。它的问题在于如果MQ消息丢了或者消费端逻辑没做好幂等数据就会一直对不上且问题暴露有延迟。AT模式的选择逻辑其实很简单它把回滚逻辑用镜像SQL自动生成业务方只需要关注自己的SQL是否正确同时保证了事务的原子性和一致性。它不适合极端高并发写热点场景但覆盖了90%以上的跨库事务业务。下面这张对比表是我在给团队做技术选型时常用的参考方案代码侵入一致性类型性能损耗维护成本适用场景AT模式低强一致最终一致由回滚保证中等可调优低跨库、跨服务常规事务场景TCC高强一致低高高并发、资源预扣场景消息队列低最终一致低中异步削峰、可容忍短暂不一致场景XA极低强一致高低数据库原生支持、低频场景实际落地时AT模式最适合订单、库存、账户这类短事务场景尤其是事务内嵌套调用不超过三层、单事务耗时少于5秒的场景。事务时间越长全局锁持有时间越长冲突概率和锁等待放大效应就越明显。4. 生产落地前的关键配置与参数调优4.1 部署架构与基本配置Seata生产部署建议采用高可用模式。TCTransaction Coordinator至少部署两个节点注册到Nacos或Consul上客户端通过注册中心感知TC地址。如果只有单机TCTC一旦挂掉所有进行中的全局事务都会卡在“待处理”状态业务直接瘫痪。我第一套生产环境就是图省事部署了单机TC结果一次发布重启期间正好有上百个全局事务处于中间态重启后部分事务状态丢失最后靠手工查库才恢复。从那以后我定的规矩是TC至少双节点且独立于业务应用部署。TC的JVM参数需要根据全局事务量估算。通常4C8G的机器可以支撑每秒几百到上千个全局事务但要注意事务分支数。单个全局事务平均5个分支的话TC的内存开销会明显增大。建议配置如下# application.ymlSeata Server端 server: port: 7091 spring: datasource: url: jdbc:mysql://localhost:3306/seata_server?useSSLfalse username: seata password: seata seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 application: seata-server group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP store: mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata_server?useUnicodetruecharacterEncodingutf8 user: seata password: seata max-active: 20 min-idle: 5 server: max-queue-size: 20000 max-commit-retry-timeout: -1 max-rollback-retry-timeout: -1重点是store.mode一定要配置成db模式。默认的文件模式存不了多少事务日志生产环境必须用数据库存储便于追溯和清理。4.2 客户端关键参数配置详解客户端业务应用里的Seata参数直接决定了性能上限和异常行为。这里列出我实测过最关键的几个seata.tm.degrade.check事务降级开关。开启后当TC不可用或全局事务处理超时时Seata会降级为本地事务执行。这个参数在压测和演练时特别有用但生产环境我建议谨慎使用因为降级会牺牲全局一致性。我一般只在TC故障演练时临时打开。client.rm.report.retry.count分支事务注册上报重试次数默认是5。如果网络有抖动调大到10会提高事务注册成功率。代价是故障时感知变慢所以不要无限调大。client.tm.commit.retry.count和client.tm.rollback.retry.count全局提交和回滚的重试次数。默认值通常够用但是如果日志里出现Retry commit字样说明网络确实不稳定这时候调大重试次数比调大超时时间更有效。client.rm.lock.retry.internal分支事务获取全局锁的重试间隔默认10毫秒。client.rm.lock.retry.times默认30次意味着锁冲突时最多等待300毫秒。对于秒杀、热卖品这类写热点集中场景300毫秒很可能不够我通常改成3秒等待把retry.times调整成300。下面是客户端侧配置文件的参考模板# application.yml业务客户端 seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group # 事务分组与TC集群的映射关系需要在Nacos中配置 service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091 # TC集群开关生产用注册中心时自动发现 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP username: nacos password: nacos client: rm: lock: retry-interval: 10 retry-times: 30 report-retry-count: 5 table-meta-check-enable: false report-success-enable: true async-commit-buffer-limit: 10000 tm: commit-retry-count: 5 rollback-retry-count: 5这里有个容易踩的坑tx-service-group名称必须和Nacos里配置的vgroup-mapping保持一致否则客户端报错“no available service”而且这个错不会第一时间上报只会在运行时日志里刷很容易遗漏。4.3 undo_log表与全局锁表的设计Seata AT模式依赖业务库里的undo_log表。这张表的建表脚本官方有提供但有几个细节需要注意-- 注意需要为每个参与分布式事务的业务库都建立该表 CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;这张表的本质是业务库里的“补偿日志”。Seata在执行SQL前后读取数据快照存入其中回滚时把镜像SQL反推执行。它有两个我走了弯路才注意到的点第一xid和branch_id必须建唯一索引这个索引在回滚时用来精确定位分支记录。如果不建回滚时全表扫一遍一旦数据量大全局事务的失败恢复时间会成倍增长。第二rollback_info存储的是序列化后的镜像数据默认用的是Java序列化。如果业务实体类没有实现Serializable回滚时会抛异常。我遇到过一次实体类没实现序列化导致回滚失败事后检查代码才加上。如果你的应用用了Kryo或Protobuf做序列化可以在客户端配置client.rm.undo.serialization改成对应实现。全局锁的管理在TC服务端自动完成业务方不直接感知。锁的粒度是“主键值”级别不是行级也不是表级。这意味着一个全局事务如果操作了多条记录就会持有多个全局锁。锁的超时时间由client.rm.lock.retry.times和retry-internal相乘决定超过等待时间仍拿不到锁就会抛LockConflictException。5. 从订单库存场景出发的完整实操5.1 场景设计与代码示例为了讲清楚整个落地过程我以一个标准的订单库存场景为例。假设有两个微服务订单服务order-service和库存服务inventory-service分别使用独立的数据库。业务流程是创建订单时同步扣减库存如果库存不足则订单创建失败库存不扣。传统写法下这两个操作分散在两个服务里各自有本地事务无法保证一致性。引入Seata后流程变成订单服务作为全局事务发起方标注GlobalTransactional注解。内部先创建订单然后通过Dubbo或OpenFeign调用库存服务扣库存。整个调用链路上只要有一个分支失败全局事务就自动回滚包括订单插入和库存扣减。代码示例订单服务Service public class OrderService { Resource private OrderDAO orderDAO; Resource private InventoryFeignClient inventoryClient; GlobalTransactional(name create-order-tx, rollbackFor Exception.class) public boolean createOrder(OrderDTO orderDTO) { // 1. 创建订单本地事务提交 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(orderDTO.getUserId()); order.setProductId(orderDTO.getProductId()); order.setCount(orderDTO.getCount()); orderDAO.insert(order); // 2. 远程扣减库存 InventoryDTO inventoryDTO new InventoryDTO(); inventoryDTO.setProductId(orderDTO.getProductId()); inventoryDTO.setCount(orderDTO.getCount()); // 远程调用库存服务 InventoryResponse response inventoryClient.deduct(inventoryDTO); if (!response.isSuccess()) { // 注意这里直接抛出异常让Seata拦截回滚 throw new RuntimeException(库存不足订单创建失败); } return true; } }库存服务代码Service public class InventoryService { Resource private InventoryDAO inventoryDAO; /** * 扣减库存本地分支事务 */ Transactional(rollbackFor Exception.class) public boolean deduct(InventoryDTO inventoryDTO) { // 执行扣减SQL由Seata自动生成镜像 int updates inventoryDAO.deductStock(inventoryDTO.getProductId(), inventoryDTO.getCount()); if (updates 0) { throw new RuntimeException(库存数据不存在); } // 检查扣减后是否剩余为负数 int currentStock inventoryDAO.getStock(inventoryDTO.getProductId()); if (currentStock 0) { throw new RuntimeException(扣减后库存为负拒绝操作); } return true; } }这里有个细节值得注意库存服务里的Transactional注解保留着Seata AT模式的分支事务并不会替代本地事务两者是叠加的。本地事务保证单库内的ACID全局事务保证跨库的一致性。我在很多项目里看到有人把本地Transactional去掉这是不正确的做法。5.2 全局事务ID的传递链路全局事务ID的传递是AT模式能跨服务生效的基础但也是最容易出问题的地方。当GlobalTransactional方法被调用时Seata会通过拦截器生成XID存放在RootContext里默认是一个ThreadLocal变量。接下来发生远程调用时Seata的集成组件如Dubbo Filter、OpenFeign拦截器会把XID从当前线程的ThreadLocal中取出放入RPC协议的attachment或header中传递出去。在订单服务中调用库存服务// OpenFeign客户端 FeignClient(name inventory-service) public interface InventoryFeignClient { PostMapping(/inventory/deduct) InventoryResponse deduct(RequestBody InventoryDTO inventoryDTO); }Seata的SeataFeignInterceptor会拦截Feign请求把XID放到请求头里库存服务接受到请求后又通过SeataHandlerInterceptor从请求头取回XID设置到库存服务对应线程的RootContext中。整个过程对业务代码透明。这里最常见的问题是异步线程丢失XID。比如createOrder方法内部用异步线程池去扣库存子线程的ThreadLocal是全新的XID带不过去库存服务那边的分支事务就无法加入当前全局事务最终结果就是订单创建成功但库存没扣。解决方法是手动传递XID或者把扣库存改成同步调用。我在项目里明确要求全局事务链路中禁用异步调用除非能保证XID传递完整。另外一个坑出现在线程池复用场景。如果线程池里的线程在处理完一个事务后ThreadLocal里残留了上一个XID下一次任务会错误地引入旧的全局事务。处理办法是使用Seata提供的TransactionTemplate或者在使用线程池执行任务前主动清空RootContext.unbind()。5.3 分支事务注册与全局提交回滚流程要真正理解AT模式落地后的运行机制需要对提交和回滚的时序有清晰认识。我把一次完整的全局事务执行流程拆开来看第一步TM订单服务调用GlobalTransactional方法向TC注册全局事务拿到XID。第二步订单服务执行的第一个本地SQL插入订单被Seata数据源代理拦截生成前后镜像写入undo_log表同时向TC注册分支事务TC记录分支和全局的关系。第三步订单服务通过Feign调用库存服务XID随请求头传递。库存服务里的扣减SQL同样被代理拦截写入undo_log并注册分支事务。第四步全局事务方法执行完毕TM向TC发送全局提交请求。第五步TC检查所有分支都注册完成且存在通知各RM进行分支提交。分支提交本质上就是删除undo_log记录业务库的改动已经是定局。第六步如果有任何分支执行失败异常向上抛到GlobalTransactional拦截器TM向TC发送全局回滚请求。TC通知所有RM执行回滚RM读取undo_log根据前后镜像生成反向SQL执行把数据恢复到修改前。提交和回滚流程中TC会记录每个分支的状态。如果某个RM在提交过程中宕机TC会不停重试直到确认该分支提交成功为止。所以生产环境中TC的重试机制是保证最终一致性的核心防线。这里有一个我反复跟团队强调的点undo_log表里的数据不代表“事务失败”了它在成功提交后也会短暂存在然后被定时任务异步删除。日志里看到undo_log有数据不要下意识认为是回滚要先看分支状态是提交还是回滚。6. 生产环境踩坑实录与性能优化经验6.1 性能瓶颈定位与优化手法AT模式在生产环境跑了一段时间后性能问题会逐渐暴露。常见的瓶颈有三个方向全局锁等待、undo_log写入开销、TC处理能力。全局锁等待是写热点场景最突出的问题。比如秒杀商品所有用户都在扣减同一个SKU的库存第二个事务必须等第一个事务提交释放全局锁否则一直重试。这里的核心优化思路是把“写热点”转成“写热点扩展点”。例如库存表可以拆成库存主表和库存流水扩展表扣减逻辑修改扩展表记录库存主表只做总额核对。全局锁只锁扩展记录而不是锁唯一的库存主记录。我实测过一组数据单SKU并发扣减从每秒200次提升到每秒2000次关键不是把Seata参数调到多大而是改了扣减策略让每次扣减不再直接更新同一行库存记录先写流水再用异步任务汇总。undo_log写入开销在事务量大时也不可忽视。每一条被代理的SQL都伴随两次镜像查询和一次undo_log插入事务响应时间平均增加1到3毫秒。优化方法有两个一是精简不必要的分支事务尽量把多个本地SQL合并成一个事务块二是如果业务上允许把不关键的查询SQL排除在Seata管控范围外比如查询操作不加GlobalTransactional只对写操作开启。TC处理能力方面我建议监控TC的线程池指标。netty-tc-thread线程数如果持续打满说明TC需要扩容或者事务分支数太多。从业务侧优化分支数比给TC堆机器更有效。6.2 网络抖动与超时问题处理分布式事务对网络抖动非常敏感。我和团队在压测阶段遇到过模拟网络延迟增大后分支事务能够成功执行但TC回调确认信息迟迟没收到TM侧就判断超时并触发回滚。结果就是业务数据已经被改掉本地已提交但全局事务判定回滚最终数据不一致。这套问题的根因在于Seata的全局事务状态与分支本地事务状态不是同时刻一致的中间存在一个窗口期。要降低这个窗口期的影响有几个实践方法一是把client.tm.commit.retry.count和client.tm.rollback.retry.count调大网络抖动时多试几次而不是立即判定失败。二是确保TC的重试任务没有超时时间限制。server.max-commit-retry-timeout和server.max-rollback-retry-timeout配置为-1表示不限制重试时间。如果限制了TC重试到超时就会放弃这个分支会一直处于中间状态。三是明确幂等要求。如果全局事务提交阶段失败TC会重试分支提交分支提交对应的业务SQL必须幂等。在库存场景扣减SQL本身不是幂等的但Seata AT模式在分支提交时只是删undo_log不会重放业务SQL所以这个场景相对安全。但是TCC模式就完全不同Confirm和Cancel必须你自己实现幂等。还有一类网络问题TC和RM之间的连接用了长连接长连接长时间空闲会被服务端或中间设备断开导致下次通信报错。解决办法是定期做心跳检查或者把网络空闲超时配置调大。Seata的Netty基础配置里connect.timeout默认是5秒我第一次在云环境部署时遇到堆外内存增长排查下来就是心跳周期和中间设备空闲断连冲突导致的。6.3 高危故障场景TC宕机与数据库死锁TC宕机是最严重的故障场景处理不好会造成全局事务状态悬挂。我梳理过一套标准应对流程第一步启动备节点TC确认注册中心中TC服务恢复可用。第二步查看业务库中undo_log表的数据。凡是日志状态为“正常”且关联的全局事务处于“未提交”状态的都需要人工介入。第三步从TC的全局事务表global_table中查询未完成事务判断它们是否所有分支都已完成。如果都完成了手动提交对应事务如果有分支失败触发回滚。第四步清理悬挂事务记录恢复业务流量。整个流程里最关键的是第二步和第三步这需要业务开发、DBA、架构组三方配合。我们的经验是TC宕机所在时间段的事务尽量不要自动清理而是通过核对业务数据判断是否需要补账。比如订单创建了但库存没扣业务侧要补偿扣减这个判断算法比任何自动化工具都可靠。数据库死锁则是另一类高频故障。AT模式引入全局锁后实际上存在两把锁本地数据库行锁和Seata全局锁。如果两个事务对不同记录加锁的顺序相反就会产生死锁。举例来说事务A先锁库存再加积分事务B先加积分再锁库存当两个事务在全局锁和本地锁之间嵌套等待时死锁就发生了。规避死锁的手段一是在业务层面统一加锁顺序例如所有事务先锁库存再锁订单再锁积分全局统一二是调整数据库的innodb_lock_wait_timeout不能让死锁检测等太长时间一般设置成2到3秒三是合理控制单事务操作记录数量文档里建议单事务不超过100条主键实践中我一般控制在20条以内。6.4 事务分组与灰度发布策略生产环境里不可能只服务一个业务线。我维护的Seata集群里跑着订单、支付、会员、积分四个业务线每个业务线都有独立的tx-service-group。这样做的优势很明显每个业务线的全局事务是隔离的一个业务线的事务风暴不会拖垮另一个。比如大促时订单服务的全局事务量骤增支付线的TC性能不受影响因为TC上不同分组的事务处理是并行的。事务分组还和灰度发布强绑定。新版本代码上线前可以只让部分流量走新事务组其余流量走老事务组。做法是在注册中心里给同一个TC虚拟集群配一个旧分组和一个新分组再通过客户端的service.vgroup-mapping控制流量比例。我常用的灰度策略是这样先在Nacos配置中心里新建一个tx-service-group: order-tx-group-gray映射到同一套TC集群。然后只让一台实例使用灰度的group配置其他实例保持旧配置。灰度实例上线观察半天确认无异常后把剩余实例都切到新配置最后删掉旧group。这套方案比直接升级TC版本更稳。因为TC版本升级涉及协议兼容性万一新旧版本不兼容影响面是全部业务线。通过分组隔离我可以在TC集群里混跑多个版本逐个验证后再迁移。7. 常见问题速查与排障技巧7.1 典型报错场景与解决方案现象可能原因排查方法解决方案客户端启动报“no available service”事务分组未正确匹配检查tx-service-group和vgroup-mapping配置修正映射关系确认注册中心中TC服务存在全局事务一直卡住不提交不回滚TC宕机或事务状态丢失查询TC的global_table状态重启TC人工核对中间态事务商品库存扣减时出现LockConflictException全局锁冲突等待超时查看日志确认冲突的SQL和锁时间调大lock.retry.times或优化热点写策略回滚时报找不到undo_log记录本地事务已提交但undo_log被清理核对分支状态和本地表数据确认是否有定时任务清理了undo_log调整清理策略分支事务执行成功但全局回滚本地提交成功但TC回调超时查看TM和TC之间的网络日志调大重试次数检查网络稳定性数据库死锁导致全局事务失败本地锁与全局锁嵌套等待查看数据库死锁日志统一加锁顺序降低事务锁记录数7.2 日志排查的关键关键字Seata的日志量比较大生产环境排查时不要全量看。我通常会按这几个关键字过滤Global transaction看到这行说明TM发起了全局事务后面跟着的是成功或失败的最终状态。Branch transaction分支事务注册、提交、回滚的日志入口能定位到具体哪个服务哪个SQL报错。retry commit或retry rollback说明TC在重复尝试提交或回滚通常表示网络或RM侧暂时不可用。LockConflict全局锁冲突日志会附带冲突的SQL和当前持有锁的事务ID可以辅助定位写热点。undo_log delete分支提交成功undo_log清理的日志说明事务正常结束。排查时我会把seata相关的logger级别临时调整到DEBUG定位完立刻调回INFO避免大促期间日志量影响性能。生产环境高峰期不建议开DEBUG曾经有一次排查问题开了DEBUG结果日志侧把磁盘写满了业务直接IO阻塞教训很惨。7.3 undo_log表数据膨胀怎么办undo_log表只存进行中的事务镜像正常提交后会被异步删除所以理论上不会膨胀。但生产环境仍然可能积累数据原因通常是全局事务长时间未结束或TC宕机后RM侧分支事务悬挂未清理。处理方法分两步先查全局事务状态如果对应的global_table里事务已经结束那undo_log里的记录就是残留可以安全删除如果事务还在进行中则不能手动删否则会破坏回滚能力。长期预防方案是加一张定时清理任务每天凌晨执行以下SQL时先核对存在性-- 先查询残留记录 SELECT xid, branch_id, log_status, log_created FROM undo_log WHERE log_created DATE_SUB(NOW(), INTERVAL 1 DAY) AND log_status IN (0, 1); -- 确认这些业务对应的xid在seata_server库中不存在有效事务后再执行删除 DELETE FROM undo_log WHERE log_created DATE_SUB(NOW(), INTERVAL 1 DAY) AND log_status IN (0, 1);注意这个删除操作必须在业务低峰期执行并且要确认数据对应的全局事务已经终结。如果业务上允许也可以考虑在事务结束后由业务侧主动调用清理接口但通用性不如定时任务。8. 长期运维与演进建议Seata落地不是装完配置就完事长期运维有几个方向值得投入。第一监控体系要补全。与Seata相关的关键指标包括全局事务提交成功率、全局事务回滚率、全局锁等待时间、undo_log残留量、TC线程池活跃度。这些指标对接Prometheus后可以在Grafana上建一个专门面板。我设定的报警阈值是全局事务回滚率连续5分钟超过10%就报警全局锁等待超过1秒就报警TC线程池活跃度超过80%持续5分钟就扩容。第二定期做故障演练。每季度做一次TC宕机演练、一次网络分区演练、一次数据库死锁演练。演练的作用是验证人工介入流程是否可执行。我有一次演练发现人工提交悬挂事务时由于DBA和开发没有统一的SQL脚本光确认事务状态就花了两小时。后来我整理了一套标准的“悬挂事务处理脚本包”分发到DBA和值班开发手里演练时间缩短到20分钟。第三版本升级策略。Seata迭代速度不慢小版本修复很多坑大版本则会引入协议变化。我的原则是不在大促前升级升级顺序是先升级TC再升级客户端升级前必须跑一遍完整的订单库存压测脚本确认内存、锁、回滚三个维度没有异常。第四归档旧数据。TC的global_table和lock_table会随时间增长尤其事务量大的业务线。建议每天归档超过7天的已完成事务记录归档到历史表防止单表数据量过大影响TC查询性能。如果团队有余力还可以把Seata的分支事务状态同步到业务系统的审计表里形成一条完整的“事务审计链路”。这样当最终对账发现数据不一致时可以从业务侧直接定位到具体是哪个事务出现了问题而不需要翻遍TC日志。9. 结尾最后聊一点我个人体会。Seata AT模式最吸引人的地方是它把分布式事务的门槛降得很低但这也意味着团队容易忽视它背后的运行机制。我见过不少项目上线半年后出了问题才回去翻undo_log表结构才知道有global_table这张表的存在。如果你正在规划分布式事务的落地我的建议是把精力前置先梳理清楚有哪些跨库操作、单事务分支数量、写热点分布、网络质量如何再决定要不要引入Seata。引入之后一定要保留至少一场故障演练因为分布式事务的隐患多半不在正常路径上而在故障路径上。再分享一个小技巧Seata的全局事务和本地事务叠加使用时建议在代码注释里写明“这里为什么需要本地事务”“如果去掉本地事务会发生什么”。我看到过有人把Transactional误删后全局事务虽然能回滚但本地查询在事务内看到的数据就是错的排查起来非常绕。希望这篇基于订单库存场景的完整落地记录能帮你少走一些弯路。如果你们也有Seata AT模式的生产经验欢迎多交流尤其是锁冲突和事务悬挂这两块不同业务形态下的处理方式差异还是挺大的。
返回列表