MQ高可用相关设置(3)

            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 msgList = pullResult.getMsgFoundList();


### Kafka


Kafka 和 RabbitMQ 相同,Kafka的消息并没有TTL这一概念,因此想要实现消息的过期功能,需要作额外的处理


这里提供一种实现方案:将消息的 TTL 的设定值以键值对的形式保存在消息的 header 字段中,在消费者端配置拦截器,消费者在消费消息时判断此条消息是否超时


[Kafka TTL实现示例]( )


## 消息去重/幂等性


* 生产时消息重复  
 由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。  
 生产者中如果消息未被确认,或确认失败,我们可以使用定时任务+(redis/db)来进行消息重试
* 消费时消息重复  
 消费者消费成功后,再给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。  
 由于重复消息是由于网络原因造成的,因此不可避免重复消息。但是我们需要保证消息的幂等性。


需要从生产者和消费端同时保证  
 生产者生产消息判断是否已经发送过。  
 消费端保证消息幂等性,即多次处理相同消息的效果与处理一次相同。


### RabbitMQ


RabbitMQ 中消息重复消费的问题可以通过以下几种方式解决:


使用消息去重:在生产者发送消息时,为每条消息生成一个唯一标识符,并将其存储到数据库或缓存中。消费者在接收到消息后,先查询该标识符是否存在,如果存在则说明该消息已被处理过,直接跳过;否则进行业务处理,并将该标识符存储到数据库或缓存中。


使用幂等性操作:即使同一条消息被消费多次,也不会对业务造成影响。例如,在更新操作时使用乐观锁或悲观锁机制来避免并发更新问题。


使用 TTL(Time To Live)特性:设置每条消息的生命周期,超过指定时间后自动删除。如果消费者在该时间内未能成功处理该消息,则可以认为该消息已经丢失。


使用 RabbitMQ 提供的 ACK 机制:当消费者成功处理一条消息时,发送 ACK 确认信号给 RabbitMQ 服务器。服务器收到 ACK 后才会将该条消息从队列中删除。如果消费者处理失败,则发送 NACK 信号给 RabbitMQ 服务器,并设置重新入队(requeue)参数为 true,在下次重新投递时再次尝试消费。


在生产者端设置 IDempotent(幂等)属性:确保相同 ID 的请求只执行一次。这样就可以避免重复发送消息,从而避免了消息的重复消费。


### RocketMQ


生产者生产时候设置一个唯一keyid



public void testRepeatProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer(“test-group”);
// 设置nameServer地址
producer.setNamesrvAddr(“localhost:9876”);
// 启动实例
producer.start();
// 我们可以使用自定义key当做唯一标识
String keyId = UUID.randomUUID().toString();
System.out.println(keyId);
Message msg = new Message(“TopicTest”, “tagA”, keyId, “我是一个测试消息”.getBytes());
SendResult send = producer.send(msg);
System.out.println(send);
// 关闭实例
producer.shutdown();
}



/**
* 在boot项目中可以使用@Bean在整个容器中放置一个单利对象
*/
public static BitMapBloomFilter bloomFilter = new BitMapBloomFilter(100); // m数组长度

@Test
public void testRepeatConsumer() throws Exception {

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("repeat-consumer-group");
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.setNamesrvAddr(MyConstant.NAME\_SRV\_ADDR);
consumer.subscribe("repeatTestTopic", "\*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                    ConsumeConcurrentlyContext context) {
        // 拿到消息的key
        MessageExt messageExt = msgs.get(0);
        String keys = messageExt.getKeys();
        // 判断是否存在布隆过滤器中
        if (bloomFilter.contains(keys)) {
            // 直接返回了 不往下处理业务
            return ConsumeConcurrentlyStatus.CONSUME\_SUCCESS;
        }
        // 这个处理业务,然后放入过滤器中
        // do sth...
        bloomFilter.add(keys);
        System.out.println("keys:" + keys);
        System.out.println(new String(messageExt.getBody()));
        return ConsumeConcurrentlyStatus.CONSUME\_SUCCESS;
    }
});
consumer.start();
System.in.read();

}


### Kafka


**生产端:**


Producer 的幂等性指的是当发送同一条消息时,数据在 Server 端只会被持久化一次,数据不丟不重,Kafka为了实现幂等性,底层设计架构中引入了ProducerID和SequenceNumbe。`enable.idempotence` 是否开启幂等性,默认 true,开启幂等性。


