MQ高可用相关设置(2)

前言

MQ作用:消峰,异步,解耦, 数据收集【收集日志】、大数据处理等。

使用MQ的好处

  • 提高系统响应速度【异步】
  • 提高系统稳定性【消峰】
  • 排序保证先进先出FIFO

下表是对Kafka与RabbitMQ功能的总结性对比及补充说明。

功能项Kafka(1.1.0版本)RabbitMQ(3.6.10版本)
优先级队列不支持支持。建议优先级大小设置在0-10之间。
延迟队列不支持支持
死信队列不支持支持
重试队列不支持不支持。RabbitMQ中可以参考延迟队列实现一个重试队列,二次封装比较简单。如果要在Kafka中实现重试队列,首先得实现延迟队列的功能,相对比较复杂。
消费模式拉模式推模式+拉模式
广播消费支持。Kafka对于广播消费的支持相对而言更加正统。支持,但力度较Kafka弱。
消息回溯支持。Kafka支持按照offset和timestamp两种维度进行消息回溯。不支持。RabbitMQ中消息一旦被确认消费就会被标记删除。
消息堆积支持支持。一般情况下,内存堆积达到特定阈值时会影响其性能,但这不是绝对的。如果考虑到吞吐这因素,Kafka的堆积效率比RabbitMQ总体上要高很多。
持久化支持支持
消息追踪不支持。消息追踪可以通过外部系统来支持,但是支持粒度没有内置的细腻。支持。RabbitMQ中可以采用Firehose或者rabbitmq_tracing插件实现。不过开启rabbitmq_tracing插件件会大幅影响性能,不建议生产环境开启,反倒是可以使用Firehose与外部链路系统结合提供高细腻度的消息追踪支持。
消息过滤客户端级别的支持不支持。但是二次封装一下也非常简单。
多租户不支持支持
多协议支持只支持定义协议,目前几个主流版本间存在兼容性问题。RabbitMQ本身就是AMQP协议的实现,同时支持MQTT、STOMP等协议。
跨语言支持采用Scala和Java编写,支持多种语言的客户端。采用Erlang编写,支持多种语言的客户端。
流量控制支持client和user级别,通过主动设置可将流控作用于生产者或消费者。RabbitMQ的流控基于Credit-Based算法,是内部被动触发的保护机制,作用于生产者层面。
消息顺序性支持单分区(partition)级别的顺序性。顺序性的条件比较苛刻,需要单线程发送、单线程消费并且不采用延迟队列、优先级队列等一些高级功能,从某种意义上来说不算支持顺序性。
安全机制(TLS/SSL、SASL)身份认证和(读写)权限控制与Kafka相似
幂等性支持单个生产者单分区单会话的幂等性。不支持
事务性消息支持支持

四种MQ对比介绍

MQ如何保证消息不丢失

消息丢失的三种情况

  1. 消息在传入服务过程中丢失
  2. MQ收到消息,暂存内存中,还没消费,自己挂掉了,内存中的数据搞丢
  3. 消费者消费到了这个消息,但还没来得及处理,就挂了,MQ以为消息已经被处理

也就是生产者丢失消息、消息列表丢失消息、消费者丢失消息;

RabbitMQ

一、生产者

开启RabbitMQ事务
生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。

// 开启事务
channel.txSelect
try {
      // 这里发送消息
} catch (Exception e) {
      channel.txRollback
 
// 这里再次重发这条消息
 
}
 
// 提交事务
channel.txCommit

设置Confirm模式:

同步确认:

//开启发布确认
channel.confirmSelect();
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if(flag){
    System.out.println("消息发送成功");
}

异步确认 :

服务端 :

消息持久化,必须满足以下三个条件,缺一不可。

  • Exchange 设置持久化
  • Queue 设置持久化
  • Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息

发送消息时设置delivery_mode属性为2,使消息被持久化保存到磁盘,即使RabbitMQ服务器宕机也能保证消息不丢失。同时,创建队列时设置durable属性为True,以确保队列也被持久化保存。

// 声明队列,并将队列设置为持久化
channel.queueDeclare(QUEUE\_NAME, true, false, false, null);
String message = "Hello, RabbitMQ!";
// 发送消息时将消息设置为持久化
channel.basicPublish("", QUEUE\_NAME, MessageProperties.PERSISTENT\_TEXT\_PLAIN, message.getBytes());

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
        .deliveryMode(2)
        .build();
channel.basicPublish("", "myQueue", properties, "Hello, RabbitMQ".getBytes());

设置备份交换机:

Map<String, Object> arguments = new HashMap<>();
arguments.put("alternate-exchange", "myAlternateExchange");
channel.exchangeDeclare("myExchange", BuiltinExchangeType.DIRECT, true, false, arguments);
channel.exchangeDeclare("myAlternateExchange", BuiltinExchangeType.FANOUT, true, false, null);

二 :服务端设置集群镜像模式

  • 单节点模式: 最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
  • 普通模式: 消息只会存在与当前节点中,并不会同步到其他节点,当前节点宕机,有影响的业务会瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
  • 镜像模式: 消息会同步到其他节点上,可以设置同步的节点个数,但吞吐量会下降。属于RabbitMQ的HA方案

消费方开启消息确认机制 :

// 开启消息确认机制
channel.basicConsume(QUEUE\_NAME, false, new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String message = new String(body, "UTF-8");
        System.out.println("Received message: " + message);
        // 手动发送消息确认
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
});

手动确认 :

codechannel.basicConsume("myQueue", false, (consumerTag, delivery) -> {
    // 处理消息
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});

RocketMQ

一、生产者提供SYNC的发送消息方式,等待broker处理结果。

RocketMQ生产者提供了3种发送消息方式,分别是:

同步发送:Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应发送结果。

Message msg = new Message("TopicTest",
        "TagA","OrderID188",
        "Hello world".getBytes(RemotingHelper.DEFAULT\_CHARSET));
//同步传递消息,消息会发给集群中的⼀个Broker节点。
SendResult sendResult = producer.send(msg);

