分布式事务解决方案:最佳实践

王秀兰_数据
2025-12-13 02:08
阅读 399

上周五晚上十点半,我盯着屏幕上不断重试失败的订单状态,心里默默问候了产品经理祖宗十八代。这已经是本周第三次因为“超卖”问题被叫起来救火了。作为一个在当前公司摸爬滚打三年多的老油条,我其实早就想换个环境了——但在这之前,得先把这个烂摊子收拾干净。

说起来有点惭愧,虽然我 GitHub Copilot 付费用户的身份已经快两年了(别笑,这玩意儿真的能救命),平时写 CRUD 代码飞快,但一遇到分布式事务这种硬核问题,还是得老老实实翻文档、看源码。最近在研究 Rust,感觉内存安全和并发模型特别优雅,但现实是……我们后端还是 Java 的天下啊!

为什么分布式事务这么让人头大?

事情的起因很简单:我们的电商平台要做一个“下单即扣库存+创建订单+发优惠券”的业务流程。听起来很常规对吧?但在微服务架构下,这三个操作分别在三个不同的服务里,每个服务都有自己的数据库。

去年双11期间,我们就因为这个问题搞出了线上事故。用户下了单,库存扣了,但订单创建失败,优惠券没发出去。结果用户投诉说“钱付了但没收到货”,客服电话被打爆,CTO 在群里@所有人:“今晚不解决就别回家了”。

当时的临时方案就是人工对账 + 补偿脚本,但这种“人肉运维”的方式显然不可持续。作为经常参加技术分享会的老鸟,我深知这种问题必须用正规的分布式事务方案来解决。

踩坑历程:从两阶段提交到 Saga 模式

最开始,团队里有个新来的实习生建议用 XA 两阶段提交。我看着他天真的眼神,内心苦笑——这哥们还没被生产环境毒打过啊!

XA 协议理论上很完美,但实际上在高并发场景下性能堪忧。我们测试环境压测了一下,TPS 直接从 3000 降到了 300,数据库连接池直接被打满。而且一旦协调者挂了,整个事务就卡在那里,简直是运维噩梦。

// 别学这个!这只是展示 XA 的复杂性
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void createOrderWithXA(Order order) {
    // 需要配置 XADataSource,每个数据库都要支持
    // 还要考虑网络超时、资源锁定等问题
    // 实际上我们很快就放弃了这个方案
}

后来我们尝试了基于消息队列的最终一致性方案。思路很简单:下单服务发送消息到 MQ,库存服务和优惠券服务消费消息进行处理。但如果消息发送失败怎么办?如果消费者处理失败怎么办?

这时候我想起了之前在某次技术分享会上听到的 TCC 模式(Try-Confirm-Cancel)。TCC 的核心思想是业务层面的两阶段提交:

  • Try 阶段:预留资源(比如冻结库存)
  • Confirm 阶段:确认执行(比如扣减冻结的库存)
  • Cancel 阶段:取消预留(比如释放冻结的库存)

听起来很美好,但实现起来工作量巨大。每个业务操作都要写三套逻辑,而且要考虑各种异常情况。我们团队评估后觉得 ROI 太低,毕竟我们不是支付宝,没必要这么严谨。

最后我们选择了 Saga 模式,这是目前我们认为最适合我们业务场景的方案。

Saga 模式实战:用 Seata 实现

Saga 模式的核心思想是:将一个长事务拆分成多个本地事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,就依次执行前面步骤的补偿操作。

我们在项目中集成了 Seata(一款开源的分布式事务解决方案),配置相对简单,对业务代码侵入性也比较小。

1. 环境配置

首先在 pom.xml 中添加依赖:

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

然后配置 application.yml

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

2. 业务代码实现

关键是在主方法上加上 @GlobalTransactional 注解:

@Service
@Slf4j
public class OrderService {
    
    @Autowired
    private InventoryClient inventoryClient;
    
    @Autowired
    private CouponClient couponClient;
    
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(OrderRequest request) {
        try {
            // 1. 创建订单(本地事务)
            Order order = orderRepository.save(buildOrder(request));
            log.info("订单创建成功: {}", order.getId());
            
            // 2. 扣减库存(远程调用,需要支持分布式事务)
            boolean inventoryResult = inventoryClient.decreaseInventory(
                request.getProductId(), request.getQuantity()
            );
            if (!inventoryResult) {
                throw new RuntimeException("库存不足");
            }
            log.info("库存扣减成功");
            
            // 3. 发放优惠券(远程调用)
            boolean couponResult = couponClient.issueCoupon(request.getUserId());
            if (!couponResult) {
                log.warn("优惠券发放失败,但不影响主流程");
                // 这里可以选择继续还是回滚,根据业务需求
            }
            
            return order;
            
        } catch (Exception e) {
            log.error("创建订单失败: {}", e.getMessage(), e);
            throw e; // 触发全局回滚
        }
    }
}

