
幂等同一请求重复执行多次结果和执行一次完全一致无副作用。一、数据库唯一约束最常用、最简单1. 唯一索引 / 联合唯一键核心思路给业务唯一标识建唯一索引重复插入直接抛唯一冲突异常不会产生脏数据。适用新增订单、流水、支付记录、幂等单据实现sqlCREATE UNIQUE INDEX idx_biz_no ON order(biz_no);biz_no由客户端生成UUID、雪花 ID、订单号每次请求带上。流程客户端生成全局唯一 bizId请求携带 bizId插入数据库重复直接报错判定重复请求优点实现简单、强可靠、无第三方依赖缺点仅拦截新增操作更新操作不生效高并发下数据库压力大2. 状态机控制更新类幂等核心业务只能单向流转重复更新直接忽略。例订单状态待支付 → 已支付 → 已发货sqlupdate order set status2 where id1 and status1;重复执行时 status 已经不是 1更新行数 0视为重复请求。适用订单、支付、退款、流程审批优点天然适配更新场景无额外组件缺点复杂多状态流程代码量大二、唯一请求令牌Token 机制接口通用3. 前端幂等 Token接口防重复提交为了防止重复操作客户端在第一次调用业务请求之前会发送一个获取 Token 的请求。服务端会生成一个全局唯一的 ID 作为 Token并将其保存在 Redis 中同时将该 ID 返回给客户端。在客户端进行第二次业务请求时必须携带这个 Token。服务端会验证这个 Token如果验证成功则执行业务逻辑并从 Redis 中删除该 Token。如果验证失败说明 Redis 中已经没有对应的 Token表示重复操作服务端会直接返回指定的结果给客户端。用注解来实现Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface Idempotent { /** * 幂等Key只有在 {link Idempotent#type()} 为 {link IdempotentTypeEnum#SPEL} 时生效 */ String key() default ; /** * 触发幂等失败逻辑时返回的错误提示信息 */ String message() default 您操作太快请稍后再试; /** * 验证幂等类型支持多种幂等方式 * RestAPI 建议使用 {link IdempotentTypeEnum#TOKEN} 或 {link IdempotentTypeEnum#PARAM} * 其它类型幂等验证使用 {link IdempotentTypeEnum#SPEL} */ IdempotentTypeEnum type() default IdempotentTypeEnum.PARAM; /** * 验证幂等场景支持多种 {link IdempotentSceneEnum} */ IdempotentSceneEnum scene() default IdempotentSceneEnum.RESTAPI; /** * 设置防重令牌 Key 前缀MQ 幂等去重可选设置 * {link IdempotentSceneEnum#MQ} and {link IdempotentTypeEnum#SPEL} 时生效 */ String uniqueKeyPrefix() default ; /** * 设置防重令牌 Key 过期时间单位秒默认 1 小时MQ 幂等去重可选设置 * {link IdempotentSceneEnum#MQ} and {link IdempotentTypeEnum#SPEL} 时生效 */ long keyTimeout() default 3600L; }切面类Around(annotation(org.opengoofy.index12306.framework.starter.idempotent.annotation.Idempotent)) public Object idempotentHandler(ProceedingJoinPoint joinPoint) throws Throwable { // 获取到方法上的幂等注解实际数据 Idempotent idempotent getIdempotent(joinPoint); // 通过幂等场景以及幂等类型获取幂等执行处理器 IdempotentExecuteHandler instance IdempotentExecuteHandlerFactory.getInstance(idempotent.scene(), idempotent.type()); Object resultObj; try { instance.execute(joinPoint, idempotent); resultObj joinPoint.proceed(); instance.postProcessing(); } catch (RepeatConsumptionException ex) { /** * 触发幂等逻辑时可能有两种情况 * * 1. 消息还在处理但是不确定是否执行成功那么需要返回错误方便 RocketMQ 再次通过重试队列投递 * * 2. 消息处理成功了该消息直接返回成功即可 */ if (!ex.getError()) { return null; } throw ex; } catch (Throwable ex) { // 客户端消费存在异常需要删除幂等标识方便下次 RocketMQ 再次通过重试队列投递 instance.exceptionProcessing(); throw ex; } finally { IdempotentContext.clean(); } return resultObj; } public static Idempotent getIdempotent(ProceedingJoinPoint joinPoint) throws NoSuchMethodException { MethodSignature methodSignature (MethodSignature) joinPoint.getSignature(); Method targetMethod joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes()); return targetMethod.getAnnotation(Idempotent.class); } }工厂类public final class IdempotentExecuteHandlerFactory { /** * 获取幂等执行处理器 * * param scene 指定幂等验证场景类型 * param type 指定幂等处理类型 * return 幂等执行处理器 */ public static IdempotentExecuteHandler getInstance(IdempotentSceneEnum scene, IdempotentTypeEnum type) { IdempotentExecuteHandler result null; switch (scene) { case RESTAPI - { switch (type) { case PARAM - result ApplicationContextHolder.getBean(IdempotentParamService.class); case TOKEN - result ApplicationContextHolder.getBean(IdempotentTokenService.class); case SPEL - result ApplicationContextHolder.getBean(IdempotentSpELByRestAPIExecuteHandler.class); default - { } } } case MQ - result ApplicationContextHolder.getBean(IdempotentSpELByMQExecuteHandler.class); default - { } } return result; } }抽象类public abstract class AbstractIdempotentExecuteHandler implements IdempotentExecuteHandler { /** * 构建幂等验证过程中所需要的参数包装器 * * param joinPoint AOP 方法处理 * return 幂等参数包装器 */ protected abstract IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint); /** * 执行幂等处理逻辑 * * param joinPoint AOP 方法处理 * param idempotent 幂等注解 */ public void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent) { // 模板方法模式构建幂等参数包装器 IdempotentParamWrapper idempotentParamWrapper buildWrapper(joinPoint).setIdempotent(idempotent); handler(idempotentParamWrapper); } }幂等token具体实现类RequiredArgsConstructor public final class IdempotentTokenExecuteHandler extends AbstractIdempotentExecuteHandler implements IdempotentTokenService { private final DistributedCache distributedCache; private final IdempotentProperties idempotentProperties; private static final String TOKEN_KEY token; private static final String TOKEN_PREFIX_KEY idempotent:token:; private static final long TOKEN_EXPIRED_TIME 6000; Override protected IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint) { return new IdempotentParamWrapper(); } Override public String createToken() { String token Optional.ofNullable(Strings.emptyToNull(idempotentProperties.getPrefix())).orElse(TOKEN_PREFIX_KEY) UUID.randomUUID(); distributedCache.put(token, , Optional.ofNullable(idempotentProperties.getTimeout()).orElse(TOKEN_EXPIRED_TIME)); return token; } Override public void handler(IdempotentParamWrapper wrapper) { HttpServletRequest request ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); String token request.getHeader(TOKEN_KEY); if (StrUtil.isBlank(token)) { token request.getParameter(TOKEN_KEY); if (StrUtil.isBlank(token)) { throw new ClientException(BaseErrorCode.IDEMPOTENT_TOKEN_NULL_ERROR); } } Boolean tokenDelFlag distributedCache.delete(token); if (!tokenDelFlag) { String errMsg StrUtil.isNotBlank(wrapper.getIdempotent().message()) ? wrapper.getIdempotent().message() : BaseErrorCode.IDEMPOTENT_TOKEN_DELETE_ERROR.message(); throw new ClientException(errMsg, BaseErrorCode.IDEMPOTENT_TOKEN_DELETE_ERROR); } } }接口调用处加上注解Idempotent( uniqueKeyPrefix ticket:lock_purchase-tickets:, key T(org.opengoofy.index12306.framework.starter.bases.ApplicationContextHolder).getBean(environment).getProperty(unique-name, ) _ T(org.opengoofy.frameworks.starter.user.core.UserContext).getUsername(), message 正在执行下单流程请稍后..., scene IdempotentSceneEnum.RESTAPI, type IdempotentTypeEnum.SPEL ) PostMapping(/api/ticket-service/ticket/purchase/v2) public ResultTicketPurchaseRespDTO purchaseTicketsV2(RequestBody PurchaseTicketReqDTO requestParam) { return Results.success(ticketService.purchaseTicketsV2(requestParam)); }三、分布式缓存幂等标记Redis 方案高并发首选4. Redis 幂等 Key通用分布式幂等核心执行前先查 Redis 是否存在业务标识存在则拒绝不存在则写入再执行业务。两种模式预占型先 SETNX keyexpire成功再执行业务失败直接返回重复后置标记业务执行成功后写入标记重复请求查到标记直接跳过防并发问题必须用原子命令SET key value NX EX 300适用分布式接口、MQ 消费、微服务调用、高并发下单优点性能极高支持分布式适配读写场景缺点Redis 宕机丢失标记会出现重复需设置合理过期时间四、消息队列 MQ 消费幂等异步场景专用5. MQ 消息唯一 ID 幂等消息生产时携带唯一 msgId业务单号 / 雪花 ID消费者消费前先查 Redis / 数据库是否已处理该 msgId已处理 → 直接 ack 丢弃未处理 → 执行业务完成写入标记拓展方案RocketMQ/RabbitMQ 原生 msgId 业务 bizId 双重校验消费完成再落标记避免消息重试导致重复适用异步通知、物流推送、库存扣减、定时任务6. MQ 事务 / 死信队列兜底配合幂等标记使用处理消费失败重试带来的重复问题。五、请求层拦截网关 / 接口层7. 网关层统一幂等校验Gateway/Spring Cloud Gateway在网关统一拦截所有请求提取请求唯一标识bizNo/token统一走 Redis 幂等校验不进入业务服务。优点统一治理业务代码无侵入缺点网关增加压力只适合带唯一标识的请求六、分布式锁辅助幂等强并发扣减场景8. 分布式锁Redlock/Redis 分布式锁当用户提交请求时服务器端可以生成一个唯一的标识使用全部参数用户标识进行MD5或者从用户提交参数里提取一个唯一标识都可以。在处理用户请求之前服务器尝试获取一个分布式锁。如果成功获取到分布式锁那么则执行接下来的正常业务逻辑流程。因为锁已经被获取这样可以确保其他请求无法使用相同的标识避免重复处理。在请求处理完成后服务器需要释放分布式锁。高并发扣库存、优惠券、积分场景以 bizId 为锁 key 抢占锁抢到锁后先校验是否已处理未处理执行业务处理完成释放锁 写入幂等标记适用高并发秒杀、库存扣减、红包发放注意不能只靠分布式锁必须搭配幂等标记锁失效会重复执行注解来实现举例抽象类/** * 抽象幂等执行处理器 */ public abstract class AbstractIdempotentExecuteHandler implements IdempotentExecuteHandler { /** * 构建幂等验证过程中所需要的参数包装器 * * param joinPoint AOP 方法处理 * return 幂等参数包装器 */ protected abstract IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint); /** * 执行幂等处理逻辑 * * param joinPoint AOP 方法处理 * param idempotent 幂等注解 */ public void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent) { // 模板方法模式构建幂等参数包装器 IdempotentParamWrapper idempotentParamWrapper buildWrapper(joinPoint).setIdempotent(idempotent); handler(idempotentParamWrapper); } }幂等分布式锁具体实现类/** * 基于 SpEL 方法验证请求幂等性适用于 RestAPI 场景 */ RequiredArgsConstructor public final class IdempotentSpELByRestAPIExecuteHandler extends AbstractIdempotentExecuteHandler implements IdempotentSpELService { private final RedissonClient redissonClient; private final static String LOCK lock:spEL:restAPI; SneakyThrows Override protected IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint) { Idempotent idempotent IdempotentAspect.getIdempotent(joinPoint); String key (String) SpELUtil.parseKey(idempotent.key(), ((MethodSignature) joinPoint.getSignature()).getMethod(), joinPoint.getArgs()); return IdempotentParamWrapper.builder().lockKey(key).joinPoint(joinPoint).build(); } Override public void handler(IdempotentParamWrapper wrapper) { String uniqueKey wrapper.getIdempotent().uniqueKeyPrefix() wrapper.getLockKey(); RLock lock redissonClient.getLock(uniqueKey); if (!lock.tryLock()) { throw new ClientException(wrapper.getIdempotent().message()); } IdempotentContext.put(LOCK, lock); } Override public void postProcessing() { RLock lock null; try { lock (RLock) IdempotentContext.getKey(LOCK); } finally { if (lock ! null) { lock.unlock(); } } } Override public void exceptionProcessing() { RLock lock null; try { lock (RLock) IdempotentContext.getKey(LOCK); } finally { if (lock ! null) { lock.unlock(); } } } }/** * SpEL 表达式解析工具 */ public class SpELUtil { /** * 校验并返回实际使用的 spEL 表达式 * * param spEl spEL 表达式 * return 实际使用的 spEL 表达式 */ public static Object parseKey(String spEl, Method method, Object[] contextObj) { ArrayListString spELFlag Lists.newArrayList(#, T(); OptionalString optional spELFlag.stream().filter(spEl::contains).findFirst(); if (optional.isPresent()) { /** * T(org.opengoofy.index12306.framework.starter.bases.ApplicationContextHolder).getBean(environment).getProperty(unique-name, ) * _T(org.opengoofy.index12306.frameworks.starter.user.core.UserContext).getUsername() * 即为unique-name的值 username这里为 _admin */ return parse(spEl, method, contextObj); } return spEl; } /** * 转换参数为字符串 * * param spEl spEl 表达式 * param contextObj 上下文对象 * return 解析的字符串值 */ public static Object parse(String spEl, Method method, Object[] contextObj) { DefaultParameterNameDiscoverer discoverer new DefaultParameterNameDiscoverer(); ExpressionParser parser new SpelExpressionParser(); Expression exp parser.parseExpression(spEl); String[] params discoverer.getParameterNames(method); StandardEvaluationContext context new StandardEvaluationContext(); if (ArrayUtil.isNotEmpty(params)) { for (int len 0; len params.length; len) { context.setVariable(params[len], contextObj[len]); } } return exp.getValue(context); } }七、接口天然幂等设计规范设计层面9. HTTP 语义规范GET/DELETE 天然幂等GET只读天然幂等随便重复请求无副作用DELETE删除同一资源多次结果一致POST非幂等必须额外加幂等方案PUT更新全量资源天然幂等多次 PUT 同一数据结果相同最佳实践修改资源优先用 PUT新增用 POST 幂等 Token八、回放日志 / 操作流水最终兜底方案10. 操作流水表每一笔业务操作落一条流水记录唯一 bizNo 为主键执行业务前查询流水表存在则直接返回成功不存在才执行业务并插入流水适用金融、支付、账务等高可靠场景资金不允许出错优点持久化兜底宕机、缓存丢失都不怕缺点每次操作多一次 DB 查询性能损耗方案选型方案适用场景性能分布式支持代码侵入数据库唯一索引新增单据、支付单中支持低状态机更新订单 / 流程状态变更中支持中Redis Token前端表单防重复高支持中Redis 幂等 Key微服务、MQ 消费通用极高完美低网关统一校验全接口统一管控高支持无侵入分布式锁 幂等标记秒杀、库存扣减高完美高操作流水表金融、资金核心低支持中HTTP PUT/GET查询、全量更新无成本原生0最简落地推荐普通后台表单前端幂等 Token Redis订单 / 支付新增数据库唯一索引 Redis 缓存加速异步 MQ 消费msgId Redis 幂等标记订单状态变更数据库状态机金融账务核心流水表兜底 Redis 前置校验微服务对外接口网关统一幂等校验