异步发送:Producer 首先构建一个向 broker 发送消息的任务,把该任务提交给线程池,等执行完该任务时,回调用户自定义的回调函数,执行处理结果。

Message msg = new Message("TopicTest","TagA","OrderID188",
        "Hello world".getBytes(RemotingHelper.DEFAULT\_CHARSET));
        
producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        countDownLatch.countDown();
        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
    }

    @Override
    public void onException(Throwable e) {
        countDownLatch.countDown();
        System.out.printf("%-10d Exception %s %n", index, e);
        e.printStackTrace();
    }
});

Oneway发送:Oneway 方式只负责发送请求,不等待应答,Producer只负责把请求发出去,而不处理响应结果。

Message msg = new Message("TopicTest" /\* Topic \*/,
        "TagA" /\* Tag \*/,
        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT\_CHARSET) /\* Message body \*/
);
//Call send message to deliver message to one of brokers.
//核⼼:发送消息。没有返回值,发完消息就不管了,不知道有没有发送消息成功
producer.sendOneway(msg);

生产者使用重试机制 :
当发送消息发生异常或超时的时候重新循环发送。默认重试3次,重试次数可以通过setRetryTimesWhenSendFailed指定。

// 同步发送消息,如果5秒内没有发送成功,则重试3次
DefaultMQProducer producer = new DefaultMQProducer("DefaultProducer");
producer.setRetryTimesWhenSendFailed(3);
producer.send(msg, 5000L);

SendResult定义说明(来自RocketMQ官方)

  • SEND_OK
    消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。
  • FLUSH_DISK_TIMEOUT
    消息发送成功但是服务器刷盘超时。此时消息已经进入服务器队列(内存),只有服务器宕机,消息才会丢失。消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态——刷盘超时。
  • FLUSH_SLAVE_TIMEOUT
    消息发送成功,但是服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave服务器超时。
  • SLAVE_NOT_AVAILABLE
    消息发送成功,但是此时Slave不可用。如果Broker服务器的角色是同步Master,即- SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态——无Slave服务器可用。

我们在调用producer.send方法时,不指定回调方法,则默认采用同步发送消息的方式,这也是丢失几率最小的一种发送方式(但是效率比较低)。

二、Borker 方面 : 设置成同步刷盘及同步复制;开启集群模式,集群同步;

1)异步刷盘

默认。消息写入 CommitLog 时,并不会直接写入磁盘,而是先写入 PageCache 缓存后返回成功,然后用后台线程异步把消息刷入磁盘。异步刷盘提高了消息吞吐量,但是可能会有消息丢失的情况,比如断点导致机器停机,PageCache 中没来得及刷盘的消息就会丢失。

2)同步刷盘

消息写入内存后,立刻请求刷盘线程进行刷盘,如果消息未在约定的时间内(默认 5 s)刷盘成功,就返回 FLUSH_DISK_TIMEOUT,Producer 收到这个响应后,可以进行重试。同步刷盘策略保证了消息的可靠性,同时降低了吞吐量,增加了延迟。要开启同步刷盘,需要增加下面配置:

flushDiskType=SYNC\_FLUSH

同步复制后,消息复制流程如下:

  • slave 初始化后,跟 master 建立连接并向 master 发送自己的 offset;
  • master 收到 slave 发送的 offset 后,将 offset 后面的消息批量发送给 slave;
  • slave 把收到的消息写入 commitLog 文件,并给 master 发送新的 offset;
  • master 收到新的 offset 后,如果 offset >= producer 发送消息后的 offset,给 Producer 返回 SEND_OK。

三、消费者

RocketMQ消费失败后的消费重试机制

手动提交消息偏移量

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
        ConsumeConcurrentlyContext context) {
        System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
        return ConsumeConcurrentlyStatus.CONSUME\_SUCCESS;
    }
});

public enum ConsumeConcurrentlyStatus {
    //业务方消费成功
    CONSUME\_SUCCESS,
    //业务方消费失败,之后进行重新尝试消费
    RECONSUME\_LATER;
}

RECONSUME_LATER “%RETRY%+ConsumeGroupName”—重试队列的主题

消息不丢失

RocketMQ怎么保证消息不丢失

Kafka

解决方案:
1、生产者调用异步回调消息。伪代码如下: producer.send(msg,callback);
2、生产者增加消息确认机制,设置生产者参数:acks = all。指定partition的leader副本接收到消息,等待所有的follower副本都同步到了消息之后,才认为本次生产者发送消息成功了;
3、生产者设置重试次数。比如:retries>=3,增加重试次数以保证消息的不丢失;
4、定义本地消息日志表,定时任务扫描这个表自动补偿,做好监控告警。
5、后台提供一个补偿消息的工具,可以手工补偿。
6、消费者Rebalance的时候


生产者设置同步发送 :

 // 异步发送 默认
 kafkaProducer.send(new ProducerRecord<>("first","kafka" + i));
 // 同步发送
 RecordMetadata first = kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i)).get(); 

生产者设置发送ack :

// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 设置 acks
properties.put(ProducerConfig.ACKS\_CONFIG, "all");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
            kafkaProducer.send(new ProducerRecord<>("first","atguigu " + i));

生产者设置重试次数。比如:retries>=3,增加重试次数以保证消息的不丢失;

// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 设置 acks
properties.put(ProducerConfig.ACKS\_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES\_CONFIG, 3);
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

三、消费者:

通过在Consumer端设置“enable.auto.commit”属性为false后,
在代码中手动调用KafkaConsumer实例的commitSync()方法提交,

这里指的是同步阻塞commit消费的偏移量,等待Broker端的返回响应,需要注意Broker端在对commit请求做出响应之前,消费端会处于阻塞状态,从而限制消息的处理性能和整体吞吐量以确保消息能够正常被消费。

如果在消费过程中,消费端突然Crash,这时候消费偏移量没有commit,等正常恢复后依然还会处理刚刚未commit的消息。

