接口幂等别再只靠前端防抖:@Idempotent 注解 + Redis 锁 + 请求指纹,重复提交直接拦
下单、提现、结算这种接口前端防抖只能挡住手快双击挡不住网络重试、客户端重复请求、后端超时重发。最典型的翻车现场用户提现点了两次钱包扣了两笔客服电话直接被打爆。qkl-boot 里用Idempotent注解 Redis 锁把重复提交拦截在业务逻辑之前这篇讲实现思路和踩坑。一个注解声明式幂等业务侧用法极简——方法上挂个注解就行不用侵入业务代码Idempotent(expireSeconds5,message请勿重复提交请稍后再试)publicvoidwithdraw(LonguserId,BigDecimalamount){// 业务逻辑}注解定义本身也很简单两个参数有效期和提示文案Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)DocumentedpublicinterfaceIdempotent{/** 幂等锁有效期单位秒同一请求在此窗口内视为重复 */intexpireSeconds()default5;/** 命中重复提交时的提示文案 */Stringmessage()default请勿重复提交请稍后再试;}切面核心setIfAbsent 抢锁IdempotentAspect用Around拦截核心就一行setIfAbsent——Redis 里键不存在才写入写进去就算抢到锁抢不到说明窗口期内有人提交过了Around(annotation(idempotent))publicObjectaround(ProceedingJoinPointpjp,Idempotentidempotent)throwsThrowable{StringterminalresolveTerminal();StringuserIdresolveUserId();StringfingerprintbuildFingerprint(pjp);StringlockKeyRedisKeys.idempotent(terminal,userId,fingerprint);BooleanacquiredredisTemplate.opsForValue().setIfAbsent(lockKey,1,Duration.ofSeconds(idempotent.expireSeconds()));if(Boolean.FALSE.equals(acquired)){thrownewQklBizException(ErrorCode.IDEMPOTENT_CONFLICT,idempotent.message());}returnpjp.proceed();}锁的维度是终端 用户 请求指纹指纹 HTTP 方法 URI 参数摘要MD5 压缩成定长字符串。这样同一个用户重复提交同一笔请求会被拦但不同用户、不同参数互不影响privateStringbuildFingerprint(ProceedingJoinPointpjp){MethodSignaturesignature(MethodSignature)pjp.getSignature();StringBuilderrawnewStringBuilder();ServletRequestAttributesattributes(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes!null){HttpServletRequestrequestattributes.getRequest();raw.append(request.getMethod()).append(:).append(request.getRequestURI()).append(:);}raw.append(serializeArgs(pjp.getArgs()));returnDigestUtils.md5DigestAsHex(raw.toString().getBytes(StandardCharsets.UTF_8));}序列化参数有讲究把不能序列化的排除掉参数里如果混进了MultipartFile或者HttpServletRequestobjectMapper.writeValueAsString直接抛异常幂等就废了。切面里先过滤privateStringserializeArgs(Object[]args){if(argsnull||args.length0)return;try{Object[]filteredArrays.stream(args).filter(a-!(ainstanceofMultipartFile)!(ainstanceofHttpServletRequest)!(ainstanceofHttpServletResponse)).toArray();returnobjectMapper.writeValueAsString(filtered);}catch(Exceptione){returnArrays.toString(args);// 兜底序列化失败就用 toString别让幂等自身挂掉}}未登录接口怎么幂等用 IP 兜底不是所有接口都要求登录匿名接口也要防重复。切面里拿不到 userId 时退回用 IP 作为用户维度privateStringresolveUserId(){LoginUserloginSecurityContextHolder.get();if(login!nulllogin.getUserId()!null){returnString.valueOf(login.getUserId());}ServletRequestAttributesattributes(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes!null){Stringipattributes.getRequest().getHeader(X-Forwarded-For);if(!StringUtils.hasText(ip)){ipattributes.getRequest().getRemoteAddr();}else{ipip.split(,)[0].trim();// 多级代理取第一个}if(StringUtils.hasText(ip)){returnip:ip;}}returnanonymous;}X-Forwarded-For 取第一个 IP这个细节踩过坑多个代理会拼接一串 IP直接用整个串做键同一个用户走不同代理链就变成不同用户幂等失效。踩坑记录同一个 IP 后面挂着一整个办公室现象办公室场景下A 同事提交了某个匿名接口的请求B 同事紧接着提交相同参数的请求居然被当成重复提交拦截了业务方来问是不是出 bug 了。排查过程看 Redis 里幂等键发现键的前缀是ip:xxx.xxx.xxx.xxx两个同事出口 IP 一样。再看接口确实挂了Idempotent参数也一致——指纹相同IP 相同于是被判定重复。定位思路IP 兜底本身就是粒度妥协。登录用户用 userId 是精准的匿名用户没有身份维度只能拿 IP 凑合。但办公室场景多用户共享出口 IP把 IP 当唯一键必然误伤。问题不在实现在选型——匿名 写操作 高频场景不该指望 IP 级别的幂等。最终解决分场景处理。第一能登录的接口一律走 userId 维度匿名兜底只是保底。第二真正的写操作提现、下单前端必须传业务单号后端用唯一键 数据库唯一索引做持久化幂等——Redis 锁有 5 秒窗口窗口外重复照样穿透数据库唯一索引才是最终防线ALTERTABLEwithdraw_recordADDUNIQUEKEYuk_biz_no(biz_no);-- 业务单号唯一重复插入直接报错Redis 拦窗口内、唯一索引兜底窗口外两层叠加提现重复问题才算真正闭环。可直接复用的清单注解 AOP 声明式幂等业务零侵入方法上挂Idempotent即可。锁键 终端 用户 请求指纹MD5setIfAbsent 过期时间窗口内重复直接拒绝。参数序列化先过滤 MultipartFile/Request/Response兜底 toString 别让切面自己炸。未登录接口用 IP 兜底X-Forwarded-For 只取第一个 IP。资金类写操作必须再加业务单号 数据库唯一索引Redis 窗口外靠它兜底。项目源码https://gitee.com/gzqkl/qkl-boot

相关新闻

硕词 AI PPT 自动生成 —— 开题答辩、毕业答辩一键完成

硕词 AI PPT 自动生成 —— 开题答辩、毕业答辩一键完成

论文写完后,答辩 PPT 制作又成为新的压力。硕词 AI 支持 AI PPT 自动生成,访问 www.shuociai.com,可根据论文内容一键生成完整答辩 PPT。 PPT 内容包括研究背景、意义、方法、框架、结论、创新点、展望等完整结构,页面简洁大方&am…

2026/10/10 4:44:19 阅读更多 →
Spirent TestCenter 实战:从零跑通 RFC 2544 吞吐量测试

Spirent TestCenter 实战:从零跑通 RFC 2544 吞吐量测试

简介:《Spirent TestCenter简易操作手册》以PPT形式呈现,是一份面向网络测试工程师、设备调试人员及网络运维者的实用入门资料,旨在帮助零基础或初级使用者快速上手思博伦TestCenter网络测试仪表,掌握网络流量配置与管理方法。内容…

2026/10/10 4:43:19 阅读更多 →
Python爬虫实战:汽车车型评测数据全流程抓取指南

Python爬虫实战:汽车车型评测数据全流程抓取指南

很多做数据分析的朋友问过我,第一个实战爬虫项目选什么好。我的建议一直很明确:选一个以文章为主、但又不只是文章的垂直内容站。车型评测就是非常典型的例子——它有标题、发布时间、作者、评分、长正文、参数表,一口气覆盖了列表页、详情页…

2026/10/10 4:43:19 阅读更多 →

最新新闻

Python BoundedSemaphore 有界信号量详解

Python BoundedSemaphore 有界信号量详解

Python BoundedSemaphore 有界信号量详解一、Python BoundedSemaphore 有界信号量详解1、 引言2、信号量基础回顾2.1、 什么是信号量2.2、 普通 Semaphore 的问题3、 BoundedSemaphore 的原理3.1、 有界约束3.2、 源码实现4、基本用法4.1、 标准「获取-释放」模式4.2、 使用上下…

2026/10/10 6:01:46 阅读更多 →
埃拉托斯特尼筛法全解析:从求第N个质数到工程级优化

埃拉托斯特尼筛法全解析:从求第N个质数到工程级优化

1. 从一道经典问题说起:求第 N 个质数到底难在哪如果你是刚接触算法不久,或者刷题时卡在“求第 100000 个质数”这种题目上,那你一定体会过那种“明明思路很简单,但一跑就超时”的挫败感。质数判定本身不复杂,教科书里…

2026/10/10 6:01:46 阅读更多 →
更新至2026年全球地缘政治风险指数GPR数据(日度+月度)

更新至2026年全球地缘政治风险指数GPR数据(日度+月度)

更新至2026年全球地缘政治风险指数GPR数据(日度月度) 1、时间:日度数据(1985.1.1-2026.5.18)、月度数据(1900.1-2026.4) 2、指标: 日度:DAY、Number of articles (10 recent newspapers, 198…

2026/10/10 6:01:46 阅读更多 →
Spring Boot整合Flyway

Spring Boot整合Flyway

一、前言 一般在我们开发项目的时候经常会更新数据库表的字段,如果同事 a 添加了表字段,没有及时给同事 b sql 脚本,可能同事 b 的代码运行就会报错,而且随着时间的推移,sql 脚本越来越多,项目上线的时候整理起来就很…

2026/10/10 6:01:46 阅读更多 →
合理对集装箱房设计节点应用是26年适应市场行情的做法

合理对集装箱房设计节点应用是26年适应市场行情的做法

国内集装箱房2026市场行情,完全可以打造成实用又出彩的别墅小院,关键在于合理规划布局注重保温采光融入自然庭院;集装箱最大风险:手续、防锈、结露冷凝、保温,前期做好,后期少踩坑。一句话建议:若你追求低成…

2026/10/10 6:01:46 阅读更多 →
单片机毕设项目:基于单片机的小型室内综合环境感知与 WIFI 远程联动调控系统设计 基于单片机的室内大气环境与安全烟雾监测自动换气远程告警装置设计(030110)

单片机毕设项目:基于单片机的小型室内综合环境感知与 WIFI 远程联动调控系统设计 基于单片机的室内大气环境与安全烟雾监测自动换气远程告警装置设计(030110)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/10 6:00:46 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →