[关闭]
@Toby-wei 2016-08-26T09:03:28.000000Z 字数 2107 阅读 32200

利用redis实现分布式锁

redis 数据库


利用redis来实现分布式锁。一般就用setnx和getset两个命令。

  • NXNot eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists.
  • getset是同步的

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

代码如下:

  1. private int timeoutMsecs
  2. private String lockKey
  3. private static long expireMsecs = 1000 * 60 * 5 // min 锁持有超时
  4. // timeoutMsecs 表示锁等待超时
  5. public JedisLock(Integer timeoutMsecs, String lockKey) {
  6. this.timeoutMsecs = timeoutMsecs
  7. this.lockKey = lockKey
  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. if (jedis.setnx(lockKey, expiresStr) == 1) {
  15. return true
  16. }
  17. String currentValueStr = jedis.get(lockKey); //redis里的时间
  18. // 表示已经锁失效,要重新设置锁
  19. if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
  20. //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
  21. // lock is expired
  22. String oldValueStr = jedis.getSet(lockKey, expiresStr)
  23. //获取上一个锁到期时间,并设置现在的锁到期时间,
  24. //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
  25. if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
  26. //如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
  27. return true
  28. }
  29. }
  30. timeout -= 100
  31. Thread.sleep(100)
  32. }
  33. return false
  34. }

所以大致的思路就是

  • 先定义好一个锁等待时间和锁超时时间,我们使用中分别设置了300ms和5min,
  • 线程执行进方法后,设置锁到期时间为当前时间加5分钟,就是System.currentTimeMillis() + 1000*60*5 + 1
  • 执行setnx,执行完返回1就是获取锁成功。返回0时,表示未获取到锁,执行下面一步
  • 判断是否已经锁到期,currentValueStr和当前时间比较,如果未过期,锁等待时间减100ms,sleep一下继续重复这个动作。如果已经到期,执行下面一步。
  • 通过getSet把redis中的锁超时时间设为自己的时间(因此自己要获取锁),这个方法是同步的。保证只能有个线程能拿到锁。执行这步返回的值是旧值,
  • 判断旧值和之前的锁超时的值是否一致。一致则获取锁成功。失败的话锁等待时间减1,sleep一下继续重复之前的逻辑。

再总结一下:

用最简单的办法就是,多个进程执行以下Redis命令:SETNX lock.foo
如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。

但是这里有个问题,如何解决死锁,

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

锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。因为这样可能同时会有多个线程获得锁。
比如就是p2,p3两个进程同时发现锁已经超时。p2执行删除操作,添加自己的key,返回1,获取锁成功。接着p3又删除p2设置的值,自己获取锁。这样子显然是不行的。

为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。

超时时我们可以这样处理。比如p4,p5发现超时之后,通过getset去更新key值,这个是同步的,能保证线程安全。然后根据getset返回的值是否大于当前时间来判断获取锁是否成功。如果小于当前时间,那获取成功。如果大于当前时间,说明已经有线程修改过了。再继续等待。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注