事务与消息零丢失方案
RocketMQ有一个非常强大的功能,就是它支持事务消息
。所谓事务消息
,它可以保证消息的正确投递,而且绝不丢失消息,要么全部投递成功,要么全部投递失败,绝对不存在成功一部分或失败一部分的可能。
事务Half消息机制
所谓Half消息
,其实是一种试探性的机制,也就是在正式发送消息之前,先向RocketMQ发送一个Half消息
。这个Half消息
对于其他系统来说是看不见
的,起到类似于回声定位
的作用,发送它的目的是看RocketMQ是否能够正常响应,然后再决定后续将执行什么样的操作。

当订单履约系统发送Half消息
成功之后,就可以更新自己的业务数据库了。不管更新业务数据库成功或失败,它都会向RocketMQ发送相应的Commit
或Rollback
请求,让RocketMQ保存或删除之前的Half消息
。

只有当Half消息
被成功提交之后,其他的下游系统,例如积分、优惠券、红包等系统才能收到订单支付成功的消息并进行后续处理。
- 如果
Half消息
发送成功,Half消息
也保存到了RocketMQ,而订单系统却因为网络延迟一直没有收到RocketMQ返回的响应,自己先执行了更新数据库的操作。为了解决这个缺陷,RocketMQ有一个补偿机制:它会定期去扫描Half消息
的状态,如果超过一定的时间还没有删除或提交它,就会通过一个预先定义回调接口来处理。这时候就可以根据订单数据的状态来决定是删除还是保存它了,确保整个流程不出现漏洞。

- 如果是回滚或提交
Half消息
的时候发生异常或失败,同样也会通过回到接口实现对Half消息
的处理。
本质上,上面整个的对Half消息
的处理过程,就是一种确保业务执行不出纰漏的消息投递机制。
消息零丢失方案
如果在给RocketMQ发送消息的时候,因为网络异常导致消息发送失败,是完全可以通过以同步的方式发送消息 + 反复重试
的方式,确保消息一定可以投递到RocketMQ中去的。
这里有一个问题,到底是先更新订单数据库还是先发消息到RocketMQ?
如果先执更新订单数据库再发送消息,那么伪代码就是这样的。
try {
// 执行本地事务更新订单数据库
orderService.completedOrder();
// 发送消息的到MQ
producer.sendMessage();
} catch (Exception e) {
// 如果消息发送失败,就重试3次
for (int i = 0; i < 3; i++) {
// 重试发送消息
flag = producer.sendMessage();
}
// 重试多次失败后回滚本地事务
if (!flag) {
orderService.rollback();
}
}
但问题是,如果在第3行代码orderService.completedOrder();
刚执行完时,订单系统崩溃了怎么办?此时的消息是没有发出去的,而且catch
中的代码是根本没机会执行的。如果要想发送消息,意味着整个代码要重新执行一遍。即使有幂等机制的支持,也会给系统性能造成不必要的拖累。
如果把本地事务和发送消息的代码放到一个事务中呢?
@Transactional
public void payOrder() {
try {
// 执行本地事务更新订单数据库
orderService.completedOrder();
// 更新Redis缓存
orderService.updateRedis();
// 更新Mongodb数据
orderService.updateMongodb();
// 更新Elasticsearch数据
orderService.updateElasticsearch();
// 更新其他数据
// ......
// 发送消息的到MQ
producer.sendMessage();
} catch (Exception e) {
// 如果消息发送失败,就重试3次
for (int i = 0; i < 3; i++) {
// 重试发送消息
flag = producer.sendMessage();
}
// 重试多次失败后抛出异常回滚本地事务
if (!flag) {
throw new Exception();
}
}
}
这种方式也会因为多次重试导致性能低下,而且像Redis
、Mongodb
和Elasticsearch
这类数据存储系统,是几乎没有回滚机制的,数据可能仍然不一致。
所以,这种同步发送消息 + 反复重试
的方式虽然可以满足功能要求,却达不到某些性能上和数据一致性、健壮性的要求。
因此,真正要满足消息一定可以投递到RocketMQ且性能良好的方案,最好的还是基于RocketMQ的事务消息机制。
关键点
从发送消息开始到所有业务执行完毕,RocketMQ全链路的消息零丢失方案包括这几个关键点。