RocketMq剖析及实战

问题抛出

  • RocketMq原理
  • 如何实现幂等

Mq基本介绍

使用场景

  • 应用解耦
    比如一个订单系统下单操作,需要支付系统和库存系统,物流系统配合。我们就可以用mq当中间件发送消息。单单一个系统挂了,不影响后面。系统恢复后,再次消费mq中信息即可。将同步调用改为异步。
  • 流量削峰
    出于经济考虑,短时间内有可能出现流量高峰。没必要为了那一点时间,从硬件上提升服务器之类。可以用到mq。把流量平分开。
  • 数据分发
    不用关心。谁要数据谁不要数据。mq当数据存储,谁想要就和mq建立连接。如章节更新。哪个服务想要,哪个服务就订阅那个topic。

Mq优点和缺点

优点:解耦,削峰,数据分发
缺点:

  1. 系统可用性降低,Mq挂了就完了。需要保证mq高可用。
  2. 系统复杂度提高。同步变成异步。需要保证消息不被重复消费,不丢失。
  3. 一致性问题。ABC同时处理消息,C处理失败了。就没一致性了。

各种MQ产品比较

特性ActiveMQRabbitMQRocketMQKafka
开发语言JavaerlangJavaScala
单机吞吐量万级万级10万级10万级
时效性ms级别us级别ms级别ms级以内
可用性高(主从架构)高(主从架构)非常高(分布式架构)非常高(分布式架构)

各角色介绍

  • Producer:消息的发送者。举例:发信者
  • Consumer:消息的接受者。举例:收信者
  • Broker:暂存和传输消息。举例:邮局
  • NameServer:管理Broker(如注册中心)。举例:各个邮局的管理机构
  • Topic:区分消息的种类。一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息。
  • Message Queue:相当于是Topic的分区;用于并行发送和接收消息。

集群特点

在这里插入图片描述

集群模式

在这里插入图片描述

消息发送

消息类别

  1. 同步消息
    发送完必须等消费方接受结果并回调。(不需要等待发送成功) 使用场景:消息通知,短信通知。
  2. 异步消息
    发送完不需要等待消费方接受结果。 (可以写回调函数接受结果 ) 使用场景:对性能要求比较高的。
  3. 单向消息。
    不需要关心消费方收没收到。使用场景:日志记录

消息模式

  1. 广播模式:每个消费者消费的消息都是相同的。
  2. 负载均衡模式:分散消费。 默认

顺序消息

实际场景中,基本上保持局部顺序即可。比如一个用户的下单操作。分为,创建订单,支付订单,完成订单三步。我们只需要保证这三步操作都打到一个broker中做到局部消费即可。
在这里插入图片描述

消息发送

DefaultMQProducer producer = new DefaultMQProducer("group1");
        Message message = new Message("Topic", "Tag", "Content".getBytes());
        /**
         * 消息
         * 消息队列选择器
         * 选择队列的业务标识 eq:订单id
         */
        SendResult sendResult = producer.send(message, new MessageQueueSelector() {
            /**
             *
             * @param mqs 队列id
             * @param msg 消息对象
             * @param arg 业务标识对象
             * @return
             */
            @Override
            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                //简单取模运算
                long orderId = (long) arg;
                long index = orderId % mqs.size();
                return mqs.get((int) index);
            }
        }, 1234);
        producer.shutdown();

消息接受

