我的RocketMQ架构图如下:
故障描述: Broker-b所在服务器宕机8小时(1:00-9:00), 重启Broker-b后, 8小时期间产生的消息被消费者订阅消费, 由于消费者等幂条件是:2小时内相同消息(msgId相同)不重复发送, 但是此时已经超过两小时, 故 1:00-7:00 期间产生的消息被重复消费。
期望: Broker-b重启后, 消费者只订阅当前时间开始的消息, 之前的消息不再订阅。
处理方案1:
设置消费者订阅消息的位置为最新的位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
这里要注意代码注释。这个参数只对一个新的consumeGroup第一次启动时有效。
就是说,如果是一个consumerGroup重启,他只会从自己上次消费到的offset,继续消费。这个参数是没用的。 而判断是不是一个新的ConsumerGroup是在broker端判断。
要知道,消费到哪个offset最先是存在Consumer本地的,定时和broker同步自己的消费offset。broker在判断是不是一个新的consumergroup,就是查broker端有没有这个consumergroup的offset记录。
另外,对于一个新的queue,这个参数也是没用的,都是从0开始消费。
优点: 处理简单, 修改一行即可。
缺点:需要新的消费者才生效, 故此方法对于broker中已包含了ConsumerGroup的, 不起作用。
处理方案2:
修改broker-b的offset, 和 broker-a的配置保持一致。
方法:
把broker-a的rocketmq路径:${userPath}/store/config/consumerOffset.json 文件拷贝到broker-b的相同位置, 重启broker-b
优点: 简单快捷, 不需要修改程序代码。
缺点:由于broker-a节点的消息队列一直在更新,offset偏移量一直在增加, 故复制过来到重启这段时间差内, 依然有少部分信息会被重复消费, 这只能依赖等幂代码的处理。
事后补救:
1. 消费者的等幂操作的条件时长设置更长(1天或者3天, 我是用redis存储msgId, 消息量不大的情况下可以设置更长的时长)
//缓存消息ID 防止消费重复
boolean setResult = RedisUtil.setnx(msgExt.getMsgId(), 60 * 60 * 24 * 1, msgExt.getMsgId().getBytes("utf-8"));
if (!setResult) {
// 存储不成功说明已经存储过, 直接返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
2. 增加rocketmq宕机告警