上一篇文章讲述的是抛弃A,保证CP的刚性分布式事务解决方案,这一篇介绍柔性分布式事务解决方案。
柔性事务遵循BASE理论,抛弃C,保证AP,以最终一致性来代替强一致性,柔性分布式事务解决方案中MQ承担非常重要的角色。
1.本地化消息表
A B两个分布式应用,各自有一个本地化消息表。
A中的业务和消息处于同一个本地事务中,也就意味着A的事务提交之后,消息表中也存放了这条消息。存完之后A系统会把这条消息发送到MQ.B接收到消息之后会写到本地消息表,同时执行业务逻辑.B应用也一样,业务和表处于同一个事务中。这里会有一个幂等操作,如果这条消息之前已经处理过,B就会回滚事务。B业务逻辑执行成功之后会更新消息的状态,同时更新A表的消息状态。
如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止
2.基于支持事务的MQ
上述方法有个比较不好地方,就是需要各个应用在本地建一个表,和业务耦合度比较高。有没有不需要本地建表的方法呢?
市面上有一些MQ支持事务的,比如RocketMQ。
在介绍这种方法之前先解释一下RocketMQ的几个名词
prepare消息 又名Half Message,半消息,标识该消息处于"暂时不能投递"状态,不会被Comsumer所消费,待服务端收到生成者对该消息的commit或者rollback响应后,消息会被正常投递或者回滚(丢弃)消息
RMQ_SYS_TRANS_HALF_TOPIC prepare消息在被投递到Mq服务器后,会存储于Topic为RMQ_SYS_TRANS_HALF_TOPIC的消费队列中
RMQ_SYS_TRANS_OP_HALF_TOPIC 在prepare消息被commit或者rollback处理后,会存储到Topic为RMQ_SYS_TRANS_OP_HALF_TOPIC的队列中,标识prepare消息已被处理
第一阶段:生产者向MQ服务器发送事务消息(prepare消息),服务端确认后回调通知生产者执行本地事务(此时消息为Prepare消息,存储于RMQ_SYS_TRANS_HALF_TOPIC队列中,不会被消费者消费)
第二阶段:生产者执行完本地事务后(业务执行完成,同时将消息唯一标记,如transactionId与该业务执行记录同时入库,方便事务回查),根据本地事务执行结果,返回Commit/Rollback/Unknow状态码
1、服务端若收到Commit状态码,则将prepare消息变为提交(正常消息,可被消费者消费)
2、收到Rollback则对消息进行回滚(丢弃消息)
3、若状态为Unknow,则等待MQ服务端定时发起消息状态回查,超过一定重试次数或者超时,消息会被丢弃
事务状态定时回查
在第二阶段中,生产者在本地事务执行完成后,需要向MQ服务器返回响应状态码,发送状态码的过程也是通过Netty发送网络请求,假设由于网络原因发送失败怎么办?本地事务已经提交/回滚了,但是Commit/Rollback状态码却没发出去,那么MQ服务器上这条prepare消息状态岂不是无法被投递/回滚
因此,MQ服务端会定时扫描存储于RMQ_SYS_TRANS_HALF_TOPIC中的消息,若消息未被处理,则向消费发送者发起回调检查,检查消息对应本地事务执行状态。从而保证消息事务状态最终能和本地事务的状态一致。上图中的4、5、6就是MQ服务端定时回查步骤。
如果B系统消费消息之后执行业务逻辑失败怎么办,没问题,MQ会一直重试。
这个方法比较好的地方在于把消息和业务系统独立开,符合分布式应用的特点。唯一的缺点就是市面上支持事务的MQ比较少。
3.最大努力通知方案
1、系统 A 本地事务执行完之后,发送个消息到 MQ;
2、这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
3、要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。