DefaultMQPushConsumer  consumer = new DefaultMQPushConsumer("group1");
        consumer.setNamesrvAddr("127.0.0.1");
        consumer.subscribe("Topic","");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println(new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

这样可以做到一个线程顺序消费一个队列。

延迟消费

延迟时间

延迟时间可选范围

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

生产者

 		DefaultMQProducer producer = new DefaultMQProducer("group1");
        Message message = new Message("Topic", "Tag", "Content".getBytes());
        //设置延时时间
        message.setDelayTimeLevel(2);
        SendResult sendResult = producer.send(message);
        producer.shutdown();

批量发送消息

DefaultMQProducer producer = new DefaultMQProducer("group1");
        producer.setNamesrvAddr("127.0.0.1 8888");
        List<Message> messageList = Lists.newArrayList();
        Message message = new Message("Topic", "Tag", "Content".getBytes());
        Message message1 = new Message("Topic", "Tag", "Content".getBytes());
        Message message2 = new Message("Topic", "Tag", "Content".getBytes());
        messageList.add(message);
        messageList.add(message1);
        messageList.add(message2);
        //就这里传个集合   不能大于4mb
        SendResult sendResult = producer.send(messageList);
        producer.shutdown();

过滤消息

  1. Tag过滤
  2. SQL过滤
  • Tag过滤
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
consumer.setNamesrvAddr("127.0.0.1");
//消费主题下所有消息
consumer.subscribe("Topic", "*");
//指定Tag1
consumer.subscribe("Topic", "Tag1");
//又想消费Tag1 还想消费Tag2
consumer.subscribe("Topic", "Tag1 || Tage2");
  • SQL过滤

发送的时候需要绑定属性

生产者

 		DefaultMQProducer producer = new DefaultMQProducer("group1");
        producer.setNamesrvAddr("127.0.0.1 8888");
        Message message = new Message("Topic", "Tag", "Content".getBytes());
        //绑定属性
        message.putUserProperty("id","10");
        SendResult sendResult = producer.send(message);
        producer.shutdown();

消费者

  		DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        consumer.setNamesrvAddr("127.0.0.1");
        //过滤i>5
        consumer.subscribe("Topic", MessageSelector.bySql("i>5"));

事务消息

在这里插入图片描述

生产者

TransactionMQProducer producer = new TransactionMQProducer("group");
        producer.setNamesrvAddr("127.0.0.1");
        producer.setTransactionListener(new TransactionListener() {
            /**
             * 在该方法中执行本地事务
             *
             * @param message
             * @param o
             * @return
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                String topic = message.getTopic();
                if (StrUtil.equals(topic, "tag1")) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (StrUtil.equals(topic, "tag2")) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } else {
                    return LocalTransactionState.UNKNOW;
                }
            }

            /**
             * 该方法是Mq进行事务状态回查
             *
             * @param messageExt
             * @return
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                System.out.println(messageExt.getTags());
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        for (int i = 0; i < 3; i++) {
            Message message = new Message("topic", tags[i], "Hello".getBytes());
            //不针对某一条消息进行事务控制 不做过滤
            TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, "");
            SendStatus sendStatus = transactionSendResult.getSendStatus();
            System.out.println(sendStatus);
        }

项目中应用

和spring集成后后有个RocketMQTemplate 用起来很方便
消息接受 实现RocketMQListener接口,并类名有RocketMQMessageListener在这里插入图片描述

1.用户下单购买章节操作。需要操作优惠券系统,用户系统,数据统计系统。
之前:下单操作的时候原先操作是rpc调用这三个系统,中间有一个失败了就不行。
现在:用mq后如果失败,会发送失败mq。这三个系统会监听。收到失败mq会修改回之前数据。

2.用户支付操作。给第三方平台快速响应。分修改订单状态,用户增加优惠券,购买日志
之前:rpc调用。
现在:将支付成功当成一个消息,发送前存库,发送成功删除数据库。保证消息没有丢失。另外三个系统监听那个topic。相当于把消息分发下去了。

高级功能

1.消息存储

1.1流程

在这里插入图片描述

  1. 消息生成者发送消息 MQ收到消息,
  2. 将消息进行持久化,在存储中新增一条记录
  3. 返回ACK给生产者
  4. MQ push消息给对应的消费者,然后等待消费者返回ACK
  5. 如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新push消息,重复执行4、5、6步骤
  6. МQ删除消息

1.2存储介质

  • 关系型数据库

DBApache下开源的另外一款MQ-ActiveMQ
(默认采用的KahaDB做消息存储)可选用DBC的方式来做消息持久化,通过简单的xml配置信息即可实现DBC消息存储。由于,普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其I0读写性能往往会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障

  • 文件系统

目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)
.消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题

性能对比

文件系统>关系型数据库DB

1.3RMQ消息的存储和发送

1)消息存储

磁盘如果使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/5,和顺序写的性能相差6000倍!因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序写,保证了消息存储的速度。

2)消息发送

Linux操作系统分为【用户态】和【内核态】
,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,一般分为两个步骤:

  1. read;读取本地文件内容
  2. write;将读取的内容通过网络发送出去。这两个看似简单的操作,实际进行了4次数据复制,分别是:
    1.从磁盘复制数据到内核态内存;
    2.从内核态内存复制到用户态内存;
    3.然后从用户态内存复制到网络驱动的内核态内存; 4,最后是从网络驱动的内核态内存复制到网卡中进行传输。

1.4消息存储结构

RocketMQ消息的存储是由ConsumeQueue和CommitLlog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个MessageQueue都有一个对应的ConsumeQueue文件。

在这里插入图片描述

刷盘机制

在这里插入图片描述
1)同步刷盘

在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态

2)异步刷盘

在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。"

3)配置

同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType参数设置的,这个参数被配置成SYNCFLUSH.ASYNC
FLUSH中的一个。

2.高可用机制

在这里插入图片描述
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。

Master和Slave的区别:在Broker的配置文件中,参数brokerld的值为0表明这个Broker是Master,大于0表明这个Broker是Slave,同时brokerRole参数也会说明这个Broker是Master还是Slave.

Master角色的Broker支持读和写, Slave角色的Broker仅支持读,也就是Producer只能和Master角色的Broker连接写入消息; Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。

2.1消消费高可用

在Consumer的配置文件中,并不需要设置是从Master读还是从Slave读,当Master不可用或者繁忙的时候,
Consumer会被自动切换到从Slave读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,
Consumer仍然可以从Slave读取消息,不影响Consumer程序。这就达到了消费端的高可用性。

2.2消息发送高可用

在创建Topic的时候,把Topic的多个Message
Queue创建在多个Broker组上(相同Broker名称,不同brokerld的机器组成一个Broker组)
,这样当一个Broker组的Master不可用后,其他组的Master仍然可用,
Producer仍然可以发送消息RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master,则要手动停止Slave角色的Broker,更改配置文件,用新的配置文件启动Broker.

在这里插入图片描述
在这里插入图片描述

2.3消息主从复制

如果一个Broker组有Master和Slave,消息需要从Master复制到Slave上,有同步和异步两种复制方式

1)同步复制
同步复制方式是等Master和Slave均写成功后才反馈给客户端写成功状态;[在同步复制方式下,如果Master出故障, Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量

2)异步复制
异步复制方式是只要Master写成功即可反馈给客户端写成功状态。在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写入Slave,有可能会丢失

3)配置
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNCMASTERSYNC-MASTER, SLAVE三个值中的一个。

4)终结
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是SYNC-FLUSH方式,由于频繁地触发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Save配置成ASYNCFLUSH的刷盘方式,主从之间配置成SYNCMASTER的复制方式,这样即使有一台机器出故障,仍然能保证数据不丢,是个不错的选择
在这里插入图片描述

3.负载均衡

3.1Producer负载均衡

Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
在这里插入图片描述

3.2Consumer负载均衡

1.集群模式

在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的·方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue.而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。默认的分配算法是AllocateMessageQueueAveragely,如下图:
在这里插入图片描述
还有另外一种平均的算法是AllocateMessageQueueAveragelyBycircle,也是平均分摊每一条queue,只是以环状轮流分queue的形式,如下图:
在这里插入图片描述

需要注意的是,集群模式下,
queue都是只允许分配只一个实例,这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue.通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。但是如果consumer实例的数量比message queue的总数量还多的话,多出来的consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了。所以需要控制让queue的总数量大于等于consumer的数量。

2.广播模式
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。在实现上,其中一个不同就是在consumer分配queue的时候,所有consumer都分到所有的queue.
在这里插入图片描述

4.消息重试

顺消息的重试

对于顺序消息,当消费者消费消息失败后,消息队列RocketMQ会自动不断进行消息重试(每次间隔时间为1秒)
,这时,应用会出现消息消费被阻寒的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

无序消息的重试

对于无序消息(普通、定时、延时、事务消息)
,当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

重试次数

在这里插入图片描述
配置方式

消费失败后,重试配置方式集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种) :

  • 返回Action.Reconsumelater
  • (推荐)返回Null
  • 抛出异常

死信队列

当一条消息初次消费失败,消息队列RocketMQ会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列RocketMQ不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。在消息队列RocketMQ中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message) ,存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)

死信特性

死信消息具有以下特性不会再被消费者正常消费。有效期与正常消息相同,均为3天,
3天后会被自动删除。因此,请在死信消息产生后的3天内及时处理。死信队列具有以下特性:,一个死信队列对应一个Group
ID,而不是对应单个消费者实例。,如果一个Group
ID未产生死信消息,消息队列RocketMQ不会为其创建相应的死信队列。·一个死信队列包含了对应Group
ID产生的所有死信消息,不论该消息属于哪个Topic.

查看死信信息

在这里插入图片描述
在这里插入图片描述

选择重新发送消息一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列RocketMQ控制台重新发送该消息,让消费者重新消费一次。

6消费幂等

消息队列RocketM l消费者在接收到消息以后,有必要根据业务上的唯-Key对消息做幂等处理的必要性。

6.1消费幂等的必要性

在互联网应用中,尤其在网络不稳定的情况下,消息队列RocketMQ的消息有可能会出现重复,这个重复简单可以概括为以下情况:

  • 发送时消息重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

  • 投递时消息重复

消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息

  • 负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及订阅方应用重启)

当消息队列RocketMQ的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息

处理方式

我们将消费的方式存库,每一次消费的时候校验一下此消息之前有没有消费过。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值