一.造成重复消费的原因

在于回馈机制。正常情况下,消费者在消费消息时候,消费完毕后,会发送一个ACK确认信息给消息队列(broker),消息队列(broker)就知道该消息被消费了,就会将该消息从消息队列中删除。

不同的消息队列发送的确认信息形式不同,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka实际上有个offset的概念。

  造成重复消费的原因?,就是因为网络原因闪断,ACK返回失败等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。(因为消息重试等机制的原因,如果一个consumer断了,rocketmq有consumer集群,会将该消息重新发给其他consumer)

这个问题针对业务场景来答,分以下三种情况:

(1)比如,你拿到这个消息做数据库的insert操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

(2)再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

(3)如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis.那消费者开始消费前,先去redis中查询有没有消费记录即可。

二.单机环境解决方案

生产者:发送消息同时set一个key做唯一标识

public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("rmq-group");
producer.setNamesrvAddr("192.168.42.22:9876;192.168.42.33:9876");
producer.setInstanceName("producer");
producer.start();
try {
for (int i = 0; i < 1; i++) {
Thread.sleep(1000); // 每秒发送一次MQ
Message msg = new Message("itmayiedu-topic", // topic 主题名称
"TagA", // tag 临时值
("itmayiedu-6" + i).getBytes()// body 内容
);
//setKey,做唯一标识
msg.setKeys(System.currentTimeMillis() + ""); SendResult sendResult = producer.send(msg);
System.out.println(sendResult.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
producer.shutdown();
}

消费者:

//保存标识的集合
static private Map<String, String> logMap = new HashMap<>(); public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rmq-group"); consumer.setNamesrvAddr("192.168.42.22:9876;192.168.42.33:9876");
consumer.setInstanceName("consumer");
consumer.subscribe("itmayiedu-topic", "TagA"); consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
String key = null;
String msgId = null;
try {
for (MessageExt msg : msgs) {
key = msg.getKeys();
//判断集合当中有没有存在key,存在就不需要重试,不存在先存key再回来重试后消费消息
if (logMap.containsKey(key)) {
// 无需继续重试。
System.out.println("key:"+key+",无需重试...");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
msgId = msg.getMsgId();
System.out.println("key:" + key + ",msgid:" + msgId + "---" + new String(msg.getBody()));
//模拟异常
int i = 1 / 0;
} } catch (Exception e) {
e.printStackTrace();
//重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
} finally {
//保存key
logMap.put(key, msgId);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started.");
}

执行效果:

三.集群环境解决方案

在生产者端要保证幂等性的话,大概可以使用以下两种方式:
    ① RocketMQ支持消息查询的功能,只要去RocketMQ查询一下是否已经发送过该条消息就可以了,不存在则发送,存在则不发送
    ② 引入redis,在发送消息到RocketMQ成功之后,向redis中插入一条数据,如果发生重试,则先去redis中查询一下该条消息是否已经发送过了,存在的话就不重复发送消息了
    生产者的这两种幂等性方案都可以实现,但是都存在一定的缺陷
    方案①,RocketMQ消息查询的性能不是特别好,如果是在高并发的场景下,每条消息在发送到RocketMQ时都去查询一下,可能会影响接口的性能
    方案②,在一些极端的场景下,redis也无法保证消息发送成功之后,就一定能写入redis成功,比如写入消息成功而redis此时宕机,那么再次查询redis判断消息是否已经发送过,是无法得到正确结果的

既然在消费者做幂等性的方案都不是特别靠谱,那就再在消费者端来做吧
消息的消费,最后都对应的是数据库的操作,只要在消息消费的时候,判断一下数据库中是否已经消费过了这条消息,就可以保证幂等性了,例如使用唯一索引,保证一条消息只被消费一次。

参考:https://blog.csdn.net/LO_YUN/article/details/104135197

去重原则:1.幂等性 2.业务去重

  幂等性:(处理必须唯一) 无论这个业务请求被(consumer)执行多少次,我们的数据库的结果都是唯一的,不可变的。

  去重策略:去重表机制,业务拼接去重策略(比如唯一流水号)

  1.建立一个消息表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突。

    高并发下去重:采用Redis去重(key天然支持原子性并要求不可重复),但是由于不在一个事务,要求有适当的补偿策略,但是对于很重要的业务,不应该支持补偿

  2.利用redis事务,主键(我们必须把全量的操作数据都存放在redis里,然后定时去和数据库做数据同步)—-即消费处理后,该处理本来应该保存在数据库的,先保存在redis,再通过一定业务方式从redis中取数据进行db持久化

  3.利用redis和关系型数据库一起做去重机制

  4.拿到这个消息做redis的set的操作.redis就是天然幂等性

  5.准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

消息重复消费是一个非常常见的问题,在很多系统调用频繁的场景下,都可能会出现超时重试的情况,还有就是系统频繁迭代,经常重启系统更新的场景,也会出现消息重复消费
生产者端发送重复的消息到RocketMQ中其实问题不大,消息只是在RocketMQ中重复了,并没有影响到系统的数据,我们只需要在最后修改数据库的时候,保证好幂等性即可

RocketMQ解决幂等性问题的更多相关文章

  1. 【Java面试】什么是幂等?如何解决幂等性问题?

    一个在传统行业工作了7年的粉丝私信我. 他最近去很多互联网公司面试,遇到的很多技术和概念都没听过. 其中就有一道题是:"什么是幂等.如何解决幂等性问题"? 他说,这个概念听都没听过 ...

  2. RocketMQ 解决 No route info of this topic 异常步骤

    原文地址:https://blog.csdn.net/chenaima1314/article/details/79403113 rocketmq运行时提示 No route info of this ...

  3. Springboot整合RocketMQ解决分布式事务

    直接上代码: 代码结构如下: 依次贴出相关类: DataSource1Config: package com.example.demo.config;import org.apache.ibatis. ...

  4. rocketmq(三 java操作rocket API, rocketmq 幂等性)

    JAVA操作rocketmq: 1.导入rocketmq所需要的依赖: <dependency> <groupId>com.alibaba.rocketmq</group ...

  5. Rocketmq原理&最佳实践

    MQ背景&选型 消息队列作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性.主要具有以下优势: 削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失.系统奔溃等问题 ...

  6. RabbitMQ消息幂等性问题

    文章目录 1. 什么是幂等性?1.1 消息队列的幂等性1.2 模拟重试机制1.2.1 生产者代码1.2.2 消费者代码1.2.3 消费者 application.yml 配置2. 如何保证消息幂等性, ...

  7. RocketMQ 源码分析之路由中心(NameServer)

    你可能没有看过 RocketMQ 的架构图,没关系,一起来学习一下,RocketMQ 架构图如下: 在 RocketMQ 中,有四个角色: Producer:消息的生产者,每个 MQ 中间件都有. C ...

  8. 消息队列之事务消息,RocketMQ 和 Kafka 是如何做的?

    每个时代,都不会亏待会学习的人. 大家好,我是 yes. 今天我们来谈一谈消息队列的事务消息,一说起事务相信大家都不陌生,脑海里蹦出来的就是 ACID. 通常我们理解的事务就是为了一些更新操作要么都成 ...

  9. MQ的面试题

    MQ的优点和缺点? 优点:解耦 异步,削峰 解耦: 所以需要用来解耦: 异步: 解决方法: 削峰: 解决方法是: 缺点:降低高可用性.增加系统的复杂程度.一致性问题 降低高可用的原因:系统引入的外部依 ...

随机推荐

  1. Coroutine 预激装饰器

    预激装饰器 讨论如何终止协程之前,我们要先谈谈如何启动协程.使用协程之前必须预激,可是这一 步容易忘记.为了避免忘记,可以在协程上使用一个特殊的装饰器.接下来介绍这样一个 装饰器. 预激协程的装饰器, ...

  2. Multicast

    Source Specific Multicast (SSM) The multicast that you are probably familiar with (PIM sparse and de ...

  3. LwIP的SNMP学习笔记

    关于这方面的资料网上非常少,做一下笔记. 在LwIP中,在\lwip-1.4.1\src\core\snmp目录下有SNMP相关的c文件,在lwip-1.4.1\src\include\lwip目录下 ...

  4. 查看php相关信息

    1.最常见的就是 创建一个  php页面  ,例如 test.php,  内容如下 <?php phpinfo();?> 直接访问 这个页面,就可以看到php的 信息了 2.其它方法  直 ...

  5. Shell脚本 server rsync 控制脚本

    [root@backup ~]# vim /etc/init.d/rsync#!/bin/bash #this script for start|stop rsync daemon service s ...

  6. -bash: warning: setlocale: LC_CTYPE: cannot change locale (zh_CN.UTF-8mb4): No such file or directory

    前几天登录服务器发现出现了这些个警告,一直没时间去处理他,今天难得有空,处理一下并记录下来,希望可以帮助到有需要的朋友. 警告信息如下: Last login: Tue May :: from 192 ...

  7. vue路由--命名视图

    有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar(侧导航) 和 main(主内容) 两个视图,这个时候命名视图就派上用场了.你可以在界面中拥有多个单独命名的视图, ...

  8. C#设计模式学习笔记:(13)模板方法模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/7837716.html,记录一下学习过程以备后续查用. 一.引言 今天我们要讲行为型设计模式的第一个模式--模 ...

  9. Linux中screen命令

    screen是一款由GNU计划开发的用于命令行终端切换的自由软件.用户可以通过该软件同时连接多个本地或远程的命令行会话,并在其间自由切换.GNU Screen可以看作是窗口管理器的命令行界面版本.它提 ...

  10. VMware使用与安装

    VMware安装 下载完Vmware -> 双击打开安装包 -> 选择下一步(如下图界面) 选择接受协议,点击下一步 选择经典进行安装.这个是默认安装,会把默认插件安装到相对应的路径 选择 ...