高并发接口如何保证幂等性?一套可复用的解决方案!

🌟自定义幂等性注解实现(支持分布式锁、EL 表达式、用户维度)

背景介绍

在微服务接口中,为了防止重复提交(例如用户重复点击按钮或网络重试等情况),我们通常需要对接口进行幂等性控制。

常见手段如:Token 机制、数据库唯一索引、分布式锁等。

本方案使用 Redis 实现分布式锁 + AOP 切面 的方式,结合自定义注解 @Idempotent 实现灵活的幂等性控制,支持:

  • 方法级幂等
  • 用户维度幂等
  • Spring EL 表达式自定义 key
  • 异常时释放 key 等机制

💡使用方式

@Idempotent(
    timeout = 3,
    timeUnit = TimeUnit.SECONDS,
    message = "请勿重复提交",
    keyResolver = UserIdempotentKeyResolver.class
)
@PostMapping("/submit")
public CommonResult<Boolean> submit(@RequestBody SubmitRequest request) {
    // 业务处理逻辑
}

🧩注解定义

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    int timeout() default 1;

    TimeUnit timeUnit() default TimeUnit.SECONDS;

    String message() default "重复请求,请稍后重试";

    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;

    String keyArg() default "";

    boolean deleteKeyWhenException() default true;
}

🧵核心逻辑:切面处理 IdempotentAspect

@Aspect
@Slf4j
public class IdempotentAspect {

    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
    private final IdempotentRedisDAO idempotentRedisDAO;

    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
        this.idempotentRedisDAO = idempotentRedisDAO;
    }

    @Around("@annotation(idempotent)")
    public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");

        String key = keyResolver.resolver(joinPoint, idempotent);
        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
        if (!success) {
            log.info("[幂等控制] 方法:{} 参数:{} 被重复调用", joinPoint.getSignature(), joinPoint.getArgs());
            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
        }

        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            if (idempotent.deleteKeyWhenException()) {
                idempotentRedisDAO.delete(key);
            }
            throw throwable;
        }
    }
}

🔑Key 解析器接口 + 三种实现

1. 默认 Key(方法签名 + 参数)

public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {

    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        return SecureUtil.md5(methodName + argsStr);
    }
}

2. 用户维度(加 userId + userType)

public class UserIdempotentKeyResolver implements IdempotentKeyResolver {

    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        Long userId = WebFrameworkUtils.getLoginUserId();
        Integer userType = WebFrameworkUtils.getLoginUserType();
        return SecureUtil.md5(methodName + argsStr + userId + userType);
    }
}

3. Spring EL 表达式支持

public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {

    private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final ExpressionParser expressionParser = new SpelExpressionParser();

    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        Method method = getMethod(joinPoint);
        Object[] args = joinPoint.getArgs();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);

        StandardEvaluationContext context = new StandardEvaluationContext();
        if (paramNames != null) {
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
        }

        Expression expression = expressionParser.parseExpression(idempotent.keyArg());
        return expression.getValue(context, String.class);
    }

    private static Method getMethod(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }
        try {
            return point.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

🧊Redis 幂等操作 DAO

@AllArgsConstructor
public class IdempotentRedisDAO {

    private static final String IDEMPOTENT = "idempotent:%s";

    private final StringRedisTemplate redisTemplate;

    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
    }

    public void delete(String key) {
        String redisKey = formatKey(key);
        redisTemplate.delete(redisKey);
    }

    private static String formatKey(String key) {
        return String.format(IDEMPOTENT, key);
    }
}

✅适用场景

  • 接口幂等性控制(下单、支付、提交等)
  • 分布式环境中的重复提交防控
  • 限制高并发下的逻辑重复执行

🚀总结

本方案具备以下优势:

  • 解耦业务逻辑,只需注解即可开启幂等控制
  • 支持灵活的 Key 策略(全局 / 用户 / 表达式)
  • 支持 Redis 的分布式锁机制,天然适用于分布式系统

如你也在项目中遇到重复提交等问题,不妨试试自定义注解 + AOP 的方式!


如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、关注我 🧡!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值