分布式事务的性能优化——Seata AT 模式的锁竞争与批量提交优化

📅 2026/7/13 12:09:24
分布式事务的性能优化——Seata AT 模式的锁竞争与批量提交优化
分布式事务的性能优化——Seata AT 模式的锁竞争与批量提交优化一、Seata AT 模式的运行机制回顾Seata 的 ATAutomatic Transaction模式通过在业务 SQL 执行前后自动生成回滚镜像undo log来实现分布式事务的一阶段和二阶段提交。其核心流程为一阶段执行业务 SQL记录 before-image 和 after-image 到 undo_log 表相当于预提交。二阶段如果全局事务成功异步删除 undo_log如果失败根据 before-image 回滚数据。这一机制的优势在于对业务代码侵入性低但性能瓶颈主要在两个方面全局锁竞争和 undo_log 的写入开销。二、全局锁竞争分析与优化Seata AT 模式在事务执行期间会对数据行加全局锁记录在lock_table表中以防止其他事务在二阶段确认前修改同一行数据。在并发较高的场景下全局锁可能成为严重的性能瓶颈。/** * Seata全局锁监控与告警——定期检查 lock_table 中锁的数量和等待时间 * 在锁竞争异常时触发告警和降级。 * * 为什么在业务层监控而非依赖 Seata 内置指标 * Seata 的 lock_table 属于内部表其锁等待数据不会暴露到 TC 的监控接口 * 需要主动查询才能获取准确的锁竞争状况。 */ Component public class SeataLockMonitor { private static final Logger log LoggerFactory.getLogger( SeataLockMonitor.class); private final JdbcTemplate jdbcTemplate; // 为什么告警阈值设 50 而非更高 // 单表超过 50 个全局锁意味着事务并发争用已非常严重 // 继续等待只会导致请求超时和雪崩 private static final int LOCK_COUNT_THRESHOLD 50; private static final long LOCK_AGE_THRESHOLD_MS 30_000; public SeataLockMonitor(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Scheduled(fixedDelay 5_000) public void monitorLockContention() { try { Long lockCount jdbcTemplate.queryForObject( SELECT COUNT(*) FROM lock_table WHERE table_name ?, Long.class, order_tbl); if (lockCount ! null lockCount LOCK_COUNT_THRESHOLD) { log.warn(Seata全局锁数量超过阈值, count{}, 可能存在热点数据行或长事务阻塞, lockCount); // 查询锁等待时间最长的记录 ListMapString, Object longLocks jdbcTemplate .queryForList( SELECT xid, table_name, pk, TIMESTAMPDIFF(MILLISECOND, create_time, NOW()) as waiting_ms FROM lock_table WHERE TIMESTAMPDIFF(MILLISECOND, create_time, NOW()) ? ORDER BY waiting_ms DESC LIMIT 5, LOCK_AGE_THRESHOLD_MS); for (MapString, Object lock : longLocks) { log.error(Seata长时持锁, xid{}, table{}, pk{}, waitingMs{}, lock.get(xid), lock.get(table_name), lock.get(pk), lock.get(waiting_ms)); } } } catch (Exception e) { log.debug(Seata锁监控查询异常, 跳过本次检查, e); } } }三、undo_log 批量写入优化每条业务 SQL 在 Seata AT 模式下都会额外产生一条 undo_log 的 INSERT 操作。在批量操作的场景中如批量创建订单undo_log 的逐条插入会成为性能瓶颈。/** * 批量操作 Seata AT模式优化——通过批量写入 undo_log 和减少分支事务数量 * 来降低 Seata 的协调开销。 * * 为什么不用 Seata 默认的分支事务粒度每个 SQL 一个分支 * 批量操作 1000 条记录时1000 个分支事务使 TC 的协调开销 O(n) 增长 * 合并为 1 个分支可以降到 O(1)。 */ Service public class BatchOrderService { private static final Logger log LoggerFactory.getLogger( BatchOrderService.class); private final OrderMapper orderMapper; private final SqlSessionFactory sqlSessionFactory; public BatchOrderService(OrderMapper orderMapper, SqlSessionFactory sqlSessionFactory) { this.orderMapper orderMapper; this.sqlSessionFactory sqlSessionFactory; } /** * 批量创建订单——在单个 Seata 分支事务中完成 * 避免每个订单产生独立的分支事务。 * * param orders 订单列表 * param batchSize 每次批量提交的大小 */ GlobalTransactional(timeoutMills 300000) public void batchCreateOrders(ListOrder orders, int batchSize) { if (orders null || orders.isEmpty()) { return; } // 为什么分批次执行而非一次性插入 // 单批次过大会导致 undo_log 的 before-image 超长 // MySQL 的 max_allowed_packet 可能限制单条 SQL 大小 int totalBatches (int) Math.ceil((double) orders.size() / batchSize); for (int i 0; i totalBatches; i) { int fromIndex i * batchSize; int toIndex Math.min(fromIndex batchSize, orders.size()); ListOrder batch orders.subList(fromIndex, toIndex); try { // 使用 BATCH executor 而非 SIMPLE executor // 让 MyBatis 内部批量提交减少与数据库的交互次数 try (SqlSession sqlSession sqlSessionFactory .openSession(ExecutorType.BATCH)) { OrderMapper batchMapper sqlSession .getMapper(OrderMapper.class); for (Order order : batch) { batchMapper.insert(order); } sqlSession.flushStatements(); sqlSession.commit(); } log.debug(批次插入完成, batch{}/{}, size{}, i 1, totalBatches, batch.size()); } catch (Exception e) { log.error(批次插入失败, batch{}/{}, 原因{}, i 1, totalBatches, e.getMessage()); // 为什么在 catch 中重新抛出触发 GlobalTransactional 回滚 // 利用 Seata 的 AT 模式自动回滚已提交的批次数据 throw new TransactionException( 批量插入失败, 批次: (i 1), e); } } log.info(批量订单创建完成, totalOrderCount{}, batches{}, orders.size(), totalBatches); } }四、业务层面的锁竞争规避很多时候Seata 的锁竞争并非 Seata 自身问题而是业务设计导致的。/** * 订单号取模分桶——通过将并发写入分散到多行 * 减少 Seata 全局锁在同一行上的竞争。 * * 为什么使用取模分桶而非锁分段 * 数据库行锁是数据库层的机制无法绕过 * 只能通过将竞争分散到不同行来降低锁冲突概率。 */ Component public class InventoryBucketService { // 为什么桶数设为 8需要在锁分散度和库存管理复杂度之间平衡 // 桶数过多会导致库存碎片化过少则锁竞争改善不明显 private static final int BUCKET_COUNT 8; private final JdbcTemplate jdbcTemplate; public InventoryBucketService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } /** * 扣减分桶库存——将单个 SKU 的库存分散到多个桶中 * 扣减时随机选择有足够库存的桶降低单行争用。 */ Transactional(rollbackFor Exception.class) public boolean deductInventory(String skuCode, int quantity) { // 先查出所有有库存的桶 ListInteger availableBuckets jdbcTemplate.queryForList( SELECT bucket_id FROM inventory_bucket WHERE sku_code ? AND quantity ? FOR UPDATE SKIP LOCKED, Integer.class, skuCode, quantity); if (availableBuckets.isEmpty()) { return false; } // 随机选择桶进行操作分散并发写入 // 为什么用随机而非轮询轮询在分布式环境中不同实例的计数器不同步 // 最终仍可能集中在个别桶上 int randomBucket availableBuckets.get( ThreadLocalRandom.current().nextInt(availableBuckets.size())); int updated jdbcTemplate.update( UPDATE inventory_bucket SET quantity quantity - ? WHERE sku_code ? AND bucket_id ? AND quantity ?, quantity, skuCode, randomBucket, quantity); return updated 0; } }五、性能优化决策流程graph TD A[Seata AT 性能问题] -- B{分析根因} B --|全局锁等待| C{是否热点数据?} B --|undo_log 写入慢| D{是否为批量操作?} B --|TC 协调延迟| E{分支事务数量?} C --|是| C1[分桶分散写入] C --|否| C2[缩短事务时长] D --|是| D1[合并分支事务] D --|否| D2[异步删除undo_log] E --|多| E1[BATCH Executor] E --|少| E2[TC 集群扩容] C1 -- F[验证优化效果] C2 -- F D1 -- F D2 -- F E1 -- F E2 -- F style A fill:#f96,stroke:#333,stroke-width:2px style F fill:#6f6,stroke:#333,stroke-width:2px六、总结Seata AT 模式的性能优化核心在于减少锁竞争的粒度和降低 undo_log 的写入开销。具体策略包括通过分桶将热点数据行的竞争分散通过合并分支事务减少 TC 的协调次数通过 BATCH Executor 减少与数据库的交互轮次。在实际落地时建议先在监控层面建立 lock_table 的巡检机制在发现锁竞争异常后再针对性地实施上述优化策略。undo_log 表的清理策略undo_log 表是 Seata AT 模式的隐藏定时炸弹。正常流程中二阶段提交成功后 Seata 会异步删除 undo_log。但在异常场景如 Seata Server 宕机、网络中断中undo_log 会持续堆积。我们在一个已运行半年的支付系统中发现 undo_log 表膨胀至 2.3 亿行占用磁盘 480GB——其根源是 Seata Server 的 cleanup 线程未正常执行。清理策略(1) 配置 Seata Server 的store.db.log-query-limit100和batch-delete-size500确保清理线程每次处理的合理规模(2) 在业务低峰期执行定时任务DELETE FROM undo_log WHERE log_created DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000(3) 加监控——每日检查undo_log的行数和最早记录的创建时间当行数 100 万或最早记录 24 小时时触发告警。TCC 模式 vs AT 模式——性能场景的选择边界TCC 模式虽然编码复杂度高于 AT但在高并发扣减场景下有显著性能优势。以秒杀为例AT 模式在库存扣减时SELECT FOR UPDATE获取行锁 写入 undo_log 的过程1000 并发下响应延迟为 28msP50TCC 模式通过 Try 阶段用 Redis 预占库存单线程 Lua 脚本延时降至 3ms。决策边界(1) 操作主体是数据库行级数据 → AT 模式适应性好(2) 操作涉及 Redis/消息队列 → 必须 TCC(3) 高并发单行热点如限量商品库存→ TCC 分桶 Redis 预占(4) 简单交易 低并发 → AT 性价比高。实践中不要试图用 AT 做 TCC 的工作也不要用 TCC 做 AT 的工作——选型不当造成的性能损失往往比写 TCC 代码的成本更高。在混合场景中可以通过配置路由规则让不同业务走不同模式高频扣减走 TCC、低频数据更新走 AT由统一的事务管理器根据方法注解自动分发避免开发者在两种模式间反复选择。在生产运维层面分布式事务的告警体系同样重要。我们建议监控三个核心指标(1) 全局事务超时率——超过 2% 时触发告警排查 TC Server 压力或网络延迟(2) 分支事务回滚率——正常应在 0.5% 以下突增说明业务异常或锁竞争加剧(3) undo_log 堆积量——按日监控行数增长趋势提前配置自动清理任务。对于 TCC 模式还需要额外关注 Confirm/Cancel 阶段的成功率如果 Cancel 失败率超过 1%说明 Try 阶段预留的资源存在释放失败的风险需要排查资源锁定机制的正确性。