重复提交导致接口生成重复订单或扣双库存,属逻辑错误;需用redis幂等控制,通过@idempotent注解+aop在service层原子校验请求唯一标识。

为什么重复提交会让接口出问题
用户连点两次按钮、网络超时重试、前端没禁用提交按钮,都可能让同一个请求发两遍。Spring Boot 默认不拦截这种行为,saveOrder() 被调两次,就可能生成两条重复订单,数据库唯一键冲突,或者扣了两次库存——这不是并发问题,是逻辑错误。
用 Redis 做幂等控制,核心就一条:每个请求带一个业务唯一标识(比如 orderNo 或 requestId),进来先查 Redis 有没有这个 key;有,直接返回已处理;没有,写入并继续执行。关键不在“存”,而在“存+校验”必须原子。
自定义注解 + AOP 怎么切到正确位置
不能在 Controller 层手动加 Redis 判断,那样侵入业务;也不能在 Service 层用 try-catch 包一层,复用性差。最干净的方式是定义一个 @Idempotent 注解,配合 AOP 在方法执行前拦截。
注意三点:
- 注解必须支持参数,比如
keyPrefix(避免不同接口 key 冲突)、timeout(单位秒,默认 10 分钟足够) - AOP 切面要限定在
@Idempotent标记的 public 方法上,且不能切@Controller类里的方法(因为参数解析还没完成,拿不到@RequestBody或路径变量) - 真正能取到完整业务参数的地方,是
@Service方法入参——所以注解应加在 Service 方法上,例如createOrder(@RequestBody OrderReq req)
示例注解定义:
@Target(ElementType.METHOD)<br>@Retention(RetentionPolicy.RUNTIME)<br>public @interface Idempotent {<br> String keyPrefix() default "";<br> int timeout() default 600;<br>}
Redis key 怎么拼才安全不撞车
Key 拼错,等于没防。常见错误是只用 requestId,但前端不传或伪造就失效;或者硬写死字符串如 "idempotent:submit",多个用户同时提交会互相覆盖。
推荐组合方式:idempotent:{keyPrefix}:{md5(业务参数)},其中:
-
keyPrefix来自注解,比如"order:create",区分场景 - 业务参数指方法参数中能代表本次操作唯一性的字段,比如
userId + orderId或整个OrderReq的 JSON 字符串(需过滤掉时间戳、随机数等不稳定字段) - 用
MD5或SHA-256哈希,不是为了加密,是为了把任意长度参数压缩成固定长度 key,避免 Redis key 过长或含特殊字符
别直接用 toString() 拼接对象,它可能返回内存地址;也别依赖 @RequestParam 名字自动提取——AOP 里拿不到注解元数据,得靠反射读参数名 + 值,建议封装工具类 IdempotentKeyBuilder.build(method, args)。
setIfAbsent 为什么必须配 Lua 脚本或 SETNX + EXPIRE
初学者常写两步:redisTemplate.hasKey(key) → 是则 return;否则 redisTemplate.opsForValue().set(key, "1", timeout, TimeUnit.SECONDS)。这是错的:中间存在竞争窗口,两个线程同时过判断,都执行 set,就漏防了。
正确做法只有两种:
- 用 Redis 原生命令
SET key value EX seconds NX(即setIfAbsent的底层),Spring Data Redis 的opsForValue().set(key, "1", Duration.ofSeconds(timeout), RedisSetOption.UPSERT)实际调的就是它 - 更稳妥的是 Lua 脚本,把“判断不存在 + 设置 + 设过期”三步压成一个原子操作,尤其在 Redis 集群或哨兵模式下兼容性更好
示例 Lua:
if redis.call("get", KEYS[1]) == false then<br> redis.call("setex", KEYS[1], tonumber(ARGV[1]), ARGV[2])<br> return true<br>else<br> return false<br>end调用时传 key 和 timeout 即可。
别忘了配置 RedisTemplate 的序列化器,StringRedisTemplate 就够用,避免用 JdkSerializationRedisTemplate 存乱码 key。
实际部署时最容易被忽略的是:Redis 故障降级策略。如果 Redis 不可用,是直接报错(破坏可用性),还是跳过幂等(容忍重复)?得在 AOP 里捕获 RedisConnectionFailureException,按业务容忍度决定 throw 还是 log 后放行。