生产者acks参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。这个参数对消息丢失的可能性有重要影响。

1)ack=0,生产者在成功写入悄息之前不会等待任何来自服务器的响应。

2)ack=1,只要集群的首领副本收到消息,生产者就会收到一个来自服务器的成功响应。

3)ack=all,只有当所有同步副本全部收到消息时,生产者才会收到一个来自 服务器的成功响应。

MQ如何保证顺序消息

严格顺序消费的注意事项

  • 生产者不能异步发送,异步发送在发送失败的情况下,就没办法保证消息顺序
    比如你连续发了1,2,3。 过了一会,返回结果1失败,2, 3成功。你把1再重新发送1遍,这个时候顺序就乱掉了。
  • 应用中应确保业务中添加事务锁,防止并发处理同一对象。
    比如修改业务员的手机号,操作员A和操作员B同时修改业务员张三的手机号,如两人的填入手机号相同无影响,如不同,操作员A输入正确,操作员B输入错误,可能造成消费顺序乱掉,手机号修改错误。
  • 对于消费端,不能并行消费,生产者顺序发送,消费端必须顺序消费。

在实现顺序消息前提下,必须保证消息不丢失。

RabbitMQ

消息单个消费者单线程消费。

RocketMQ

RocketMQ 提供了两种顺序消息模式:全局顺序消息和分区顺序消息。

  • 全局顺序消息:适用于性能要求不高的场景,所有消息按照严格的先入先出(FIFO)的顺序来发布和消费。全局顺序消息实际上是一种特殊的分区顺序消息,即Topic中只有一个分区,因此全局顺序和分区顺序的实现原理相同。由于分区顺序消息有多个分区,所以分区顺序消息比全局顺序消息的并发度和性能更高。
  • 分区顺序消息:适用于性能要求高的场景,所有消息根据Sharding Key进行区块分区,同一个分区内的消息按照严格的先进先出(FIFO)原则进行发布和消费。同一分区内的消息保证顺序,不同分区之间的消息顺序不做要求。对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序来发布和消费。分区顺序消息比全局顺序消息的并发度和性能更高。

通常情况下,RocketMQ 默认为每个 Topic 分配4个队列。队列数量是在broker的配置文件中设置的,具体来说是在broker.conf文件中通过defaultTopicQueueNums属性来设置。
如果你想为特定的Topic设置队列数量,可以使用管理工具或者控制台来完成。

mqadmin updateTopic -b brokerIP:10911 -n nameserverIP:9876 -t YourTopicName --queue-nums 8

-b 参数后面是一个broker的IP和端口,-n 参数后面是nameserver的IP和端口,-t 参数后面是你要更新的Topic名称,–queue-nums 参数后面是你想要设置的队列数量。

请注意,更改队列数量是一个影响较大的操作,因为它需要重新分配broker上的主题资源。在生产环境中,通常需要在维护窗口内进行,并确保有足够的备份和监控系统以应对可能出现的问题。


发送方使用MessageQueueSelector选择队列 :

Message msg = new Message("OrderTopicTest", "order\_"+orderId,
        "KEY" + orderId,("order\_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT\_CHARSET));
//消息队列的选择器
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
    //第一个参数:所有的消息,第二个参数:发送的消息,第三个参数:根据什么发送,这里面传的是orderId
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        Integer id = (Integer) arg;
        int index = id % mqs.size();
        return mqs.get(index);
    }
    //同一个订单id可以放到同一个队列里面去
}, orderId);

消费方使用MessageListenerOrderly选择队列 :

consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        //自动提交
        context.setAutoCommit(true);
        for(MessageExt msg:msgs){
            System.out.println("收到消息内容 "+new String(msg.getBody()));
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

Kafka

Kafka是分布式多partition的,它会将一个topic中的消息尽可能均匀的分发到每个partition上。那么问题就来了,这样怎么保证同一个topic消息的顺序呢?

kafka可以通过partitionKey,将某类消息写入同一个partition,一个partition只能对应一个消费线程,以保证数据有序。
也就是说生产者在写消息的时候,可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。

先后两条消息发送时,前一条消息发送失败,后一条消息发送成功,然后失败的消息重试后发送成功,造成乱序。为了解决重试机制引起的消息乱序为实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。

对于每个PID,该Producer发送消息的每个<Topic, Partition>都对应一个单调递增的Sequence Number。
同样,Broker端也会为每个<PID, Topic, Partition>维护一个序号,并且每Commit一条消息时将其对应序号递增。
对于接收的每条消息,如果其序号比Broker维护的序号大一,则Broker会接受它,否则将其丢弃
如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息
如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息
发送失败后会重试,这样可以保证每个消息都被发送到broker

消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。

指定发送partition的分区:

//没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值
// 依次指定 key 值为 a,b,f ,数据 key 的 hash 值与 3 个分区求余,
//kafkaProducer.send(new ProducerRecord<>("first","a","atguigu " + i), new Callback() {}
kafkaProducer.send(new ProducerRecord("first", 0, "", "atguigu" + i)
        , new Callback() {
            @Override
            public void onCompletion(RecordMetadata metadata, Exception e) {
                if (e == null) {
                    System.out.println(" 主题: " +
                            metadata.topic() + "->" + "分区:" + metadata.partition()
                    );
                } else {
                    e.printStackTrace();
                }
            }
        });

当部分Broker宕机,会触发Reblance,导致同一Topic下的分区数量有变化,此时生产端有可能会把顺序消息发送到不同的分区,这时会发生短暂消息顺序不一致的现象,如果生产端指定分区发送,则该分区所在的Broker宕机后将直接不可用;

广播消息&集群消息

RabbitMQ

广播消息:创建一个fanout类型的交换机,并绑定多个队列,就是广播消息

集群消息:direct和topic类型的交换机就是集群模式

RocketMQ

RocketMQ主要提供了两种消费模式:集群消费以及广播消费。我们只需要在定义消费者的时候通过setMessageModel(MessageModel.XXX)方法就可以指定是集群还是广播式消费,默认是集群消费模式,即每个Consumer Group中的Consumer均摊所有的消息。

生产者:

public class MQProducer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {
        // 创建DefaultMQProducer类并设定生产者名称
        DefaultMQProducer mqProducer = new DefaultMQProducer("producer-group-test");
        // 设置NameServer地址,如果是集群的话,使用分号;分隔开
        mqProducer.setNamesrvAddr("10.0.91.71:9876");
        // 消息最大长度 默认4M
        mqProducer.setMaxMessageSize(4096);
        // 发送消息超时时间,默认3000
        mqProducer.setSendMsgTimeout(3000);
        // 发送消息失败重试次数,默认2
        mqProducer.setRetryTimesWhenSendAsyncFailed(2);
        // 启动消息生产者
        mqProducer.start();
        // 循环十次,发送十条消息
        for (int i = 1; i <= 10; i++) {
            String msg = "hello, 这是第" + i + "条同步消息";
            // 创建消息,并指定Topic(主题),Tag(标签)和消息内容
            Message message = new Message("CLUSTERING\_TOPIC", "", msg.getBytes(RemotingHelper.DEFAULT\_CHARSET));
            // 发送同步消息到一个Broker,可以通过sendResult返回消息是否成功送达
            SendResult sendResult = mqProducer.send(message);
            System.out.println(sendResult);
        }
        // 如果不再发送消息,关闭Producer实例
        mqProducer.shutdown();
    }
}

