Rocketmq 重复消息,幂等
一、为什么出现消息重复
从 Product 看
Rocketmq 提供三种发送消息模式
同步发送:Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应 发送结果。DefaultMQProducerImpl 中如果没有设置 超时、发送失败,就会重发。
异步发送:先构建一个broker发送消息的任务,把任务提交给线程池,等执行完任务时,回调用户自定义的回调函数,执行处理结果。
Oneway发送:只负责发送请求,不等待应答,
注:同步发送、异步发送 如果发送成功,返回结果出现网络问题,会导致重新发送,多条重复消息。
从 Consumer 看
Broker 消息进度丢失,导致消息重复投递给 Consumer。
Consumer 消费成功,但是因为网络问题,JVM 异常崩溃,导致rocketmq没收到 消费成功确认,会重复推送。
注:从性能考虑,消费进度 用异步定时同步给 Broker。
二、Rocketmq ack 机制保证消息消费成功。
ACK
发送者为保证消息肯定消费成功,需要使用方明确标识消费成功,rocketmq 才会认为消息消费成功。中途断电,抛出异常等都不会认为成功(会重新投递)。
public enum Action { /** * 消费成功,继续消费下一条消息 */ CommitMessage, /** * 消费失败,告知服务器稍后再投递这条消息,继续消费其他消息 */ ReconsumeLater, }
例:
消费者回执
import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.ConsumeContext;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.MessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RuleForwardListener implements MessageListener { private Logger log = LoggerFactory.getLogger(RuleForwardListener.class); @Override public Action consume(Message message, ConsumeContext context) { try { String msg = new String(message.getBody()); log.info("rrpc response message:{}", msg); return Action.CommitMessage; // 消费成功 } catch (Throwable t) { log.error("rrpc-response error", t); return Action.ReconsumeLater; // 消费失败 } } }
仅当回执函数返回 CommitMessage时,rocketmq 就会认为此消息消费成功。
返回 ReconsumeLater ,rocketmq 认为消息消费失败。
为保证消息肯定被至少消费成功一次,rocketmq 会把消息重发回 broker(topic不是原topic 而是消费组的 retry topic),在延迟的某个时间点(默认 10s, 可设置),再次投递到这个 ConsumerGroup。如果一直这样重复消费都失败,默认 16次,就会投递到 DLQ 死信队列。应用可以监控死信队列来做人工干预。
修改重试时间
broker 日志中发现默认重试时间:
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
可以在配置文件中设置:
messageDelayLevel = 10s 1m 5m
(或者在逻辑中,人工干预重复次数。达到次数后,返回 CommitMessage )
三、解决重复消费 - 消费者实现 消费幂等。
根据业务上 唯一 key 对消息做幂等处理。
当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果相同,并且多次消费并未对业务系统产生任何负面影响,那么整个过程就可实现消息幂等。
Message 中的 key
Message message = new Message(msgTopic, msgTag, "uuid", messageBody);
SendResult sendResult = producer.send(message);