Lua脚本在redis分布式锁场景的运用

redis分布式锁,Lua,Lua脚本,lua redis,redis lua 分布式锁,redis setnx ,redis分布式锁, Lua脚本在redis分布式锁场景的运用。java

锁和分布式锁

锁是什么?

锁是一种能够封锁资源的东西。这种资源一般是共享的,一般会发生使用竞争的。python

为何须要锁?

须要保护共享资源正常使用,不出乱子。
比方说,公司只有一间厕所,这是个共享资源,你们须要共同使用这个厕所,因此避免不了有时候会发生竞争。若是一我的正在使用,另一我的进去了,咋办呢?若是两我的同时钻进了一个厕所,那该怎么办?结果如何?谁先用,仍是一块儿使用?特别的,假如是一男一女同时钻进了厕所,事情会怎样呢?反正我是不懂……程序员

若是这个时候厕所门前有个锁,每一个人都无法随便进入,而是须要先获得锁,才能进去。而获得这个锁,就须要里边的人先出来。这样就能够保证同一时刻,只有一我的在使用厕所,这我的在上厕所的期间不会有不安全的事情发生,不会中途被人闯进来了。redis

Java中的锁

在 java 编码的时候,为了保护共享资源,使得多线程环境下,不会出现“很差的结果”。咱们可使用锁来进行线程同步。因而咱们能够根据具体的状况使用synchronized 关键字来修饰一个方法,或者一段代码。这个方法或者代码就像是前文中提到的“受保护的厕所,加锁的厕所”。也可使用 java 5之后的 Lock 来实现,与 synchronized 关键字相比,Lock 的使用更灵活,能够有加锁超时时间、公平性等优点。算法

分布式锁

上面咱们所说的 synchronized 关键字也好,Lock 也好。其实他们的做用范围是啥,就是当前的应用啊。你的代码在这个 jar 包或者这个 war 包里边,被部署在 A 机器上。那么实际上咱们写的 synchronized 关键字,就是在当前的机器的 JVM在执行代码的时候发生做用的。假设这个代码被部署到了三台机器上 A,B,C。那么 A 机器中的部署的代码中的synchronized 关键字并不能控制 B,C 中的内容。spring

假如咱们须要在 A,B,C 三台机器上运行某段程序的时候,实现“原子操做”,synchronized 关键字或者 Lock 是不能知足的。很显然,这个时候咱们须要的锁,是须要协同这三个节点的,因而,分布式锁就须要上场了,他就像是在A,B,C的外面加了一个层,经过它来实现锁的控制。shell

redis 如何实现加锁

在redis中,有一条命令,能够实现相似 “锁” 的语法是这样的:编程

SETNX key value

他的做用是,将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不作任何动做。设置成功,返回 1 ;设置失败,返回 0安全

使用 redis 来实现锁的逻辑就是这样的

线程 1 获取锁  -- > setnx mylock lockvalue
              -- >  1  获取锁成功
线程 2 获取锁  -- > setnx mylock lockvalue 
              -- >  0  获取锁失败  (继续等待,或者其余逻辑)
线程 1 释放锁  -- > 
线程 2 获取锁  -- > setnx mylock lockvalue
              -- > 1 获取成功

锁超时

在这个例子中,咱们梳理了使用 redis setnx 命令 来实现锁的逻辑。这里还须要考虑的是,锁超时的问题 ,由于当线程 1 获取了锁以后,若是业务逻辑执行很长很长时间,那么其余线程只能死等,这可不行。因此须要加上超时,结合这些考虑的状况,实际的 Java 代码能够这样写:

public static boolean lock(String key,String lockValue,int expire){
        if(null == key){
            return false;
        }
        try {
            Jedis jedis = getJedisPool().getResource();
            String res = jedis.set(key,lockValue,"NX","EX",expire);
            jedis.close();
            return res!=null && res.equals("OK");
        } catch (Exception e) {
            return false;
        }
    }

retry

这里执行加锁,不必定能成功。当别人正在持有锁的时候,加锁的线程须要继续尝试。这个“继续尝试”一般是“忙等待”,实现代码以下:

