前言:本文介绍了一种基于redis的分布式锁,利用jedis实现应用(本文应用于多客户端+一个redis的架构,并未考虑在redis为主从架构时的状况)java
文章理论来源部分引自:https://i.cnblogs.com/EditPosts.aspx?opt=1redis
1、基本原理缓存
一、用一个状态值表示锁,对锁的占用和释放经过状态值来标识。架构
二、redis采用单进程单线程模式,采用队列模式将并发访问变成串行访问,多客户端对Redis的链接并不存在竞争关系。并发
2、基本命令app
一、setNX(SET if Not eXists)分布式
语法:ui
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。spa
若给定的 key 已经存在,则 SETNX 不作任何动做。线程
SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写
返回值:
设置成功,返回 1 。
设置失败,返回 0
二、getSet
GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。
返回值:
返回给定 key 的旧值。
当 key 没有旧值时,也便是, key 不存在时,返回 nil 。
三、get
GET key
当 key 不存在时,返回 nil ,不然,返回 key 的值。
若是 key 不是字符串类型,那么返回一个错误
3、取锁、解锁以及示例代码:
/** * @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);
4、一些问题
一、为何不直接使用expire设置超时时间,而将时间的毫秒数其做为value放在redis中?
以下面的方式,把超时的交给redis处理:
lock(key, expireSec){ isSuccess = setnx key if (isSuccess) expire key expireSec }
这种方式貌似没什么问题,可是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。
二、为何前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,而后和外面的判断超时时间的时间戳比较呢?
由于是分布式的环境下,能够在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:
C0超时了,还持有锁,C1/C2同时请求进入了方法里面
C1/C2获取到了C0的超时时间
C1使用getSet方法
C2也执行了getSet方法
假如咱们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将得到锁,加了以后,能保证C1和C2只能一个能得到锁,一个只能继续等待。
注意:这里可能致使超时时间不是其本来的超时时间,C1的超时时间可能被C2覆盖了,可是他们相差的毫秒及其小,这里忽略了
5、不完善之处
一、使用时须要预估业务逻辑处理时间,一旦业务逻辑发生错误,那么只能等到超时以后其余线程才能拿到锁,可能会出现问题