重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其  
 中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。  
 所以幂等性只能保证的是在单分区单会话内不重复。


kafka在0.11.0版本后,给每个Producer端分配了一个唯一的ID,每条消息中也会携带一个序列号,这样服务端便可以对消息进行去重,但是如果是两个Producer生产了两条相同的消息,那么kafka无法对消息进行去重,所以我们可以在消息头中自定义一个唯一的消息ID然后在consumer端对消息进行手动去重。


当Producer发送消息(x2,y2)给Broker时,Broker接收到消息并将其追加到消息流中。此时,Broker返回Ack信号给Producer时,发生异常导致Producer接收Ack信号失败。对于Producer来说,会触发重试机制,将消息(x2,y2)再次发送,但是,由于引入了幂等性,在每条消息中附带了PID(ProducerID)和SequenceNumber。相同的PID和SequenceNumber发送给Broker,而之前Broker缓存过之前发送的相同的消息,那么在消息流中的消息就只有一条(x2,y2),不会出现重复发送的情况。


缺点:Kafka 的 Exactly Once 幂等性只能保证单次会话内的精准一次性,不能解决跨会话和跨分区的问题;


**消费端:**


由于为了保证不少消费消息,配置了手动提交,由于处理消息期间,其他consumer的加入,进行了重平衡,或者consumer提交消息失败,进而导致接收到了重复的消息。  
 我们可以通过自定义唯一消息ID对消息进行过滤去重重复的消息。


## 事务消息


### RabbitMQ


RabbitMQ中与事务机制有关的方法有三个:txSelect(), txCommit()以及txRollback();  
 txSelect用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务,在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了。  
 如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。



// 开启事务
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {

 channel.txSelect();
 try {
     // 发送消息
     channel.basicPublish("", QUEUE\_NAME, MessageProperties.PERSISTENT\_TEXT\_PLAIN, MESSAGE.getBytes());
     System.out.println("消息发送成功");

     // 提交事务
     channel.txCommit();
 } catch (IOException e) {
     // 回滚事务
     channel.txRollback();
     System.out.println("消息发送失败,事务回滚");
 }

} catch (IOException | TimeoutException e) {
e.printStackTrace();
}


### RocketMQ


RocketMQ支持事务消息,整体流程如下图:


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b25e77252ca24337ad0323d627a77206.png#pic_center)


1、Producer向broker发送半消息。  
 2、Producer端收到响应,消息发送成功,此时消息是半消息,标记为“不可投递”状态,Consumer消费不了。  
 3、Producer端执行本地事务。  
 4、正常情况本地事务执行完成,Producer向Broker发送Commit/Rollback,如果是Commit,Broker端将半消息标记为正常消息,Consumer可以消费,如果是Rollback,Broker丢弃此消息。  
 5、异常情况,Broker端迟迟等不到二次确认。在一定时间后,会查询所有的半消息,然后到Producer端查询半消息的执行情况。  
 6、Producer端查询本地事务的状态。  
 7、根据事务的状态提交commit/rollback到broker端。(5,6,7是消息回查)



> 
> 如果MQServer没有接收到producer执行本地事务后发送的commit或者rollback指令,MQServer会向producer调用事务发送方法让producer进行检查,通过返回commit或者rollback来告知MQServer,producer中的本地事务是否执行成功。
> 
> 
> 


事务的三种状态:


* TransactionStatus.CommitTransaction:提交事务消息,消费者可以消费此消息
* TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
* TransactionStatus.Unknown :中间状态,它代表需要检查消息队列来确定状态。



/**
* 事务消息生产者
*/
public class TransactionMessageProducer {
/**
* 事务消息监听实现
*/
private final static TransactionListener transactionListenerImpl = new TransactionListener() {

    /\*\*

* 在发送消息成功时执行本地事务
* @param msg
* @param arg producer.sendMessageInTransaction的第二个参数
* @return 返回事务状态
* LocalTransactionState.COMMIT_MESSAGE:提交事务,提交后broker才允许消费者使用
* LocalTransactionState.RollbackTransaction:回滚事务,回滚后消息将被删除,并且不允许别消费
* LocalTransactionState.Unknown:中间状态,表示MQ需要核对,以确定状态
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// TODO 开启本地事务(实际就是我们的jdbc操作)

        // TODO 执行业务代码(插入订单数据库表)
        // int i = orderDatabaseService.insert(....)
        // TODO 提交或回滚本地事务(如果用spring事务注解,这些都不需要我们手工去操作)

        // 模拟一个处理结果
        int index = 8;
        /\*\*

* 模拟返回事务状态
*/
switch (index) {
case 3:
System.out.printf(“本地事务回滚,回滚消息,id:%s%n”, msg.getKeys());
return LocalTransactionState.ROLLBACK_MESSAGE;
case 5:
case 8:
return LocalTransactionState.UNKNOW;
default:
System.out.println(“事务提交,消息正常处理”);
return LocalTransactionState.COMMIT_MESSAGE;
}
}

    /\*\*

* Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),
* 由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback
* @param msg
* @return 返回事务状态
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 根据业务,正确处理: 订单场景,只要数据库有了这条记录,消息应该被commit
String transactionId = msg.getTransactionId();
String key = msg.getKeys();
System.out.printf(“回查事务状态 key:%-5s msgId:%-10s transactionId:%-10s %n”, key, msg.getMsgId(), transactionId);

        if ("id\_5".equals(key)) { // 刚刚测试的10条消息, 把id\_5这条消息提交,其他的全部回滚。
            System.out.printf("回查到本地事务已提交,提交消息,id:%s%n", msg.getKeys());
            return LocalTransactionState.COMMIT\_MESSAGE;
        } else {
            System.out.printf("未查到本地事务状态,回滚消息,id:%s%n", msg.getKeys());
            return LocalTransactionState.ROLLBACK\_MESSAGE;
        }
    }
};

public static void main(String[] args) throws MQClientException, IOException {
    // 1. 创建事务生产者对象
    // 和普通消息生产者有所区别,这里使用的是TransactionMQProducer
    TransactionMQProducer producer = new TransactionMQProducer("GROUP\_TEST");

    // 2. 设置NameServer的地址,如果设置了环境变量NAMESRV\_ADDR,可以省略此步
    producer.setNamesrvAddr("192.168.100.242:9876");

    // 3. 设置事务监听器
    producer.setTransactionListener(transactionListenerImpl);

    // 4. 启动生产者
    producer.start();

    for (int i = 0; i < 10; i++) {
        String content = "Hello transaction message " + i;
        Message message = new Message("TopicTest", "TagA", "id\_" + i, content.getBytes(RemotingHelper.DEFAULT\_CHARSET));

        // 5. 发送消息(发送一条新订单生成的通知)
        SendResult result = producer.sendMessageInTransaction(message, i);

        System.out.printf("发送结果:%s%n", result);
    }

    System.in.read();
    // 6. 停止生产者
    producer.shutdown();
}

}


[rocketmq相关]( )


### Kafka


在使用Kafka事务前,需要开启幂等特性,将 `enable.idempotence` 设置为 true


Kafka 0.11.0 版本开始引入了事务性功能。实现事务性消息的过程涉及到生产者(Producer)和消费者(Consumer)两个方面:


**一、生产者事务**: 生产者可以通过事务将一批消息发送到 Kafka,并保证这批消息要么全部发送成功,要么全部发送失败,实现消息的原子性。


1)在使用事务之前,生产者需要先初始化一个事务,即调用 initTransactions() 方法。这样会将生产者切换到事务模式。


2)然后,生产者开始事务,将待发送的消息放入事务中,而不是直接发送到 Kafka。


3)在事务中,可以将多条消息添加到一个或多个主题的不同分区中。在事务中,如果发送消息成功,则会将消息暂存于事务缓冲区,不会立即发送到 Kafka。


4)在所有消息都添加到事务中后,可以调用 commitTransaction() 方法提交事务。如果所有消息提交成功,则整个事务提交成功,所有消息会被一起发送到 Kafka。


5)如果在事务过程中出现错误或者某条消息发送失败,可以调用 abortTransaction() 方法回滚事务,之前添加到事务中的消息不会发送到 Kafka。



Properties properties = new Properties();
properties.put(org.apache.kafka.clients.producer.ProducerConfig.TRANSACTIONAL_ID_CONFIG, transactionId);

KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

// 初始化事务
producer.initTransactions();
// 开启事务
producer.beginTransaction();

