目录html
redis分布式锁,Lua,Lua脚本,lua redis,redis lua 分布式锁,redis setnx ,redis分布式锁, Lua脚本在redis分布式锁场景的运用。java
锁是一种能够封锁资源的东西。这种资源一般是共享的,一般会发生使用竞争的。python
须要保护共享资源正常使用,不出乱子。
比方说,公司只有一间厕所,这是个共享资源,你们须要共同使用这个厕所,因此避免不了有时候会发生竞争。若是一我的正在使用,另一我的进去了,咋办呢?若是两我的同时钻进了一个厕所,那该怎么办?结果如何?谁先用,仍是一块儿使用?特别的,假如是一男一女同时钻进了厕所,事情会怎样呢?反正我是不懂……程序员
若是这个时候厕所门前有个锁,每一个人都无法随便进入,而是须要先获得锁,才能进去。而获得这个锁,就须要里边的人先出来。这样就能够保证同一时刻,只有一我的在使用厕所,这我的在上厕所的期间不会有不安全的事情发生,不会中途被人闯进来了。redis
在 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中,有一条命令,能够实现相似 “锁” 的语法是这样的:编程
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; } }
这里执行加锁,不必定能成功。当别人正在持有锁的时候,加锁的线程须要继续尝试。这个“继续尝试”一般是“忙等待”,实现代码以下:
/** * 获取一个分布式锁 , 超时则返回失败 * @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; }
根据上面所述,咱们在加锁的时候执行了: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 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; } }
如此,咱们便实现了锁的安全释放。同时,咱们还须要结合业务逻辑,进行具体健壮性的保证,好比若是结束了必定不能忘记释放锁,异常了也要释放锁,某种状况下是否须要回滚事务等。总结这个分布式锁使用的过程即是:
上面的文字中,咱们讨论如何使用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的时候才会发生,大多数状况下、大多数系统均可以容忍,可是不是全部的系统都能容忍这种瑕疵。
为了解决故障转移状况下的缺陷,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异常处理流程及解决方法