暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis分布式锁

东神殿下 2021-12-07
264

使用分布式锁要满足的几个条件:

  1. 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)

  2. 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)

  3. 同步访问(即有很多个进程同时访问同一个共享资源)


什么是分布式锁?

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。


进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。


分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。


应用的场景

线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。


有这样一个情景,线程A和线程B都共享某个变量X。

如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。

如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

 

分布式锁可以基于很多种方式实现,比如zookeeper,redis...  不管哪种方式,它的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。


这里主要讲解如何用redis实现分布式锁


使用redis的setNX命令实现分布式锁

实现的原理

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。


基本命令解析

1)setNX (SET if Not Exists)

语法:

SETNX key value
复制

将 key的值设为value,当且仅当key不存在。

若给定的key已经存在,则SETNX不做任何动作。

SETNX是【SET if  Not eXists】(如果不存在,则SET)的简写


返回值:

设置成功,返回1

设置失败,返回0


栗子:

192.168.199.128:6379> EXISTS job # job 不存在
(integer) 0
192.168.199.128:6379> SETNX job "programmer" # job 设置成功
(integer) 1
192.168.199.128:6379> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
192.168.199.128:6379> GET job # 没有被覆盖
"programmer"
复制
所以我们使用执行下面的命令SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key)foo加锁

客户端可以尝试以下方式:

SETNX lock.foo <current Unix time + lock timeout + 1>
复制
如果SETNX返回1,说明客户端已经获得了锁,SETNX将键lock.foo的值设置为锁的超时时间(当前时间+锁的有效时间)。之后客户端可以通过DEL lock.foo来释放锁。
如果SETNX返回0,说明key已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,值到成功获得锁或重试超时(timeout)。

2)getSET
先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。
语法:
GETSET key value
复制
将给定key的值设为value,并返回key的旧值(old value)。
当key存在但不是字符串类型时,返回一个错误。

返回值:
返回给定key的旧值[之前的值]。
当key没有旧值时,也即是,key不存在时,返回nil。

注意的关键点:(回答面试的核心点)
  1. 同一时刻只能有一个进程获取到锁。

    sentx

  2. 释放锁:

    锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;

    (最简单的方式就是del,如果在删除之前死锁了)


ex:
53秒设置--58秒到期
当前时间为56秒,没有过期
当前时间为59秒,过期(当前时间大于设置的时间)

死锁情况是在判断超时后,直接操作业务,设置过期时间,执行业务,然后删除释放锁。其他进程再次通过setnx来枪锁。

解决死锁:
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么办?
我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已经失效,可以被重新使用。

发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有者执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
C0操作超时了,但它还持有者锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。C1发送DEL lock.foo C1 发送SETNX lock.foo并且成功了。C2发送DEL lock.foo C2发送SETNX lock.foo并且成功了。这样一来,C1,C2都拿到了锁!问题大了!

幸好这种问题是可以避免的,那C3客户端应该怎样做?
C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 C3发送GET lock.foo以检查锁是否超时了。
如果没超时,则等待或重试。反之,如果已超时,C3通过下面的操作来尝试获得锁:GETSET lock.foo 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:为了让分布式锁的算法更稳健些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

伪代码:
public static boolean lock(String lockName) {
Jedis jedis = RedisPool.getJedis();
//lockName可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
System.out.println(Thread.currentThread() + "开始尝试加锁!");
Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
if (result != null && result.intValue() == 1){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {//判断是否死锁
String lockValueA = jedis.get(lockName);
//得到锁的过期时间,判断小于当前时间,说明已超时但是没释放锁,通过下面的操作来尝试获得锁。下面逻辑防止死锁
[已经过期但是没有释放锁的情况]
if (lockValueA != null && Long.parseLong(lockValueA) < System.currentTimeMillis()){
String lockValueB = jedis.getSet(lockName,
String.valueOf(System.currentTimeMillis() + 5000));
//这里返回的值是旧值,如果有的话。之前没有值就返回null,设置的是新超时。
if (lockValueB == null || lockValueB.equals(lockValueA)){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {
return false;
}
} else {
return false;
}
}
}


复制

文章转载自东神殿下,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论