try {
// 处理业务逻辑
ProducerRecord<String, String> record1 = new ProducerRecord<String, String>(topic, “msg1”);
producer.send(record1);
ProducerRecord<String, String> record2 = new ProducerRecord<String, String>(topic, “msg2”);
producer.send(record2);
ProducerRecord<String, String> record3 = new ProducerRecord<String, String>(topic, “msg3”);
producer.send(record3);
// 处理其他业务逻辑
// 提交事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
// 中止事务,类似于事务回滚
producer.abortTransaction();
}
producer.close();


事务手动提交  
 在一个事务中如果需要手动提交消息,需要先将 `enable.auto.commit` 参数设置为 false,然后调用 `sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId)` 方法进行手动提交,该方式特别适用于 消费-转换-生产模式的状况



producer.initTransactions();
while (true){
org.apache.kafka.clients.consumer.ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
if (!records.isEmpty()){
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
producer.beginTransaction();
try {
for (TopicPartition partition: records.partitions()){
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(“topic-sink”, record.key(), record.value());
producer.send(producerRecord);
}
long lastConsumedOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
offsets.put(partition, new OffsetAndMetadata(lastConsumedOffset + 1));
}
// 手动提交事务
producer.sendOffsetsToTransaction(offsets, “groupId”);
producer.commitTransaction();
}catch (ProducerFencedException e){
// log the exception
producer.abortTransaction();
}
}
}


**二、消费者事务**: 消费者可以通过事务来保证消息的读取和消息处理的原子性。


1)消费者可以将消息的偏移量(Offset)保存在事务中,并在处理完消息后将偏移量提交到事务中。


2)当事务成功提交后,偏移量会被记录到消费者组中,表示这批消息已经被成功消费。


3)如果事务失败或回滚,偏移量不会被提交,下次消费者启动时会从上次提交的偏移量处继续消费。


通过使用事务,生产者和消费者都可以实现对消息的原子性处理,保证消息的可靠传输和处理。这对于一些需要强一致性的应用场景非常重要,例如金融交易系统或者订单处理系统等。但需要注意,使用事务会增加一定的系统开销,因此在实现事务时需要权衡性能和可靠性。


为了实现事务,Kafka引入了事务协调器(TransactionCoodinator)负责事务的处理,所有的事务逻辑包括分派PID等都是由TransactionCoodinator 负责实施的。


broker节点有一个专门管理事务的内部主题 \_\_transaction\_state,TransactionCoodinator 会将事务状态持久化到该主题中。


**事务消息流程:**


1. 查找 `TransactionCoordinator`:生产者会先向某个broker发送 FindCoordinator 请求,找到 TransactionCoordinator 所在的 broker节点.
2. 获取PID:生产者会向 TransactionCoordinator 申请获取 `PID`,TransactionCoordinator 收到请求后,会把 transactionalId 和对应的 PID 以消息的形式保存到主题 \_\_transaction\_state 中,保证 <transaction\_Id,PID>的对应关系被持久化,即使宕机该对应关系也不会丢失
3. 开启事务:调用 beginTransaction()后,生产者本地会标记开启了一个新事务
4. 发送消息:生产者向用户主题发送消息,过程跟普通消息相同,但第一次发送请求前会先发送请求给TransactionCoordinator 将 transactionalId 和 TopicPartition 的对应关系存储在 \_\_transaction\_state 中
5. 提交或中止事务:Kafka除了普通消息,还有专门的控制消息(ControlBatch)来标志一个事务的结束,控制消息有两种类型,分别用来表征事务的提交和中止  
 该阶段本质就是一个两阶段提交过程:
	1. 将 PREPARE\_COMMIT 或 PREPARE\_ABORT 消息写入主题 \_\_transaction\_state
	2. 将COMMIT 或 ABORT 信息写入用户所使用的普通主题和 \_\_consumer\_offsets
	3. 将 COMPLETE\_COMMIT 或 COMPLETE\_COMMIT\_ABORT 消息写入主题 \_\_transaction\_state


[学习博客]( )


## 消息积压


**消息积压的常见原因:**


