基于redis的一种分布式锁
前言:本文介绍了一种基于redis的分布式锁,利用jedis实现应用(本文应用于多客户端+一个redis的架构,并未考虑在redis为主从架构时的情况)
文章理论来源部分引自:https://i.cnblogs.com/EditPosts.aspx?opt=1
一、基本原理
1、用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
2、redis采用单进程单线程模式,采用队列模式将并发访问变成串行访问,多客户端对Redis的连接并不存在竞争关系。
二、基本命令
1、setNX(SET if Not eXists)
语法:
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写
返回值:
设置成功,返回 1 。
设置失败,返回 0
2、getSet
GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。
返回值:
返回给定 key 的旧值。
当 key 没有旧值时,也即是, key 不存在时,返回 nil 。
3、get
GET key
当 key 不存在时,返回 nil ,否则,返回 key 的值。
如果 key 不是字符串类型,那么返回一个错误
三、取锁、解锁以及示例代码:
/** * @Description:分布式锁,通过控制redis中key的过期时间来控制锁资源的分配 * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁. * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间) * 执行过程: * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁 * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值 * @param key * @param expireTime 有效时间段长度 * @return */ public boolean getLockKey(String key, final long expireTime) { // 1.setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2 if (getJedis().setnx(key, new Date().getTime() + expireTime + "") == 1) return true; String oldExpireTime = getJedis().get(key); // 2.get(lockkey)获取值oldExpireTime // ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3 if (null != oldExpireTime && "" !=oldExpireTime && Long.parseLong(oldExpireTime) < new Date().getTime()) { // 3计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) // 会返回当前lockkey的值currentExpireTime。 Long newExpireTime = new Date().getTime() + expireTime; String currentExpireTime = getJedis().getSet(key, newExpireTime + ""); // 4.判断currentExpireTime与oldExpireTime // 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了, //那么当前请求可以直接返回失败,或者继续重试。防止java多个线程进入到该方法造成锁的获取混乱。 if (!currentExpireTime.equals(oldExpireTime)) { return false; } else { return true; } } else { // 锁被占用 return false; } } /** * * @Description: 如果业务处理完,key的时间还未到期,那么通过删除该key来释放锁 * @param key * @param dealTime 处理业务的消耗时间 * @param expireTime 失效时间 */ public void deleteLockKey(String key,long dealTime, final long expireTime) { if (dealTime < expireTime) { getJedis().del(key); } }
示例:
// 循环等待获取锁 StringBuilder key = new StringBuilder(KEY_PRE); key.append(code).append("_"); key.append(batchNum); long lockTime = 0; try { while (true) { boolean locked = redisCacheClient.getLockKey( key.toString(), 60000); if (locked) { lockTime = System.currentTimeMillis(); break; } Thread.sleep(200); } } catch (InterruptedException e) { } //业务逻辑... //业务逻辑进行完,解锁 long delLockDateTime =System.currentTimeMillis(); long dealTime = delLockDateTime - lockTime; deleteLockKey(key.toString(), dealTime, 60000);
四、一些问题
1、为什么不直接使用expire设置超时时间,而将时间的毫秒数其作为value放在redis中?
如下面的方式,把超时的交给redis处理:
lock(key, expireSec){ isSuccess = setnx key if (isSuccess) expire key expireSec }
这种方式貌似没什么问题,但是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。
2、为什么前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,然后和外面的判断超时时间的时间戳比较呢?
因为是分布式的环境下,可以在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:
C0超时了,还持有锁,C1/C2同时请求进入了方法里面
C1/C2获取到了C0的超时时间
C1使用getSet方法
C2也执行了getSet方法
假如我们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将获得锁,加了之后,能保证C1和C2只能一个能获得锁,一个只能继续等待。
注意:这里可能导致超时时间不是其原本的超时时间,C1的超时时间可能被C2覆盖了,但是他们相差的毫秒及其小,这里忽略了
五、不完善之处
1、使用时需要预估业务逻辑处理时间,一旦业务逻辑发生错误,那么只能等到超时之后其他线程才能拿到锁,可能会出现问题