新闻详情 资讯动态

全面了解最新资讯与建站知识,洞察行业趋势。

行业资讯

分布式事务优化:本地事务+补偿机制实践

发布时间:2026/11/5 4:15:09
分布式事务优化:本地事务+补偿机制实践 1. 为什么我们需要重新思考分布式事务方案在传统数据库架构中当业务发展到一定规模后系统往往会面临一个关键选择是继续使用单机数据库承受性能压力还是转向分布式架构引入额外的复杂度。特别是在需要保证数据一致性的场景下这个选择尤为艰难。我经历过太多这样的架构讨论会开发团队坚持要上分布式事务DBA团队则拼命反对理由永远是2PC两阶段提交性能太差。双方各执一词最后往往演变成一场没有赢家的拉锯战。但事实上很多场景下我们可能走入了思维定式——并不是所有跨服务的数据操作都需要2PC这种重量级方案。2. 本地事务补偿机制的核心思想2.1 基本原理剖析这种方案的核心在于充分利用MySQL单机事务的ACID特性将需要跨服务保证一致性的操作拆分为两个阶段主事务阶段在本地事务中完成核心数据变更同时记录补偿操作补偿阶段当后续步骤失败时根据记录执行逆向操作这种模式最经典的实现就是TCCTry-Confirm-Cancel模式。以一个电商下单为例-- Try阶段主事务 BEGIN; -- 1. 扣减库存核心操作 UPDATE inventory SET stock stock - 1 WHERE item_id 1001; -- 2. 记录补偿日志关键步骤 INSERT INTO tcc_log (biz_id, operation, params) VALUES (order_123, restore_inventory, {item_id:1001,delta:1}); COMMIT; -- Confirm阶段无事务 -- 调用支付服务成功后删除补偿日志 DELETE FROM tcc_log WHERE biz_id order_123; -- Cancel阶段需要时执行 -- 如果支付失败读取补偿日志执行逆向操作 BEGIN; UPDATE inventory SET stock stock 1 WHERE item_id 1001; DELETE FROM tcc_log WHERE biz_id order_123; COMMIT;2.2 与2PC的本质区别这种方案与传统的2PC有几点关键差异事务边界不同2PC是跨数据库的分布式事务而本地事务补偿是在单库内完成失败处理方式2PC依赖协调者进行全局回滚补偿机制是事后补救性能影响2PC需要多次网络通信和锁保持补偿机制主事务结束后立即释放资源3. 详细实现方案设计3.1 数据模型设计需要设计几个核心表来支持这个机制CREATE TABLE tcc_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL COMMENT 业务ID, operation VARCHAR(128) NOT NULL COMMENT 补偿操作类型, params JSON NOT NULL COMMENT 补偿参数, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_biz_id (biz_id) ) ENGINEInnoDB; CREATE TABLE async_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL COMMENT 关联的业务ID, task_type VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待处理,1-处理中,2-成功,3-失败, retry_count INT NOT NULL DEFAULT 0, execute_after DATETIME NOT NULL COMMENT 最早执行时间, params JSON NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_execute (status, execute_after) ) ENGINEInnoDB;3.2 核心流程实现完整的事务补偿流程实现如下主事务执行Transactional public void createOrder(OrderDTO order) { // 1. 扣减库存 inventoryService.reduceStock(order.getItemId(), order.getQuantity()); // 2. 生成订单记录 orderMapper.insert(order); // 3. 记录补偿日志 TccLog log new TccLog(); log.setBizId(order.getOrderNo()); log.setOperation(restore_inventory); log.setParams(JSON.toJSONString(Map.of( itemId, order.getItemId(), quantity, order.getQuantity() ))); tccLogMapper.insert(log); // 4. 创建异步任务调用支付服务 AsyncTask task new AsyncTask(); task.setBizId(order.getOrderNo()); task.setTaskType(PAYMENT); task.setExecuteAfter(new Date()); // 立即执行 task.setParams(JSON.toJSONString(order)); asyncTaskMapper.insert(task); }补偿处理器实现public void handlePaymentResult(String orderNo, boolean success) { if(success) { // 成功则删除补偿日志 tccLogMapper.deleteByBizId(orderNo); } else { // 失败则执行补偿 TccLog log tccLogMapper.selectByBizId(orderNo); if(log ! null) { MapString, Object params JSON.parseObject(log.getParams()); inventoryService.increaseStock( (Long)params.get(itemId), (Integer)params.get(quantity) ); tccLogMapper.deleteByBizId(orderNo); } } }4. 关键问题与解决方案4.1 补偿操作的幂等性补偿操作必须保证幂等因为网络问题可能导致补偿被多次触发。解决方案在补偿日志表中记录执行状态使用乐观锁控制并发补偿操作本身设计为可重复执行改进后的补偿日志表ALTER TABLE tcc_log ADD COLUMN status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待补偿,1-补偿中,2-已完成; ALTER TABLE tcc_log ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本;对应的补偿执行逻辑Transactional public void compensate(String bizId) { // 乐观锁控制 TccLog log tccLogMapper.selectForUpdate(bizId); if(log null || log.getStatus() 2) { return; // 已处理过 } if(log.getStatus() 1 System.currentTimeMillis() - log.getUpdateTime().getTime() 60000) { return; // 其他线程正在处理 } // 更新状态为处理中 tccLogMapper.updateStatus(bizId, 1, log.getVersion()); // 执行补偿逻辑 MapString, Object params JSON.parseObject(log.getParams()); inventoryService.increaseStock( (Long)params.get(itemId), (Integer)params.get(quantity) ); // 更新状态为已完成 tccLogMapper.updateStatus(bizId, 2, log.getVersion() 1); }4.2 补偿遗漏问题系统崩溃可能导致补偿未被触发。解决方案定时扫描长时间未完成的业务设置合理的超时时间实现补偿重试机制定时任务实现示例Scheduled(fixedRate 60000) public void scanTimeoutOrders() { // 查询超过30分钟未完成的订单 ListOrder timeoutOrders orderMapper.selectTimeoutOrders(30); for(Order order : timeoutOrders) { // 检查是否已经处理过补偿 TccLog log tccLogMapper.selectByBizId(order.getOrderNo()); if(log ! null log.getStatus() 0) { compensateExecutor.execute(() - compensate(order.getOrderNo())); } } }5. 性能优化实践5.1 批量补偿处理当需要处理大量补偿时单个处理效率低下。可以优化为批量模式public void batchCompensate(ListString bizIds) { // 按业务类型分组 MapString, ListString groups bizIds.stream() .collect(Collectors.groupingBy(bizId - { TccLog log tccLogMapper.selectByBizId(bizId); return log.getOperation(); })); // 批量处理同类型补偿 groups.forEach((operation, ids) - { ListTccLog logs tccLogMapper.selectBatchForUpdate(ids); // 按操作类型路由到不同的处理器 switch(operation) { case restore_inventory: batchRestoreInventory(logs); break; case cancel_order: batchCancelOrders(logs); break; // 其他操作类型... } }); }5.2 补偿日志归档长期积累的补偿日志会影响性能需要定期归档-- 创建归档表 CREATE TABLE tcc_log_archive LIKE tcc_log; -- 每月归档一次 INSERT INTO tcc_log_archive SELECT * FROM tcc_log WHERE status 2 AND created_at DATE_SUB(NOW(), INTERVAL 3 MONTH); DELETE FROM tcc_log WHERE status 2 AND created_at DATE_SUB(NOW(), INTERVAL 3 MONTH);6. 实际应用中的经验总结6.1 适合场景判断经过多个项目实践我发现这种方案特别适合以下场景主从业务模式核心业务在主系统边缘业务在子系统有明确补偿逻辑的业务如库存增减、账户余额变动等对实时性要求不严格的场景允许秒级延迟最终一致6.2 踩坑记录补偿顺序问题多个补偿操作间如果有依赖关系需要特别注意执行顺序。我们曾经遇到过先退款后恢复库存导致的负库存问题。日志表索引设计最初没有为biz_id加唯一索引导致重复补偿。后来加上了唯一索引并配合乐观锁才彻底解决。网络超时设置调用外部服务时超时时间设置过短会导致误补偿。现在我们会根据历史数据动态调整超时阈值。6.3 监控指标建议为了确保方案可靠运行建议监控以下指标补偿触发率补偿次数/总事务数平均补偿延迟从主事务完成到补偿执行的时间差补偿成功率单次补偿耗时我们使用的Prometheus监控配置示例- name: tcc_metrics rules: - record: tcc_compensation_rate expr: rate(tcc_compensation_total[5m]) / rate(tcc_transaction_total[5m]) - record: tcc_compensation_latency_seconds expr: histogram_quantile(0.95, sum(rate(tcc_compensation_latency_seconds_bucket[5m])) by (le))这种本地事务补偿的方案在保证数据一致性的同时避免了分布式事务的性能开销。经过多个项目的验证在单机MySQL环境下TPS可以比2PC方案提升3-5倍同时99%的补偿能在1秒内完成。对于大多数业务规模适中的系统这确实是一个值得考虑的折中方案。

想做一个「会获客」的企业网站?

留下需求,1 小时内获取专属建站方案与透明报价。

免费咨询方案
↑