redis分布式锁unlock方法
来源:互联网 发布:超级玛丽安卓源码 编辑:程序博客网 时间:2024/06/17 08:00
redis作为分布式锁的运用,网上有无数的案例,这里提供一个我自己设计的unlock解锁方案。
相对于加锁,解锁的过程相对简单,之前我项目里解锁就是直接delete lock_key,由于加锁的过程设置了超时时间,简单的delete lock_key显然有造成误删锁的风险(下面会具体介绍误删的原因)。本人梳理了下解锁的流程,设计了一个大部分情况下可以避免误删的解锁方案。
首先引用一个典型的加锁流程。利用的jedis客户端,其中setnx方法判断锁是否被其他线程占用,如果被占用的话也不是马上退出,而是比较占用的锁是否超时。这儿getSet方法用来避免多个线程同时删除过期锁,从而同时获取到锁的情况,保证只有一个线程能修改过期锁。这个过程网上有无数的案例,在此不作过多描述。下面分析该方法的unlock方法如何解锁。
/* * 典型加锁流程 * */ public boolean acquireLock(String lock) { boolean success = false; Jedis jedis = pool.getResource(); //过期时间3分钟 long value = System.currentTimeMillis() + 3 * 60 * 1000 + 1; long acquired = jedis.setnx(lock, String.valueOf(value)); //SETNX成功,则成功获取一个锁 if (acquired == 1) success = true; //SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时 else { long oldValue = Long.valueOf(jedis.get(lock)); //超时 if (oldValue < System.currentTimeMillis()) { String getValue = jedis.getSet(lock, String.valueOf(value)); // 获取锁成功 if (Long.valueOf(getValue) == oldValue) success = true; // 已被其他进程捷足先登了 else success = false; } //未超时,则直接返回失败 else success = false; } pool.returnResource(jedis); return success; }
关于unlock方法,如果直接删除lock_key,显然可能存在这样的错误:
当T1加锁成功,但执行过程被挂起,导致执行时间超过3分钟,另一个线程T2修改了T1的锁过期时间,此时T2加锁成功。当T1执行到解锁的流程,如果T1直接delete lock_key,删除的是T2修改过的lock_key,这时候T2未必执行完毕。如果再有T3过来,将直接加锁成功,导致T2和T3并发。
问题的关键是,T1解锁的时候,没有验证删除的是自己锁。为了让T1线程能“认识”自己加的锁,我们修改了lock方法,返回加锁的时间戳,用于unlock判断。加锁代码:
/* * 加锁成功返回时间戳,加锁失败返回空对象null * */ public String lock(String lock) { Jedis jedis = pool.getResource(); long value = System.currentTimeMillis() + 3 * 60 * 1000 + 1; String timeStamp = String.valueOf(value); long acquired = jedis.setnx(lock, timeStamp); //SETNX成功,则成功获取一个锁 if (acquired == 1) { return timeStamp; } else { long oldValue = Long.valueOf(jedis.get(lock)); //超时 if (oldValue < System.currentTimeMillis()) { String getValue = jedis.getSet(lock, timeStamp); // 这种情况不能加锁 if (Long.valueOf(getValue) != oldValue) { timeStamp = null; } } else { //锁未超时,也不能加锁 timeStamp = null; } } pool.returnResource(jedis); //只有加锁成功情况,才返回了加锁的时间戳 return timeStamp; }lock方法可以返回时间戳,当调用unlock方法时,用这个时间戳作为验证的参数
/* * 解锁要验证加锁返回的时间戳 * */ public void unlock(String lock, String timeStamp) { Jedis jedis = pool.getResource(); String value = jedis.get(lock); .......................................(1) long expireTime = Long.parseLong(value); if (System.currentTimeMillis() > expireTime && value.equals(timeStamp)) { jedis.del(lock); ...............................................(2) } pool.returnResource(jedis); }
这儿解释一下delete lock_key的条件:
1、当锁未超时(小于currentTimeMillis)
2、当锁的value和timeStamp相等
为什么要验证锁未超时?因为如果锁已经超时,就算value和timeStamp相等,即线程自己加的锁,如果直接删除这个超时的锁,可能删除的是另一个线程的锁,具体过程像这样:
(1)T1线程查看锁是否是自己的timeStamp:
String value = jedis.get(lock);
if(value.equals(timeStamp)) ==> true
//判断timeStamp是不是和value相同,结果相同,因此T1线程正准备删除key
(2)这时候T2线程过来获取锁,由于T1的锁已经超时,T2直接加锁成功了,这时候lock的value其实不在是timeStamp
(3)T1执行到删除的命令,然后悲剧发生,T2的lock_key被删掉了。。。
整个错误的核心在于,unlock方法里面(1)、(2)两个步骤之间,不能有别的线程加锁。
按照这个思路,redis的watch似乎可以更简洁的处理lock和unlock的过程。但据说watch涉及到redis事务开销,很少有用watch实现分布锁。如果读者有更好的方式,欢迎向博主推荐
(个人原创,转载请注明出处)