redis分布式锁

本文只考虑redis单机部署情况。
redis分布式锁

1、安全性:互斥。保证同一时刻只有一个线程持有锁;
2、避免死锁:最终,即使锁定资源的客户端崩溃或被分区,也始终有可能获取锁。
3、容错性:只要大多数Redis节点都启动了,客户端就可以获取和释放锁。
4、自己的锁自己解,不能解别人的锁。
多个客户端同时发起请求,为了保证数据的一致性,使用redis加锁,将关键数据作为key,锁不存在时才能设置锁(互斥),并设置过期时间(避免死锁)。

setnx命令

redis版本低时,设置锁和过期时间是分开执行的,不是原子操作,会有各种问题。

try{
	...
	String threadId = Thread.currentThread().getId(); //将线程作为value,保证唯一性
	if(jedis.setnx(key, threadId) == 1){
		//此时程序崩溃或节点挂掉,过期时间没有设置,就会发生死锁,别的线程再也无法获得锁了
		jedis.expire(key, expireTime);	
	}else{
		return;
	}
	...
}catch(Exception e){
	logger.error(e);
}finally{
	jedis.del(key);  //删除锁 如果客户端A设置了锁,因为某些原因导致执行的很慢,没执行完锁过期了,客户端B获取了锁,此时A执行完,删除锁,这样实际删除的就是B的锁。
}

set命令

redis在版本2.6.12开始,set命令包含设置过期时间的功能,保证操作的原子性。

String result = jedis.set(lockKey, value,  "NX", "PX", expireTime);

"NX"表示没有锁时设置锁,“XX”–有锁时设置锁
“PX”–过期时间单位是秒,“PX”–过期时间单位是毫秒。
expireTime–过期时间

其它问题

虽然上面一步已经满足了我们的需求,但是还是要考虑其它问题?
1、 redis发现锁失败了要怎么办?中断请求还是循环请求?
2、 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
3、 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁,这时候客户端A执行完了,会不会在删锁的时候把B的锁给删掉?

解决方案

1、锁失败就循环请求。
2、可以在抢锁失败后睡眠几毫秒后再请求。
3、加锁时每个请求设置一个value,解锁时判断value是不是自己的,是自己的时候再删除锁。

do{
	String value = UUID.randomUUID().toString();
	String result = jedis.set(lockKey, value, "NX", "PX", 60);
        
     if ("OK".equals(result)) {         
          if(value.equals(jedis.get(key)){ //判断锁是不是自己的
           		...    //执行内部代码
				jedis.del(key);
				//此时也有问题:判断和释放锁是两个独立操作,不是原子性。A客户端加锁,在执行这行代码之前锁过期了,B获取了锁,那么这行代码就把B的锁删除了。
				continue;//跳出循环
		  }
      }else {
			Thread.sleep(100); //睡眠,降低抢锁频率,缓解redis压力
	  }
}while(!"OK".equals(result)) //循环获取锁
解锁方案

使用Lua脚本语言解锁

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua脚本
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

首先获取锁对应的value,如果value与自己的值一致,则删除锁。
第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。保证操作的原子性。

出现并发的可能性

虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。

假设过期时间是30秒,当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件;

参考阅读

https://redis.io/topics/distlock

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值