每个参与方服务也需要配置 Seata,并实现对应的接口。以库存服务为例:

@RestController
public class InventoryController {
    
    @Autowired
    private InventoryService inventoryService;
    
    @PostMapping("/inventory/decrease")
    public boolean decreaseInventory(@RequestBody InventoryRequest request) {
        // 这个方法会被 Seata 代理,自动参与全局事务
        return inventoryService.decrease(request.getProductId(), request.getQuantity());
    }
}

3. 补偿机制设计

Saga 模式的关键在于补偿操作的设计。我们需要确保:

  • 补偿操作是幂等的(多次执行结果一致)
  • 补偿操作能够真正回滚业务状态
  • 补偿操作本身也要考虑失败的情况

比如库存服务的补偿操作:

// 库存服务中的补偿逻辑
public void compensateDecreaseInventory(String productId, Integer quantity) {
    // 幂等性检查:避免重复补偿
    if (alreadyCompensated(productId, getCurrentGlobalTxId())) {
        return;
    }
    
    try {
        // 增加库存
        inventoryRepository.increase(productId, quantity);
        markAsCompensated(productId, getCurrentGlobalTxId());
    } catch (Exception e) {
        log.error("补偿库存失败", e);
        // 这里可能需要人工介入或者重试机制
        throw e;
    }
}

生产环境经验总结

经过几个月的线上运行,我们总结了一些关键经验:

性能对比

方案 TPS 事务成功率 开发复杂度 运维复杂度
XA 两阶段提交 300 99.9%
消息队列最终一致性 2500 98.5%
TCC 模式 2000 99.99%
Saga (Seata) 2200 99.8%

安全意识不能忘

在实现分布式事务时,安全问题往往被忽视。我们特别注意了以下几点:

  1. 数据一致性验证:定期跑对账脚本,确保各服务间数据一致
  2. 权限控制:Seata 的注册中心和配置中心都要做访问控制
  3. 日志审计:所有事务操作都要记录详细日知,便于追踪问题
  4. 网络隔离:Seata Server 和业务服务之间使用内网通信
// 对账脚本示例(每天凌晨执行)
@Component
public class ReconciliationJob {
    
    @Scheduled(cron = "0 0 2 * * ?")
    public void reconcileOrdersAndInventory() {
        List<Order> orders = orderRepository.findCompletedOrdersYesterday();
        for (Order order : orders) {
            Integer actualInventory = inventoryClient.getActualInventory(order.getProductId());
            Integer expectedInventory = calculateExpectedInventory(order.getProductId());
            
            if (!actualInventory.equals(expectedInventory)) {
                log.error("库存不一致! productId: {}, actual: {}, expected: {}", 
                    order.getProductId(), actualInventory, expectedInventory);
                // 发送告警通知
                alertService.sendAlert("库存数据不一致");
            }
        }
    }
}

团队协作心得

实施分布式事务最大的挑战其实不是技术,而是团队协作。每个微服务团队都需要理解分布式事务的原理,按照约定的方式开发。我们建立了以下规范:

  • 所有参与分布式事务的方法必须有明确的入参和出参
  • 异常处理要统一,不能吞掉异常
  • 接口文档要及时更新
  • 压测时要模拟网络延迟和超时场景

写在最后

现在回想起来,那个周五晚上的加班虽然痛苦,但也促使我们真正解决了这个技术债。系统上线三个月来,再也没有出现过超卖问题,用户投诉率下降了 80%。

作为一个准备跳槽的程序员,这段经历让我深刻认识到:真正的工程能力不是会多少框架,而是在复杂约束下找到平衡点的能力。既要考虑技术的先进性,也要考虑团队的接受度;既要保证系统的可靠性,也要控制开发成本。

下周我要去参加一个 Java 技术分享会,准备把这次的经验分享给大家。说不定还能遇到合适的下家呢(笑)。

如果你也在为分布式事务头疼,不妨试试 Saga 模式。当然,具体方案还是要根据你的业务场景来选择。记住,没有最好的方案,只有最适合的方案。

最后,感谢我的 GitHub Copilot,虽然它不能帮我解决分布式事务的问题,但至少让我写 CRUD 代码的时候少敲了很多键盘,保护了我的腱鞘 😅

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