AI摘要

作者分享从单体迁微服务后首踩“分布式事务”坑:订单与库存分库导致数据不一致,最终用本地消息表+MQ实现最终一致性,并反思网络、幂等、运维等复杂度,提醒微服务非银弹,须权衡业务与团队 readiness。

当公司决定从单体架构迁移到微服务时,我和所有新人一样兴奋。我们觉得拆分成小服务,各自独立开发部署,一切都会变得美好。然而,我很快就遇到了分布式系统的“第一道坎”——分布式事务问题,这彻底打破了我的幻想。

场景:简单的下单流程,不简单的事务问题

在一个简化的下单流程中,涉及两个服务:

  • 订单服务(Order Service)​:创建订单。
  • 库存服务(Inventory Service)​:扣减商品库存。

在单体架构中,用一个本地数据库事务就能保证:订单创建和库存扣减要么都成功,要么都失败。代码简单可靠。

@Transactional // 单体应用的美好时代
public void createOrder(OrderRequest request) {
    // 1. 在订单表插入记录
    orderMapper.insert(order);
    // 2. 扣减库存
    inventoryService.deductStock(request.getSku(), request.getQuantity());
    // 本地事务保证1和2的原子性
}

拆分成微服务后,订单库和库存库也分离开了。上面的代码变成了:

// 在订单服务中
// @Transactional 注解此刻显得苍白无力!
public void createOrder(OrderRequest request) {
    // 1. 本地事务:创建订单(状态为“待扣减库存”)
    orderMapper.insert(order); // 操作订单数据库

    // 2. 通过网络调用库存服务的API
    inventoryFeignClient.deductStock(request.getSku(), request.getQuantity()); // 操作另一个库存数据库
}

问题来了​:

  • 如果步骤1成功,但步骤2调用库存服务失败(比如网络超时、库存服务宕机),会发生什么?
  • 结果​:订单创建了,但库存没扣。出现了数据不一致!系统多了一个永远无法支付的订单(因为库存实际不足)。

这就是典型的分布式事务问题。我们无法再用一个本地事务框住所有操作。

尝试与探索:我们如何解决这个问题?

我们调研了几种方案:

  1. 两阶段提交(2PC)​:强一致性方案,但性能差,实现复杂,不适合高并发互联网场景。否决。
  2. TCC(Try-Confirm-Cancel)​:需要修改业务逻辑,实现tryconfirmcancel三个阶段,侵入性强。对于“扣库存”这种业务,try阶段(冻结库存)还算合理,但对于其他更复杂的业务,改造难度大。暂不考虑。
  3. 本地消息表(最终一致性)​:这是一种非常实用且侵入性较小的方案。我们最终采用了这个方案。

最终方案:基于本地消息表的最终一致性

核心思想​:将分布式事务拆分成一系列本地事务,依靠消息队列和重试机制,保证数据最终一致。

具体实现步骤​:

  1. 下单时(订单服务)​:
@Transactional
public void createOrder(OrderRequest request) {
    // 1. 创建订单,状态为 "PENDING"(进行中)
    Order order = ...;
    orderMapper.insert(order);

    // 2. 在同一个数据库事务中,插入一条消息记录到本地消息表
    MessageEvent event = new MessageEvent();
    event.setType("INVENTORY_DEDUCT");
    event.setPayload(JSON.toJSONString(new InventoryDeductEvent(order.getId(), sku, quantity)));
    event.setStatus("NEW");
    eventMapper.insert(event);

    // 事务提交,订单和消息记录要么同时成功,要么同时失败
}
  1. 异步发送消息​:

    • 有一个定时任务,扫描本地消息表中状态为“NEW”的消息。
    • 将消息发送到消息队列(如RocketMQ/Kafka)。如果发送成功,将本地消息状态更新为“SENT”。
  2. 消费消息(库存服务)​:

    • 库存服务监听消息队列中的“INVENTORY\_DEDUCT”主题。
    • 收到消息后,幂等地执行库存扣减逻辑。
    • 扣减成功,则业务完成。
  3. 处理失败​:

    • 如果步骤2发送消息失败,定时任务会重试。
    • 如果步骤3库存扣减失败(比如库存不足),库存服务可以记录失败,并触发补偿机制(如向订单服务发送一个“扣减失败”的消息,让订单服务将订单状态更新为“失败”)。

这样做的效果​:

  • 一致性​:保证了订单创建和库存扣减的最终一致性。即使在最坏情况下,可能会有一段时间的延迟(比如订单是“PENDING”状态),但不会出现永久的不一致。
  • 性能​:避免了长时间的分布式锁,性能比2PC好很多。

深刻的反思:微服务带来的复杂性

  1. 分布式事务是首要挑战​:微服务拆分了数据,带来了复杂的数据一致性问题。你必须放弃简单的本地事务思维,拥抱最终一致性。
  2. 网络是不可靠的​:必须时刻考虑网络超时、重试、幂等性。我们的Feign调用必须设置超时时间,并考虑重试策略。所有服务接口都必须实现​幂等​,防止因重试导致的数据错乱。
  3. 运维复杂度飙升​:需要维护消息队列、服务注册发现、配置中心等一系列中间件,监控和排错的难度成倍增加。
  4. 设计模式变化​:从传统的基于贫血模型的MVC,转向更注重领域建模和边界清晰的DDD(领域驱动设计)会更有帮助。

结论​:

微服务不是银弹,它用分布式系统的各种复杂性(网络、事务、一致性)换来了团队独立、技术异构和弹性伸缩等好处。在决定拆分之前,一定要问自己:我的业务和团队是否真的需要微服务?是否已经准备好应对它带来的挑战?

这次踩坑经历是我三年成长中的一个重要里程碑。它让我明白,架构没有好坏,只有合适与否。深刻理解不同架构带来的trade-off,是一个后端程序员走向成熟的必经之路。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:微服务不是银弹:我在分布式系统中遇到的第一个“坑”与反思
▶ 本文链接:https://www.huangleicole.com/backend-related/41.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