基于redis的一种分布式锁

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于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、使用时需要预估业务逻辑处理时间,一旦业务逻辑发生错误,那么只能等到超时之后其他线程才能拿到锁,可能会出现问题

以上是关于基于redis的一种分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

分布式锁的两种实现方式(基于redis和基于zookeeper)

分布式锁的两种实现方式(基于redis和基于zookeeper)

基于redis的分布式锁

基于redis的分布式锁二种应用场景

分布式锁三大技术方案实战——基于redis方式实现分布式锁(终极篇)

基于Redis的分布式锁的简单实现