/**
     * 获取一个分布式锁 , 超时则返回失败
     * @param key           锁的key
     * @param lockValue     锁的value
     * @param timeout       获取锁的等待时间,单位为 秒
     * @return              获锁成功 - true | 获锁失败 - false
     */
    public static boolean tryLock(String key,String lockValue,int timeout,int expire){
        final long start = System.currentTimeMillis();
        if(timeout > expiredNx) {
            timeout = expiredNx;
        }
        final long end = start + timeout * 1000;
        boolean res = false; // 默认返回失败
        while(!(res = lock(key,lockValue,expire))){ // 调用了上面的 lock方法
            if(System.currentTimeMillis() > end) {
                break;
            }
        }
        return res;
    }

redis 如何释放锁

根据上面所述,咱们在加锁的时候执行了:setnx mylock lockvalue , 这种加锁的本质其实就是 “占座位”,我把一本书放在自习室第一排的第一个座位上,别人就不能坐了,就得等着我走了,把东西拿走了,他就可使用这个座位了。因此很容易想到,在咱们须要释放锁的时候,只须要调用 del mylock 就好了,这样别的线程想去执行加锁的时候执行就能够执行 setnx mylock lockvalue 了。

不应释放的锁

可是,直接执行del mylock 是有问题的,咱们不能直接执行 del mylock 为何?—— 会致使 “信号错误”,释放了不应释放的锁 。假设以下场景:

时间线 线程1 线程2 线程3
时刻1 执行 setnx mylock val1 加锁 执行 setnx mylock val2 加锁 执行 setnx mylock val2 加锁
时刻2 加锁成功 加锁失败 加锁失败
时刻3 执行任务... 尝试加锁... 尝试加锁...
时刻4 任务继续(锁超时,自动释放了) setnx 得到了锁(由于线程1的锁超时释放了) 仍然尝试加锁...
时刻5 任务完毕,del mylock 释放锁 执行任务中... 得到了锁(由于线程1释放了线程2的)
...

上面的表格中,有两个维度,一个是纵向的时间线,一个是横线的线程并发竞争。咱们能够发现线程 1 在开始的时候比较幸运,得到了锁,最早开始执行任务,可是,因为他比较耗时,最后锁超时自动释放了他都还没执行完。 所以,线程 2 和线程3 的机会来了。而这一轮,线程2 比较幸运,获得了锁。但是,当线程2正在执行任务期间,线程1 执行完了,还把线程2的锁给释放了。这就至关于,原本你锁着门在厕所里边尿尿,进行到一半的时候,别人进来了,由于他配了一把和你如出一辙的钥匙!这就乱套了啊

所以,咱们须要安全的释放锁——“不是个人锁,我不能瞎释放”。因此,咱们在加锁的时候,就须要标记“这是个人锁”,在释放的时候在判断 “ 这是否是个人锁?”。这里就须要在释放锁的时候加上逻辑判断,合理的逻辑应该是这样的:

1. 线程1 准备释放锁 , 锁的key 为 mylock  锁的 value 为 thread1_magic_num
2. 查询当前锁 current_value = get mylock
3. 判断    if current_value == thread1_magic_num -- > 是  我(线程1)的锁
          else                                   -- >不是 我(线程1)的锁
4. 是个人锁就释放,不然不能释放(而是执行本身的其余逻辑)。

为了实现上面这个逻辑,咱们是没法经过 redis 自带的命令直接完成的。若是,再写复杂的代码去控制释放锁,则会让总体代码太过于复杂了。因此,咱们引入了lua脚本。结合Lua 脚本实现释放锁的功能,更简单,redis 执行lua脚本也是原子的,因此更合适,让合适的人干合适的事,岂不更好。

经过Lua脚本实现锁释放

Lua是啥,Lua是一种功能强大,高效,轻量级,可嵌入的脚本语言。其官方的描述是:

Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.

Lua 调用 redis 很是简单,而且 Lua 脚本语法也易学,对于有别的编程语言基础的程序员来讲,在不学习Lua脚本语法的状况下,直接看 Lua 的代码 也是能够看懂的。例子以下:

if redis.call('get', KEYS[1]) == ARGV[1] 
    then 
        return redis.call('del', KEYS[1]) 
    else 
        return 0 
end

上面的代码,逻辑很简单,if 中的比较若是是true , 那么 执行 del 并返回del结果;若是 if 结果为false 直接返回 0 。这不就知足了咱们释放锁的要求吗?——“ 是个人锁,我就释放,不是个人锁,我不能瞎释放”。

