• 基于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、使用时需要预估业务逻辑处理时间,一旦业务逻辑发生错误,那么只能等到超时之后其他线程才能拿到锁,可能会出现问题

  • 相关阅读:
    第四篇 -- 收获颇丰的一天
    第十六篇 -- SuperIO学习
    第八篇 -- 用U盘制作启动盘装Win10系统
    第二十五篇 -- 学习第三十四天打卡20190726
    zabbix钉钉报警
    windows 远程连接“发生身份验证错误 要求的函数不受支持”
    限制IP远程访问
    查看、踢出在线用户
    codis
    Linux、windows安装java
  • 原文地址:https://www.cnblogs.com/lige-H/p/8195535.html
Copyright © 2020-2023  润新知