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

简介: 公司旧系统中发现一个严重bug:用户支付一年服务费,系统却将有效期增加了两年。经分析,原因是消息队列(MQ)向第三方服务发送了两次消息,且该接口未实现幂等性控制。此问题可能导致财务损失和信誉受损。解决方案包括:生产者端通过请求频率限制、幂等键等防重措施;消费者端利用缓存和数据库确保幂等性;消息队列层配置去重功能、TTL和死信队列等。

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


添加图片注释,不超过 140 字(可选)


原因分析:

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

问题反思:

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

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


问题严重性:


添加图片注释,不超过 140 字(可选)

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

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


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

一、消息重复原因

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

1、生产者重复或重试

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

2、消费者代码问题

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

3、网络问题

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

4、消息队列集群问题

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

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

二、消息重复解决方案

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

1、生产者端解决方案

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

第一道防线

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

第二道防线

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

第三道防线

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


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

2、消费者端解决方案

消息到达消费者端后

第一道防线

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

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

第二道防线

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

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

3、消息队列层解决方案

  • 利用消息队列中间件的去重功能,如设置成手动ACK、去重插件等。
  • 设置消息的有效时间(TTL),防止过期消息重复投放。
  • 配置死信队列(DLX),存储处理失败的消息。


相关实践学习
快速体验阿里云云消息队列RocketMQ版
本实验将带您快速体验使用云消息队列RocketMQ版Serverless系列实例进行获取接入点、创建Topic、创建订阅组、收发消息、查看消息轨迹和仪表盘。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
目录
相关文章
|
11月前
|
Java 测试技术 Spring
Spring Boot随机端口怎么动态扩容?
在Spring Boot中,可以通过`${random.int(2000,8000)}`在配置文件中设置随机端口,确保每次启动时端口不同。此外,还可以通过检测机制确保生成的随机端口未被占用,避免端口冲突。具体实现包括使用`System.setProperty`设置有效随机端口、自定义属性源以及直接设置`server.port=0`让Spring Boot自动选择空闲端口。推荐使用`server.port=0`以简化配置并避免冲突。
223 8
|
7月前
|
人工智能 Java API
MCP客户端调用看这一篇就够了(Java版)
本文详细介绍了MCP(Model Context Protocol)客户端的开发方法,包括在没有MCP时的痛点、MCP的作用以及如何通过Spring-AI框架和原生SDK调用MCP服务。文章首先分析了MCP协议的必要性,接着分别讲解了Spring-AI框架和自研SDK的使用方式,涵盖配置LLM接口、工具注入、动态封装工具等步骤,并提供了代码示例。此外,还记录了开发过程中遇到的问题及解决办法,如版本冲突、服务连接超时等。最后,文章探讨了框架与原生SDK的选择,认为框架适合快速构建应用,而原生SDK更适合平台级开发,强调了两者结合使用的价值。
10627 33
MCP客户端调用看这一篇就够了(Java版)
|
12月前
|
缓存 数据库 索引
所有的接口都需要幂等吗?
幂等性(Idempotency)源自数学,指多次执行某操作结果不变。在计算机科学中,它确保在网络通信、重试机制和并发操作下系统状态一致。常见应用如HTTP方法中的GET、PUT、DELETE及数据库操作中的SELECT、UPDATE、DELETE等。实现幂等性可通过唯一请求ID、数据库约束、状态检查等方法。并非所有业务都需要幂等处理,需根据业务逻辑、系统容错策略及性能复杂度权衡。
164 0
|
11月前
|
负载均衡 算法
架构学习:7种负载均衡算法策略
四层负载均衡包括数据链路层、网络层和应用层负载均衡。数据链路层通过修改MAC地址转发帧;网络层通过改变IP地址实现数据包转发;应用层有多种策略,如轮循、权重轮循、随机、权重随机、一致性哈希、响应速度和最少连接数均衡,确保请求合理分配到服务器,提升性能与稳定性。
2392 11
架构学习:7种负载均衡算法策略
|
NoSQL Java 数据库
重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验
重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验
329 0
|
11月前
|
Java 测试技术 应用服务中间件
Spring Boot 配置文件总结
Spring Boot 提供全局配置文件 `application.properties` 和 `application.yml`,用于修改自动配置的默认值。前者使用键值对配置,后者使用缩进和冒号。不同环境(开发、测试、生产)可切换配置文件,通过 `spring.profiles.active` 指定。例如,开发环境端口为4790,测试环境为4791,生产环境为4792。配置示例展示了属性、List、Map定义及引用方法。
375 14
|
存储 Java API
在springboot中缩短一个url链接
URL缩短服务是现代应用中常见的需求,用于将长URL映射为简短的唯一代码,便于分享。该服务具备多种功能,如自动过期、访问统计、防止重复及安全机制。通过Spring Boot构建RESTful API,使用H2数据库存储数据,Java UUID生成短码,并通过定时任务清理过期URL。用户可通过API提交长URL获取短链接,查询访问量,系统会自动重定向并记录访问次数。每天午夜自动清理过期URL,确保数据整洁。此项目结构清晰,涵盖实体类、Repository、Service和Controller等核心组件,适合快速开发和扩展。
281 2
|
12月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
714 2
|
11月前
|
JSON 监控 API
京东商品列表 API 接口系列(京东 API)
本简介介绍了使用 Python 调用京东商品搜索和详情 API 的方法。首先需安装 `requests` 库,`hashlib` 和 `time` 为内置库无需安装。API 支持按关键词、类别等条件搜索商品,返回 JSON 格式的商品列表,包括 ID、名称、价格等信息。通过商品 ID 可获取详细信息如描述、规格等。示例代码展示了如何生成签名并发送请求。应用场景包括市场调研、竞品监测和价格预警等,为企业决策提供数据支持。
|
12月前
|
消息中间件 存储 中间件
说说MQ在你项目中的应用(二)商品支付
本文总结了消息队列(MQ)在支付订单业务中的应用,重点分析了RabbitMQ的优势。通过异步处理、系统解耦和流量削峰等功能,RabbitMQ确保了支付流程的高效与稳定。具体场景包括用户下单、支付请求、商品生产和物流配送等环节。相比Kafka,RabbitMQ在低吞吐量、高实时性需求下表现更优,提供了更低延迟和更高的可靠性。
387 0