一、引子

最近在阅读代码的时候发现了这样一个神奇的现象。我们是XX的基础服务,对外提供了很多消息。通过我们接口对XX数据的更新(插入、更新、删除)操作都会发送MQ通知下游,下游消费消息感知XX变化。

代码中比较老的逻辑是通过注解的形式,对数据库操作进行AOP处理,在数据库操作成功后,尝试发送消息,消息发送失败不做补偿处理,也不做事务回滚。

后来要求修改商品运费模板的行为也进行消息上报,后人开发过程中没有使用AOP进行传递,而是简单进行了编码,先发送变更消息,再更新数据库,数据库更新失败则更新接口返回失败,消息不做撤回处理。

这两种方案都会存在消息与实际变更不一致的问题,第一种是丢消息,第二种是假消息。

实际上,假消息的影响要比丢消息严重得多,但是无论那种都会造成消息与数据库不一致的问题。能否有一种方案可以让数据与消息保持强一致呢?这就是今天要聊的话题——事务性消息。

二、事务性消息

2.1 什么是事务性消息

如同RocketMQ官方所描述的一样:Apache RocketMQ实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。

2.2 执行顺序与各阶段异常

RocketMQ官网提供的事务性消息示意图
RocketMQ官网提供的事务性消息示意图

通过上图可以很直观地看到,事务性消息包裹在本地事务外层;消息commit失败后通过反查的形式进行补偿,最终引导至消费者消费。然后不妨仔细思考一下在不同阶段出现异常时,系统会发生什么?

异常阶段 结果
发送半消息、接收半消息 接口异常,消息被丢弃
执行本地事务异常 接口异常,消息被丢弃
提交消息事务异常 本地事务已经执行完毕,通过MQ回查事务状态进行补偿,接口视为成功
查询事务异常 重新查询进行补偿,消息幂等在消费者完成

三、更多

我们是否真的需要事务性消息

其实这个问题是没有绝对固定的答案的。

首先,如果本地事务只涉及到简单的数据库修改,完全可以通过binlog2kafka的形式。消费binlog消息,然后再广播到其他系统,避免使用较重的事务性消息,提高系统的吞吐量。

但是如果整个本地事务包含更多的内容,无法只通过单一条件完成消息的触发,事务性消息或许就是更好的选择了。

其次,我们需要考虑这个消息对整个系统而言真的是必要的吗?没有这个消息就一定会导致流程失败吗?

Wikipedia CAP示意图
Wikipedia CAP示意图

这里就不得不提到CAP原理,在存在一定通信延迟的系统中,AP和CP就只能二选一。

当消息相对于整个系统而言不是特别重要的时候,丢消息是可以接受的时候,不妨放下流程与消息的一致性,通过其他渠道查询消息是否被系统消费,再通过运营策略进行补偿。比较典型的场景有:发送验证码短信、发送营销短信。验证码发送失败后依然可以点击重试,姑且认为这次的发送动作是成功的吧。

但是,对于商家点击发货,同步更新物流侧、保障侧周知已发货,然后物流、保障开始计算物流时效以及是否晚发等场景。就必须要保证消息一定发送成功。如果消息发送失败,那么这次发货动作就应该失败。

所以,说到底,还是A与C之间的权衡。