消费者:

public class MQConsumerB {
    public static void main(String[] args) throws MQClientException {
        // 创建DefaultMQPushConsumer类并设定消费者名称
        DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("consumer-group-test");
        // 设置NameServer地址,如果是集群的话,使用分号;分隔开
        mqPushConsumer.setNamesrvAddr("10.0.91.71:9876");
        // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
        // 如果不是第一次启动,那么按照上次消费的位置继续消费
        mqPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME\_FROM\_FIRST\_OFFSET);
        // 设置消费模型,集群还是广播,默认为集群
        mqPushConsumer.setMessageModel(MessageModel.CLUSTERING);
        // 消费者最小线程量
        mqPushConsumer.setConsumeThreadMin(5);
        // 消费者最大线程量
        mqPushConsumer.setConsumeThreadMax(10);
        // 设置一次消费消息的条数,默认是1
        mqPushConsumer.setConsumeMessageBatchMaxSize(1);
        // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息,如果订阅该主题下的所有tag,则使用\*
        mqPushConsumer.subscribe("CLUSTERING\_TOPIC", "\*");
        // 注册回调实现类来处理从broker拉取回来的消息
        mqPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            // 监听类实现MessageListenerConcurrently接口即可,重写consumeMessage方法接收数据
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt messageExt = msgList.get(0);
                String body = new String(messageExt.getBody(), StandardCharsets.UTF\_8);
                System.out.println("消费者接收到消息: " + messageExt.toString() + "---消息内容为:" + body);
                // 标记该消息已经被成功消费
                return ConsumeConcurrentlyStatus.CONSUME\_SUCCESS;
            }
        });
        // 启动消费者实例
        mqPushConsumer.start();
    }
}

setMessageModel(MessageModel.CLUSTERING);//设置集群消息
setMessageModel(MessageModel.BROADCASTING); //设置广播消息

1、在Rocket集群消费模式下,(订阅)同一个主题(Topic)下的消息,对于不同的消费者组是一种“广播形式”,即每个消费者组的都会消费消息。

2、在Rocket集群消费模式下,(订阅)同一个主题(Topic)下的消息,对于相同的消费者组的消费者而言是一种集群模式,即同一个消费者组内的所有消费者均分消息并消费。

集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
⼴播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

案例博客

Kafka

实现与RocketMQ一致

针对Kafka同⼀条消息只能被同⼀个消费组下的某⼀个消费者消费的特性,要实现多播只要保证这些消费者属于不同的消费组即可。我们再增加⼀个消费者,该消费者属于testGroup-2消费组,结果两个客户端都能收到消息。

消息重试

重试带来的副作用
不停的重试看起来很美好,但也是有副作用的,主要包括两方面:消息重复,服务端压力增大

  • 远程调用的不确定性,因请求超时触发消息发送重试流程,此时客户端无法感知服务端的处理结果;客户端进行的消息发送重试可能会导致消费方重复消费,应该按照用户ID、业务主键等信息幂等处理消息。
  • 较多的重试次数也会增大服务端的处理压力。

RabbitMQ

消费者默认是自动提交,如果消费时出现了RuntimException,会导致消息直接重新入队,再次投递(进入队首),进入死循环,继而导致后面的消息被阻塞。消费失败的时候,消息队列会一直重复的发送消息,导致程序死循环!

消息阻塞带来的后果是:后边的消息无法被消费;RabbitMQ服务端继续接收消息,占内存和磁盘越来越多。

重试机制有2种情况

  • 消息是自动确认时,如果抛出了异常导致多次重试都失败,消息被自动确认,消息就丢失了
  • 消息是手动确认时,如果抛出了异常导致多次重试都失败,消息没被确认,也无法nack,就一直是unacked状态,导致消息积压。

重试配置 :

消费者设置手动确认提交,当不确认消息,并且设置重新放回队列的时候,这时候消息会无线的重试!

// RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费;
// 第三个参数true,表示这个消息会重新进入队列
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false);

在手动确认的条件下,使用如上配置消息在重试三次之后,就会放入死信队列,事实上手动提交的时候,basicNack的最后一个参数requeue = true时(设置重新入队列),消息会被无限次的放入消费队列重新消费,直至回送ACK。但是当requeue = false 的时候,此时消息达到重试指定次数就会后立马进入到死信队列。通过给队列绑定死信交换机DLX(basic.reject / basic.nack且requeue=false消息进入绑定的DLX),保存并处理异常或多次重试的信息。

