分布式锁的实现

前言

我们知道在多线程中,有时候为了保证一个代码块只被一个线程执行,从而通过加锁的方式实现,如Java中的synchronized语法。这可以保证一个进程不同线程的同步,那分布式集群中又是如何来保证线程间的同步呢?

redis实现分布式锁

要想实现分布式集群的同步,可以采用分布式锁,实现分布式锁的方式有很多,如rediszookeeperMemcached。先来讲一下如何利用redis来实现分布式锁。

版本一

我们知道redis是一种以key-value形式存储的一种NoSQL,可以在需要同步的代码前去判断一个key是否存在,如果这个key不存在,那么才可以执行同步快代码。如果key存在说明有其他线程在执行,就等待该线程释放这个key为止。用下面版本一代码来说明。

1
2
3
4
5
6
7
while(true){
if (jredis.exesit(key)==0)//(1) 如果不存在该key,才可跳出循环执行逻辑代码
break;
}
jredis.set(key,value)
逻辑处理...
jredis.del(key)

版本二

但是上面的代码是有问题的,因为exesit和set方法不能够保证原子性,两个线程可以同时执行到(1),然后跳出循环。我们可以利用Redis的setnx(set while not exesit)命令。此命令是原子性操作,只有在key不存在的情况下,才能set成功。伪代码改动成版本二。

1
2
3
4
5
6
while(true){
if (jredis.setnx(key,vaule)==1)//当对key设置成功,才可以执行下面的逻辑代码
break;
}
逻辑处理...
jredis.del(key)

版本三

看起来已经没问题了,但是假设一个极端的情况,当一个线程在执行完逻辑部分的代码之后,突然宕机了,导致del key没能执行,那边其他线程将永远不能跳出死循环了。

所以我们需要为了防止一个key的”长生不死”,在set的时候,给这个key设定一个有效时间,时间一到就会自动del掉这个key。当然我们也要保证设定这个有效时间的方法和setnx方法是原子性的。幸好Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:

1
2
3
4
5
6
while(true){
if (jredis.set(key,vaule,30,nx)==1)//set中可以加入nx参数表示意思与setnx一样,30是该key的有效时间
break;
}
逻辑处理...
jredis.del(key)

版本四

嘎嘎嘎,没错,版本三还是存在问题。再考虑一种极端情况由于特殊原因,如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。

怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

1
2
3
4
5
6
7
8
9
String threadId = Thread.currentThread().getId()
while(true){
if (jredis.set(key,threadId,30,nx)==1)//set中可以加入nx参数表示意思与setnx一样,30是该key的有效时间
break;
}
逻辑处理...
if(threadId .equals(redisClient.get(key))){
jredis.del(key) //(2)
}

版本五

但这么做,又会出现一个问题,因为判断和删除并不是一个原子性操作。假设A线程在版本四(2)代码处停留了30秒,那么A还是会删除B设定的锁。所以这块可以用lua脚本去完成,redis可以保证lua脚本是以原子性的操作去执行的。关于redis和lua的说明,可以看这篇博客点击查看

1
2
3
4
5
6
7
8
String threadId = Thread.currentThread().getId()
while(true){
if (jredis.set(key,threadId,30,nx)==1)//set中可以加入nx参数表示意思与setnx一样,30是该key的有效时间
break;
}
逻辑处理...
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));

除了用Lua脚本,还可以实现守护线程,对将要过期的key续命,比如执行到29秒的时候,还没释放,守护线程就给这个key再设定20秒,当线程A执行完任务,会显式关掉守护线程。以上是基于程序员小灰的文章的一个总结,感谢!