# Redis分布式锁:使用LUA脚本解决释放过期锁问题

979 阅读4分钟

Redis分布式锁:使用LUA脚本解决释放过期锁问题

一、开启分布式锁

在Redis集群中,乐观锁失效了,因为这种锁无法传递到集群中的其他节点。

但还有分布式锁,这种锁是基于redis底层的,本质上是一种互斥信号量。

开启分布式锁: setnx lock value, "nx"实际上表示"if not exists",这里指若当前键不存在的话才进行创建并赋值。那么,如果在集群中,在操作之前,都加上一个这样的锁,并进行if(lock)判断,能够成功获取锁的才能操作数据,否则等待。这样就实现了分布式锁。

二、分布式锁的四个条件

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

  • 加锁和解锁必须具有原子性。

三、实现分布式锁的四个条件

  1. 互斥性:用setnx key valueset key value nx来实现,当key已存在时,无法再对key进行写操作。

  2. 不会发生死锁: 设置锁的过期时间即可。用set key value ex seconds实现,表示当前key的创建后经过seconds秒后过期。也可用set key value px milliseconds实现,只是单位不同。

  3. 保证加锁和解锁为同一客户端: 这就要保证每个客户端锁的唯一性,既然锁的key必定是相同的,那么就要让value唯一,使用UUID对value赋值即可。在解锁时判断当前锁的值是否和当前客户端所持有的UUID相等,若相等,则解锁,否则不做操作,等待锁自己过期。

  4. 加锁和解锁必须有原子性:这要让加锁和解锁作为一个原子操作出现,即这两个过程不能被其他操作打断。 考虑这样一个情景:A客户端在释放锁时,UUID成功匹配了锁的value,下一步即是删除锁,但这时锁过期了,然后B客户端抢到了锁,并加了锁,但A客户端的操作没有结束,此时A客户端就会将B客户端的锁释放掉,导致错删锁。 这种情况即是原子操作可以避免的。我们用LUA脚本来做删除锁的判断即删除操作,就可以让此过程不被其他操作干扰。

四、Java中实现满足上述四个条件的分布式锁案例

LUA脚本:

//KEYS[1]指的就是lockKey,ARGV[1]指第一个参数lockKeyValue,这里用于确定当前锁是否是当前进程的锁
if redis.call('get',KEYS[1])==ARGV[1]
then
	return redis.call('del',KEYS[1])
else 
	return 0
end

testLockLua方法

@GetMapping("testLockLua")
public void testLockLua(){
    String productId = "1";
    String lockKey = "sk:" + productId;//设置锁的key,每个锁管理一个product的数据
    String lockKeyValue = UUID.randomUUID().toString();//锁的值
    //设置锁:3秒锁过期
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKeyValue, 3, TimeUnit.SECONDS);
    System.out.println(lock);
    //若能够获取到锁
    if (lock) {
        Object numVal = redisTemplate.opsForValue().get("num");
        if (StringUtils.isEmpty(numVal)) return;
        //若num 存在则将num+1
        int num = Integer.parseInt(numVal + "");
        redisTemplate.opsForValue().set("num",++num);
        //用Lua脚本释放锁,以防出现在删除时锁过期,而删除了其他进程的锁
        String luaScript = "if redis.call('get',KEYS[1])==ARGV[1]\n" + //KEYS[1]指的就是lockKey,ARGV[1]指第一个参数lockKeyValue,这里用于确定当前锁是否是当前进程的锁
                "then\n" +
                "\treturn redis.call('del',KEYS[1])\n" +//如果锁是属于当前进程的,那么就释放这个锁
                "else \n" +
                "\treturn 0\n" +
                "end";
        //设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,会给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(luaScript);
        defaultRedisScript.setResultType(Long.class);
        redisTemplate.execute(defaultRedisScript, Arrays.asList(lockKey), lockKeyValue);
    }else {
        //不能获取到锁
        try {
            Thread.sleep(1000);//等待1秒
            testLockLua();//等待后再次执行方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

其中脚本中的参数:KEYS[1]

redisTemplate.execute(defaultRedisScript, Arrays.asList(lockKey), lockKeyValue);

方法中参数Arrays.asList(lockKey)中的第一个值,ARGV[1]就是指lockKeyValue

RedisTemplate execute方法可以知道各个参数的意义。

@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
   return scriptExecutor.execute(script, keys, args);
}