Spring-RabbitMQ中重试实现:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 消费方手动应答
      direct:
        retry:
          enabled: true    # 开启消费重试机制
          max-attempts: 3  # 最大重试机制,默认为3
          initial-interval: 1000  # 重试间隔,单位毫秒,默认1000

注意:max-attempts 指的是尝试次数,就是说最开始消费的那一次也是计算在内的,那么 max-attempts: 3 便是重试两次,另外一次是正常消费,消息重试了3次,之后会抛出ListenerExecutionFailedException的异常。后面附带着Retry Policy Exhausted,提示我们重试次数已经用尽了。消息重试次数用尽后,消息就会被抛弃。

@Component
@Slf4j
public class MessageReceiver {
 
    @Autowired
    private MsgService msgService;
 
    @Autowired
    private MsgLogService msgLogService;
	
	@RabbitListener(queues = RabbitConstants.TEST\_FANOUT\_QUEUE\_A)
    public void testMsgReceiver(Message message, Channel channel) throws IOException, InterruptedException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        Msg msg = JSON.parseObject(message.getBody(), Msg.class);
        try {
            int a = 1 / 0;
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            Map<String, Object> headers = message.getMessageProperties().getHeaders();
            //重试次数
            Integer retryCount;
            String mapKey = "retry-count";
            if (!headers.containsKey(mapKey)) {
                retryCount = 0;
            } else {
                retryCount = (Integer) headers.get(mapKey);
            }
            if (retryCount++ < RETRY) {
                log.info("已经重试 " + retryCount + " 次");
                headers.put("retry-count", retryCount);
                //当消息回滚到消息队列时,这条消息不会回到队列尾部,而是仍是在队列头部。
                //这时消费者会立马又接收到这条消息进行处理,接着抛出异常,进行 回滚,如此反复进行
                //而比较理想的方式是,出现异常时,消息到达消息队列尾部,这样既保证消息不回丢失,又保证了正常业务的进行。
                //因此我们采取的解决方案是,将消息进行应答。
                //这时消息队列会删除该消息,同时我们再次发送该消息 到消息队列,这时就实现了错误消息进行消息队列尾部的方案
                //1.应答
                channel.basicAck(deliveryTag, false);
                //2.重新发送到MQ中
                AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder().contentType("application/json").headers(headers).build();
                channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
                        message.getMessageProperties().getReceivedRoutingKey(), basicProperties,
                        message.getBody());
            } else {
 
                log.info("现在重试次数为:" + retryCount);
                /\*\*
 \* 重要的操作 存盘
 \* 手动ack
 \* channel.basicAck(deliveryTag,false);
 \* 通知人工处理
 \* log.error("重试三次异常,快来人工处理");
 \*/
				//消息存盘
                MsgLog msgLog = new MsgLog();
                msgLog.setMsgId(msg.getMsgId());
                msgLog.setMsg(new String(message.getBody(),"utf-8"));
                msgLog.setExchange(message.getMessageProperties().getReceivedExchange());
                msgLog.setRoutingKey(message.getMessageProperties().getReceivedRoutingKey());
                msgLog.setTryCount(retryCount);
                msgLog.setStatus(MsgLogStatusEnum.FAIL.getStatus());
                msgLogService.save(msgLog);
 
                /\*\*
 \* 不重要的操作放入 死信队列
 \* 消息异常处理:消费出现异常后,延时几秒,然后从新入队列消费,直到达到ttl超时时间,再转到死信,证明这个信息有问题需要人工干预
 \*/
                //休眠2s 延迟写入队列,触发转入死信队列
                //Thread.sleep(2000);
                //channel.basicNack(deliveryTag, false, true);
            }
        }
    }
    
	@RabbitListener(queues = RabbitConstants.TEST\_DDL\_QUEUE\_A)
    public void deadTestReceiver(Message message, Channel channel) throws IOException {
        log.info("消息将放入死信队列", new String(message.getBody(), "UTF-8"));
        String str = new String(message.getBody());
        //转换为消息实体
        Msg msg = JSON.parseObject(str, Msg.class);
        log.info("收到的消息为{}", msg);
    }
}

处理unacked消息: consumer前面加了一个缓冲容器,容器能容纳最大的消息数量就是PrefetchCount。如果容器没有满RabbitMQ就会将消息投递到容器内,如果满了就不投递了。当consumer对消息进行ack以后就会将此消息移除,从而放入新的消息。

重试机制

Spring-Rabbitmq配置重试
Spring-Rabbitmq重试机制

RockeMq

Producer端重试:
消息发送时,默认情况下会进行2次重试。如果重试次数达到上限,消息将不会被再次发送。

重试配置 :

DefaultMQProducer producer = new DefaultMQProducer("ordermessage");
producer.setRetryTimesWhenSendFailed(x)同步,参数默认值是2
producer.setRetryTimesWhenSendAsyncFailed()异步,参数默认值是2

Consumer端重试:

  • 当 Consumer 端遇到异常时,消息通常会重复重试16次,重试的时间间隔包括10秒、30秒、1分钟、2分钟、3分钟等。
  • 如果 Consumer 端没有返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS 或 ConsumeConcurrentlyStatus.RECONSUME_LATER,且消息没有消费成功,MQ 会无限制地发送给消费端,直到达到最大重试次数。
  • 在集群模式下,如果消费业务逻辑代码返回Action.ReconsumerLater、NULL 或抛出异常,消息最多会重试16次。如果重试16次后消息仍然失败,则会被丢弃。
  • 消息队列 RocketMQ 默认允许每条消息最多重试16次,每次重试的间隔时间根据配置的间隔而变化。如果消息在16次重试后仍然失败,则不再投递该消息。理论上,如果消息持续失败,最长可能需要4小时46分钟内完成这16次重试。

Consumer 消费失败,这里有 3 种情况,消费者重试示例 :