其中的KEYS[1] , ARGV[1] 是参数,咱们只调用 jedis 执行脚本的时候,传递这两个参数就能够了。

使用redis + lua 来实现释放锁的代码以下:

private static final Long lockReleaseOK = 1L;
static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua脚本,用来释放分布式锁

public static boolean releaseLock(String key ,String lockValue){
    if(key == null || lockValue == null) {
        return false;
    }
    try {
        Jedis jedis = getJedisPool().getResource();
        Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue));
        jedis.close();
        return res!=null && res.equals(lockReleaseOK);
    } catch (Exception e) {
        return false;
    }
}

如此,咱们便实现了锁的安全释放。同时,咱们还须要结合业务逻辑,进行具体健壮性的保证,好比若是结束了必定不能忘记释放锁,异常了也要释放锁,某种状况下是否须要回滚事务等。总结这个分布式锁使用的过程即是:

  • 加锁时 key 同,value 不一样。
  • 释放锁时,根据value判断,是否是个人锁,不能释放别人的锁。
  • 及时释放锁,而不是利用自动超时。
  • 锁超时时间必定要结合业务状况权衡,过长,太短都不行。
  • 程序异常之处,要捕获,并释放锁。若是须要回滚的,主动作回滚、补偿。保证总体的健壮性,一致性。

用redis作分布式锁真的靠谱吗

上面的文字中,咱们讨论如何使用redis做为分布式锁,并讨论了一些细节问题,如锁超时的问题、安全释放锁的问题。目前为止,彷佛很完美的解决的咱们想要的分布式锁功能。然而事情并无这么简单,用redis作分布式锁并不“靠谱”。

不靠谱的状况

上面咱们说的是redis,是单点的状况。若是是在redis sentinel集群中状况就有所不一样了。关于redis sentinel 集群能够看这里。在redis sentinel集群中,咱们具备多台redis,他们之间有着主从的关系,例如一主二从。咱们的set命令对应的数据写到主库,而后同步到从库。当咱们申请一个锁的时候,对应就是一条命令 setnx mykey myvalue ,在redis sentinel集群中,这条命令先是落到了主库。假设这时主库down了,而这条数据还没来得及同步到从库,sentinel将从库中的一台选举为主库了。这时,咱们的新主库中并无mykey这条数据,若此时另一个client执行 setnx mykey hisvalue , 也会成功,即也能获得锁。这就意味着,此时有两个client得到了锁。这不是咱们但愿看到的,虽然这个状况发生的记录很小,只会在主从failover的时候才会发生,大多数状况下、大多数系统均可以容忍,可是不是全部的系统都能容忍这种瑕疵。

redlock

为了解决故障转移状况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,须要多个redis实例,加锁的时候,它会想多半节点发送 setex mykey myvalue 命令,只要过半节点成功了,那么就算加锁成功了。释放锁的时候须要想全部节点发送del命令。这是一种基于【大多数都赞成】的一种机制。感兴趣的能够查询相关资料。在实际工做中使用的时候,咱们能够选择已有的开源实现,python有redlock-py,java 中有Redisson redlock。

redlock确实解决了上面所说的“不靠谱的状况”。可是,它解决问题的同时,也带来了代价。你须要多个redis实例,你须要引入新的库 代码也得调整,性能上也会有损。因此,果真是不存在“完美的解决方案”,咱们更须要的是可以根据实际的状况和条件把问题解决了就好。

至此,我大体讲清楚了redis分布式锁方面的问题(往后若是有新的领悟就继续更新)。

redis单点、redis主从、redis集群cluster配置搭建与使用

Netty开发redis客户端,Netty发送redis命令,netty解析redis消息

spring如何启动的?这里结合spring源码描述了启动过程

SpringMVC是怎么工做的,SpringMVC的工做原理

spring 异常处理。结合spring源码分析400异常处理流程及解决方法

Mybatis Mapper接口是如何找到实现类的-源码分析

使用Netty实现HTTP服务器

Netty实现心跳机制

Netty系列

Lua脚本在redis分布式锁场景的运用

CORS详解,CORS原理分析

相关文章
相关标签/搜索