zoukankan      html  css  js  c++  java
  • SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)

      防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以redis分布式锁为例。

      一、本地锁(Guava)

      1、导入依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>21.0</version>
            </dependency>

      2、自定义本地锁注解

    package com.example.demo.utils;
    
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface LocalLock {
        String key() default "";
        //过期时间,使用本地缓存可以忽略,如果使用redis做缓存就需要
        int expire() default 5;
    }

      3、本地锁注解实现

    package com.example.demo.utils;
    
    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Method;
    import java.util.concurrent.TimeUnit;
    
    @Aspect
    @Configuration
    public class LockMethodInterceptor {
        //定义缓存,设置最大缓存数及过期日期
        private static final Cache<String,Object> CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();
    
        @Around("execution(public * *(..))  && @annotation(com.example.demo.utils.LocalLock)")
        public Object interceptor(ProceedingJoinPoint joinPoint){
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            LocalLock localLock = method.getAnnotation(LocalLock.class);
            String key = getKey(localLock.key(),joinPoint.getArgs());
            if(!StringUtils.isEmpty(key)){
                if(CACHE.getIfPresent(key) != null){
                    throw new RuntimeException("请勿重复请求!");
                }
                CACHE.put(key,key);
            }
            try{
                return joinPoint.proceed();
            }catch (Throwable throwable){
                throw new RuntimeException("服务器异常");
            }finally {
    
            }
        }
    
        private String getKey(String keyExpress, Object[] args){
            for (int i = 0; i < args.length; i++) {
                keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
            }
            return keyExpress;
        }
    
    }

      4、控制层

        @ResponseBody
        @PostMapping(value ="/localLock")
        @ApiOperation(value="重复提交验证测试--使用本地缓存锁")
        @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
        @LocalLock(key = "localLock:test:arg[0]")
        public String localLock(String token){
    
            return "sucess====="+token;
        }

      5、测试

      第一次请求:

      

       未过期再次访问:

      

    二、Redis分布式锁

      1、导入依赖

      导入aop依赖和redis依赖即可

      2、配置

      配置redis连接信息即可

      3、自定义分布式锁注解

    package com.example.demo.utils;
    
    import java.lang.annotation.*;
    import java.util.concurrent.TimeUnit;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface CacheLock {
        //redis锁前缀
        String prefix() default "";
        //redis锁过期时间
        int expire() default 5;
        //redis锁过期时间单位
        TimeUnit timeUnit() default TimeUnit.SECONDS;
        //redis  key分隔符
        String delimiter() default ":";
    }

      4、自定义key规则注解

      由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。

    package com.example.demo.utils;
    
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface CacheParam {
        String name() default "";
    }

      5、定义key生成策略接口

    package com.example.demo.service;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.springframework.stereotype.Service;
    
    public interface CacheKeyGenerator {
        //获取AOP参数,生成指定缓存Key
        String getLockKey(ProceedingJoinPoint joinPoint);
    }

      6、定义key生成策略实现类

    package com.example.demo.service.impl;
    
    import com.example.demo.service.CacheKeyGenerator;
    import com.example.demo.utils.CacheLock;
    import com.example.demo.utils.CacheParam;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.util.ReflectionUtils;
    import org.springframework.util.StringUtils;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.lang.reflect.Parameter;
    
    public class CacheKeyGeneratorImp implements CacheKeyGenerator {
        @Override
        public String getLockKey(ProceedingJoinPoint joinPoint) {
            //获取连接点的方法签名对象
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            //Method对象
            Method method = methodSignature.getMethod();
            //获取Method对象上的注解对象
            CacheLock cacheLock = method.getAnnotation(CacheLock.class);
            //获取方法参数
            final Object[] args = joinPoint.getArgs();
            //获取Method对象上所有的注解
            final Parameter[] parameters = method.getParameters();
            StringBuilder sb = new StringBuilder();
            for(int i=0;i<parameters.length;i++){
                final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class);
                //如果属性不是CacheParam注解,则不处理
                if(cacheParams == null){
                    continue;
                }
                //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
                sb.append(cacheLock.delimiter()).append(args[i]);
            }
            //如果方法上没有加CacheParam注解
            if(StringUtils.isEmpty(sb.toString())){
                //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)
                final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
                //循环注解
                for(int i=0;i<parameterAnnotations.length;i++){
                    final Object object = args[i];
                    //获取注解类中所有的属性字段
                    final Field[] fields = object.getClass().getDeclaredFields();
                    for(Field field : fields){
                        //判断字段上是否有CacheParam注解
                        final CacheParam annotation = field.getAnnotation(CacheParam.class);
                        //如果没有,跳过
                        if(annotation ==null){
                            continue;
                        }
                        //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
                        field.setAccessible(true);
                        //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
                        sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object));
                    }
                }
            }
            //返回指定前缀的key
            return cacheLock.prefix() + sb.toString();
        }
    }

      7、分布式注解实现

    package com.example.demo.utils;
    
    import com.example.demo.service.CacheKeyGenerator;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisStringCommands;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.types.Expiration;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Method;
    
    @Aspect
    @Configuration
    public class CacheLockMethodInterceptor {
    
    
    
        @Autowired
        public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){
            this.cacheKeyGenerator = cacheKeyGenerator;
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        private final StringRedisTemplate stringRedisTemplate;
        private final CacheKeyGenerator cacheKeyGenerator;
    
        @Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")
        public Object interceptor(ProceedingJoinPoint joinPoint){
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            CacheLock cacheLock = method.getAnnotation(CacheLock.class);
            if(StringUtils.isEmpty(cacheLock.prefix())){
                throw new RuntimeException("前缀不能为空");
            }
            //获取自定义key
            final String lockkey = cacheKeyGenerator.getLockKey(joinPoint);
            final Boolean success = stringRedisTemplate.execute(
                    (RedisCallback<Boolean>) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit())
                            , RedisStringCommands.SetOption.SET_IF_ABSENT));
            if (!success) {
                // TODO 按理来说 我们应该抛出一个自定义的 CacheLockException 异常;这里偷下懒
                throw new RuntimeException("请勿重复请求");
            }
            try {
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                throw new RuntimeException("系统异常");
            }
        }
    }

      8、主函数调整

      主函数引入key生成策略

        @Bean
        public CacheKeyGenerator cacheKeyGenerator(){
            return new CacheKeyGeneratorImp();
        }

      9、Controller

        @ResponseBody
        @PostMapping(value ="/cacheLock")
        @ApiOperation(value="重复提交验证测试--使用redis锁")
        @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
        //@CacheLock
        @CacheLock()
        public String cacheLock(String token){
            return "sucess====="+token;
        }
    
        @ResponseBody
        @PostMapping(value ="/cacheLock1")
        @ApiOperation(value="重复提交验证测试--使用redis锁")
        @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
        //@CacheLock
        @CacheLock(prefix = "redisLock.test",expire = 20)
        public String cacheLock1(String token){
            return "sucess====="+token;
        }
    
        @ResponseBody
        @PostMapping(value ="/cacheLock2")
        @ApiOperation(value="重复提交验证测试--使用redis锁")
        @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
        //@CacheLock
        @CacheLock(prefix = "redisLock.test",expire = 20)
        public String cacheLock2(@CacheParam(name = "token") String token){
            return "sucess====="+token;
        }

      10、测试

      (1)由于cacheLock方法的CacheLock注解没有加prefix前缀,因此会报错

      (2)没有加CacheParam注解

      第一次调用:

      缓存信息:

      可以发现key为prifix的值

       第二次调用:

     

       (3)增加了CacheParam注解

      第一次调用:

      

       缓存信息:

      可以发现缓存的内容为prefix+@CacheParam

      

       第二次调用:

  • 相关阅读:
    apply()与call()的区别
    VS Code 配置vue开发环境
    settimeout 和 setinterval
    JAVA内存泄漏和内存溢出的区别和联系
    Oracle Distinct(过滤重复)用法
    Oracle Order By排序用法详解
    Oracle Select语句
    登陆权限--token的使用
    登陆权限--token 的生成和验证
    MySQL基础增删改查
  • 原文地址:https://www.cnblogs.com/liconglong/p/11728136.html
Copyright © 2011-2022 走看看