重复消费:高并发场景下怎么保证消息不会重复消费?
布隆过滤器
- 布隆过滤器用于检索元素是否在集合里
- 基本原理是集合加入某个元素时,通过哈希算法把元素映射到比特数组的N个点位,把对应点位设置为1
- 查找时只需要看对应比特位是不是1
- 如果对应的N个点都是1,这个元素可能存在。如果布隆过滤器认为元素存在,实际上不存在,叫假阳性
- 任何一个点是0,那么必然不存在
重复消费的可能原因
- 生产者重复发送:业务发送消息时,收到一个超时响应,很难判断是否真的发出去了,就会考虑重试,重试就可能导致一个消息发送多次
- 消费者重复消费:消息处理完后,准备提交,这个时候突然宕机了,没有提交。恢复后就会再次消费同一消息
避免重复消费的原则:一定要把消费逻辑设计成幂等的
基本思路
唯一索引
本地事务将数据插入到唯一索引
-
最简单的幂等方案就是唯一索引。在业务处理时,更具消息内容往业务表中插入一条数据,业务表有唯一索引
- 如果插入成功说明没有被处理过,可以继续处理
- 如果插入出现唯一索引错误,说明这条消息被处理过
-
亮点
- 使用唯一索引,最好的方式是把唯一索引和业务操作组合在一起做成一个事务。收到消息时先开起事务,把数据插入到唯一索引,然后执行业务操作,最后提交事务,提交事务后业务就成功了,就算接下来提交消息失败了也没事,后面重复请求会被唯一索引拦截。
非本地事务将数据插入到唯一索引
-
没有本地事务时,只能追求最终一致性,要依赖第三方检测
-
先把数据插入到唯一索引中,避免重复消费,此时数据保持初始化状态。然后执行业务操作,执行之后,把唯一索引的数据更新为成功状态
-
可能出问题的地方是:第二步成功了,但是第三步失败了
- 这个时候就需要启动一个异步检测系统定时扫描初始状态的唯一索引数据。检测唯一索引的数据和业务数据是否一致
- 如果不一致,说明业务操作成功,把唯一索引的数据标准成功
- 如果这时业务失败就出发重试
亮点方案:布隆过滤器+Redis+唯一索引
这个方案就是通过布隆过滤器+Redis+唯一索引实现层层削流,确保到达数据库的流量最小化
-
方案思路是:使用唯一索引前尽可能削减流量。
- 第一层使用布隆过滤器。
- 为了避免假阳性和进一步削流,布隆过滤器和数据库之间引入一个Redis
-
基本流程:
- 第一步,一个请求进来时,利用布隆过滤器判断有没有被处理过。如果布隆过滤器说没有处理过,就可以直接处理。如果处理过,就需要执行下一步
- 第二步,利用Redis存储近期处理过的key。如果key存在说明被处理过,直接返回。否则进入第三步
- 第三步,利用唯一索引,如果唯一索引冲突,说明已经处理过了
方案存在的细节知识
-
更新顺序
-
业务方第一次处理完请求,是先更新布隆过滤器还是Redis,或者先更新数据库唯一索引?
- 先更新数据库唯一索引,因为数据库中的数据时最准确的。把更新数据库的操作放在业务本地事务中。业务方提交时一起提交,数据库事务提交后,再去更新过滤器和Redis。这时失败也没影响,最终重复请求被处理时会报唯一索引冲突
-
-
Redis key的过期时间
- Redis是为了进一步削流,要确保重复请求过来时,key还没过期。某个业务重试请求10分钟内到,就可以设置11分钟
-
简化方案
-
业务并发不高时可以考虑简化方案
- 第一种只用布隆过滤器+唯一索引。Redis资源不足或者重复请求间隔太长时比较适合
- 第二种中用Redis+唯一索引。Redis资源多,且担心布隆过滤器假阳性问题时比较适合
- 建议先用第一种简化方案,等发现假阳性严重时,再引入Redis
-