【RocketMQ分布式事务消息、RocketMQ分布式事务的发展流程、RocketMQ分布式事务二阶段提交解决方案、分布式案例实操学习、RocketMQ分布式事务使用场景以及注意事项】

一.知识回顾

【0.RocketMQ专栏的内容在这里哟,帮你整理好了,更多内容持续更新中】
【1.Docker安装部署RocketMQ消息中间件详细教程】
【2.RocketMQ生产者发送消息的三种方式:发送同步消息、异步消息、单向消息&案例实战&详细学习流程】
【3.RocketMQ消费者进行消费消息的二种方式:集群消费、广播消费&案例实战&详细学习流程&集群消费模、广播模式的适用场景&注意事项】
【4.RocketMQ中的顺序消息、生产者顺序生产消息、消费者顺序消费消息、顺序包括全局有序和分块有序、代码样例实战】
【5.RocketMQ中延时消息的生产与消费、批量消息的生产与消费、消息的过滤、消息的Tag过滤和SQL过滤、SQL过滤解决SQL92问题,代码样例实战】

二.RocketMQ分布式事务的发展流程

2.1 举个栗子来说明分布式事务发展的来龙去脉

业务场景:用户A转账100元给用户B,具体的步骤:
1、用户A的账户先扣除100元
2、再把用户B的账户加100元
如果在同一个数据库中进行,事务可以保证这两步操作,要么同时成功,要么同时不成功。这样就保证了转账的数据一致性。如下图所示:
在这里插入图片描述

但是在微服务架构中因为各个服务都是独立的模块,都是远程调用,没法在同一个事务中,所以就会遇到分布式事务问题。如下图所示:

在这里插入图片描述

2.2 从单体架构的事务向微服务架构事务的发展流程

在这里插入图片描述

三.RocketMQ分布式事务处理流程

3.1 通过上面的案例来学习分布式事务存在的问题

在这里插入图片描述

RocketMQ分布式事务方式:把扣款业务和加钱业务异步化,扣款成功后,发送“扣款成功消息”到RocketMQ,加钱业务订阅“扣款成功消息”,再对用户B加钱(扣款消息中包含了源账户和目标账户ID,以及钱数)
对于扣款业务来说,需规定是先扣款还是先向MQ发消息
场景一:先扣款,后向MQ发消息
先扣款再发送消息,万一发送消息失败了,那用户B就没法加钱,存在问题。
场景二:先向MQ发像消息,后扣款
扣款成功消息发送成功,但用户A扣款失败,可加钱业务订阅到了消息,用户B加了钱,同样存在问题。

3.2 RocketMQ解决分布式事务的方案(俩阶段提交)

3.2.1 分析原因,痛点所在

通过上面我们的分析知道问题所在:也就是没法保证扣款和发送消息,同时成功,或同时失败;导致数据不一致。 为了解决以上问题,RocketMq把消息分为两个阶段:半事务阶段和确认阶段。

3.2.2 俩阶段提交—半事务阶段:

半事务阶段主要发一个消息到RocketMQ中,但该消息只储存在commotlog文件当中,但ConsumeQueue中不可见,也就是消费端无法看到此消息。

3.2.3 俩阶段提交—确认阶段(commit/rollback):

确认阶段主要是把半事务消息保存到ConsumeQueue中,即让消费端可以看到此消息,也就是可以消费此消息。如果是rollback就不保存。

3.2.4 俩阶段提交的整个流程

在这里插入图片描述

整个流程如下:

  1. A在扣款之前,先发送半事务消息
  2. 发送预备消息成功后,执行本地扣款事务
  3. 扣款成功后,再发送确认消息
  4. B消息端(加钱业务)可以看到确认消息,消费此消息,进行加钱业务逻辑

确认消息注意事项:

  1. 确认消息可以为Commit消息,可以被订阅者消费;
  2. 也可以是Rollback消息,即执行本地扣款事务失败后,提交Rollback消息,即删除那个半事务消息,订阅者无法消费。

   Rollback可以解决以下问题:

  • 问题1:如果发送半事务消息失败,下面的流程不会走下去,这个是正常的。
  • 问题2:如果发送半事务消息成功,但执行本地事务失败。这个也没有问题,因为此半事务消息不会被消费端订阅到,消费端不会执行业务。(后续的Rollback事务回查也可以解决这个问题,如果本地事务没有执行成功,RocketMQ回查业务,发现没有执行成功,就会发送RollBack确认消息,把消息进行删除。)

   Rollback不可以解决的问题:

  • 问题1:如果发送半事务消息成功,并且执行本地事务成功,但发送确认消息失败了,这个就有问题了,因为用户A扣款成功了,但加钱业务没有订阅到A发送来的确认消息,就无法完成加钱的业务逻辑。这里出现了数据不一致。

解决方案:
RocketMQ解决上面问题的核心思路就是通过事务回查,也就是RocketMQ会定时遍历commitlog中的半事务消息。
对于上述的问题,发送半事务消息成功,本地扣款事务成功,但发送确认消息失败;因为RocketMQ会进行回查半事务消息,在回查后发现业务已经扣款成功了,就补发发送commit确认消息;这样加钱业务就可以订阅此消息了。
在这里插入图片描述
消费端进行消息消费的时候需要注意的点:

  1. RocketMQ不能保障消息的重复,所以在消费端一定要做幂等性处理。
  2. 如果消费端发生消费失败,同时也需要做重试,如果重试多次,消息会进入死信队列,这个时候也需要进行特殊的处理。(一般就是把A已经处理完的业务进行回退)
