zoukankan      html  css  js  c++  java
  • 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁【都是基于redis单点下】

    一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁

    1.pom.xml

    <!-- Redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>

    2.RedisLock 工具类 (注入spring)

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    import org.springframework.stereotype.Component;
    import java.util.Collections;
    
    
    /**
     *  spring boot 1.5.X
     * 使用redis 的 lua脚本  基于单点实现分布式锁
     *
     * lua脚本作为原子性操作,保证加锁和设置超时时间 为原子性操作
     * @author sxd
     * @date 2019/5/27 10:52
     */
    @Component
    public class RedisLock {
    
        @Autowired
        RedisTemplate redisTemplate;
    
        private static final Long SUCCESS = 1L;
    
        /**
         * 获取锁
         *
         * @param lockKey       redis的key
         * @param value         redis的value要求是随机串,防止释放其他请求的锁
         * @param expireTime    redis的key 的过期时间  防止死锁,导致其他请求无法正常执行业务
         * @return
         */
        public  boolean lock(String lockKey, String value, int expireTime) {
    
            String script = "if redis.call('setNx',KEYS[1],ARGV[1])  then " +
                    "   if redis.call('get',KEYS[1])==ARGV[1] then " +
                    "      return redis.call('expire',KEYS[1],ARGV[2]) " +
                    "   else " +
                    "      return 0 " +
                    "   end " +
                    "end";
    
            RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
    
            //对非string类型的序列化
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, String.valueOf(expireTime));
    
            return SUCCESS.equals(result);
    
        }
    
        /**
         * 释放锁
         *
         * @param lockKey   redis的key
         * @param value     redis的value  只有value比对一致,才能确定是本请求 加的锁 才能正常释放
         * @return
         */
        public  boolean unlock(String lockKey, String value) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    
            RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
    
            try {
                Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value);
                if (SUCCESS.equals(result)) {
                    return true;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
    
    }
    View Code

    3.controller使用

    /**
         * 使用分布式锁 逻辑
         * 1.准备好 key  value  expireTime
         * value要求是随机字符串
         * expireTime 是根据业务 衡量决定的 锁过期时间
         *
         * 2.获取锁
         * 成功获取,则执行业务,执行完成,释放锁
         * 失败获取,则重试获取,注意获取锁的时间间隔,直到获取成功,执行业务,最后释放锁
         *
         * 注意:
         *     对于redis加锁的业务,尽量用在耗时短的业务上。
         *
         */
        @RequestMapping("/test")
        public void test(){
            boolean flag = false; //标识  是否正常获取锁
            String uuid = UUID.randomUUID().toString(); //redis的value  是一串随机数
            flag = lock.lock("mykey1",uuid,5);
            if (flag){
                business(uuid);
            }else {
    
                //如果未正常获取锁  可以通过重试 直到获取锁成功
                while (!flag){
                    try {
    
                        //重试 时间间隔  减少与redis交互次数
                        Thread.sleep(3000);
                        System.out.println("重试");
                        flag = lock.lock("mykey1",uuid,5);
                        if (flag){
                            business(uuid);
                        }else {
                            continue;
                        }
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            }
        }
    
        public void business(String uuid){
            
            try {
                System.out.println("加锁成功,执行业务");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //业务执行完成  正常释放锁
                lock.unlock("mykey1",uuid);
            }
        }
    View Code

    二.spring boot 2.x 基于redis 的LUA脚本 实现分布式锁

    1.pom.xml

    <!-- redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!--spring2.0集成redis所需common-pool2-->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
                <version>2.4.2</version>
            </dependency>
            <!-- 使用redis的LUA脚本 需要序列化操作的jar-->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
            </dependency>

    2.替代SpringBoot自动配置的RedisTemplate的RedisConfig类

    package com.sxd.swapping.config;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    import java.io.Serializable;
    
    
    /**
     * @author sxd
     * @date 2019/5/27 16:13
     */
    /**
     * @Description Redis配置类,替代SpringBoot自动配置的RedisTemplate,参加RedisAutoConfiguration
     */
    @Configuration
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Serializable> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            //Jackson序列化器
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            //字符串序列化器
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            //普通Key设置为字符串序列化器
            template.setKeySerializer(stringRedisSerializer);
            //Hash结构的key设置为字符串序列化器
            template.setHashKeySerializer(stringRedisSerializer);
            //普通值和hash的值都设置为jackson序列化器
            template.setValueSerializer(jackson2JsonRedisSerializer);
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
            return template;
        }
    
    }
    View Code

    3.RedisLock工具类,自动注入Spring

    package com.sxd.swapping.utils;
    
    import org.apache.log4j.Logger;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Component;
    import java.util.Arrays;
    import java.util.List;
    
    /**
     *
     * spring boot 2.x版本
     * @author sxd
     * @date 2019/5/27 16:11
     */
    @Component
    public class RedisLock2 {
    
    
        Logger  logger = Logger.getRootLogger();
    
        static final Long LOCK_SUCCESS = 1L;
    
        static final Long LOCK_EXPIRED = -1L;
    
        @Autowired
        RedisTemplate redisTemplate;
    
        //定义获取锁的lua脚本
        private final static DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>(
                "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"
                , Long.class
        );
    
    
    
        //定义释放锁的lua脚本
        private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(
                "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end"
                , Long.class
        );
    
    
    
        /**
         * 加锁
         * @param key redis键值对 的 key
         * @param value redis键值对 的 value  随机串作为值
         * @param timeout redis键值对 的 过期时间   pexpire 以毫秒为单位
         * @param retryTimes 重试次数   即加锁失败之后的重试次数,根据业务设置大小
         * @return
         */
        public boolean lock(String key,String value ,long timeout, int retryTimes) {
            try {
    
                logger.debug("加锁信息:lock :::: redisKey = " + key + " requestid = " + value);
                //组装lua脚本参数
                List<String> keys = Arrays.asList(key);
                //执行脚本
                Object result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys,value,timeout);
                //存储本地变量
                if(LOCK_SUCCESS.equals(result)) {
    
                    logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
                    return true;
                } else if (retryTimes == 0) {
                    //重试次数为0直接返回失败
                    return false;
                } else {
                    //重试获取锁
                    logger.info("重试加锁:retry to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
                    int count = 0;
                    while(true) {
                        try {
                            //休眠一定时间后再获取锁,这里时间可以通过外部设置
                            Thread.sleep(100);
                            result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
                            if(LOCK_SUCCESS.equals(result)) {
    
                                logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
                                return true;
                            } else {
                                count++;
                                if (retryTimes == count) {
                                    logger.info("加锁失败:fail to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);
                                    return false;
                                } else {
                                    logger.warn(count + " times try to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);
                                    continue;
                                }
                            }
                        } catch (Exception e) {
                            logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e);
                            break;
                        }
                    }
                }
            } catch (Exception e1) {
                logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e1);
            }
            return false;
        }
    
    
    
    
    
    
    
    
        /**
         * 释放KEY
         * @param key   释放本请求对应的锁的key
         * @param value 释放本请求对应的锁的value  是不重复随即串 用于比较,以免释放别的线程的锁
         * @return
         */
        public boolean unlock(String key,String value) {
            try {
    
                //组装lua脚本参数
                List<String> keys = Arrays.asList(key);
                logger.debug("解锁信息:unlock :::: redisKey = " + key + " requestid = " + value);
                // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
    
                Object result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys, value);
                //如果这里抛异常,后续锁无法释放
                if (LOCK_SUCCESS.equals(result)) {
                    logger.info("解锁成功:release lock success:" + Thread.currentThread().getName() + ", Status code reply=" + result);
                    return true;
                } else if (LOCK_EXPIRED.equals(result)) {
                    //返回-1说明获取到的KEY值与requestId不一致或者KEY不存在,可能已经过期或被其他线程加锁
                    // 一般发生在key的过期时间短于业务处理时间,属于正常可接受情况
                    logger.warn("解锁异常:release lock exception:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result);
                } else {
                    //其他情况,一般是删除KEY失败,返回0
                    logger.error("解锁失败:release lock failed:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result);
                }
            } catch (Exception e) {
                logger.error("解锁异常:release lock occured an exception", e);
            }
    
            return false;
        }
    
    
    
    
    
    }
    View Code

    4.使用

     @Autowired
        RedisLock2 lock2;
    
        @Autowired
        RedisTemplate redisTemplate;
    
    
        @RequestMapping("/test3")
        public void test3(){
            ValueOperations vops = redisTemplate.opsForValue();
            String uuid = UUID.randomUUID().toString();
    
            //加锁
            if (lock2.lock("mykey1",uuid,5000,3)){
    
                try {
    //                执行业务
                    System.out.println("加锁成功,做业务");
    
                    vops.increment(REDIS_COUNT_KEY,1);
                    Thread.sleep(3000);
    
                    System.out.println("业务执行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //解锁
                    lock2.unlock("mykey1",uuid);
                }
            }
        }
    View Code
  • 相关阅读:
    浅析Java中 new 和不 new 对象的区别
    uniapp打包后提示本应用使用HBuilderX 3.1.12 或对应的cli版本编译,而手机端SDK版本是3.1.13,不匹配的版本可能造成应用异常的解决办法
    浅析端口被占用如何做、windows报错'telnet'不是内部或外部命令,也不是可运行的程序如何处理
    苹果审核不过出现"您的 App 包含 NSUserTrackingUsageDescription...."解决办法
    CSS知识点:text-align-last段落最后一行设置对齐方式、transform:rotateY()翻转
    [转]scrollHeight, clientHeight, offsetHeight的区别
    【转】CSS变量(自定义属性)实践指南
    【转】angular 自定义 component decorator
    【转】Angular 编译打包 & Docker发布
    Angular FormControl Example | Angular 9 FormControl (理解angular formControl)
  • 原文地址:https://www.cnblogs.com/sxdcgaq8080/p/10931246.html
Copyright © 2011-2022 走看看