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调用库存服务失败(比如网络超时、库存服务宕机),会发生什么?
- 结果:订单创建了,但库存没扣。出现了数据不一致!系统多了一个永远无法支付的订单(因为库存实际不足)。
这就是典型的分布式事务问题。我们无法再用一个本地事务框住所有操作。
尝试与探索:我们如何解决这个问题?
我们调研了几种方案:
- 两阶段提交(2PC):强一致性方案,但性能差,实现复杂,不适合高并发互联网场景。否决。
- TCC(Try-Confirm-Cancel):需要修改业务逻辑,实现
try、confirm、cancel三个阶段,侵入性强。对于“扣库存”这种业务,try阶段(冻结库存)还算合理,但对于其他更复杂的业务,改造难度大。暂不考虑。 - 本地消息表(最终一致性):这是一种非常实用且侵入性较小的方案。我们最终采用了这个方案。
最终方案:基于本地消息表的最终一致性
核心思想:将分布式事务拆分成一系列本地事务,依靠消息队列和重试机制,保证数据最终一致。
具体实现步骤:
- 下单时(订单服务):
@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);
// 事务提交,订单和消息记录要么同时成功,要么同时失败
}异步发送消息:
- 有一个定时任务,扫描本地消息表中状态为“NEW”的消息。
- 将消息发送到消息队列(如RocketMQ/Kafka)。如果发送成功,将本地消息状态更新为“SENT”。
消费消息(库存服务):
- 库存服务监听消息队列中的“INVENTORY\_DEDUCT”主题。
- 收到消息后,幂等地执行库存扣减逻辑。
- 扣减成功,则业务完成。
处理失败:
- 如果步骤2发送消息失败,定时任务会重试。
- 如果步骤3库存扣减失败(比如库存不足),库存服务可以记录失败,并触发补偿机制(如向订单服务发送一个“扣减失败”的消息,让订单服务将订单状态更新为“失败”)。
这样做的效果:
- 一致性:保证了订单创建和库存扣减的最终一致性。即使在最坏情况下,可能会有一段时间的延迟(比如订单是“PENDING”状态),但不会出现永久的不一致。
- 性能:避免了长时间的分布式锁,性能比2PC好很多。
深刻的反思:微服务带来的复杂性
- 分布式事务是首要挑战:微服务拆分了数据,带来了复杂的数据一致性问题。你必须放弃简单的本地事务思维,拥抱最终一致性。
- 网络是不可靠的:必须时刻考虑网络超时、重试、幂等性。我们的Feign调用必须设置超时时间,并考虑重试策略。所有服务接口都必须实现幂等,防止因重试导致的数据错乱。
- 运维复杂度飙升:需要维护消息队列、服务注册发现、配置中心等一系列中间件,监控和排错的难度成倍增加。
- 设计模式变化:从传统的基于贫血模型的MVC,转向更注重领域建模和边界清晰的DDD(领域驱动设计)会更有帮助。
结论:
微服务不是银弹,它用分布式系统的各种复杂性(网络、事务、一致性)换来了团队独立、技术异构和弹性伸缩等好处。在决定拆分之前,一定要问自己:我的业务和团队是否真的需要微服务?是否已经准备好应对它带来的挑战?
这次踩坑经历是我三年成长中的一个重要里程碑。它让我明白,架构没有好坏,只有合适与否。深刻理解不同架构带来的trade-off,是一个后端程序员走向成熟的必经之路。