为什么需要事务消息?
用一个经典的场景来举个例子:在电商行业中,用户下订单后,是需要和其他服务联动来完成后续的操作的,例如:
- 订单服务:创建订单
- 库存服务:扣减库存
- 支付服务:商品付款
- 积分服务:增加积分
- 物流服务:创建物流单(待发货)

这些服务的操作都需要满足原子性,要么全部成功,要么全部失败。为了业务的高性能,我们会选择异步消息通知的方式去实现分布式事务。
方案一:先提交本地事务,再发送消息通知下游服务
如果我们在成功创建订单后提交了本地事务,然后再通过消息通知的方式去通知库存服务区扣减库存,订单服务的操作流程如下:
- 开启事务
- 创建订单
- 提交事务
- 发送消息到消息队列
这时候,如果本地事务提交成功,但是消息发送的时候,发送失败了,并且没有其他补救措施,这就会导致商品的库存没有扣减,产生了数据不一致的问题。对于这个,我们可能会想到方案二。
方案二:先发送消息,再开启本地事务并提交
流程如下:
- 发送消息到消息队列
- 开启事务
- 创建订单
- 提交事务
在发送消息成功后,才开启本地事务,如果消息发送失败,则不开启本地事务。
这个方案也有弊端:如果消息发送成功,下游服务已经消费了消息了,但是本地事务执行失败,这就不能及时地阻断分布式事务的流程,导致可能发生库存已经扣减,但是订单还没有创建的问题。那如果把消息发送包在本地事务里面呢?
方案三:在创建完订单后,在本地事务中去发送消息通知
流程如下:
- 开启事务
- 创建订单
- 发送消息到消息队列
- 提交事务
这样,如果消息发送失败了,可以回滚本地事务。
但是,如果我们在事务中对第三方服务进行的io操作,这可能会导致当前服务的资源锁定时间过长而导致性能问题,影响其他业务对同一资源的访问。
还有一个问题是,如果我们消息已经发送出去了,但是提交事务失败了,这时本地事务是可以回滚的,但是消息发出去了就不能回滚了。
所以这个方案也是不推荐使用的。
那么还有其他的方案吗?这时就需要使用事务消息的方式来进行消息投递了。
事务消息的原理
在 RocketMQ 中有一种事务消息,它能通过两阶段提交的方式来保证本地事务和消息投递操作的原子性。 事务消息的交互流程如下:

以订单服务为例:
- 订单服务在执行下订单操作之前,先发送半事务消息到 RocketMQ 服务端;
- RocketMQ 服务返回半事务消息发送成功;
- 接着订单服务开始执行本地事务,并提交本地事务;
- 如果订单服务的本地事务提交失败,则发送 Rollback 消息到 RocketMQ 服务端,服务端就不会把消息投递到下游服务;如果订单服务的本地事务提交成功,则发送 Commit 消息到 RocketMQ 服务端,服务端这时才会把消息投递到下游服务器,下游服务消费到消息就会执行对应的事务操作;
- 如果 RocketMQ 迟迟没有接收到订单服务发来的 Commit 或 Rollback 消息,则会主动回查事务的状态(订单服务需要提供对应的回查接口),订单服务则会检查本地事务的状态,然后再根据事务的状态返回 Commit 或 Rollback 消息。
通过RocketMQ事务消息的两阶段投递的方式,以及RocketMQ回查事务状态来兜底,从而保证了「本地事务+下游服务的消息投递」是原子性的,能够保证最终的一致性,并且也不会影响当前服务的性能。
事务消息的局限?
RocketMQ 事务消息本身只能保证消息的可靠投递,它可以让消息投递进行回滚,从而阻止下游服务继续执行事务,但是当下游服务消费失败时,则需要额外机制保证数据一致性。例如:
- 重试+死信队列:对消费失败的消息进行重试,超过一定重试次数后进入死信队列,处理死信队列的消息(补偿操作),并增加监控;
- 本地事务表+定时任务校对:事务消息的生产者在本地记录事务的状态到DB中,当下游服务消费消息后回调生产者通知事务的状态,并且事务消息的生产者需要定时检查自己的本地事务表,对于长时间未完成的事务进行处理。
- Saga模式:给每个子事务设置补偿操作,当下游服务消费消息失败时,发送消息通知上游服务执行事务补偿。
资料参考: RocketMQ 事务消息(阿里云官方文档)
技术写作如同代码开发,需要持续迭代。感谢您成为这篇博客的Reviewer,期待您的宝贵意见 ✍️