尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
接口幂等别再只靠前端防抖:@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
RELATED

相关推荐

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

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

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

📅 2026/10/10 4:44:24
小程序 Canvas 画招聘海报:750×1334 设计稿 + 网络图预下载,我踩过的 4 个坑

小程序 Canvas 画招聘海报:750×1334 设计稿 + 网络图预下载,我踩过的 4 个坑

招聘岗位要生成分享海报,设计师给了 7501334 的设计稿,要求小程序端 Canvas 渲染。真上手才发现,Canvas 画图这活儿水太深——设计稿 1:1 照搬不行,网络图直接 draw 不行,多渲染几次画面还会越变越小。这套逻辑我写进了…

📅 2026/10/10 4:44:24
用工订单状态机:从待确认到已完成,我拿 6 个状态挡住了所有非法跳变

用工订单状态机:从待确认到已完成,我拿 6 个状态挡住了所有非法跳变

零工平台的工单,生命周期比想象中长:用户提交申请 → 雇主确认 → 开工打卡 → 完工结算 → 双方评价 → 完成。早期版本里状态就是个字符串字段,service 里随手 order.setOrderStatus("WORKING"),改着改着就乱了——有…

📅 2026/10/10 4:44:24
MORE NEWS

更多资讯

📰

多模数据库实战指南:告别“数据库动物园”,重塑统一数据架构

在数据库这个圈子里待久了,你会发现一个很有意思的现象:各家企业的技术栈里,数据库往往是最“花花绿绿”的那一块。业务系统用MySQL,用户画像用Redis,搜索走Elasticsearch,图关系丢Neo4j,日志时…

📰

拓扑学如何成为数据科学底层逻辑:从持久同调到聚类降维

1. 从拓扑学到数据科学:为什么数学系的“冷门课”成了分析利器看到“拓扑学”三个字,很多做数据科学的朋友第一反应是“这和我的工作有什么关系”。我当年也是这么想的。直到做高维数据降维、做聚类评估、做流形学习的时候,发现一堆论文里反复…

📰

Mistral Large 4在网络安全中的实战能力与工程化落地

1. 项目概述:为什么“Mistral Large 4”在网络安全场景中不是工具,而是新一类协作者 最近在几个行业技术群和某高校实验室的攻防复盘会上,频繁听到一句评价:“用Mistral Large 4写规则、读日志、推演TTPs,像多了一个不…

📰

Linux压缩解压缩:从tar归档到zstd流式处理的工程实践

1. 项目概述:为什么“Linux 压缩与解压缩”不是一句命令,而是一套生存技能?在某高校实验室部署一批边缘计算节点时,我遇到过一个典型场景:运维同事发来一条消息:“打包失败,tar: Cannot write t…

📰

从零搭建本地记忆增强系统:claude-mem 项目拆解与实操

1. 从零搭建一个本地记忆增强系统:claude-mem 项目拆解第一次看到 claude-mem 这个项目名的时候,我脑子里蹦出来的第一个念头是:终于有人把「记忆」这件事从大模型的上下文窗口里拎出来单独做了。做过对话类应用的朋友应该都有体会&#xff0…

📰

msado15.dll 32位与64位注册兼容性实战指南

简介:本资源为Windows平台ADO数据库开发必备的msado15.dll全版本合集,面向C/VB等传统Windows桌面应用开发者、遗留系统维护工程师及COM组件调试人员,解决因架构不匹配导致的ADO组件注册失败、找不到指定模块或运行时崩溃等典型问题。压缩包共…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