利用rocketMQ解决分布式事务
在rocketMQ中生产者有三种角色 NormalProducer(普通)、OrderProducer(顺序)、TransactionProducer(事务)
根据名字大概可以看出各个代表着什么作用,我们这里用 TransactionProducer(事务)来解决问题。
public class TestTransactionProducer { public static void main(String[] args){ //事务回查监听器 TransactionCheckListenerImpl checkListener = new TransactionCheckListenerImpl(); //事务消息生产者 TransactionMQProducer producer = new TransactionMQProducer("transactionProducerGroup"); //MQ服务器地址 producer.setNamesrvAddr("192.168.56.105:9876;192.168.106:9876"); //注册事务回查监听 producer.setTransactionCheckListener(checkListener); //本地事务执行器 TransactionExecuterimpl executerimpl = null; try { //启动生产者 producer.start(); executerimpl = new TransactionExecuterimpl(); Message msg1 = new Message("TransactionTopic", "tag", "KEY1", "hello RocketMQ 1".getBytes()); Message msg2 = new Message("TransactionTopic", "tag", "KEY2", "hello RocketMQ 2".getBytes()); SendResult sendResult = producer.sendMessageInTransaction(msg1, executerimpl, null); System.out.println(new Date() + "msg1"+sendResult); sendResult = producer.sendMessageInTransaction(msg1, executerimpl, null); System.out.println(new Date() + "msg2"+sendResult); } catch (MQClientException e) { e.printStackTrace(); } producer.shutdown(); } }
生产者这里用到是:TransactionMQProducer。
这里涉及到2个角色:本地事务执行器(代码中的TransactionExecuterImpl)、服务器回查客户端Listener(代码中的TransactionCheckListener)。
如果事务消息发送到MQ上后,会回调 本地事务执行器;但是此时事务消息是prepare状态,对消费者还不可见,需要 本地事务执行器 返回RMQ一个确认消息。
/** 执行本地事务 */ public class TransactionExecuterimpl implements LocalTransactionExecuter{ @Override public LocalTransactionState executeLocalTransactionBranch(final Message message, final Object o) { try{ //DB操作 应该带上事务 service -> dao //如果数据操作失败 需要回滚 同事返回RocketMQ一个失败消息 意味着 消费者无法消费到这条失败的消息 //如果成功 就要返回一个rocketMQ成功的消息,意味着消费者将读取到这条消息 //o就是attachment //测试代码 if(new Random().nextInt(3) == 2){ int a = 1 / 0; } System.out.println(new Date()+"===> 本地事务执行成功,发送确认消息"); }catch (Exception e){ System.out.println(new Date()+"===> 本地事务执行失败!!!"); return LocalTransactionState.ROLLBACK_MESSAGE; } return LocalTransactionState.COMMIT_MESSAGE; } }
事务消息是否对消费者可见,完全由事务返回给RMQ的状态码决定(状态码的本质也是一条消息)。
/** * 未决事务,服务器端回查客户端 */ public class TransactionCheckListenerImpl implements TransactionCheckListener { @Override public LocalTransactionState checkLocalTransactionState(MessageExt messageExt) { System.out.println("服务器端回查事务消息: "+messageExt.toString()); //由于RocketMQ迟迟没有收到消息的确认消息,因此主动询问这条prepare消息,是否正常? //可以查询数据库看这条数据是否已经处理 return LocalTransactionState.COMMIT_MESSAGE; } }
/** * @Date: Created in 2018/2/11 15:37 */ public class TestConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup"); consumer.setNamesrvAddr("192.168.56.105:9876;192.168.56.106:9876"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //消费普通消息 // consumer.subscribe("TopicTest","*"); //消费事务消息 consumer.subscribe("TransactionTopic","*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt ext:msgs) { try { System.out.println(new Date() + new String(ext.getBody(),"UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.println("Consumer Start............"); } }
注意到本地事务执行失败的消息,RMQ并没有check listener?这是为什么呢?因为RMQ在3.0.8的时候还是支持check listener回查机制的,但是到了3.2.6的时候将事务回查机制“阉割”了!
那么3.0.8的时候,RMQ是怎么做事务回查的呢?看一看源码,你会知道,其实事务消息开始是prepare状态,然后RMQ会将其持久化到MySQL当中,然后如果收到确认消息,就删除掉这条prepare消息,如果迟迟收不到确认消息,那么RMQ会定时的扫描prepare消息,发送给produce group进行回查确认!
到这里,问题来了,要知道3.2.6版本,没有回查机制了,会存在问题么?
当然会存在问题!假设,我们发送一条转账事务消息给RMQ,成功后回调本地事务,DB减操作成功,刚准备给RMQ一个确认消息,此时突然断电,或者网络抖动,使得这条确认消息没有发送出去。此时RMQ中的那条转账事务消息,始终处于prepare状态,消费者读取不到,但是却已经完成一方的账户资金变动!!!
1 账号服务在扣款的时候宕机了,这时候可能扣款成功,也可能扣款失败;
2 由于网络稳定性无法保证,通知扣积分服务可能失败,但是扣款成功了
3 扣款成功,并且通知成功,但是增加积分的时候失败了。
实际上,rocketmq的事务消息解决的是问题1和问题2这种场景,也就是解决本地事务执行与消息发送的原子性问题。即解决Producer执行业务逻辑成功之后投递消息可能失败的场景。
而对于问题3这种场景,rocketmq提供了消费失败重试的机制。但是如果消费重试依然失败怎么办?rocketmq本身并没有提供解决这种问题的办法,例如如果加积分失败了,则需要回滚事务,实际上增加了业务复杂度,而官方给予的建议是:人工解决。RocketMQ目前暂时没有解决这个问题的原因是:在设计实现消息系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题。
事务回查机制
由于开源版本的rocketMQ3.0.6之后的版本被阉割了事务会回查机制,所以这部分的实现需要自己来实现。
梳理一下上图的流程:
正常的流程:A银行产生一条转账消息发往MQ(操作t1、t2表),MQ接收到的消息此时对B银行不可见,当A银行的本地事务提交后,再向MQ发送一条确认事务提交的消息,此时MQ接收到的消息对B银行可见,B银行来消费这条消息,完成B银行的转账操作(操作t3、t5表)。
异常的流程:如果A银行在第二阶段发送确认消息的时候没有发送成功,导致B银行不能消费到消息,这时候就需要用到t5和t2表来实现回查。t5表保存的转账日志肯定都是A银行已经操作成功的,我们需要将t5表一段时间内的数据发送给A银行来跟t2表做一个对账业务,发送的可以使两边共有的id这样的字段(目的是为了找出这一段时间内A银行确认消息发送失败的数据,然后再次向MQ发送确认消息).这一段时间怎么来确定呢,t4这时候派上用场了,B银行定时扫描t5表的定时任务每次启动的时候,取出存在t4表的time字段的时间命名为oldTime,然后将当前的系统时间更新到t4表的time,然后在t5表中取出大于oldTime时间的数据发送给A系统,既然取出数据是根据time判断的,那么表t2、t5肯定得有一个updateTime字段在操作数据的时候维护进去。
如何解决消息重复投递的问题
增加转账消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。
原文链接: https://blog.csdn.net/wisdomhealth/article/details/87934594