* 生产者速度快于消费者:如果消息生产者(产生消息的系统或组件)的速度比消息消费者(处理消息的系统或组件)的速度快,就会导致消息积压。
* 消费者故障:如果消息消费者遇到故障或处理速度变慢,未能及时处理消息,也会导致积压。
* 高峰负载:系统在高峰时段接收到大量的消息,超过了正常处理速度。
* 消息处理失败:如果某些消息由于错误或异常而无法被正常处理,它们可能会在队列中积压。
* 配置不合理:如果消息队列的容量设置过小或者消费者的线程数设置过少,都可能导致消息积压。


**消息积压的常见表现:**


* 系统资源使用率上升:消息积压可能导致系统资源使用率上升,例如CPU利用率、内存占用、磁盘活动等。这是因为积压的消息需要占用系统资源来存储和管理。
* 消息丢失或过期:如果消息队列没有足够的容量来处理新消息,可能会导致旧消息被丢弃或过期,从而导致数据丢失。
* 系统警报和异常:一些监控系统会检测到消息积压问题,并发出警报或异常通知,以提醒运维团队或开发人员。
* 队列故障或堵塞:在极端情况下,消息积压可能导致消息队列系统的故障或堵塞,导致无法正常工作。


**消息积压可能导致的后果:**


* 延迟:积压的消息会导致正常消息的处理延迟,从而影响了系统的响应时间。
* 系统负载:积压消息可能导致消息队列系统和消息处理组件的负载过高,甚至可能引发系统故障。
* 数据丢失:消息积压导致消息久久不被消费,有可能导致超时被丢弃掉,从而导致数据丢失。
* 不一致性:如果消息处理不及时,可能会导致系统数据的不一致性问题。


**解决方案 :**


1. 增加消费者数量  
 如果消息消费者的处理速度无法满足消息产生的速度,可以通过增加消费者的数量来提高消费能力。这样可以将负载分散到多个消费者上,加快消息处理速度,减少积压。
2. 增加消息队列的容量  
 如果消息队列的容量设置过小,可能会导致消息积压。可以通过增加消息队列的容量来缓解积压问题。但需要注意,过大的消息队列容量可能会增加消息处理的延迟。
3. 优化消息消费的逻辑  
 检查消息消费逻辑是否存在性能瓶颈或者不必要的复杂计算。优化消息消费的逻辑可以提高消费速度,减少消息积压。
4. 设置消息消费失败的处理机制  
 当消息消费失败时,可以根据业务需求选择合适的处理方式。可以将失败的消息记录下来,后续再次消费;或者将失败的消息发送到死信队列进行处理。
5. 监控和报警机制  
 建立监控和报警机制,及时发现消息积压的情况并采取相应的措施。可以通过监控指标、日志或者专业的监控工具来实现。


**具体实现方案 :**


* 消息积压处理办法:临时紧急扩容。
* 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
* MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
* mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。


针对mq消息积压其实存在一套成熟的打法,一共分三步:


* 事前处理机制:jmeter测压
* 事中处理机制:扩容,然后快速消费完积压的消息
* 事后处理机制
	+ 提高消费并行度
	+ 跳过非重要的消费
	+ 优化每条消息的消费过程。


[示例博客]( )


## MQ集群架构


### RabbitMQ


RabbitMQ 集群有两种模式:


* 普通集群
* 镜像集群


普通集群模式,就是将 RabbitMQ 部署到多台服务器上,每台服务器启动一个 RabbitMQ 实例,多个实例之间进行消息通信。


此时我们创建的队列 Queue,它的元数据(主要就是 Queue 的一些配置信息)会在所有的 RabbitMQ 实例中进行同步,但是队列中的`消息只会存在于一个 RabbitMQ 实例上`,而不会同步到其他队列。


当我们消费消息的时候,如果连接到了另外一个实例,那么那个实例会通过元数据定位到 Queue 所在的位置,然后访问 Queue 所在的实例,拉取数据过来发送给消费者。


这种集群可以提高 RabbitMQ 的消息吞吐能力,但是无法保证高可用,因为一旦一个 RabbitMQ 实例挂了,消息就没法访问了,如果消息队列做了持久化,那么等 RabbitMQ 实例恢复后,就可以继续访问了;如果消息没做持久化,那么消息就丢了。


它和普通集群最大的区别在于 Queue 数据和元数据不再是单独存储在一台机器上,而是同时存储在多台机器上。也就是说每个 RabbitMQ 实例都有一份镜像数据(副本数据)。每次写入消息的时候都会自动把数据同步到多台实例上去,这样一旦其中一台机器发生故障,其他机器还有一份副本数据可以继续提供服务,也就实现了高可用。


