高并发解决方法----分布式Redis锁
1、分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、 只允许解自己的锁。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
2、需要了解的redis命令
Redis是单线程操作,线程安全
2.1 setnx (set if not exists) 例如:setnx akey avalue =>1; setnx akey avalue =>0;
2.2 setex(过期时间秒级)例如:setex akey 10 avalue;有效期10秒
2.3 psetex(过期时间毫秒级) 例如: psetex akey 10000 avalue;有效期10秒
Redis 2.6.12开始set指令可以执行上述操作:
例如Set key value NX PX milliseconds;//成功返回OK,失败返回NULL;
3、执行逻辑:
3.1 获取并发锁
3.2 判断是否取到锁
3.3 有锁:执行业务方法;无锁:排队/退出
3.4 删除并发锁
4、加锁/解锁
4.1 循环:判断等待时间是否>=0;
4.2 set方法设置key/value/过期时间,判断是否成功;成功:返回true
4.3 如果等待时间==0,直接返回失败
4.4 睡眠等待100毫秒,等待时间递减;循环判断
// 加锁
Public Boolean getLock(Jedis jedis,int timeOut, int expireTime){
Long waitTime = 100l;//默认等待100毫秒
While(timeOut>=0){// 循环
If(“OK”.equals(jedis.set(key,UUID,”NX”,”PX”, expireTime))){
Return true;
if (timeout == 0) {//需要等待的时间
return false;
if (timeout < waitTime) {
waitTime = timeout;
Thread.sleep(waitTime);// 等一会
timeout -= waitTime;
return false;
}
// 解锁
public static boolean releaseLock(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;
}
// 调用并发执行方法
public void execute(*****){
if(getLock(**)){
callBack.invoke();
}else{
throw new BizException("");
}catch(Exception e){
throw new BizFailException("");
}finally{
releaseLock(**);
}
问答:
1、如果业务执行过程中发生异常,key没有删除,怎么办?
答:给key设置默认过期时间,时间到了自动删除
2、如果使用setnx、psetex指令,这是两个指令,非原子性操作。如果在setnx指令完成后,在psetex指令之前redis宕机了,也会发生死锁?
答:使用set key value NX PX seconds;一条命令,就是原子性操作,避免这种情况。
3、客户端A获取锁的时候设置了key的过期时间为2秒,然后客户端A在获取到锁之后,业务逻辑方法doSomething执行了3秒(大于2秒),当执行完业务逻辑方法的时候,客户端A获取的锁已经被Redis过期机制自动释放了,因此客户端A在获取锁经过2秒之后,该锁可能已经被其他客户端获取到了。当客户端A执行完doSomething方法之后接下来就是执行releaseLock方法释放锁了,但是此时的锁是别人的。如何规避删除那别人的锁?
答:value可以设置一个UUID随机值,删除锁的时候判断UUID是否是当去自己设置的那个值,是的话才删除。
4、问题3中,因为getValue和deleteKey是两个命令,也就不是原子性操作,意味着当一个客户端执行完getKey方法并在执行deleteKey方法之前,也就是在这2个方法执行之间,其他客户端是可以执行其他命令的。考虑这样一种情况,在客户端A执行完getKey方法,并且该key对应的值也等于先前的随机值的时候,接下来客户端A将会执行deleteKey方法。假设由于网络或其他原因,客户端A执行getKey方法之后过了1秒钟才执行deleteKey方法,那么在这1秒钟里,该key有可能也会因为过期而被Redis清除了,这样一来另一个客户端,姑且称之为客户端B,就有可能在这期间获取到锁,然后接下来客户端A就执行到deleteKey方法了,如此一来就又出现误释放别的客户端申请的锁的问题了。
答:使用LUA脚本执行删除操作,一条命令就不存在上述问题,“if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”; //LUA脚本。这样一条语句就判断删除自己的锁了。
5、 Redis锁的过期时间小于业务的执行时间该如何续期?
答:利用看门狗机制定时检查锁。比如默认情况下,加锁的时间是30秒。如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期。把锁重置成30秒。那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗。