如果对方没做幂等!记一次生产订单重复的反思

最近公司公司的旧系统中发现了一个bug。业务部门反馈,尽管用户只支付了一年的服务费用,系统却将有效期增加了两年。

原因分析:

到底是什么原因呢?

经过日志分析,发现消息队列(MQ)向第三方服务发送了两次消息。由于第二方服务的接口没有实现幂等性控制,导致了这一重大的bug。

问题反思:

想一想,其实这种问题很简单,怎么会出这种问题呢?

一般来说系统开发中不免会出现不少类似的问题,类似问题的出现并不罕见。一般系统都是从无到有,业务从少到多、早期可能也就几个或者一个研发人员开发出来的,后面升级或重构甚至推倒重来。。。

问题严重性:

这类bug对于系统和业务的影响极大,尤其是涉及金钱的业务,可能会导致严重的财务损失和信誉问题。

在系统开发过程中,正常的服务会确保服务的幂等性,尤其是在涉及金钱交易的业务中。

下面我们就来复盘一下重复消息的产生原因及相应的解决方案:

一、消息重复原因

在消息队列(MQ)系统中,消息重复的情况主要有以下几种原因:

1、生产者重复或重试

生产者代码没有阻止重复请求或处理发送消息后的响应情况,在连接超时等异常情况下,重复发送了相同的消息。当然还有一种情况就是生产者本身设置了重试机制,但重试机制不完善造成重复发送消息。

2、消费者代码问题

消费者在处理消息过程中出现异常,没有正确地手动确认消息,那么该消息会重新投递,导致重复。

3、网络问题

网络延迟或临时断开连接可能导致MQ消费者没有收到确认消息,从而重新消费同一条消息。

4、消息队列集群问题

MQ集群节点之间的状态同步问题,可能导致消息被多个节点重复投递。

当然还有别的一些原因.....

二、消息重复解决方案

针对消息重复的问题,可以从以下几个方面采取解决措施:

1、生产者端解决方案

消息的生产者端的消息溯源还是用户的请求。

第一道防线

首先,前端可以通过禁用按钮、显示加载状态等方式防止用户重复点击提交按钮。禁用按钮是指在用户点击提交按钮后,立即将该按钮禁用,防止再次点击。显示加载状态则是在提交请求后显示加载动画或状态提示,告知用户请求正在处理中。高级一点的做法是使用JavaScript脚本控制按钮的状态,确保用户无法重复提交。不过,前端防重只是第一道防线,因为前端措施容易被绕过,例如通过浏览器开发者工具修改页面元素或捕获和重发请求报文。

第二道防线

如果前端防重措施被绕过,用户可以直接通过程序生成大量请求,此时服务后台需要采取进一步的防重措施。后台可以通过请求频率限制、幂等键、时间戳、哈希值等方式来防止重复请求。请求频率限制是指限制每个用户在一定时间内的请求次数,防止短时间内大量重复请求。幂等键则为每个请求生成唯一的标识符,并在后台存储和检查这些标识符,确保每个请求只处理一次。时间戳和哈希值也是有效的防重手段,通过附加时间戳验证请求的时效性,或对请求内容生成哈希值并检查其唯一性,确保相同内容的请求只处理一次。

第三道防线

在消息的生产者端,也需要采取类似的防重措施以确保消息不被重复发送。例如,在发送消息前生成唯一的消息ID(例如UUID),并将其包含在消息体中。发送消息后,同步等待服务器的回执确认,确保消息只发送一次。此外,可以将消息ID存储在数据库或缓存中,并在发送前检查该ID是否已存在,防止重复发送。当然生产者端一般不会利用这个ID或者叫幂等键来去重,一般会结合消息者端一起来实现。

通过这些多层次的防重措施,能够有效减少消息重复的发生,保障系统的稳定性和可靠性。

2、消费者端解决方案

消息到达消费者端后

第一道防线

消费者端可以使用幂等键来判断请求是否已经处理过。通常情况下,缓存不宜存放过多数据,而重复请求大多数集中在一定时间范围内,因此将幂等键存储在缓存中是一种有效的解决方案。

具体来说,幂等键可以与请求的唯一标识关联,并存储在缓存系统(如Redis或Memcached)中。为每个幂等键设置一个合理的过期时间,可以有效地过滤掉在这个时间范围内的重复请求。通过这种方法,大部分重复请求可以在缓存层被拦截,从而减少对后端服务的压力。

第二道防线

尽管缓存机制可以一定程度上确保幂等性,但是当缓存过期后,可能会再次收到相同的请求。为了更加彻底地解决这个问题,我们需要在数据库层面进一步加强幂等性保证。

当收到请求到达数据库层时,首先检查该请求的幂等键是否已经存在于数据库中。如果存在,则说明该请求已经被处理过,直接返回之前的结果即可;如果不存在,则继续执行请求的业务逻辑。处理完成后,将请求的幂等键及结果持久化到数据库中。当然大多数人还是会选择更简单点的直接把幂等键设置为唯一索引,当报键值重复异常时就忽略些请求直接返回。

3、消息队列层解决方案

  • 利用消息队列中间件的去重功能,如设置成手动ACK、去重插件等。
  • 设置消息的有效时间(TTL),防止过期消息重复投放。
  • 配置死信队列(DLX),存储处理失败的消息。
相关推荐
喝不完一杯咖啡2 天前
【RocketMQ】记录一次RocketMQ消费延迟问题排查思路
java·消息队列·rocketmq
luo♛4 天前
RabbitMQ数据隔离
分布式·消息队列·rabbitmq
韵秋梧桐5 天前
基于RabbitMQ原理的自定义消息队列实现
java·分布式·消息队列·rabbitmq·生产者消费者模型
jupiter_8885 天前
消息队列 有序 消费模式 主题 分区 高可用 持久 日志 崩溃恢复 事务 重试投递 崩溃最多丢失多少数据 日志模式
消息队列
AlexDeng5 天前
RocketMQ实战:一键在docker中搭建rocketmq和doshboard环境
消息队列·rocketmq
crossoverJie10 天前
Pulsar客户端消费模式揭秘:Go语言实现ZeroQueueConsumer
后端·消息队列
CodeBlogMan10 天前
【主流技术】聊一聊消息队列 RocketMQ 的基本结构与概念
中间件·消息队列·进阶
程序猿小D13 天前
第一百一十八节 Java面向对象设计 - Java接口
java·开发语言·jvm·jdk·接口·哈希算法·面向对象