RabbitMQ、RocketMQ、Kafka等消息队列如果不做任何的防护措施都是有可能出现消息重复消费的情况的。保证消息的不可重复消费一般都是需要开发人员来进行相对于的设置。
Kafka 实际上有个 offset 的概念,每个写入的消息都会有一个 offset ,代表的是消息的序号,在 consumer 消费之后,每隔一段时间(定时定期),都会将自己消费过的 offset 进行提交,标识一下哪些数据是自己已经消费了的。下次重启或者其他情况需要继续消费的时候就从该位置继续消费。
理想是美好的,现实确实很骨感的。有时候系统重启,如果是比较急的情况下,直接 kill 掉了进程进行重启。这时候如果有些consumer 消费完数据还没来得及提交 offset,这时候就尴尬了,重启之后就会将这段数据进行再次消费。
eg:有这么个场景。数据1、2、3依次进入 kafka,kafka 就会给这三个数据分配一个offset,代表这条数据的序号,现在假设他们的offset 分别为 152、153、154;消费者从 kafka 消费的时候就是按照这个顺序去进行消费的。假如当消费者消费了 offset = 153 的这条数据。重启之后,消费者会去找 kafka 将它上次消费到的那个地方的后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1、2就会再次提交过来,如果没有进行去重操作的话,这就导致了重复消费。
如果消费者消费数据是进行写库操作的话,此时 1、2两条数据就在数据库内插入了2次,这个时候数据就不对了。
其实重复消费不可怕,可怕的是没有考虑到重复消费之后,怎么保证幂等性。
eg:
现在一个系统,消费者每次消费一条数据就往数据库当中插入一条数据,要是你重复消费了两次,你就插入了两条数据,这个时候数据对不上了。但是如果你第二次消费的时候,判断一下这个数据是否已经消费过了,若已经消费了就直接丢弃,这样就只保留了一条数据。
一条数据重复出现两次,数据库里面只有一条数据,这就是保证了幂等性。
幂等性:通俗的来说,就是一个数据或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
现在来说说怎么保证消息队列消费的幂等性。
其实还是得结合业务来思考:
- 如果你拿数据需要写库,你可以先根据主键查询一下,如果这条数据有了,就不插入了,update 一下就行了。
- 如果你拿数据是写Redis,这样关系就不是很大,因为每次 set 都是天然得幂等性的。
- 如果不是上面的这两种情况,就会稍微的复杂一些。在生产者发送每条数据的时候加上一个全局唯一的ID,类似于订单ID之类的东西,如果你消费到这条数据的时候就根据ID去 Redis 中查询一下,这条数据有没有消费过,如果没有消费过就将这个ID写入 Redis ,然后进行后面的业务逻辑处理;如果是消费过的就不进行处理,进而保证了幂等性。
- 基于数据库的唯一主键来保证重复数据不会重复插入多条。因为有个唯一约束,重复插入数据就会报错,不会导致数据库中出现脏数据。