public class MessageListenerImpl implements MessageListener {
	@Override
	public Action consume(Message message, ConsumeContext context) {
		//处理消息
		doConsumeMessage(message);
		//方式1:返回 Action.ReconsumeLater,消息将重试
		return Action.ReconsumeLater;
		//方式2:返回 null,消息将重试
		return null;
		//方式3:直接抛出异常, 消息将重试
		throw new RuntimeException("Consumer Message exceotion");
	}
}

如果希望消费失败后不重试,可以直接返回Action.CommitMessage

若达到最大重试次数后消息还没有成功被消费,消息将不会被再次发送,则消息将被投递至死信队列。

消息重试只针对集群消费模式生效;广播消费模式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。在老版本的RocketMQ中,一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。但是在4.7.1版本中,每次重试MessageId都会重建。

通过consumer.setMaxReconsumeTimes(20);将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2小时。

Properties properties = new Properties();
// 配置对应 Group ID的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes, "20");
Consumer consumer =ONSFactory.createConsumer(properties);

或者:

最大重试次数:消息消费失败后,可被重复投递的最大次数。

consumer.setMaxReconsumeTimes(10);

重试间隔:消息消费失败后再次被投递给Consumer消费的间隔时间,只在顺序消费中起作用。

consumer.setSuspendCurrentQueueTimeMillis(5000);

顺序消费和并发消费的重试机制并不相同,顺序消费消费失败后会先在客户端本地重试直到最大重试次数,这样可以避免消费失败的消息被跳过,消费下一条消息而打乱顺序消费的顺序,而并发消费消费失败后会将消费失败的消息重新投递回服务端(在一个特色的队列中保存),再等待服务端重新投递回来,在这期间会正常消费队列后面的消息。

并发消费失败后并不是投递回原Topic,而是投递到一个特殊Topic,其命名为%RETRY%$ConsumerGroupName,$ConsumerGroupName为消费组的名称,集群模式下并发消费每一个ConsumerGroup会对应一个特殊Topic,并会默认订阅该Topic

消费类型重试间隔最大重试次数
顺序消费间隔时间可通过自定义设置,SuspendCurrentQueueTimeMillis最大重试次数可通过自定义参数MaxReconsumeTimes取值进行配置。该参数取值无最大限制。若未设置参数值,默认最大重试次数为Integer.MAX
并发消费间隔时间根据重试次数阶梯变化,取值范围:1秒~2小时。不支持自定义指定时间配置最大重试次数可通过自定义参数MaxReconsumeTimes取值进行配置。默认值为16次,该参数取值无最大限制,建议使用默认值

(1)重试队列:如果Consumer端因为各种类型异常导致本次消费失败,为防止该消息丢失而需要将其重新回发给Broker端保存,保存这种因为异常无法正常消费而回发给MQ的消息队列称之为重试队列。
考虑到异常恢复需要一些时间,RocketMQ会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。
RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。
考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。

(2)死信队列:由于有些原因导致Consumer端长时间的无法正常消费从Broker端Pull过来的业务消息,为了确保消息不会被无故的丢弃,那么超过配置的“最大重试消费次数”后就会移入到这个死信队列中。在RocketMQ中,SubscriptionGroupConfig配置常量默认地设置了两个参数,一个是retryQueueNums为1(重试队列数量为1个),另外一个是retryMaxTimes为16(最大重试消费的次数为16次)。Broker端通过校验判断,如果超过了最大重试消费次数则会将消息移至这里所说的死信队列。这里,RocketMQ会为每个消费组都设置一个Topic命名为“%DLQ%+consumerGroup"的死信队列。一般在实际应用中,移入至死信队列的消息,需要人工干预处理;

重试队列&死信队列

官方文档

中文示例文档

重试配置

Kafka

生产者重试:

生产者设置重试次数。比如:retries>=3,增加重试次数以保证消息的不丢失;如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1,否则在重试此失败消息的时候,其他的消息可能发送成功了。

// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 设置 acks
properties.put(ProducerConfig.ACKS\_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES\_CONFIG, 3);
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

kafka消费者重试需要自己实现:

kafka消费者手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。

  • commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。
  • commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了。

两者的相同点是,都会将本次提交的一批数据最高的偏移量提交;不同点是,同步提交阻塞当前线程,一直到提交成
功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。

kafka消费者重试后选择操作:

  • kafka消费者重试会导致的问题:问题是若是这条消息(通过目前的代码)可能永远不能消费成功,导致消费者不会继续处理后续的任何问题,导致消费者阻塞;
  • 跳过,跳过这条没有消费的消息;这个其实是不合理的,可能会造成数据不一致性;

例如设置为同步提交并try…catch

// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE\_AUTO\_COMMIT\_CONFIG, false);
//3. 创建 kafka 消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
try {
    consumer.commitSync();// 处理成功,同步提交 offset ;会阻塞当前线程
} catch (Exception e) {
	// 处理失败,不提交offset,消息会重试
    e.printStackTrace();
    // 判断当前重试次数,超过一定次数放入指定重试队列
}finally {
    consumer.close();
}
// 异步提交 offset
//consumer.commitAsync();

消费者重试解决方案:

需要建立一个专门用于重试的topic(retry topic);当消费者没有正确消费这条消息的时候,则将这条消息转发(发布)到重试主题(retry topic)上,然后提交消息的偏移量,以便继续处理下一个消息;
注意:这个时候,这个没有正确消费的消息,其实对于这个消费者来说,也算是消费完成了,因为也正常提交了偏移量,只不过是业务没有正确处理,而且这个消息被发布到另一个topic中了(retry topic);顺序消息慎用

之后再创建一个重试消费者,用于订阅这个重试主题,只不过这个重试消费者,跟之前那个消费者处理相同的业务,两个逻辑是一样的;
如果这个重试消费者,也无法小得这条消息,那就把这个消息发布到另一个重试主题上,并提交该消息的偏移量;
一直循环,递归发送到不同的重试队列,最后,当创建了很多重试消费者的时候,在最终重试消费者无法处理某条消息后,把该消息发布到一个死信队列。

