如何利用rocketMQ解决分布式事务

在rocketMQ中生产者有三种角色 NormalProducer(普通)、OrderProducer(顺序)、TransactionProducer(事务) 
根据名字大概可以看出各个代表着什么作用,我们这里用 TransactionProducer(事务)来解决问题。

先举个列子来说明下我们解决方案的设计方式吧:最经典的莫过于银行转账了,网上到处都有,时序图如下

 è¿éåå¾çæè¿°
我们的解决方案与这个大致没什么区别。 
下面贴一下测试代码:

/**
 * @Date: Created in  2018/2/12 15:55
    执行本地事务
 */
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;
    }
}

 

/**
 * @Date: Created in  2018/2/12 15:48
 * 未决事务,服务器端回查客户端
 */
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/12 15:24
 * 测试本地事务
 */
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();
    }
}

 

/**
 * @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............");
    }
}


3.0.6之前的版本这样写就可以了,但是之后的版本被关于事务回查这个借口被阉割了,不会在进行事务回查操作。没有回查机制的话如上面那个时序图所示,我们在第五步向MQ发送消息如果失败的话,会造成A银行扣款成功而B银行收款未成功的数据不一致的情况,所以,关于事务回查这块的需要由我们自己来设计实现一下,保证数据的一致性。

事务回查机制
由于开源版本的rocketMQ3.0.6之后的版本被阉割了事务会回查机制,所以这部分的实现需要自己来实现。

 4943997-da19e5c908cb838a.png | center | 830x338
梳理一下上图的流程: 
正常的流程: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字段在操作数据的时候维护进去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值