什么情况下会出现消息丢失,如何解决?
我们先看看生产消息到消费消息全流程
从全流程看,投递消息,存储消息,消费消息,各个环节都可能出现消息丢失,下面我们分别探讨下。
1 生产者投递消息给消息队列过程中,可能出现网络异常(如网络抖动)导致消息丢失。
我们可以通过捕获网络异常,出现异常时可以重试投递消息,一般可以设置3次即可。一般经过重试之后,投递失败的概率已经大大降低了。如果重试3次后依然未能成功,需要抛出未能成功投递消息的异常。
如果业务要求对消息不丢失要求比较高,3次重试失败后可以将消息存到数据库,设置定时任务,间隔一段时间再重试,直到成功投递,再修改数据库的数据状态为已投递。
2 消息队列服务端出现异常宕机,也可能出现消息丢失。
Kafka 中大量使用了页缓存(pagecache),消息都是先被写入页缓存,然后由操作系统负责刷盘任务,虽然Kafka提供了同步刷盘及间断性强制刷盘的功能,同步刷盘是可以保证消息不丢失,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失。不过考虑到性能,一般我们都会设置异步间断性强制批量刷盘,而消息可靠性可以由多副本机制来保障。
3 消费者如果设置 offset 自动提交,也可能出现消息丢失。
消费者设置了offset自动提交,如果拉取的消息还没有处理完,offset 已经自动提交了,此时如果服务掉电宕机了,可能导致这部分还没处理完的消息丢失了。另外因为消息解析异常,导致消息不能正确被处理,也可能导致消息丢失。因此设置offset自动提交时需要知道有这个问题,如果不希望消息丢失,就不要设置自动提交offset,可以消费处理完当前消息,再手动提交。
还有一种情况是服务部署在k8s,服务缩容,有些服务被下线掉,如果设置了自动提交,也可能导致消息丢失,这种可以通过做个优雅关闭的控制,监听服务关闭事件,需要等到消息都被消费完成后才能关闭服务。
以上分析消息丢失的一些解决方法,那我们怎么知道消息丢失了呢?如何检查呢?
答案是需要构建消息全局ID,可以通过在生产端的拦截器去给消息加上全局ID,并保存到数据库(如构建表,字段为全局id,status),消费端消费消息完成后可以修改对应表中的status,之后通过定时任务检测表中id的status,就能知道消息是否有丢失。关于全局ID的构建可以参考这篇文章 mp.weixin.qq.com/s/zZhQ055vw…
什么情况下会出现消息重复消费,如何解决?
1 生产者发送消息出现异常,重试机制可能出现重复消费。
上面我们讲到为了避免生产端生产的消息丢失,引入了重试机制。但是如果是因为网络抖动或者响应超时,虽然生产端接收到了异常,但是消息可能已经发送成功了,之后又重试,可能导致重复发送了两条消息。
2 消费端消费消息时,为了提升性能,我们会拉取一批消息处理,比如拉取了50条消息,只处理了20条消息,offset 没能提交,这时候服务宕机了。之后服务正常启动后,继续拉取消息消费,因为之前offset 未提交,之前处理的20条消息,还会重复消费。除了服务宕机的场景,消费端服务扩容,增加新实例,会触发重平衡,旧实例原来消费的分区被分给了新的实例去消费。重平衡前如果旧实例没提交offset,也会导致新的实例分到对应分区后,重复消费之前旧实例已经处理过的消息。
如何解决呢?
答案是通过存储幂等来解决。
问题又来了,什么是幂等,在这个场景下怎么实现呢。
幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。
在该场景中,幂等就是多次重复消费,结果都是一样的,不会出现数据重复或者脏数据的问题。
怎么实现?
针对插入操作:数据库唯一主键
数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于插入时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,如果是分库分表的,该主键不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性,全局id的生成我们上面有给了一个文章链接,可以参考。
针对更新操作:数据库乐观锁
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。在生产端增加一个版本号,这样每次消费端对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值,版本号如果小于等于当前版本号,则不能操作,否则,可以对版本号做递增操作后再更新。
总结
虽然我们讨论了解决消息丢失和消息重复消费的一些方案,但是实际项目中,还是要具体问题具体分析,要保证消息不丢失,不重复所做的处理,显然带来更大的复杂度和维护量,性能也有一些折损。并不是所有的服务都要求消息不能丢失的,比如收集一些日志消息,个别日志丢失,还是允许的。为了避免丢失,把数据存储到数据库,也是会造成性能有较大的下降的。也不是所有服务都要求消息不能重复消费,毕竟做幂等性的校验,也是会带来性能的损耗。另外关于offset的提交,也有两面性,offset 提交了,消息还没处理完,服务宕机了,就会出现消息丢失,如果是先处理消息,后提交offset,在还没提交offset前,服务宕机了,会出现重复消费,因此都要我们根据业务场景做一些权衡。这边给个小编的建议,如果要在消息丢失和消息重复做个选择,我们还是优先选择允许消息重复,而不让消息丢失。
最后,首发文章都在公众号,欢迎关注公众号: 程序员榕树