消息队列RocketMQ入门实践--关键特性(三)

系列文章目录

消息队列RocketMQ入门实践(一)
消息队列RocketMQ入门实践(二)


文章目录

  • 系列文章目录
  • 前言
  • 一、顺序消息
    • 1.1 顺序消息的原理
    • 1.2 代码示例
    • 1.3 顺序消息缺陷
  • 二、事务消息
    • 2.1 回顾什么是事务
    • 2.2 分布式事务
    • 2.3 实现原理
    • 2.4 执行流程
    • 2.5 代码示例:
  • 总结


前言

嗨,大家好,我是希留。

经过前面两篇文章的学习,相信大家对RocketMQ已经有了一个基本的了解了,这篇文章就来说一说RocketMQ的几个关键特性,废话不多说,咱开始吧。

项目源码:Gitee地址


一、顺序消息

在有的业务中,consumer在消费消息时,是需要按照生产者发送消息的顺序进行消费的,比如在电商系统中,订单的消息,会有创建订单、订单支付、订单完成,如果消息的顺序发生改变,那么这样的消息就没有意义了。

1.1 顺序消息的原理

消息队列RocketMQ入门实践--关键特性(三)_第1张图片
要保证消息的顺序消费,有三个关键点:

  • (1)消息顺序发送
  • (2)消息顺序存储
  • (3)消息顺序消费

第一点,消息顺序发送,多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性

第二点,消息顺序存储,mq的topic下会存在多个queue,要保证消息的顺序存储,同一个业务编号的消息需要被发送到一个queue中。对应到mq中,需要使用MessageQueueSelector来选择要发送的queue,即对业务编号进行hash,然后根据队列数量对hash值取余,将消息发送到一个queue中

第三点,消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费

1.2 代码示例

生产者代码示例如下:

public class OrderProducer {

    public static void main(String[] args) throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("xiliu_producer_group");
        producer.setNamesrvAddr("42.194.222.32:9876");
        producer.start();

        for (int i = 0; i<100; i++) {
            String msgStr = "order -->" + i;
            // 模拟生产订单id
            int orderId = i % 10;

            Message message = new Message("broker-a","order_msg",msgStr.getBytes(RemotingHelper.DEFAULT_CHARSET));

            SendResult sendResult = producer.send(message,(mqs, msg, arg) -> {
                    Integer id = (Integer) arg;
                    int index = id % mqs.size();
                    return mqs.get(index);
                    }, orderId);
            System.out.print(sendResult);
        }
        producer.shutdown();
    }
}

消费者代码示例如下:

public class OrderConsumer {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("xiliu_consumer_group");
        consumer.setNamesrvAddr("42.194.222.32:9876");
        // 订阅topic,接收此Topic下的order_msg的消息
        consumer.subscribe("broker-a", "order_msg");
        // 使用顺序消息监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println("queueId:" + msg.getQueueId() + ",orderId:" + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
    }
}

运行结果如下,消息顺序消费,符合预期。
消息队列RocketMQ入门实践--关键特性(三)_第2张图片

1.3 顺序消息缺陷

  • 发送顺序消息无法利用集群 FailOver 特性
  • 消费顺序消息的并行度依赖于队列数量
  • 队列热点问题,个别队列由于哈希不均导致消息过多,消费速度跟不上,产生消息堆积问题
  • 遇到消息失败的消息,无法跳过,当前队列消费暂停

因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

二、事务消息

RocketMQ有一个比较关键的特性是,它支持事务消息。事务想必知道关系型数据的库的朋友应该比较了解,不了解的朋友也没有关系,咱往下继续。

2.1 回顾什么是事务

聊什么是事务,最经典的例子就是转账操作,用户A转账给用户B100元的过程如下:

  • 用户A发起转账请求,用户A账户减去100元
  • 用户B的账户增加100元

如果,用户A账户减去100元后,出现了故障(如网络故障),那么需要将该操作回滚,用户A账户增加100元。这就是事务。

2.2 分布式事务

