消息发送一致性的保证
首先来看看消息发送一致性的定义:消息发送一致性是指产生消息的业务动作与消息发送的一致,就是说,如果业务操作成功了,那么由这个操作产生的消息一定要发送出去,否则就丢失消息了。另一方面,如果这个业务行为没有发生或者失败,那么就不应该把消息发送出去。
怎么做?
先来看看一段伪代码,是在某些实践中的用法,从中可以看到以下两个问题:
void doBusiness(){
//业务操作:例如写数据库,调用服务等
//发送消息:例如发邮件,发微博等等
}
问题1:业务操作在前,发送消息在后,如果业务失败了还好(当然业务自己不觉得好),如果成功了,而这时应用出了问题,那么消息就发不出去了。
问题2:如果业务成功,应用也没有挂掉,但这个时候消息系统挂了,也会导致消息发送不出去。
再来看看另外一种做法
void doBusiness(){
//发送消息
//业务操作
}
这种方式更不可靠,在业务还没有做时消息就发出去了。
如果引入分布式事务或者2PC方案,又会给系统带来额外的开销以及复杂性。
有其他办法吗?
如果能找到一种解决方案,这种方案对正常流程的影响要尽可能小,而在有问题的场景能解决问题。
针对这个问题,可以引入以下方案来解决,保证消息的最终一致性:
消息正向流程
下面来分析下每个步骤可能产生的异常情况:
1) 业务应用发消息给消息中间件。如果这一步失败了,无论是网络原因、消息中间件原因还是业务应用自身的原因,业务操作都不会做,消息也没有被存储到中间件中,业务操作和消息的状态是一致的,没有问题;
2) 消息中间件入库失败。结果有两种可能,一个是消息中间件失效,那么业务应用收不到返回消息;一个是插入消息失败,并且有能力返回给业务应用,这两种情况消息存储中都没有消息,业务操作也不会继续执行,状态一致,没有问题;
3) 业务应用接收消息中间件返回结果异常。异常原因可能是网络,消息中间件问题,也可能是业务应用自身的原因。如果业务应用自身没有问题,那么业务应用并不知道消息在消息中间件的处理结果,就会按照发送失败来处理,如果这个时候消息已经入库成功,就会造成不一致。如果是业务应用有问题,而消息已经入库成功的话,也会造成不一致,如果消息中间件未处理成功,这个时候还是一致的;
4) 业务应用进行业务操作,这一步不会有问题;
5) 业务应用发送业务操作结果给消息中间件。如果这一步出现异常,那么消息中间件将不知道如何处理已经存储的消息,可能会造成不一致;
6) 消息中间件更新消息状态。如果这一步出现异常,消息中间件也不知道如何处理已经存储的消息,可能会造成不一致。
总之,对于可能出现不一致的情况,消息中间件中存储的消息状态为待处理,需要消息中间件主动询问业务应用来进行补偿性的操作,可以说是发送消息的一个反向的过程,如下图所示:
消息反向流程
同样,这个流程也会出现很多异常。不过这四步的流程就是为了确认业务处理的结果,真正的操作只是根据业务处理结果来更改消息的状态,如果失败就失败了,只需要定时重复这个反向流程就可以了。
以上,发消息的正向流程与反向流程合起来就是解决业务操作与发送消息一致性的方案。在大多数情况下,反向流程是不需要工作的,现在来看看正向流程是否带来了额外的负担:
传统方式 |
解决一致性的方案 |
1、 业务操作; 2、 发消息给中间件; 3、 消息入库; 4、 中间件返回结果 |
1、 发消息给中间件; 2、 消息入库; 3、 中间件返回结果; 4、 业务操作; 5、 发送业务结果给中间件; 6、 更新消息状态 |
可以看到解决一致性的方案只是增加了一次网络操作和一次更新存储的消息状态的操作,整体上带来的额外开销并不大。而且这种方案可以封装为一个固化的逻辑,然后把业务操作包装成一个对象传递进来即可。
参考材料:《大型网站系统与JAVA中间件实践》