### RocketMQ


**单Master模式**


这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。不建议线上环境使用,可以用于本地测试。


**多Master模式**


一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下


* 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
* 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。


**多Master多Slave模式-异步复制**


每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下


* 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;
* 缺点:Master宕机,磁盘损坏情况下会丢失少量消息(非同步刷盘的情况下)


**多Master多Slave模式-同步双写**


每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下:


* 优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
* 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机  
 如果是想不存在消息丢失的情况,那么在多Master的情况下要配置消息同步刷盘,而在 多Master多Slave模式-同步双写 的情况下配置同步刷盘。


以上主从结构是只做数据备份,没有容灾功能的。也就是说当⼀个master节点挂了后,slave节点是⽆法切换成master节点继续提供服务的。注意这个集群⾄少要是3台,允许少于⼀半的节点发⽣故障。这个Dledger需要在RocketMQ4.5以后的版本才⽀持


RocketMQ有一个集群模式叫做:`Dleger模式`。当主节点失活时,能够自动重新触发选举。


DLedger集群节点状态leader、candidate、follower。(Raft算法)


* leader:接受客户端请求,本地写入日志数据,并将数据复制给follower;定期发送心跳数据给follower维护leader状态
* candidate:master故障后节点的中间状态,只有处于candidate状态的节点才会发送投票选举请求,master选举完成后,节点状态为leader或者follower
* follower:负责同步leader的日志数据;接受leader心跳数据,重置倒计时器保持follower状态,并将心跳响应返回给leader


springboot使用中指定多个nameServer地址以连接集群:



rocketmq.config.namesrvAddr=worker1:9876;worker2:9876;worker3:9876


[集群架构原博客]( )


### Kafka


## 集群搭建


### RabbitMQ


[集群介绍及搭建]( )  
 [集群搭建]( )


[其他搭建示例]( )


[SpringBoot中的集群配置餐宿]( )


### RocketMQ



#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH


[搭建示例博客]( )


[集群搭建]( )


[RocketMq-主从集群搭建]( )


[主从同步双写集群搭建]( )


[SpringBoot整合RocketMQ搭建集群及使用]( )


**DLedger集群**


DLedger是基于Raft协议实现的一个轻量级的Java类库,RocketMQ引入其来实现Leader选举,commitLog同步等一致性操作。


RocketMQ-on-DLedger Group 指的是需要实现自动容灾切换的一组RocketMq服务器,该群组至少要有三个节点。


使用时只需要增加DLedger相关配置即可,具体配置如下:


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/91599eb9fef84e5fa016a753a683ebbc.png#pic_center)  
 Dledger主要配置:


master01集群第一台机 :



#多副本自动主从选举Dledger相关
enableDLegerCommitLog = true
dLegerGroup = master01
dLegerSelfId = n0
dLegerPeers = n0-192.168.60.101:10911;n1-192.168.60.102:10911;n2-192.168.60.103:10911


master01集群第二台机 :



#多副本自动主从选举Dledger相关
enableDLegerCommitLog = true
dLegerGroup = master01
dLegerSelfId = n1
dLegerPeers = n0-192.168.60.101:10911;n1-192.168.60.102:10911;n2-192.168.60.103:10911


[DLedger集群]( )


[DLedger集群搭建2]( )


### Kafka


装好JDK环境,下载并安装好ZooKeeper ,下载好Kafka安装包,并解压,这里示例为`/opt/module/kafka`目录下。


**Linux下配置环境变量** :



vim /etc/profile


然后加入配置`KAFKA_HOME=opt/module/ kafka`。  
 每台集群机器都要配置并刷新环境:`source /etc/profile`。


在安装目录`/opt/module/kafka`下创建配置Kafka日志目录:`mkdir logs`。


**配置Kafka服务的配置**



vim server.properties


配置以下:



broker.id=1
log.dirs=/opt/module/kafka/logs
zookeeper.connect=hadoop101:2181,hadoop102:2181,hadoop103:2181


`broker.id`:全局唯一编号,Kafka集群中的此编号不能重复。  
 `log.dirs`:设置 Kafka运行日志存放的路径  
 `zookeeper.connect`:配置连接Zookeeper集群地址(Kafka也依赖于zk 来管理集群)。


