1、产生重复消费的原因
无论是那种消息队列,造成重复消费原因其实都是类似的。正常情况下,消费者在消费消息时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发送的确认信息形式不同,例如RabbitMQ是发送一个ACK确认消息
,RocketMQ是返回一个CONSUME_SUCCESS成功标志
,kafka offset的概念
,
kafka offset概念:就是每个消息写进去,都有一个offset,代表他的序号,然后consumer消费了数据之后,
每隔一段时间,会把自己消费过的消息的offset提交一下
,代表已经消费过了,下次要是重启,就继续从上次消费到的offset来消费
MQTT中规定了三种传递标准:
At most once:至多一次。消息传递时,最多会被送达一次,没有什么可靠性,允许消息丢失。
At least once:至少一次。消息传递时,至少会被送打一次,保证消息可靠性,但存在多次消费的可能。
Exactly once:恰好一次。消息传递时,只会被送达一次,不允许丢失也不允许重复。
依照以上标准,似乎我们只要保证消息队列符合Exactly once的标准,就可以在保证消息可靠性的前提下解决重复消费的问题
。无非就是时间和空间的切换,采用Exactly once
标准固然符合要求,但也势必会带来一定的性能损耗
,就跟分布式锁类似,而对于At least once,我们则可在业务层面保证数据不会重复消费
。
幂等性
是指一个操作其执行任意多次所产生的影响均与一次执行的影响相同。比如你同样的参数调用我这个接口,调用多少次结果都相同
kafka产生重复消费的原因:
- 强行kill线程,导致消费后的数据,
offset没有提交(消费系统宕机、重启等)
。- 设置offset为自动提交,关闭kafka时,
如果在close之前,调用 consumer.unsubscribe()
则有可能部分offset没提交,下次重启会重复消费。- (重复消费最常见的原因):消费后的数据,当offset还没有提交时,partition就断开连接。比如,通常会遇到消费的数据,处理很耗时,导致超过了Kafka的session timeout时间(0.10.x版本默认是30秒),那么就会re-blance重平衡,此时有一定几率offset没提交,会导致重平衡后重复消费。
- 当消费者
重新分配partition
的时候,可能出现从头开始消费的情况,导致重发问题。- 当消费者消费的速度很慢的时候,可能在
一个session周期内还未完成,导致心跳机制检测报告出问题
。- 并发很大,可能在
规定的时间(session.time.out默认30s)内没有消费完
,就会可能导致reblance重平衡,导致一部分offset自动提交失败,然后重平衡后重复消费
2、避免方式
- 让每个消息携带一个全局的唯一ID(雪花算法、UUID等等),即可保证消息的幂等性,具体消费过程为:
- 消费者获取到消息后先根据id去查询redis/db是否存在该消息
- 如果不存在,则正常消费,消费完毕后写入redis/db
- 如果存在,则证明消息被消费过,直接丢弃。
- 通过数据库的唯一键实现消息的幂等性
可以在设计
消息结构
的时候设置一个对应数据库唯一键的列字段
,业务成功后将此字段作为唯一键保存入数据库
。当同样的ID做保存的时候就会出现违反数据库唯一约束异常
,这里的主键可以是单独的,也可以是组合的列。这种方式可以在任何支持“INSERT IF NOT EXIST
”的存储系统中适用。
- 通过版本号/数据快照实现幂等
其实这种方式有点
类似于乐观锁
的实现方式,就是需要消息中带有此业务当前一个瞬时状态的值
,通过这个值与业务当前数据比较来判断是否执行更新操作。
比如:将订单号为00001的状态从01变更为02(当订单00001的状态为01就改为02),这样一条重复的消息进来是不会对00001的订单做任何影响的。此时状态就是一个前置条件。
再比如:将商品A的库存从500中减1(当商品A的总库存为500则减1)。类似这些需要在设计消息结构的时候带上一些业务属性活数据。
另外一种办法就是给消息增加一个类似数据库的version字段
,在每次消费更新的时候比较当前数据的版本号是否与消息中带的版本号一致,来判断是否执行消费。