3.2.5 事务回查的流程?或者说事务回查找的是哪张表呢?有没有什么好的解决方案呢?

背景分析:如果本地事务执行了很多张表,难道我们需要对所有表都要进行判断是否执行成功呢?这样的操作不仅效率慢,而且都是再做同样的事情,耦合度过高。那我们该怎么解决呢?方案如下:

在这里插入图片描述

解决方案:
设计一张Transaction表,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功,Transaction中应当已经记录该TransactionId的状态为【已完成】。当RocketMQ事务回查时,只需要检查对应的TransactionId的状态是否是【已完成】就好,而不用关心具体的业务数据。

四.光说不练假把式~~~分布式事务实战学习

4.1 生产者案例代码

/**
 * A系统---消息生产者
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        //创建事务监听器,对我们的事务进行监听,需要负责执行本地事务以及一些事务的回查操作
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
        producer.setNamesrvAddr("121.5.139.205:9876");
        //创建线程池
        ExecutorService executorService = new ThreadPoolExecutor(4, 8, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        //设置生产者回查线程池
        producer.setExecutorService(executorService);
        //生产者设置监听器
        producer.setTransactionListener(transactionListener);
        //启动消息生产者
        producer.start();
        //1、半事务的发送
        try {
            Message msg = new Message("TransactionTopic", null, ("A向B系统转1314元").getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = producer.sendMessageInTransaction(msg, null);
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
            System.out.println(sendResult.getSendStatus()+"-"+df.format(new Date()));//半事务消息是否成功
        } catch (MQClientException | UnsupportedEncodingException e) {
            //TODO 如果半事务还没有提交成功,那么直接ROllback
            //LocalTransactionState.ROLLBACK_MESSAGE;
            e.printStackTrace();
        }
        //2、半事务的发送成功
        //一些长时间等待的业务,需要通过事务回查来处理
        for (int i = 0; i < 100; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

/*
* 事务监听类
*/
class TransactionListenerImpl implements TransactionListener {
    //执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        //TODO 执行本地事务 update A...
        System.out.println("update A ... where transactionId:"+msg.getTransactionId() +":"+df.format(new Date()));
        //todo 情况1:本地事务成功
        //return LocalTransactionState.COMMIT_MESSAGE;
        //TODO 情况2:本地事务失败
        //System.out.println("rollback");
        //return LocalTransactionState.ROLLBACK_MESSAGE;
        //TODO 情况3:业务复杂,还处于中间过程或者依赖其他操作的返回结果,UNKNOW
        System.out.println("业务比较复杂,没有处理完,无法确定是Success >or< Fail!");
        return LocalTransactionState.UNKNOW;
    }
    //事务回查  默认是60s,一分钟检查一次
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        //打印每次回查的时间
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        System.out.println("checkLocalTransaction:"+df.format(new Date()));// new Date()为获取当前系统时间
        //TODO 情况3.1:业务回查成功!
        System.out.println("业务回查:执行本地事务成功,确认消息");
        return LocalTransactionState.COMMIT_MESSAGE;
        //TODO 情况3.2:业务回查回滚!
        //System.out.println("业务回查:执行本地事务失败,删除消息");
        //return LocalTransactionState.ROLLBACK_MESSAGE;
        //TODO 情况3.3:业务回查还是UNKNOW!
        //System.out.println("业务比较复杂,没有处理完,无法确定是Success >or< Fail!");
        //return LocalTransactionState.UNKNOW;
    }
}


4.2 消费者案例代码

/**
 * 事务消息-消费者 B
 */
public class TranscationComuser {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TranscationComsuer");
        consumer.setNamesrvAddr("121.5.139.205:9876");
        consumer.subscribe("TransactionTopic", "*");
        consumer.setMessageModel(MessageModel.CLUSTERING);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                try {
                    //todo  开启事务
                    for(MessageExt msg : msgs) {
                        //todo 执行本地事务 update B...(幂等性)
                        System.out.println("update B ... where transactionId:"+msg.getTransactionId());
                        //todo 本地事务成功
                        System.out.println("commit:"+msg.getTransactionId());
                        System.out.println("执行本地事务成功,确认消息");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("执行本地事务失败,重试消费,尽量确保B处理成功");
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

4.3 运行结果

在这里插入图片描述

在这里插入图片描述

五.RocketMQ分布式事务使用的注意事项

  1. 事务消息不支持延时消息和批量消息。
  2. 通过Broker的配置文件可以配置事务回查的间隔时间:BrokerConfig. transactionCheckInterval。
  3. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  4. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  5. 事务性消息可能不止一次被检查或消费。
  6. 事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
  7. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  8. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

好了,到这里【RocketMQ分布式事务消息、RocketMQ分布式事务的发展流程、RocketMQ分布式事务二阶段提交解决方案、分布式案例实操学习、RocketMQ分布式事务使用场景以及注意事项】就先学习到这里,关于RocketMQ更多知识,持续学习创作中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

硕风和炜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值