SpringBoot POST接口如何基于Idempotency-key实现幂等校验
SpringBoot POST接口Idempotency-key幂等实现方案
单纯靠@Cacheable默认逻辑无法完全匹配你的需求:@Cacheable默认命中缓存后会直接返回缓存的接口响应值,而你需要命中重复幂等键时返回409 Conflict状态码,且要求仅业务记录创建成功后才写入缓存,因此需要补充少量自定义逻辑,以下是可直接落地的实现方式:
核心逻辑对齐
先明确和需求匹配的执行流:
- 请求进入时先从请求头提取
Idempotency-key - 用该key查缓存,存在对应记录直接返回409
- 缓存不存在则执行业务创建逻辑
- 业务执行成功(无异常)才将key和响应结果写入缓存,设置合理过期时间
- 业务执行失败则不写缓存,允许同key重试
方案1:自定义AOP切面实现(推荐,无并发穿透问题)
该方案灵活度最高,可精准控制全流程逻辑,兼容本地缓存、分布式缓存所有场景。
步骤1:基础配置
- 项目引入Spring Cache依赖,启动类添加
@EnableCaching注解 - 生产环境建议替换默认内存缓存为Caffeine(本地缓存)或Redis(分布式缓存),支持自定义过期时间
- 缓存key统一规则:
idempotency:{接口标识}:{幂等键值},避免跨接口key冲突
步骤2:编写幂等键提取工具类
public class IdempotencyUtil { public static String getCurrentRequestKey() { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs == null) return null; HttpServletRequest request = attrs.getRequest(); // 请求头名称大小写不敏感,直接读取即可 return request.getHeader("Idempotency-key"); } }
步骤3:自定义幂等标记注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // 缓存过期时间,单位秒,默认5分钟 long expireSeconds() default 300; }
步骤4:编写切面处理核心逻辑
@Aspect @Component public class IdempotentAspect { private static final String CACHE_NAME = "idempotency_cache"; @Autowired private CacheManager cacheManager; @Around("@annotation(idempotentConfig)") public Object handleIdempotency(ProceedingJoinPoint joinPoint, Idempotent idempotentConfig) throws Throwable { String idempotencyKey = IdempotencyUtil.getCurrentRequestKey(); // 按需添加校验:幂等键为空直接返回400错误 Assert.hasText(idempotencyKey, "Idempotency-key不能为空"); Cache cache = cacheManager.getCache(CACHE_NAME); Assert.notNull(cache, "幂等缓存初始化失败"); // 第一步:校验重复请求 Cache.ValueWrapper existRecord = cache.get(idempotencyKey); if (existRecord != null) { throw new ResponseStatusException(HttpStatus.CONFLICT, "重复请求,请勿重复提交"); } // 第二步:执行业务逻辑 Object response; try { response = joinPoint.proceed(); } catch (Exception e) { // 业务执行抛异常,不写入缓存,允许同key重试 throw e; } // 第三步:业务执行成功才写入缓存 cache.put(idempotencyKey, response); // 若使用Redis缓存,可在此处额外调用expire命令设置自定义过期时间 return response; } }
高并发场景下如果用Redis做缓存,可将查询和写入逻辑替换为
RedisTemplate#opsForValue().setIfAbsent()原子操作,完全避免并发请求穿透到业务层。
步骤5:接口上添加注解即可生效
@PostMapping("/project/createStudent") @Idempotent(expireSeconds = 600) // 该接口幂等缓存10分钟过期 public StudentVO createStudent(@RequestBody CreateStudentDTO dto) { // 原有业务逻辑:参数校验、创建学生记录、组装返回值 return studentService.create(dto); }
方案2:基于@Cacheable的轻量实现(适合低并发场景)
如果不想写自定义切面,也可以通过@Cacheable的SpEL表达式直接读取请求头的幂等键作为缓存key,结合前置校验实现需求:
- 先在Controller中注入
CacheManager,编写前置校验方法
@Autowired private CacheManager cacheManager; private void checkDuplicateRequest(String key) { Cache cache = cacheManager.getCache("idempotency_cache"); if (cache != null && cache.get(key) != null) { throw new ResponseStatusException(HttpStatus.CONFLICT, "重复请求"); } }
- 接口上配置
@Cacheable,指定key为请求头中的幂等键,通过unless参数控制仅成功响应才写入缓存
@PostMapping("/project/createStudent") @Cacheable( cacheNames = "idempotency_cache", key = "#request.getHeader('Idempotency-key')", unless = "#result == null" // 可根据自身业务返回结构调整,比如返回码非200时不存缓存 ) public StudentVO createStudent(@RequestBody CreateStudentDTO dto, HttpServletRequest request) { // 前置校验重复请求 String key = request.getHeader("Idempotency-key"); checkDuplicateRequest(key); // 原有业务逻辑 return studentService.create(dto); }
注意:该方案存在并发穿透风险,高并发下同个幂等键的多个请求可能同时通过前置校验进入业务层,仅适合并发量低于100QPS的内部接口使用。
注意事项
- 幂等缓存必须设置过期时间,避免无效key长期占用缓存空间,过期时间根据业务场景设置为5-15分钟即可,覆盖前端重试、网络超时的典型时间窗口
- 客户端生成的幂等键需保证全局唯一,推荐使用UUID v4格式,不要用业务字段作为幂等键
- 分布式部署场景必须用Redis等集中式缓存做幂等存储,本地缓存会导致多节点间校验不生效
内容的提问来源于stack exchange,提问作者user3462941
相关产品推荐
相关产品推荐

