首先我们看看,在分布式场景下,一个下单业务的执行流程
由于订单、购物车、商品分别是三个不同的服务,而每个服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个服务都会执行自己的本地事务:
- 交易服务:下单事务
- 商品服务: 扣减商品库存
- 优惠券服务: 扣减用户优惠券
整个业务中,各个本地事务是有关联的。因此每个服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败,如果有分支事务执行失败,则应该通过回滚数据的方式,来保证各个服务之间数据的一致性
基于本地消息表实现分布式事务
本地消息表的核心在于,通过消息中间件的 消费重试,与 业务补偿 (通过一系列反向的操作来恢复数据)来保证事务的最终一致性
大致执行流程如下
业务执行流程
- 执行本地业务,并持久化消息到消息表A (这两个操作应当在一个事务中,原子性执行)
- 由后台线程轮询消息表A ,投递执行消息到消息中间件,如果投递成功,则更新消息表中消息为 [已投递] (先持久化到消息表,再由后台线程投递到消息中间件,是为了保证消息一定能投递到消息中间件中)
- 下游服务接受到消息后,先持久化到消息表B ,然后执行本地业务,如果执行失败则重新尝试消费,直到执行成功或者达到重试最大限度
业务回滚流程
- 如果业务始终无法执行成功,则会更新消息表B中消息为 [待回滚]
- 由后台线程轮询消息表B状态为 [待回滚] 的消息,投递回滚消息到消息中间件,最终被上游消费,上游通过业务补偿实现回滚
- 如果回滚消息重试最大限度仍不能被成功消费,则会进入死信队列,此时需要人工进行数据修复
简单理解,上游通过消息中间件调用下游服务,通过消息中间件的消费重试机制最大限度地尝试执行业务。如果始终无法执行成功,则下游通过消息中间件通知上游回滚,由上游的业务补偿逻辑实现回滚。通过将消息持久化的方式来避免过程中消息的丢失,保证消息的投递与本地事务执行是原子的。
注意事项:
- 消费者需要注意接口的幂等性,避免重复消费
通过事务消息优化
在上述的本地消息表方案中,为了保证生产端的消息发送与本地事务执行的原子性,生产者需要额外创建消息表,还要由后台线程对本地消息表进行轮询并投递消息到消息中间件,业务负担较大。采用消息中间件 (如RocketMQ) 的事务消息,可以对该过程进行优化。
- 生产者向消息中间件发送半(half)消息 (该消息对消费者不可见),如果发送成功,消息中间件响应ACK
- 在消息发送者确认消息发送成功提交后,开始执行本地事务
- 如果本地事务执行成功,发送commit消息到中间件,消息中间件将半事务消息标记为可投递,并投递给消费者
- 如果本地事务执行失败,发送rollback消息到中间件,消息中间件将回滚事务,不会将半事务消息投递给消费者
- 为了避免生产者应用发生断网、宕机、重启等特殊情况,无法二次确认消息,消息中间件将定期对消息生产者发起消息回查