hadoop101和hadoop102、hadoop103三台服务都得配置。


**启动集群**:


先启动三台服务器的Zookeeper,在 Kafka 安装目录下,使用 `bin/zookeeper-server-start.sh` 命令启动 ZooKeeper 服务,并指定配置文件 `config/zookeeper.properties`。



zk start


依次在hadoop101、hadoop102、hadoop103节点上启动kafka,在kafka安装目录下的config目录下执行:



kafka-server-start.sh -daemon ./server.properties


\*\*关闭集群:\*\*依次在hadoop101、hadoop102、hadoop103节点上关闭kafka



kafka-server-stop.sh stop


[搭建原文]( )


## MQ刷盘机制/集群同步


### RabbitMQ


普通消息都是保存在内存中,只有配置交换机、对列、消息都为持久化时才会刷到磁盘。  
 当RabbitMQ的内存紧张时、或生产者发消息指定了消息持久化时,消息会写入磁盘


写入文件前会有一个Buffer,大小为1M(1048576),数据在写入文件时,首先会写入到这个Buffer,如果Buffert已满,则会将Buffer写入到文件(未必刷到磁盘):  
 有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每隔25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘;


### RocketMQ


同步刷盘、异步刷盘  
   RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。


RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式:


1)异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,立刻返回发送端发送成功,有单独的线程执行刷盘;写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入。


2)同步刷盘方式:在返回写成功状态时,消息已经被写入磁盘。同步调用MappedByteBuffer的force()方法,同步等待刷盘结果,进行刷盘结果返回告知发送端。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。



> 
> 同步刷盘还是异步刷盘,是通过Broker配置文件里的flushDiskType参数设置的,这个参数被设置成SYNC\_FLUSH、ASYNC\_FLUSH中的一个
> 
> 
> 


消息存储时,先将消息存储到内存,再根据不同的刷盘策略进行刷盘


同步刷盘:


异步刷盘:


[刷盘源码]( )


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


* 同步复制是等Master和Slave均写成功后才反馈给客户端写成功状态;
* 异步复制方式是只要Master写成功即可反馈给客户端写成功状态


这两种复制方式各有优劣:



![img](https://img-blog.csdnimg.cn/img_convert/b08d7256422fa6d0fd2efd935cdf01ed.png)
![img](https://img-blog.csdnimg.cn/img_convert/a75913992d0af1f58f98c6938c9e5354.png)
![img](https://img-blog.csdnimg.cn/img_convert/507b084fa39d66b5f77b882c01a55ac8.png)

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

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

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**



**关闭集群:**依次在hadoop101、hadoop102、hadoop103节点上关闭kafka

kafka-server-stop.sh stop

搭建原文

MQ刷盘机制/集群同步

RabbitMQ

普通消息都是保存在内存中,只有配置交换机、对列、消息都为持久化时才会刷到磁盘。
当RabbitMQ的内存紧张时、或生产者发消息指定了消息持久化时,消息会写入磁盘

写入文件前会有一个Buffer,大小为1M(1048576),数据在写入文件时,首先会写入到这个Buffer,如果Buffert已满,则会将Buffer写入到文件(未必刷到磁盘):
有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每隔25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘;

RocketMQ

同步刷盘、异步刷盘
  RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。

RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式:

1)异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,立刻返回发送端发送成功,有单独的线程执行刷盘;写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入。

2)同步刷盘方式:在返回写成功状态时,消息已经被写入磁盘。同步调用MappedByteBuffer的force()方法,同步等待刷盘结果,进行刷盘结果返回告知发送端。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。

同步刷盘还是异步刷盘,是通过Broker配置文件里的flushDiskType参数设置的,这个参数被设置成SYNC_FLUSH、ASYNC_FLUSH中的一个

消息存储时,先将消息存储到内存,再根据不同的刷盘策略进行刷盘

同步刷盘:

异步刷盘:

刷盘源码

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

  • 同步复制是等Master和Slave均写成功后才反馈给客户端写成功状态;
  • 异步复制方式是只要Master写成功即可反馈给客户端写成功状态

这两种复制方式各有优劣:

[外链图片转存中…(img-FBrmxmtK-1714415145582)]
[外链图片转存中…(img-BIBos5PI-1714415145582)]
[外链图片转存中…(img-uFIcvK1B-1714415145583)]

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值