一、在分布式的集群中,如何保证不同的节点的线程同步?
对于单进程的并发场景,我们可以使用语言和类库提供的锁,对于分布式的场景我们可以使用分布式锁
我们需要知道分布式锁的3个核心要素。
-
加锁
-
最简单的方法是sentx(如果key存在,不做任何操作)命令,key是锁的唯一标识,我们可以根据业务来决定命名,比如我们想给一种商品的秒杀活动加锁,我们可以给key命名“lock_商品id“。而value设置成什么了,我们可以姑且设置成1。
setnx(key,1)当前一个线程执行setnx返回1,说明key原本就不存在。该线程就成功的到锁,如果返回的是0,说明key已经存在,该线程抢锁失败。
-
-
解锁
-
有加锁,就需要解锁。当得到的线程执行完任务,需要释放锁。让其他线程进入,释放锁最简单的方式是del删除key。
del(key)
-
-
锁超时
-
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显示的释放锁,这块资源就会被永久的锁住。别的线程永远也访问不了。
-
所以我们必须要给锁设置一个超时的时间,以保证即使得到锁的线程挂掉,没有显示的释放锁。这把锁也要在规定的时间进行释放,sentx不支持奢设置超时参数,所以我们需要额外的指令
expire(key,30) -
综合起来,我们实现分布式锁实现的第一版代码如下
if(setnx(key,1)==1){ #设置key的超时时间 expire(key,30); try( # TODO do something )fially{ del(key) } }
-
上面的方法中,存在着3个致命的问题
-
sentx和expire的非原子性
- 设想一个极端的场景,当线程执行sentx,成功得到了锁
graph LR A[线程A] -->|sentnx|B[被锁住同步代码块] C[线程B] -->B[同步代码块]setnx刚执行成功,还未来得及执行设置过期时间的指令,线程a就挂掉了,这样以来,这把锁就没有过期时间,线程A又关掉了,其他线程永远也无法访问该代码块
-
怎么解决?sentx指令本事是不支持传入超时时间的,在redis2.6.12以上版本为set指令增加了可选参数
set(key,1,30.NX)这样就可以取代setnx指令
-
del导致误删
-
又是一个极端的场景,假如某个线程成功的到了锁,并且成功的设置了超时的时间是30s
graph LR F[线程A] -->|Set设置超时30s的超时时间|H[被锁住同步代码块] G[线程B] -->H[同步代码块]如果某些原因,导致线程A执行的很慢很慢,过了30s都没有执行完,这时候锁过期,自动释放,线程得到了锁
graph LR F[线程A] -->|Set设置超时30s的超时时间|H[被锁住同步代码块] G[线程B] -->|执行set成功得到了锁|H[被锁住同步代码块]随后线程A执行完任务,接着执行del指令来释放锁,但这个时候线程B还没有执行完,线程A删除的是线程B的锁
graph LR F[线程A] -->|执行完成 DEL锁|H[被锁住同步代码块] G[线程B] -->H[被锁住同步代码块]
-
-
怎么避免这种场景,可以在del删除释放锁之前,做一个判断,验证当前锁是不是当前线程自己加的锁
-
我们可以在加锁的时候把当前线程的id当作value,并在删除之前验证key对应的value是不是当前线程的id
-
加锁
String threadId=Thread.currentThread.getId(); set(key,threadId,30,NX) -
解锁
if(threadId.equals(redis.get(key))){ del(key) }
-
-
这样的操作又隐含了一个新的问题,判断和释放锁是2个独立的操作,不是原子性的
-
我们需要用lua脚本实现
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));这样验证和删除的操作就是原子性
-
-
出现并发的可能性
-
还是之前描述的场景,虽然我们避免了线程误删掉key的情况,但统一时间又A、B2个线程在访问代码块,仍然是有问题的,
-
我们需要让获得锁的线程开启一个守护线程,用来给快要过期的锁“延长锁的时间“
graph LR F[线程A ] -->|过了29秒key要过期了|H[被锁住同步代码块] A[线程A的守护线程] -->|expire延长20s|H[被锁住同步代码块] G[线程B] -->H[被锁住同步代码块]当线程A完成,会显示的关掉守护线程
graph LR F[线程A ] -->|A线程执行完成|H[被锁住同步代码块] G[线程B] -->H[被锁住同步代码块]
-
如果线程A忽然断电,由于线程A和守护线程在 同一个进程,守护线程也会停止,这把锁到释放的时候,守护线程无法续航,也会自动释放
-
-