• 【SpringBoot应用篇】SpringBoot+Redis实现接口幂等性校验


    幂等性

    幂等性的定义是:一次和屡次请求某一个资源对于资源自己应该具备一样的结果(网络超时等问题除外)。也就是说,其任意屡次执行对资源自己所产生的影响均与一次执行的影响相同。

    WEB系统中: 就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生不同的结果。

    什么状况下须要保证幂等性
    SQL为例,有下面三种场景,只有第三种场景须要开发人员使用其余策略保证幂等性:
    SELECT col1 FROM tab1 WHER col2=2,不管执行多少次都不会改变状态,是自然的幂等。
    UPDATE tab1 SET col1=1 WHERE col2=2,不管执行成功多少次状态都是一致的,所以也是幂等操做。
    UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。

    解决方法

    这里主要使用token令牌分布式锁解决

    Pom

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.2.RELEASEversion>
        <relativePath/>
    parent>
    <dependencies>
    	<dependency>
    	    <groupId>org.projectlombokgroupId>
    	    <artifactId>lombokartifactId>
    	    <version>1.18.4version>
    	    <scope>providedscope>
    	dependency>
    	<dependency>
    	   <groupId>org.springframework.bootgroupId>
    	   <artifactId>spring-boot-starter-jdbcartifactId>
    	dependency>
    	<dependency>
    	   <groupId>mysqlgroupId>
    	   <artifactId>mysql-connector-javaartifactId>
    	dependency>
    	<dependency>
    	   <groupId>org.springframework.bootgroupId>
    	   <artifactId>spring-boot-starter-data-redisartifactId>
    	dependency>
    	
    	<dependency>
    		<groupId>org.springframework.bootgroupId>
    		<artifactId>spring-boot-starter-aopartifactId>
    	dependency>
    	
    	<dependency>
    		<groupId>com.baomidougroupId>
    		<artifactId>mybatis-plus-boot-starterartifactId>
    		<version>3.5.2version>
    	dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    token令牌

    这种方式分红两个阶段:
    1、客户端向系统发起一次申请token的请求,服务器系统生成token令牌,将token保存到Redis缓存中,并返回前端(令牌生成方式可以使用JWT)
    2、客户端拿着申请到的token发起请求(放到请求头中),后台系统会在拦截器中检查handler是否开启幂等性校验。取请求头中的token,判断Redis中是否存在该token,若是存在,表示第一次发起支付请求,删除缓存中token后开始业务逻辑处理;若是缓存中不存在,表示非法请求。

    yml

    spring:
      redis:
        host: 127.0.0.1
        timeout: 5000ms
        port: 6379
        database: 0
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study_db?serverTimezone=GMT%2B8&allowMultiQueries=true
        username: root
        password: root
    redisson:
      timeout: 10000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    @ApiIdempotentAnn

    @ApiIdempotentAnn幂等性注解。说明: 添加了该注解的接口要实现幂等性验证

    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ApiIdempotentAnn {
        boolean value() default true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ApiIdempotentInterceptor

    这里可以使用拦截器或者使用AOP的方式实现。

    幂等性拦截器的方式实现

    @Component
    public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
    
        /**
         * 前置拦截器
         *在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,
         * 也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //如果hanler不是和HandlerMethod类型,则返回true
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
            //转化类型
            final HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取方法类
            final Method method = handlerMethod.getMethod();
            // 判断当前method中是否有这个注解
            boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);
            //如果有幂等性注解
            if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {
                // 需要实现接口幂等性
                //检查token
                //1.获取请求的接口方法
                boolean result = checkToken(request);
                //如果token有值,说明是第一次调用
                if (result) {
                    //则放行
                    return super.preHandle(request, response, handler);
                } else {//如果token没有值,则表示不是第一次调用,是重复调用
                    response.setContentType("application/json; charset=utf-8");
                    PrintWriter writer = response.getWriter();
                    writer.print("重复调用");
                    writer.close();
                    response.flushBuffer();
                    return false;
                }
            }
            //否则没有该自定义幂等性注解,则放行
            return super.preHandle(request, response, handler);
        }
    
        //检查token
        private boolean checkToken(HttpServletRequest request) {
            //从请求头对象中获取token
            String token = request.getHeader("token");
            //如果不存在,则返回false,说明是重复调用
            if(StringUtils.isBlank(token)){
                return false;
            }
            //否则就是存在,存在则把redis里删除token
            return redisTemplate.delete(token);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    MVC配置类

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        private ApiIdempotentInterceptor apiIdempotentInceptor;
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/**");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ApiController

    @RestController
    public class ApiController {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
    
        /**
         * 前端获取token,然后把该token放入请求的header中
         * @return
         */
        @GetMapping("/getToken")
        public String getToken() {
            String token = UUID.randomUUID().toString().substring(1, 9);
            stringRedisTemplate.opsForValue().set(token, "1");
            return token;
        }
    
        //定义int类型的原子类的类
        AtomicInteger num = new AtomicInteger(100);
    
        /**
         * 主业务逻辑,num--,并且加了自定义接口
         * @return
         */
        @GetMapping("/submit")
        @ApiIdempotentAnn
        public String submit() {
            // num--
            num.decrementAndGet();
            return "success";
        }
    
        /**
         * 查看num的值
         * @return
         */
        @GetMapping("/getNum")
        public String getNum() {
            return String.valueOf(num.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    分布式锁 Redisson

    Redisson是redis官网推荐实现分布式锁的一个第三方类库,通过开启另一个服务,后台进程定时检查持有锁的线程是否继续持有锁了,是将锁的生命周期重置到指定时间,即防止线程释放锁之前过期,所以将锁声明周期通过重置延长)

    Redission执行流程如下:(只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下(锁续命周期就是设置的超时时间的三分之一),如果线程还持有锁,就会不断的延长锁key的生存时间。因此,Redis就是使用Redisson解决了锁过期释放,业务没执行完问题。当业务执行完,释放锁后,再关闭守护线程,

    pom

    <dependency>
    	 <groupId>org.redissongroupId>
    	 <artifactId>redisson-spring-boot-starterartifactId>
    	 <version>3.13.6version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    @RedissonLockAnnotation

    分布式锁注解

    @Target(ElementType.METHOD) //注解在方法
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RedissonLockAnnotation {
        /**
         * 指定组成分布式锁的key,以逗号分隔。
         * 如:keyParts="name,age",则分布式锁的key为这两个字段value的拼接
         * key=params.getString("name")+params.getString("age")
         */
        String keyParts();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    DistributeLocker

    分布式锁接口

    public interface  DistributeLocker {
        /**
         * 加锁
         * @param lockKey key
         */
        void lock(String lockKey);
    
        /**
         * 释放锁
         *
         * @param lockKey key
         */
        void unlock(String lockKey);
    
        /**
         * 加锁,设置有效期
         *
         * @param lockKey key
         * @param timeout 有效时间,默认时间单位在实现类传入
         */
        void lock(String lockKey, int timeout);
    
        /**
         * 加锁,设置有效期并指定时间单位
         * @param lockKey key
         * @param timeout 有效时间
         * @param unit    时间单位
         */
        void lock(String lockKey, int timeout, TimeUnit unit);
    
        /**
         * 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false
         * @param lockKey
         * @return true-获取锁成功 false-获取锁失败
         */
        boolean tryLock(String lockKey);
    
        /**
         * 尝试获取锁,获取到则持有该锁leaseTime时间.
         * 若未获取到,在waitTime时间内一直尝试获取,超过watiTime还未获取到则返回false
         * @param lockKey   key
         * @param waitTime  尝试获取时间
         * @param leaseTime 锁持有时间
         * @param unit      时间单位
         * @return true-获取锁成功 false-获取锁失败
         */
        boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
                throws InterruptedException;
    
        /**
         * 锁是否被任意一个线程锁持有
         * @param lockKey
         * @return true-被锁 false-未被锁
         */
        boolean isLocked(String lockKey);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    RedissonDistributeLocker

    redisson实现分布式锁接口

    public class RedissonDistributeLocker implements DistributeLocker {
    
        private RedissonClient redissonClient;
    
        public RedissonDistributeLocker(RedissonClient redissonClient) {
            this.redissonClient = redissonClient;
        }
    
        @Override
        public void lock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
        }
    
        @Override
        public void unlock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.unlock();
        }
    
        @Override
        public void lock(String lockKey, int leaseTime) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(leaseTime, TimeUnit.MILLISECONDS);
        }
    
        @Override
        public void lock(String lockKey, int timeout, TimeUnit unit) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(timeout, unit);
        }
    
        @Override
        public boolean tryLock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            return lock.tryLock();
        }
    
        @Override
        public boolean tryLock(String lockKey, long waitTime, long leaseTime,
                               TimeUnit unit) throws InterruptedException {
            RLock lock = redissonClient.getLock(lockKey);
            return lock.tryLock(waitTime, leaseTime, unit);
        }
    
        @Override
        public boolean isLocked(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            return lock.isLocked();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    RedissonLockUtils

    redisson锁工具类

    public class RedissonLockUtils {
    
        private static DistributeLocker locker;
    
        public static void setLocker(DistributeLocker locker) {
            RedissonLockUtils.locker = locker;
        }
    
        public static void lock(String lockKey) {
            locker.lock(lockKey);
        }
    
        public static void unlock(String lockKey) {
            locker.unlock(lockKey);
        }
    
        public static void lock(String lockKey, int timeout) {
            locker.lock(lockKey, timeout);
        }
    
        public static void lock(String lockKey, int timeout, TimeUnit unit) {
            locker.lock(lockKey, timeout, unit);
        }
    
        public static boolean tryLock(String lockKey) {
            return locker.tryLock(lockKey);
        }
    
        public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
                                      TimeUnit unit) throws InterruptedException {
            return locker.tryLock(lockKey, waitTime, leaseTime, unit);
        }
    
        public static boolean isLocked(String lockKey) {
            return locker.isLocked(lockKey);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    RedissonConfig

    Redisson配置类

    @Configuration
    public class RedissonConfig {
    
        @Autowired
        private Environment env;
    
        /**
         * Redisson客户端注册
         * 单机模式
         */
        @Bean(destroyMethod = "shutdown")
        public RedissonClient createRedissonClient() {
            Config config = new Config();
            SingleServerConfig singleServerConfig = config.useSingleServer();
            singleServerConfig.setAddress("redis://" + env.getProperty("spring.redis.host") + ":" + env.getProperty("spring.redis.port"));
            singleServerConfig.setTimeout(Integer.valueOf(env.getProperty("redisson.timeout")));
            return Redisson.create(config);
        }
    
        /**
         * 分布式锁实例化并交给工具类
         * @param redissonClient
         */
        @Bean
        public RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {
            RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);
            RedissonLockUtils.setLocker(locker);
            return locker;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    RedissonLockAop

    这里可以使用拦截器或者使用AOP的方式实现。

    分布式锁AOP切面拦截方式实现

    @Aspect
    @Component
    @Slf4j
    public class RedissonLockAop {
        /**
         * 切点,拦截被 @RedissonLockAnnotation 修饰的方法
         */
        @Pointcut("@annotation(cn.zysheep.biz.redis.RedissonLockAnnotation)")
        public void redissonLockPoint() {
        }
    
        @Around("redissonLockPoint()")
        @ResponseBody
        public ResultVO checkLock(ProceedingJoinPoint pjp) throws Throwable {
            //当前线程名
            String threadName = Thread.currentThread().getName();
            log.info("线程{}------进入分布式锁aop------", threadName);
            //获取参数列表
            Object[] objs = pjp.getArgs();
            //因为只有一个JSON参数,直接取第一个
            JSONObject param = (JSONObject) objs[0];
            //获取该注解的实例对象
            RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).
                    getMethod().getAnnotation(RedissonLockAnnotation.class);
            //生成分布式锁key的键名,以逗号分隔
            String keyParts = annotation.keyParts();
            StringBuffer keyBuffer = new StringBuffer();
            if (StringUtils.isEmpty(keyParts)) {
                log.info("线程{} keyParts设置为空,不加锁", threadName);
                return (ResultVO) pjp.proceed();
            } else {
                //生成分布式锁key
                String[] keyPartArray = keyParts.split(",");
                for (String keyPart : keyPartArray) {
                    keyBuffer.append(param.getString(keyPart));
                }
                String key = keyBuffer.toString();
                log.info("线程{} 要加锁的key={}", threadName, key);
                //获取锁
                if (RedissonLockUtils.tryLock(key, 3000, 5000, TimeUnit.MILLISECONDS)) {
                    try {
                        log.info("线程{} 获取锁成功", threadName);
    
                        // Thread.sleep(5000);
    
                        return (ResultVO) pjp.proceed();
                    } finally {
                        RedissonLockUtils.unlock(key);
                        log.info("线程{} 释放锁", threadName);
                    }
                } else {
                    log.info("线程{} 获取锁失败", threadName);
                    return ResultVO.fail();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    ResultVO

    统一响应实体

    @Data
    public class ResultVO<T> {
        private static final ResultCode SUCCESS = ResultCode.SUCCESS;
        private static final ResultCode FAIL = ResultCode.FAILED;
    
        private Integer code;
    
        private String message;
    
        private T  data;
    
        public static <T> ResultVO<T> ok() {
    
            return result(SUCCESS,null);
        }
    
        public static <T> ResultVO<T> ok(T data) {
            return result(SUCCESS,data);
        }
    
        public static <T> ResultVO<T> ok(ResultCode resultCode) {
            return result(resultCode,null);
        }
    
        public static <T> ResultVO<T> ok(ResultCode resultCode, T data) {
            return result(resultCode,data);
        }
    
        public static <T> ResultVO<T> fail() {
            return result(FAIL,null);
        }
    
        public static <T> ResultVO<T> fail(ResultCode resultCode) {
            return result(FAIL,null);
        }
    
    
        public static <T> ResultVO<T> fail(T data) {
            return result(FAIL,data);
        }
    
        public static <T> ResultVO<T> fail(ResultCode resultCode, T data) {
            return result(resultCode,data);
        }
    
        private static <T>  ResultVO<T> result(ResultCode resultCode, T data) {
            ResultVO<T> resultVO = new ResultVO<>();
            resultVO.setCode(resultCode.getCode());
            resultVO.setMessage(resultCode.getMessage());
            resultVO.setData(data);
            return resultVO;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    BusiController

    @RestController
    public class ApiController {
    	@PostMapping(value = "testLock")
    	@RedissonLockAnnotation(keyParts = "name,age")
    	public ResultVO testLock(@RequestBody JSONObject params) {
    	    /**
    	     * 分布式锁key=params.getString("name")+params.getString("age");
    	     * 此时name和age均相同的请求不会出现并发问题
    	     */
    	    //TODO 业务处理dwad
    	    return ResultVO.ok();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    去重表机制

    往去重表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,也可以是多字段的唯一性组合。
    在这里插入图片描述
    这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性

    另外,使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。

  • 相关阅读:
    以汇川中型PLC(AM系列)为例,CODESYS平台变量与字节数组互转的多种方法
    商场促销--策略模式
    论文解读 | [ICCV2021] 用于任意形状文本检测的自适应边界建议网络
    C8051F020 SMBus一直处于busy状态解决办法
    STM32,复位和时钟控制
    SpringCloud Gateway源码跟踪解析
    Day21力扣打卡
    react中函数组件和class组件的区别
    pat basic 1049 数列的片段和
    SAP中应用角度展示产成品不同工艺路线下的期间产量
  • 原文地址:https://blog.csdn.net/qq_45297578/article/details/127761174