大数据最新MQ高可用相关设置,大数据开发岗面试必问

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

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

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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 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 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 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 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();
}
}
}

img
img
img

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

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

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

000));
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();
}
}
}

[外链图片转存中…(img-Jdo61LuU-1715439619187)]
[外链图片转存中…(img-fcrPdCC9-1715439619188)]
[外链图片转存中…(img-9Xhb03pA-1715439619188)]

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

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

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

  • 29
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然,下面是MQ(消息队列)面试中经常会遇到的6个经典问题: 1. 什么是消息队列(MQ)? 消息队列是一种常用的中间件技术,用于在不同的应用程序之间传递消息。它将消息发送到队列中,然后接收者从队列中接收消息,实现了应用程序之间的解耦。 2. MQ的优点是什么? MQ具有以下几个优点: - 异步通信:发送方发送消息后即可继续处理其他任务,接收方可以在合适的时候处理消息。 - 解耦合:发送方和接收方之间通过消息队列进行通信,彼此不需要直接知道对方的存在,实现了解耦合。 - 削峰填谷:可以通过消息队列平滑处理系统的峰值流量,保证系统的稳定性。 - 可靠性:消息队列通常具备高可靠性和持久化特性,可以确保消息不丢失。 3. RabbitMQ和Kafka有什么区别? RabbitMQ和Kafka是两种常见的消息队列系统,它们有以下区别: - RabbitMQ是一个传统的消息队列系统,采用AMQP协议,支持多种消息模式。适用于实时性要求较高、强一致性的场景。 - Kafka是一个高吞吐量、分布式的日志处理平台,采用发布-订阅模式。适用于大数据量、高并发的场景。 4. 如何保证MQ高可用性? 保证MQ高可用性的方法主要有以下几种: - 集群部署:通过在多个节点上部署MQ实例,实现故障转移和负载均衡。 - 数据复制:将数据进行复制到多个节点上,确保数据的备份和容灾能力。 - 心跳机制:定期发送心跳检测消息,检测MQ节点的可用性。 - 监控和报警:监控MQ集群的运行状态,及时发现并解决问题。 5. 如何确保MQ的消息不丢失? 确保MQ消息不丢失的方法主要有以下几种: - 持久化:将消息存储到磁盘上,即使MQ节点宕机也能够恢复。 - ACK机制:发送方在发送消息后等待接收方的确认消息(ACK),确保消息被正确接收。 - 消息重试:当发送方发送消息失败时,可以进行重试操作,直到成功为止。 6. 如何保证MQ的顺序性? 保证MQ消息顺序性的方法主要有以下几种: - 单一消费者:每个队列只有一个消费者,确保消息按照顺序被处理。 - 分区顺序:将消息按照某个字段进行分区,同一分区内的消息按顺序处理。 - 消费者缓存:消费者接收到消息后缓存起来,按照顺序处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值