Redis分布式锁实现方案(使用lua脚本)

2,734 阅读3分钟

1、使用lua脚本的好处

  1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
  2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入(java等客户端则会执行多次命令完成一个业务,违反了原子性操作)。
  3. 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。
  4. redis在服务器端内置lua解释器(版本2.6以上)
  5. redis-cli提供了EVAL与EVALSHA命令执行Lua脚本

2、实现

2.1、脚本准备

lock.lua

if redis.call('exists', KEYS[1]) == 0
then
    redis.call('hset', KEYS[1], ARGV[1], 1);
    redis.call('expire', KEYS[1], ARGV[2]);
    return 1;
end
if redis.call('hexists', KEYS[1], ARGV[1]) == 1
then
    redis.call('hincrby', KEYS[1], ARGV[1], 1);
    redis.call('expire', KEYS[1], ARGV[2]);
    return 1;
end
return 0

unLock.lua

if redis.call('hexists',KEYS[1],ARGV[1]) == 1
then
    redis.call('del',KEYS[1]);
    return 1;
else
    return 0;
end

2.2、RedisLock 实现

/**
 * redis实现分布式锁
 * 基于hset lua
 */
@Component
public class RedisLock {
    private static ResourceLoader resourceLoader = new DefaultResourceLoader();
    private static final int DEFAULT_TIME_OUT = 10; // 30 s 默认失效时间
    private static final int DEFAULT_RELOCK_TIME = 3; // 10 s  续约周期执行时间
    private static RedisSerializer<String> argsSerializer  = new StringRedisSerializer();
    private static RedisSerializer<String> resultSerializer = new StringRedisSerializer();
    private static RedisTemplate<String, String> redisTemplate;
    private static Redisson redisson;

    /**
     * 解决静态属性使用@Autowired注入
     * @param redisTemplate
     */
    @Autowired
    public RedisLock(RedisTemplate<String, String> redisTemplate, Redisson redisson){
        this.redisTemplate = redisTemplate;
        this.redisson = redisson;
    }

    //获取锁
    public static boolean myTryLock(String lockKey, String lockValue){
        try {
            //获取lua脚本
            DefaultRedisScript lockRedisScript =new DefaultRedisScript<>();
            lockRedisScript.setLocation(new ClassPathResource("script/lock.lua"));
            // 这个值类型要跟lua返回值类型一致才行,否则就会报 java.lang.IllegalStateException
            lockRedisScript.setResultType(Long.class);
            List<String> keys = Collections.singletonList(lockKey);
            Object result = redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, keys, lockValue, String.valueOf(DEFAULT_TIME_OUT));
            if("1".equals(String.valueOf(result))){
                //获取锁成功,开启续约
                autoWatchDog(lockKey, lockValue);
                return true;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    //释放锁(通过lua脚本判断只能删除自己的锁)
    public static boolean myUnLock(String lockKey, String lockValue){
        try {
            //获取lua脚本
            DefaultRedisScript unLockRedisScript = new DefaultRedisScript<>();
            unLockRedisScript.setLocation(new ClassPathResource("script/unLock.lua"));
            // 这个值类型要跟lua返回值类型一致才行,否则就会报 java.lang.IllegalStateException
            unLockRedisScript.setResultType(Long.class);
            List<String> keys = Collections.singletonList(lockKey);
            Object result = redisTemplate.execute(unLockRedisScript, argsSerializer, resultSerializer, keys, lockValue);
            if("1".equals(String.valueOf(result))){
                return true;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    public static void autoWatchDog(String lockKey, String lockValue){
        new Thread(new WatchDog(lockKey, lockValue)).start();
    }


    //实现自动续约,看门狗
    @Data
    static class WatchDog implements Runnable{
        private String lockKey;
        private String lockValue;

        public WatchDog(String lockKey, String lockValue){
            this.lockKey = lockKey;
            this.lockValue = lockValue;
        }

        @Override
        public void run() {
            System.out.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" +  "-看门狗进程启动-----");
            try {
                Timer timer = new Timer();
                timer.schedule(
                    new TimerTask() {
                        public void run() {
                            //获取lua脚本
                            DefaultRedisScript lockRedisScript = new DefaultRedisScript<>();
                            lockRedisScript.setLocation(new ClassPathResource("script/lock.lua"));
                            // 这个值类型要跟lua返回值类型一致才行,否则就会报 java.lang.IllegalStateException
                            lockRedisScript.setResultType(Long.class);
                            List<String> keys = Collections.singletonList(lockKey);
                            Object result = redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, keys, lockValue, String.valueOf(DEFAULT_TIME_OUT));
                            if(!"1".equals(String.valueOf(result))){
                                System.out.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-"+  "-看门狗进程终止-----");
                                timer.cancel();
                            }else{
                                //获取key过期时间
                                Long expire = redisTemplate.opsForValue().getOperations().getExpire(lockKey);
                                System.err.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" +  "-看门狗进程续约成功,剩余时间-----" + expire);
                            }
                        }
                    }, 0, RedisLock.DEFAULT_RELOCK_TIME * 1000);

            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    //根据key获取value
    public static String myGetValueByKey(String lockKey){
        return redisTemplate.opsForValue().get(lockKey);
    }


    //redisson获取锁
    public static RLock redissonGetLock(String lockKey){
        return redisson.getLock(lockKey);
    }

    //redisson释放锁
    public static void redissonUnLock(RLock rLock){
        rLock.unlock();
    }


    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(
                new TimerTask() {
                    public void run() {
                        System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" +11111);
                    }
                }, 0, 3000);
    }

}

2.3、测试类

/**
 * 测试redis分布式锁
 */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestRedisLock {
    final static String lockKey = "jiaobaba";
    
    public TestRedisLock() {
        //在构造函数上写上这个
        System.setProperty("es.set.netty.runtime.available.processors","false");
    }

    @Test
    public void test() throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) {
            String lockValue = UUID.randomUUID().toString();
            new Thread(()->{
                while (!func(lockValue)){};
//                func1();

                downLatch.countDown();
            }).start();

        }

        downLatch.await();

        System.out.println("全部任务执行完毕");
    }

    /**
     * 自己实现分布式锁测试
     * @param lockValue
     * @return
     */
    public boolean func(String lockValue){
        Boolean flag = true;
        try{
            //获取锁
            flag = RedisLock.myTryLock(lockKey, lockValue);
            if(!flag){
                return false;
            }

            System.out.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" + "-获取锁成功,执行业务");

            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" +  "-业务执行完毕");

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            boolean unLock = RedisLock.myUnLock(lockKey, lockValue);
            if(unLock){
                System.out.println(lockValue + "-"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) + "-" +  "-业务执行完,释放锁");
            }
        }

        //标识获取锁成功
        return flag;
    }

    /**
     * redisson 分布式锁测试
     */
    public void func1(){
        RLock rLock = RedisLock.redissonGetLock(lockKey);
        try{
            //加锁
            rLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁成功,执行业务");
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            RedisLock.redissonUnLock(rLock);
            System.out.println(Thread.currentThread().getName() + "业务执行完,释放锁");
        }
    }
}