作为一名java程序员,面试时时常会遇到类似这样的问题:
- 如何保证消息不被重复消费?
- 订单时常出现重复支付,该怎么办?
- 如何避免订单重复提交?
……
这就可能涉及到消息消费问题,关于消息消费问题,这个是消息队列的基本问题,面试官之所以问这样的问题我想本质上还是考验求职者对使用消息队列如何保证幂等性。
关于重复消费的问题,首先我们需要知道是有哪些场景会出现:
类似 RabbitMQ、kafka这样优秀的消息队列中间件都有可能会出现消息重复消费的问题,这并不是框架的问题,而是框架本身就没有考虑限制消息重复消费问题,把这个操作交给开发者自己来处理了。
下面以 kafka为例简单阐述下消息重复消费问题。
我们知道,kafka 有个offset 的概念,既每当有消息写进去时,每个消息都会有一个对应的offset,代表消息的序号,然后 consumer 消费了数据后,每隔一段时间,定期会把自己消费过的消息的 offset 提交,表示“这些我已经消费过了,下次我就从上次消费到的 offset 地方开始消费”。
听上去很美好,但现实很骨感,实际场景中往往会有一些出乎意外的事情出现。
譬如有时候需要重启系统,正常重启还好,要是直接 kill 掉进程重启,这就难受了。
因为这会导致 consumer 有的消息虽然处理了,但是没来得及执行提交 offset动作,而重启后少数消息就会被再次消费。
例如数据 A/B/C 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,假设分配到的 offset 依次为 101/102/103。
消费者从 kafka 去消费的时候,也是按照这个顺序去消费。
假如当消费者消费了 offset=102 的这条数据,刚准备要去执行提交 offset 到 zookeeper,此时消费者进程被重启了。
那么此时消费过的数据 A/B 的 offset 实际上并没有提交,kafka 也就不知道之前已经消费到了 offset=102 这条数据。
重启后,consumer 会对 kafka 老哥说,老哥,你接着把上次消费到的那个地方后面的数据继续给我。
那么问题来,由于之前的 offset 并没有提交成功,此时数据 A/B 会再次传过来,要是消费者没有做去重操作的话,就会导致重复消费了。
要是消费者是做这样的操作:
消费一条消息就往数据库里插入一条数据的话,这时可能就把数据 A/B 在数据库里插入了两次了,于是就产生了脏数据了。
其实重复消费不可怕,可怕的是忽略了重复消费后,该如何保证幂等性?
譬如有这样一个业务需求,需要消费一条消息就往数据库里插入一条记录,要是有一条消息被重复消费两次,那么是不是就插入两条记录了?
我想是不是可以先这样,当消息消费到第二次的时候,我们先判断是否之前是否消费过了,要是消费过了就直接不要了,这样就可以保证只有一条数据,从而保证了数据的正确性。
一条数据重复消费两次,而数据库里就只有一条数据,这不就保证了系统的幂等性。
什么叫幂等性?
幂等(idempotent)是一个数学与计算机学概念,常见于抽象代数中。在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
通俗点说,就是你已经有女朋友了,最近不知怎么啦,走桃花运了,有很多美女为你投怀送抱, 这种情况下你都不会动心,而不是来者不拒,多多益善。
貌似有点扯远了,回归正题。
关于幂等性,我的理解是,就是有一条数据或者一个请求,重复发起多次请求,我们要确保对应的数据不会受影响。
那么问题来了,如何保证美女为你投怀送抱, 都不会动心呢?这个问题难不难,把机会留给男同胞们。
我们还是来探讨如何保证消息队列消费的幂等性?
这个需要结合具体业务来思考:
比如你消费后就直接执行入库操作,那么我们可以先根据主键去查询,要是该数据已经有了就不要执行插入动作,转向更新动作。
比如用 redis,那问题不大,因为每次都是 set,属于天然幂等性。
或许有的业务场景较复杂些,上面两个场景都不是,可能需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 orderId 之类,然后你这里消费到了,下一次再重复消费了,先根据这个 orderId 去比如 redis 里查,如果没有消费过,就处理,要是处理过了,就不要重复操作。
关于消息消费,作为一名开发者,我们不仅要考虑消费完整性,还需要考虑消息被重复消费问题。
譬如如何避免重复消费?或者说即使重复消费了也要保障数据正确性?
上面仅仅是个人看法,在项目实际运作过程中,如何保证消息队列的消费是幂等性的,还得需要结合具体的业务具体分析。
由于笔者水平有限,文中纰漏之处在所难免,权当抛砖引玉,不妥之处,请大家批评指正。
-END-