基于Redis实现分布式锁

/ 技术 / 无站内评论 / 316浏览

背景

在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value

返回值:

- 1,当 key 的值被设置 
- 0,当 key 的值没被设

例子:

redis> SETNX mykey “hello” 
(integer) 1 
redis> SETNX mykey “hello” 
(integer) 0 
redis> GET mykey 
“hello” 
redis>

当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

使用SETNX实现分布式锁

多个进程执行以下Redis命令:

SETNX lock.foo <current Unix time + lock timeout + 1>

如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。 
如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。

解决死锁

考虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。

上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。

然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:

从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。

为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。 
我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:

另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。

java之jedis实现 

expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放 
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会

  1. /** 
  2.  * Acquire lock. 
  3.  *  
  4.  * @param jedis 
  5.  * @return true if lock is acquired, false acquire timeouted 
  6.  * @throws InterruptedException 
  7.  *             in case of thread interruption 
  8.  */  
  9. public synchronized boolean acquire(Jedis jedis) throws InterruptedException {  
  10.     int timeout = timeoutMsecs;  
  11.     while (timeout >= 0) {  
  12.         long expires = System.currentTimeMillis() + expireMsecs + 1;  
  13.         String expiresStr = String.valueOf(expires); //锁到期时间  
  14.   
  15.         if (jedis.setnx(lockKey, expiresStr) == 1) {  
  16.             // lock acquired  
  17.             locked = true;  
  18.             return true;  
  19.         }  
  20.   
  21.         String currentValueStr = jedis.get(lockKey); //redis里的时间  
  22.         if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {  
  23.             //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的  
  24.             // lock is expired  
  25.   
  26.             String oldValueStr = jedis.getSet(lockKey, expiresStr);  
  27.             //获取上一个锁到期时间,并设置现在的锁到期时间,  
  28.             //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的  
  29.             if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {  
  30.                 //如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁  
  31.                 // lock acquired  
  32.                 locked = true;  
  33.                 return true;  
  34.             }  
  35.         }  
  36.         timeout -= 100;  
  37.         Thread.sleep(100);  
  38.     }  
  39.     return false;  
  40. }  


参考资料

http://blog.csdn.net/lihao21/article/details/49104695

http://blog.csdn.net/ugg/article/details/41894947

http://m635674608.iteye.com/blog/2296022


召唤蕾姆
琼ICP备18000156号

鄂公网安备 42011502000211号