优缺点:
优点:可以重试,可能在重试中,就正常消费了;
缺点:可能一直重试,都不会正常消费,一直到死信队列中;可能我们就是消费的是一个错误的消息,比如说缺字段,或者数据库中根本就没有这个业务的id;或者消息中包含特殊字段,导致无法消费;这种是消息本身的错误,导致无法消费,除非你去解决消息,不然是永远不会成功的;

重试示例原文


在Spring-Kafka中,消费失败的重试次数可以通过配置来实现。默认情况下,当使用Spring-Kafka时,如果Consumer消费失败,会尝试重新消费最多10次,直到达到配置的重试次数。

Spring-Kafka3.0 版本默认失败重试次数为10次,准确讲应该是1次正常调用+9次重试,这个在这个类可以看到org.springframework.kafka.listener.SeekUtils

在Spring Boot的application.properties或application.yml文件中添加以下配置:

spring.kafka.consumer.retries: 10

这将把重试次数设置为10次。

@KafkaListener(topics = "your\_topic", retryTemplate = @RetryTemplate(maxRetries = 10))  
public void consumeMessage(String message) {  
    // 处理消息的逻辑 
}

要实现失败重试10次,可以考虑以下方案:

使用Kafka的自动提交模式,并设置auto.commit.interval.ms为适当的值,以便在每次消费消息后自动提交偏移量。
在代码中手动控制消费流程,每次消费消息后手动提交偏移量。
使用Kafka的消费者API提供的commitSync方法手动提交偏移量,并捕获可能抛出的异常,以便在失败时进行重试。
在代码中设置重试机制,例如使用循环语句实现重试10次的功能。
需要注意的是,在实现失败重试时,需要确保重试不会导致消息被重复消费或产生死循环等问题。因此,建议在重试时设置适当的间隔时间、限制重试次数或在重试前先检查消息的状态等措施。

Kafka中实现重试的主要方式是使用生产者-消费者模型。

生产者负责将消息发送到Kafka,如果发送失败,生产者会根据错误类型判断是否可以重试。如果可以重试,它将重新发送消息到Kafka。

消费者从Kafka中读取消息并处理,如果处理失败,消费者可以选择将消息放回队列(即进行重试)。消费者可以选择将消息放回队列中的某一个位置(例如队尾、队首或其他位置),以便在重试时能够按照不同的策略处理失败的消息。

值得注意的是,如果消费者重试时仍然失败,可能需要采取其他措施,例如将消息发送到另一个Kafka主题(topic)中进行处理,或者将消息写入到其他存储系统中等等。

Kafka重试使用示例场景

Spring-Kafka消费端消费重试和死信队列

Spring-Kafka 3.0 消费者消费失败处理方案

kafka自定义消息重试

所有MQ消息重试注意 :
虽然增加重试次数可以提高消息的可靠性,但过度的重试可能会导致消息处理的延迟和资源的浪费。因此,需要根据实际情况和业务需求进行权衡和调整。

回退队列

延时消息

RabbitMQ

死信和延迟使用示例

Rabbitmq 插件实现延迟队列

如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

官网上下载,下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins rabbitmq-plugins enable rabbitmq_delayed_message_exchang

RocketMQ

定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,仅支持内置预设值的延迟时间间隔的延迟消息;预设值的延迟时间间隔。
发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。

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

定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

Message msg = new Message("TopicTest","TagA" ,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT\_CHARSET) /\* Message body \*/
);
//messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);

Kafka

Kafka没有直接的延迟消息设置
Kafka延迟发送的解决思路:利用Redis的ZSet集合,实现Redis缓存队列

生产者在调用延迟发送方法时,消息并不会立刻被投递到Topic中,转而发送到延迟队列

将当前时间戳与延迟时间进行相加,将结果作为ZSet的score进行设置。

除此之外,延迟队列有包含线程池、分布式锁。每5s循环一次,对比当前时间戳与ZSet的score。拉取缓存队列中到期的消息,将消息重新组装,投递到Topic并进行消费。

Kafka延迟消息实现代码

死信队列

RabbitMQ

死信的来源

  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false(不再重新入队)

需要给交换机设置绑定死信队列,并为死信队列设置专门的消费者。

Channel channel = RabbitMqUtils.getChannel();

//声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL\_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD\_EXCHANGE,BuiltinExchangeType.DIRECT);

//声明死信队列
String deadQueue = "dead-queue";
//死信队列绑定死信交换机与 routingkey
channel.queueBind(deadQueue,DEAD\_EXCHANGE,"lisi");
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD\_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
channel.queueDeclare(queuelame, true, false, false,agruments);

使用示例

死信和延迟使用示例

RocketMQ

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

在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

RocketMQ死信队列具有如下特征:

  • 死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的
  • 死信存储有效期与正常消息相同,均为3天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。(commitLog文件的过期时间)
  • 死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup,即每个消费者组都有一个死信队列
  • 如果一个消费者组未产生死信消息,则不会为其创建对应的死信队列

死信队列:

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
  • 死信队列是一个特殊的Topic,名称为%DLQ%consumerGroup

默认创建出来的死信队列,他里面的消息是无法读取的,在控制台和消费者中都无法读取。这是因为这些默认的死信队列,他们的权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写)。需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

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

使用示例伪代码:

// 设置死信队列的名称
String dlqTopic = "YourDeadLetterQueueTopic";
 
// 创建消息生产者
DefaultMQProducer producer = new DefaultMQProducer("YourProducerGroup");
producer.setNamesrvAddr("YourNameServerAddress");
producer.start();
 
// 创建消息,并设置死信队列
Message msg = new Message("YourTopic", "YourTag", "YourMessageBody".getBytes(RemotingHelper.DEFAULT\_CHARSET));
msg.setDelayTimeLevel(3); // 设置延时级别,可选
msg.setWaitStoreMsgOK(false); // 不等待消息存储确认,可选
msg.setReconsumeTimes(3); // 设置重试次数,超过后将进入死信队列
msg.setDeadLetterTopic(dlqTopic); // 设置死信队列的topic
 
