接口幂等怎么做才靠谱?Redis token + 数据库唯一键,我两层都上了
导读
求职招聘系统里,“重复提交"是个高频事故源:用户手抖点了两次"投递简历”,前端没拦住,后端就插了两条投递记录;重试机制误触发,发布职位变成发布两次。接口幂等是后端必须扛住的活,不能指望前端。这篇讲我在 qkl-boot 里落地的两层幂等方案。
第一层:Redis token 机制(防正常重复提交)
思路是"一单一令":前端在提交前先向后端要一个 token,提交时带上这个 token,后端只认第一次用掉它的请求。
@RestController@RequestMapping("/common")publicclassIdempotentController{@AutowiredprivateStringRedisTemplateredis;/** 前端提交前先拿一个幂等令牌 */@GetMapping("/idempotent-token")publicResultgetToken(){Stringtoken=UUID.randomUUID().toString();redis.opsForValue().set("idem:"+token,"1",Duration.ofMinutes(10));returnResult.ok(token);}}关键在"校验 + 删除"必须原子——用 Redis 的 delete 判断返回值:
// 幂等校验切面(简化版)publicvoidcheckToken(Stringtoken){// delete 返回 1 说明 token 存在且被删除(第一次),返回 0 说明已被用过或不存在Booleanok=redis.delete("idem:"+token);if(!Boolean.TRUE.equals(ok)){thrownewBizException("重复提交,请勿频繁操作");}}delete本身是原子的,多个请求同时来,只有一个能删成功,其余全部被拦。这是 token 方案的核心——校验和消费必须是一步。
第二层:数据库唯一键兜底(防极端并发穿透)
Redis 方案在单实例下够用,但极端并发下如果 Redis 短暂不可用或者 token 校验和业务执行之间出了问题,还是可能穿透。真正的兜底是数据库层的唯一约束。
以"投递简历"为例,给投递记录表加唯一索引:
ALTERTABLEjob_deliveryADDUNIQUEKEYuk_user_job(user_id,job_id);业务层捕获唯一键冲突:
@ServicepublicclassDeliveryService{publicvoiddeliver(LonguserId,LongjobId,StringidemToken){// 第一层:Redis token 校验(切面已做)try{deliveryMapper.insert(userId,jobId);}catch(DuplicateKeyExceptione){// 第二层:数据库唯一键兜底,重复投递直接返回成功(已存在)log.info("重复投递已拦截: user={} job={}",userId,jobId);return;}}}两层都上之后,前端正常防抖走 token,极端穿透由数据库唯一键接住,重复数据从根上就插不进去。
踩坑:token 校验和业务执行不是原子的
现象:压测投递接口,200 并发下出现几条重复投递记录。明明 token 校验用 delete 是原子的,怎么会穿透?
排查:看代码发现——token 校验在一个切面里(@Around前置校验 + 删除),业务方法在切面之后执行。但业务方法内部又调了远程服务(简历解析)耗时较长,token 在切面里已经被删了,业务还没执行完。此时如果前端超时重试,会重新拿新 token 再提交——新 token 校验当然通过,业务又执行了一遍。
定位:问题不在 token 机制本身,在超时重试的语义:前端把"接口超时"误判为"接口失败",重新走了一遍完整流程(拿新 token + 提交)。token 防的是"同一 token 重复提交",防不了"重试后拿新 token 再提交"。
解决:两层一起用。token 保证单次请求不被重复执行;数据库唯一键保证同一个业务动作(用户+职位)无论来几次都只有一条记录。后者才是业务幂等的真兜底。压测验证:200 并发下数据库记录数精确等于岗位数,零重复。
可直接复用的清单
- token 的"校验 + 删除"必须原子,用
redis.delete()返回值判断,别先 get 再 del - 唯一键冲突用
DuplicateKeyException捕获,重复操作按成功返回(幂等语义) - token 设置过期时间(10 分钟够了),防止 token 堆积
- 幂等 token 适合"防重复提交",业务幂等(同人同动作)靠数据库唯一键
- 涉及远程调用的接口,token 超时重试语义要想清楚,别把超时当失败
这套两层方案在 qkl-boot 里跑了大半年,投递、发布、报名这些高频写接口全走它,没再出过重复数据。核心一句话:Redis 管单次,数据库管业务,谁也别指望单靠一层。
项目源码:https://gitee.com/gzqkl/qkl-boot