利用redis解决防重复提交问题

最近遇到个问题是在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。

既然知道了原因,该如何解决。当时我的第一想法就是用注解 + AOP。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。

解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用Redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。
这样还需要注意的一个问题是,如果你先去Redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个操作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩操作变成一个原子操作。

1.导入依赖

 <dependency> 

    <groupId>org.redisson</groupId>

     <artifactId>redisson-spring-boot-starter</artifactId> 

    <version>3.15.0</version> 

    </dependency>

2.使用注解

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME) 

@Documented 

public @interface NoRepeat { 

/**

* 锁名称

*/ 

String lockName() default "no-repeat-default-lock-"; 

/**

* 参数key,支持Spel 

*/

String key() default "";

/**

* 获取锁的最长时间,单位默认为秒 

*/ 

long waitTime() default 5;

/**

* 租赁时间,单位默认为秒

* 加锁的时间。超过这个时间后锁便自动解开了 

*/ 

3.编写切面

@Slf4j 

    @Aspect 

    @Component 

    public class SystemAspect { 

    @Resource private RedissonClient redissonClient; 

    /**

    * SPEL表达式解析器 

    */ 

    private final SpelExpressionParser parser = new SpelExpressionParser(); 

    LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Around(value = "@annotation(NoRepeat)") 

    public Object doNoRepeatAround(ProceedingJoinPoint pjp) throws InterruptedException 

    { 

    NoRepeat noRepeat = CsUtil.getJoinPointAnnotation(pjp, NoRepeat.class); 

    String key = parseKey(pjp, noRepeat); 

    RLock fairLock = redissonClient.getFairLock(noRepeat.lockName() + key); 

    boolean res = fairLock.tryLock(noRepeat.waitTime(), noRepeat.leaseTime(), TimeUnit.SECONDS); 

    if (res) { 

    log.info("拿到锁");

     try {return pjp.proceed(); 

    } catch (Throwable e) { 

    log.error(e.getMessage(), e); 

    throw new ServiceException(e.getMessage()); 

    } finally { fairLock.unlock(); log.info("释放锁操作"); }

     } else { log.error("重复的请求"); 

    return new Response<Boolean>(999, "重复的请求"); 

        } 

    }

    private String parseKey(ProceedingJoinPoint pjp, NoRepeat noRepeat) { 

    Object[] args = pjp.getArgs(); 

    Method method = ((MethodSignature) pjp.getSignature()).getMethod(); 

    String[] params = discoverer.getParameterNames(method); 

    EvaluationContext context = new StandardEvaluationContext(); 

    for (int len = 0; len < Objects.requireNonNull(params).length; len++) { 

    context.setVariable(params[len], args[len]); }

    String keySpel = noRepeat.key(); Expression keyExpression = parser.parseExpression(keySpel);

     return keyExpression.getValue(context, String.class); 

        } 

    } 
  1. 使用方式
@RestController 

    public class TestController { 

    @ApiOperation("身份绑定(注册)接口") 

    @NoRepeat(key = "#user.name") 

    @PostMapping("/v1/test/noRepeat") 

    public Response<User> doSome(@RequestBody User user) {

     return new Response<>(); 

    }

    @Data 

    static class User { 

    private String name;

         }

     }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值