Redis如何保证接口的幂等性?

        出现接口幂等这种问题几乎都是商城的项目,基本上也就是下单的接口会出现的问题。如何防止接口中同样的数据提交,以及如何保证消息不被重复消费,我们这边使用AOP+Redis进行实现。

注意:本篇文章仅使用于单机的场景,对于分布式、高并发场景,还是建议使用分布式锁。

        解决方案:redis的set方法。

代码实现

  • 自定义注解Idempotent
/**
 * @author YanChengHao
 * @data 2024/08/28
 * 自定义注解 作用:防止下单重复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // value表示接口的唯一标识,可以为空
    String value() default "";
}

其中的value表示接口的唯一标识,可以为空,下边的IdempotentAspect中会讲到

  •         定义IdempotentAspect的切片
/**
 * @author YanChengHao
 * @data 2024/08/28
 * 这里主要是定义一个切片的环绕通知,在里边处理主要的接口防刷逻辑
 */
@Aspect
@Component
public class IdempotentAspect {
    @Resource
    private IdempotentProcessor idempotentProcessor;

    private static final ThreadLocal<String> identifierHolder = new ThreadLocal<>();


    @Around("@annotation(idempotent)")
    public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 获取方法参数和相关信息
        Signature signature = joinPoint.getSignature();
        MethodSignature methodsignature = (MethodSignature) signature;
        Method method = methodsignature.getMethod();
        Object[] args = joinPoint.getArgs();
        // 根据方法名和参数生成唯一标识
        String identifier =idempotent.value().isEmpty()
                ? idempotentProcessor.generateIdentifier(method.getName(), args):
                idempotent.value();
        // 将 identifier 存储在 ThreadLocal 中
        identifierHolder.set(identifier);
        // 使用幂等性处理器处理幂等性逻辑
        boolean isIdempotent = idempotentProcessor.process(identifier);
        if(!isIdempotent){
            //如果已经存在相同的操作
            throw new CommonException("the same requests 请勿重复操作");
        }
        return joinPoint.proceed();
    }

    /**
     * 当方法执行完之后执行该后置通知方法,对当前存储的redis进行释放的操作
     * @param idempotent
     */
    @After("@annotation(idempotent)")
    public void AfterIdempotent(Idempotent idempotent) {
        String identifier = identifierHolder.get();
        if (identifier != null) {
            // 删除该缓存
            idempotentProcessor.clear(identifier);
            // 清除 ThreadLocal 中的 identifier
            identifierHolder.remove();
        }
    }
}

这里主要是定义一个切片的环绕通知,在里边处理主要的接口防刷逻辑

  •         幂等性处理类IdempotentProcessor
/**
 * @author YanChengHao
 * @data 2024/08/28
 * 接口的唯一标识变成了方法名+方法的参数
 */
public interface IdempotentProcessor {
    /**
     * 根据标识判断是否重复
     * @param identifier 标识
     * @return 是否重复
     */
    boolean process(String identifier);

    /**
     * 删除缓存
     * @param identifier 标识
     * @return
     */
    boolean clear(String identifier);

    /**
     * 生成唯一标识
     * @param methodName 方法名
     * @param args
     * @return 唯一标识
     */
    default String generateIdentifier(String methodName, Object[] args){
        String argsString= Stream.of(args)
                .filter(Objects::nonNull)
                .map(Object::toString)
                .collect(Collectors.joining());
        // 获取当前人的ID
        UserAuthorization user = ControllerContent.getUserAuthorization();
        // 使用方法名 和 参数字符串 以及 当前登录人ID进行拼接
        System.out.println("用户的信息:" + user);
        return methodName + argsString + user.getUserId();
    }
}

接口的唯一标识变成了方法名+方法的参数+用户唯一ID

  • 幂等性处理接口IdempotentProcessor的实现类RedisIdempotentProcessor
/**
 * @author YanChengHao
 * @data 2024/08/28
 */
@Component
public class RedisIdempotentProcessor implements IdempotentProcessor{
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean process(String identifier) {
        String isIdempotent =redisTemplate.opsForValue().get(identifier);
        if(!StringUtils.isEmpty(isIdempotent)){
        // 如果标识已存在,则返回 false,表示幂等性已满足
             return false;
        } else {
        // 如果标识不存在,则将标识存储到 Redis 中,并设置过期时间 5秒过期时间
            redisTemplate.opsForValue().set(identifier,"true",5, TimeUnit.SECONDS);
            return true;
        }
    }

    @Override
    public boolean clear(String identifier) {
        Boolean delete = redisTemplate.delete(identifier);
        // 当前key不存在 或被删除 为 false
        return delete;
    }

我这边存储时间设置的5秒,这边切面类有@After,在方法执行完之后释放缓存

好的所有的准备已经就绪,现在我们写一个测试的接口测试一下:

直接写上一个注解即可。我们还是采用jmeter进行测试

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值