定义:
重复消息又分为重复发送和重复消费
重复发送指的是从生产端发送消息保证消息不丢失,但不保证消息不会重发
重复消费指的是消费端重复消费。如消费消息时,一般建议处理业务后,手动提交offset,但是在提交时,程序崩了;再次重启消费应用时,会继续消费上次未提交代码。如:
while(true){
consumer.poll();//拉取消息
xxx//业务处理
consumer.commit();//提交消息位移
}
第三步,提交位移时,程序崩溃再次恢复重复执行第二步的业务
解决方案:
幂等,即相同的请求参数请求1次和请求100次,得到的结果和对业务的影响是一样的。
实现方式:存储+唯一key
唯一key:业务数据的唯一标识,比如订单号
存储:一般是mysql或者redis存储
两种实现方式:强校验和弱校验
-
强校验:数据库作为存储,唯一key存储+业务逻辑处理放到一个事务中,要么同时成功,要么同时失败。
优点 缺点 在并发情况下,只会严格执行一次。数据库唯一性+事务回滚能保证业务只执行一次 处理慢:基于磁盘查询,速度慢 可提供查询流水:处理流水记录的持久化,在某些异常问题排查情况下,能提供查询记录 历史数据需要额外处理 实现回滚:实现难度低,一个注解便可以实现 适用于资金类业务 -
弱校验:使用redis进行唯一key存储和防重。比如订单消息,订单号是一条数据的唯一标识。使用lua脚本设置该消息正在消费中;此时重复消息来,进行相同的设置,发现该订单号,已经被标识为正在处理中,那这条消息放入延时队列中,延时重试吧;如果发现已经消费成功,则直接返回,不执行业务了;业务执行完,设置该key执行成功。 大概过程是这样的
优点 | 缺点 |
---|---|
基于内存,处理速度快 | 数据有过期时间和redis自身特性,造成可能不能防重 |
无需清理key,redis自动清除 | 比起数据库稍微复杂,需要写lua脚本 |
消息一定能消费成功 | 架构复杂,为了保证一定能消费成功,引入了延迟队列 |
注意:
-
布隆过滤器也可以防重,占用空间小,速度快,为什么不使用?
布隆过滤器底层逻辑是,校验一个key如果不存在,绝对不会存在;但是某个key如果存在,那么他是可能存在,又可能不存在。所以这会导致防重查询不准确,最终导致漏消息,这太不能接受了。
-
可以使用redis的setnx既能保证并发性,又能保证唯一key,为什么不使用?
不使用它,主要是redis的 setnx操作和后续的业务执行,不是一个事务单元;即可能setnx成功了,后续业务执行时进程崩溃了,然后在消息重试的时候,又发现setnx里有值了,最终会导致消费失败的消息重试时,造成消息丢失情况。所以才引入了redis lua+延时消息。在lua脚本里记录消费业务的执行状态,延时消息保证消息一定不会丢失。
示例代码:
lua代码:
local status = redis.call('get',KEYS[1]);
if status == nil //不存在,则redis放入唯一key和过期时间
then
redis.call('SETEX',KEYS[1],ARGV[1],1)
return "2" //设置成功
else //存在,返回处理状态
return status
end
消费者伪代码:
//调用lua脚本,获得处理状态
String key = null; //唯一id
int expiredTimeInSeconds = 10*60; //过期时间
String status = evalScript(key,expiredTimeInSeconds);
if(status.equals("2")){//设置成功,继续业务处理
//业务处理
}
if(status.equals("1")){ //已在处理中
//发送到延时队列
}
if(status.equals("3")){ //已处理成功
//什么都不做
}
总结
-
生产端的重复发送和消费端的重复消费会导致消息重复
-
解决重复消息的底层逻辑是幂等
-
实现幂等的核心思想是:唯一key+存储
-
具体实现方式:基于数据库强校验和基于redis的弱校验