// 发送消息
producer.send(msg);
 
producer.shutdown();

消费者 :

// 创建消费者来处理死信
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("YourConsumerGroup");
consumer.setNamesrvAddr("YourNameServerAddress");
consumer.subscribe(dlqTopic, "\*"); // 订阅死信队列
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 处理死信消息
    for (MessageExt msg : msgs) {
        // 你的死信处理逻辑
        System.out.println("Dead message: " + new String(msg.getBody()));
    }
    return ConsumeConcurrentlyStatus.CONSUME\_SUCCESS;
});
 
consumer.start();

RocketMQ死信队列

Kafka

kafka没有死信队列,需要自己在重试基础上实现。

详情看上面的重试队列

消息过期时间

RabbitMQ

消息在队列中的生存时间一旦超过设置的TTL值时,就会变成死信,消费者将无法再收到该消息
如果不设置 TTL ,则表示此消息不会过期
如果TTL设置为 0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃

设置方式:

  • 通过创建队列时设置属性x-message-ttl(单位毫秒)设置,队列中所有消息都有相同的过期时间
  • 对消息本身进行单独设置,每条消息的TTL可以不同
  • 若两种方式一起使用,则消息的TTL以两者之间较小的那个数值为准

在定义队列时(调用channel.queueDeclare()时),在arguments中添加x-message-ttl参数,单位是毫秒:

 // 创建Queue
 Map<String , Object> arguments = new HashMap<>();
 arguments.put("x-message-ttl" , 10000);
 channel.queueDeclare(queueName , true , false , false , arguments);

在发送消息时(调用channel.basicPublish()时),在properties中设置expiration属性,单位也是毫秒:

// 4 发送消息
for (int i = 0; i < 5; i++) {

    AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
            .deliveryMode(2)
            .contentEncoding("UTF-8")
            .expiration("10000")
            .build();

    String msg = "RabbitMQ: TTL message" + i;
    channel.basicPublish(exchange , routingKey , properties , msg.getBytes());
}

示例代码

RocketMQ

API学习过程中并没有过期消息这以说法,以下为网上博客中的整理,是否可行待测试

RocketMQ支持消息设置过期时间,过期的消息会被自动删除。消息过期时间的处理机制主要有以下几点:

  • 消息发送时可以设置消息的过期时间,单位为毫秒。如果不设置,默认为0,即永不过期。
  • RocketMQ服务器会启动一个定时任务,每隔一定时间检测一次消息过期时间。
  • 消息存储在commitlog文件中,每个消息都有对应的物理偏移量。RocketMQ会记录每个topic-queue对应的最大偏移量值。
  • 检测消息过期时,会从最小的偏移量开始查找,直到找到第一个未过期的消息。之间的所有过期消息都会被标记为过期。
  • 过期消息在消费时会被过滤掉,不会被消费者消费到。生产者也无法再发送该消息。
  • 过期消息会在定期进行消息清理时被删除。
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
 
public class MessageExpirationExample {
    public static void main(String[] args) {
        // 创建一个Message实例
        Message msg = new Message("YourTopic", "YourTag", "YourMessageBody".getBytes(RemotingHelper.DEFAULT\_CHARSET));
 
        // 设置消息过期时间,例如30秒
        long timeDelay = 30 \* 1000;
        long now = System.currentTimeMillis();
        long deliverTime = now + timeDelay;
 
        // 设置消息的延迟级别,这里用于设置消息过期时间
        msg.setSysFlag(msg.getSysFlag() | MessageSysFlag.DELAY);
 
        // 设置消息的存活时间,即过期时间
        msg.setDelayTimeLevel(String.valueOf(timeDelay));
 
        // 发送消息
        // producer.send(msg); // 此处应该替换为你的生产者发送代码
    }
}

// 发送消息,设置过期时间为3秒
Message msg = new Message("TopicTest", "TagA", "OrderID001", "Hello".getBytes());
msg.setDelayTimeLevel(3);   // 3秒过期

SendResult sendResult = producer.send(msg);  

// 3秒后,消息变为过期状态,不可消费
PullResult pullResult = pullConsumer.pull(pullRequest);  
List<MessageExt> msgList = pullResult.getMsgFoundList();  

Kafka

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

费时会被过滤掉,不会被消费者消费到。生产者也无法再发送该消息。

  • 过期消息会在定期进行消息清理时被删除。
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
 
public class MessageExpirationExample {
    public static void main(String[] args) {
        // 创建一个Message实例
        Message msg = new Message("YourTopic", "YourTag", "YourMessageBody".getBytes(RemotingHelper.DEFAULT\_CHARSET));
 
        // 设置消息过期时间,例如30秒
        long timeDelay = 30 \* 1000;
        long now = System.currentTimeMillis();
        long deliverTime = now + timeDelay;
 
        // 设置消息的延迟级别,这里用于设置消息过期时间
        msg.setSysFlag(msg.getSysFlag() | MessageSysFlag.DELAY);
 
        // 设置消息的存活时间,即过期时间
        msg.setDelayTimeLevel(String.valueOf(timeDelay));
 
        // 发送消息
        // producer.send(msg); // 此处应该替换为你的生产者发送代码
    }
}

// 发送消息,设置过期时间为3秒
Message msg = new Message("TopicTest", "TagA", "OrderID001", "Hello".getBytes());
msg.setDelayTimeLevel(3);   // 3秒过期

SendResult sendResult = producer.send(msg);  

// 3秒后,消息变为过期状态,不可消费
PullResult pullResult = pullConsumer.pull(pullRequest);  
List<MessageExt> msgList = pullResult.getMsgFoundList();  

Kafka

[外链图片转存中…(img-egPa0j0m-1714415106500)]
[外链图片转存中…(img-kG7PdsXU-1714415106501)]
[外链图片转存中…(img-lvrXMEjJ-1714415106501)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值