当前位置: 首页 > news >正文

【Java系统接口幂等性解决实操】

Java系统接口幂等性解决实操

  • 背景:
  • 1.幂等性
  • 2.实现方法
  • 3.确定方案
  • 4.幂等性核心
  • 5.实例代码

背景:

今天在之前的系统中碰到一个以往见过的面试题的实际使用情况,背景是这样子:

我们之前做的电商小程序,最近用户下单的时候出现了一个问题,从商城选了一款iPhone 16 Pro下单,创建订单支付金额后发现自己的订单列表的订单还是待支付(订单列表默认显示待支付状态的),上午的时候就来向客户反馈了,客户下午来找到我,说了情况。

我看下了后台发现这个用户订单的已经支付过了,没看到待支付的那个单子(因为系统有调度任务定时清理未支付的订单,用户上午的问题客户下午才来找,后台看不到要去数据库或者日志看),上k8s找了下当时的日志,发现确实同时创建了1、2两个订单,服务器时间只相差零点几秒左右,是前端同时调用了两次创建订单的接口,一开始我想着可能是前端页面没做控制,同一时间多次点击下单按钮就会多次调用接口。

于是我就给前端同事打电话说了一下情况,调整了一下前端的代码,完事之后测试了一下还是会出现这种情况。我们就商讨了一下,是否需要后端也加一下校验,防止订单重复提交,但是一时间又不知道从何下手,用户的参数相同时那就是买了一个iPhone 16 Pro然后又想再买一个iPhone 16 Pro的情况也不是没有,如何在参数上做校验。恍然间我想起来了以前刷面试题的时候有遇到一个幂等性的问题,好像就是防止接口重复调用的吧,但是具体怎么搞就不知道了,然后去刷DS。

1.幂等性

幂等性:
理论:

是数学和计算机科学中的一个重要概念,指同一操作多次执行与单次执行的效果一致。

实际:

在高并发的电商场景下,用户可能会因为网络延迟或其他原因多次点击提交按钮,导致生成重复订单(携带相同参数短时间内重复调用接口)。

2.实现方法

  1. 唯一标识‌:客户端生成唯一请求 ID(如 UUID),后端校验该 ID 是否已处理过。若已存在,则拒绝请求;若不存在,则处理请求并存储 ID。

  2. 状态机‌:确保业务操作仅在特定状态下执行,如订单状态从“待支付”到“已支付”仅允许一次变更。‌‌

  3. Token机制‌:客户端预获取Token,服务端验证后立即失效,避免重复提交。‌‌

  4. 分布式锁:通过 Redis 的 SETNX(SET if Not eXists)命令,为每个订单生成一个分布式锁。处理订单时,只有成功获取锁的请求才能继续执行,防止重复提交。

  5. 数据库唯一约束:在数据库订单表中设置唯一索引(如 用户ID + 商品ID + 订单号),当重复提交订单时,数据库会抛出唯一约束异常,后端捕获异常并拒绝重复请求。

3.确定方案

最后确定了下方案,因为我们的电商小程序只是分布式部署,QPS极低(低的可怜,理论上讲我们的所有用户同时去下单都不会达到高并发的标准,QPS达到1000以上,没错,基本没什么用户),所以使用唯一标识方法就足够了。

  1. 前端在进入创建订单页面时生成一个唯一的 uniqueId,用于标识本次订单创建请求;

  2. 第一次请求到达后端时,后端将 uniqueId 存入 Redis,并设置一定有效期(如5分钟);

  3. 后续的请求中,后端先验证 uniqueId 是否已存在于 Redis 中,存在:表示是重复请求,直接拒绝;不存在:表示是新请求,继续处理订单业务逻辑,并将 uniqueId 存入 Redis。

由此结合了幂等性校验和缓存验证,在代码中需要先进行幂等性处理,再去进行业务校验。为什么,因为幂等性校验的核心是确保同一请求不会重复执行业务逻辑,避免重复提交、重复支付的问题。

4.幂等性核心

其核心思想是:

提前拦截重复请求:如果请求是重复的,直接返回结果(如“请勿重复操作”),就不用进入后续的业务流程了。
节省资源:避免重复的数据库操作、业务逻辑处理或第三方接口调用,减少系统消耗。

5.实例代码

我项目的真实代码,已部署!!!

		//校验创建订单请求的唯一id 接口幂等性处理String uniqueId = bo.getUniqueId();if (StringUtils.isNotBlank(uniqueId)) {if (redisService.hasKey(RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId)){throw new BizException(BizErrorCodeEnum.ORDER_UNIQUEID_EXIST);}String uniqueIdKey = RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId;redisService.set(uniqueIdKey, uniqueId, loginTimeOut);}//业务校验//业务校验代码

不过这里建议是优化一下redis操作,由于Redis 的 hasKey() + set() 是两个独立操作,存在并发请求时可能都会通过验证的问题,使用 Lua 脚本可以确保校验和写入的原子性。

		// Lua 脚本:如果 key 不存在,则设置值并返回 true,否则返回 falseString script = "if redis.call('exists', KEYS[1]) == 0 then " +"redis.call('setex', KEYS[1], ARGV[2], ARGV[1]); " +"return 1; else return 0 end";// 执行 Lua 脚本String uniqueId = bo.getUniqueId();Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList(RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId),"1", 300 // 300 秒 = 5 分钟);if (result == 0L) {throw new BusinessException("请勿重复提交订单");}

那为什么我项目的代码没有这样写呢 ,xianzaidouxiewanleyihouganshenme (拼音不是乱码)?

果然经验不是靠八股文背出来的,还得亲身从项目中体会、实践。

http://www.lryc.cn/news/601511.html

相关文章:

  • DeepSeek实战--无头浏览器抓取技术
  • Java常用日志框架介绍
  • 五度标调法调域统计分析工具
  • 设计模式(五)创建型:原型模式详解
  • [spring6: Mvc-异步请求]-源码分析
  • 设计模式(三)创建型:抽象工厂模式详解
  • 微服务架构面试题
  • Flutter开发实战之测试驱动开发
  • linux根据pid获取服务目录
  • Gradio.NET 中文快速入门与用法说明
  • IIS发布.NET9 API 常见报错汇总
  • 从 .NET Framework 到 .NET 8:跨平台融合史诗与生态演进全景
  • 9-大语言模型—Transformer 核心:多头注意力的 10 步拆解与可视化理解
  • 电商项目_核心业务_数据归档
  • Java枚举类enum;记录类Record;密封类Sealed、permits
  • Java面试宝典:MySQL执行原理一
  • 300.最长递增子序列,674. 最长连续递增序列,
  • Ubuntu服务器安装与运维手册——操作纯享版
  • 负载均衡Haproxy
  • [AI8051U入门第十一步]W5500-服务端
  • 嵌入式学习日志————对射式红外传感器计次
  • 【MySQL篇】:MySQL基础了解以及库和表的相关操作
  • DP之背包基础
  • SignalR 全解析:核心原理、适用场景与 Vue + .NET Core 实战
  • ASP.NET Core 高并发万字攻防战:架构设计、性能优化与生产实践
  • 一个MySQL的数据表最多能够存多少的数据?
  • 迷宫生成与路径搜索(A算法可视化)
  • 调用通义千问大模型实现流式对话
  • 用 Python 轻松实现时间序列预测:Darts N-BEATS
  • 安卓怎么做一个像QQ一样的开关切换控件