随着项目越来越复杂,越来越服务化,就会导致系统间的事务问题,这个就是分布式事务问题。
分布式事务分类有这几种:

  • 基于单个JVM,数据库分库分表了(跨多个数据库)。
  • 基于多JVM,服务拆分了(不跨数据库)。
  • 基于多JVM,服务拆分了 并且数据库分库分表了。

解决分布式事务问题的方案有很多,使用消息实现只是其中的一种。

2.3 实现原理

RocketMQ实现分布式事务消息是采用的两阶段提交的补偿型方案,即:发送半消息(Half Message)和消息回查(Message Status Check)
消息队列RocketMQ入门实践--关键特性(三)_第3张图片

Half(Prepare) Message

指的是暂不能投递的消息,发送方已经将消息成功发送到了 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半消息。

Message Status Check

由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ 服务端通过扫描发现某条消息长期处于“半消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该过程即消息回查。

2.4 执行流程

消息队列RocketMQ入门实践--关键特性(三)_第4张图片

  • (1)发送方向 MQ 服务端发送消息。
  • (2)MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
  • (3) 发送方开始执行本地事务逻辑。
  • (4) 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
  • (5) 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后,MQ Server 将对该消息发起消息回查。
  • (6)发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • (7)发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤4对半消息进行操作。

2.5 代码示例:

下面我们来看一个简单的例子:

生产者

public class TransactionProducer {

    public static void main(String[] args) throws Exception{
        TransactionMQProducer producer = new TransactionMQProducer("xiliu_producer_group");
        producer.setNamesrvAddr("42.194.222.32:9876");
        // 设置超时时间
        producer.setSendMsgTimeout(8000);
        // 设置事物监听器
        producer.setTransactionListener(new TransactionListenerImpl());
        producer.start();

        // 发消息
        Message message = new Message("broker-a","transaction_msg","用户A给用户B转账500元".getBytes(RemotingHelper.DEFAULT_CHARSET));
        SendResult sendResult = producer.sendMessageInTransaction(message,null);

        System.out.printf("%s%n", sendResult);
        producer.shutdown();
    }
}

事物监听器实现类TransactionListenerImpl

public class TransactionListenerImpl implements TransactionListener{

    private static Map<String, LocalTransactionState> STATE_MAP = new HashMap<>();
    /**
     * 执行具体的业务逻辑
     **/
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        try{
            // 模拟调用服务
            System.out.println("用户A账户减500元");
            Thread.sleep(500);

            // 模拟出现异常,返回回滚状态
            System.out.println(1/0);

            System.out.println("用户B账户加500元");
            Thread.sleep(500);

            STATE_MAP.put(message.getTransactionId(),LocalTransactionState.COMMIT_MESSAGE);
            // 二次提交确认
            return LocalTransactionState.COMMIT_MESSAGE;
        }catch (Exception e){
            e.printStackTrace();
        }
        STATE_MAP.put(message.getTransactionId(),LocalTransactionState.ROLLBACK_MESSAGE);
        // 回滚
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }

    /**
     * 消息回查
     **/
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        return STATE_MAP.get(messageExt.getTransactionId());
    }
}

消费者

public class TransactionConsumer {

    public static void main(String[] args) throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("xiliu_consumer_group");
        consumer.setNamesrvAddr("42.194.222.32:9876");
        // 订阅事物消息tag
        consumer.subscribe("broker-a","transaction_msg");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt msg : list) {

                    try {
                        System.out.println(new String(msg.getBody(), "UTF-8"));
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

经过测试,返回commit状态时,消费者能够接收到消息,返回rollback状态时,消费者接收不到消息。
消息队列RocketMQ入门实践--关键特性(三)_第5张图片
在这里插入图片描述

总结

感谢大家的阅读,以上就是今天要讲的内容,本文介绍了RocketMQ的关键特性里面的顺序消息和事物消息的实现原理及代码示例,而RocketMQ还有很多的特性,我们下篇文章在来说一说,敬请期待。

若觉得本文对你有帮助的话,帮忙点赞评论关注,支持一波哟~

你可能感兴趣的:(Java,SpringBoot,消息中间件,java,消息队列,rocketmq)