我在以前总结幂等性的时候,写过一种分布式锁的实现,惋惜当时没有真正应用过,着实的心虚啊。正好这段时间对这部分实践了一下,也算是对以前填坑了。html
分布式锁按照网上的结论,大体分为三种:一、数据库乐观锁; 二、基于Redis的分布式锁;3.、基于ZooKeeper的分布式锁;mysql
关于乐观锁的实现其实在以前已经讲的很清楚了,有兴趣的移步:使用mysql乐观锁解决并发问题 。今天先简单总结下redis的实现方法,后面详细研究过ZooKeeper的实现原理后再具体说说ZooKeeper的实现。redis
在传统单体应用单机部署的状况下,可使用Java并发相关的锁,如ReentrantLcok或synchronized进行互斥控制。可是,随着业务发展的须要,原单体单机部署的系统,渐渐的被部署在多机器多JVM上同时提供服务,这使得原单机部署状况下的并发控制锁策略失效了,为了解决这个问题就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。sql
一、互斥性,和单体应用同样,要保证任意时刻,只能有一个客户端持有锁数据库
二、可靠性,要保证系统的稳定性,不能产生死锁bash
三、一致性,要保证锁只能由加锁人解锁,不能产生A的加锁被B用户解锁的状况并发
Redis实现分布式锁不一样的人可能有不一样的实现逻辑,可是核心就是下面三个方法。less
SETNX
SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不作,返回0。
Expire
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
Delete
delete key
删除keydom
首先讲一个目前网上应用最多的一种实现分布式
实现思路:
1.获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁以避免产生死锁,锁的value值为一个随机生成的UUID,经过此在释放锁的时候进行判断。
2.获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3.释放锁的时候,经过UUID判断是否是该锁,如果该锁,则执行delete进行锁释放。
public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {
try {
// 定义 redis 对应key 的value值(uuid) 做用 释放锁 随机生成value,根据项目状况修改
String identifierValue = UUID.randomUUID().toString();
// 定义在获取锁以后的超时时间
int expireLock = (int) (timeOut / 1000);// 以秒为单位
// 定义在获取锁以前的超时时间
//使用循环机制 若是没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁
// 使用循环方式重试的获取锁
Long endTime = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < endTime) {
// 获取锁
// 使用setnx命令插入对应的redislockKey ,若是返回为1 成功获取锁
if (jedis.setnx(lockKey, identifierValue) == 1) {
// 设置对应key的有效期
jedis.expire(lockKey, expireLock);
return identifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}复制代码
这种实现方法也是目前应用最多的实现,我一直觉得这确实是正确的。然而因为这是两条Redis命令,不具备原子性,若是程序在执行完setnx()以后忽然崩溃,致使锁没有设置过时时间。那么仍是会发生死锁的状况。网上之因此有人这样实现,是由于低版本的jedis并不支持多参数的set()方法。
固然这种状况Jedis的设计者也显然想到了,新版的Jedis能够同时set多个参数,具体实现以下:
实现思路:
基本上和原来的逻辑相似,只是将setnx和expire的操做合并为一步,改成使用新的set多参的方法。
set(final String key, final String value, final String nxxx, final String expx,final long time)
key和value天然不用多说。nxxx参数只能够传String 类型的NX(仅在不存在的状况下设置)和XX(和普通的set操做同样会作更新操做)两种。
expx是指到期时间单位,可传参数为EX (秒)和 PX (毫秒),time就是具体的过时时间了,单位为前面expx所指定的。
而后咱们对上面的代码进行改造以下:
/**
* @param acquireTimeout
* 在获取锁以前的超时时间
* @param timeOut
* 在获取锁以后的超时时间
*/
public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {
try {
// 定义 redis 对应key 的value值(uuid) 做用 释放锁 随机生成value,根据项目状况修改
String identifierValue = UUID.randomUUID().toString();
// 定义在获取锁以前的超时时间
//使用循环机制 若是没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁
// 使用循环方式重试的获取锁
Long endTime = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < endTime) {
// 获取锁
// set使用NX参数的方式就等同于 setnx()方法,成功返回OK.PX以毫秒为单位
if ("OK".equals(jedis.set(lockKey, lockKey, "NX", "PX", timeOut))) {
return identifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}复制代码
好了,获取锁的操做基本上就上面这些,有同窗可能要问,为何不直接返回一个Boolean型的true或false呢?
正如我前面所说的,要保证解锁的一致性,因此就须要经过value值来保证解锁人就是加锁人,而不能直接返回true或false了。
下面在说下解锁的过程。
仍是先举一个错误的例子:
实现思路:
释放锁的时候,经过传入key和加锁时返回的value值,判断传入的value是否和key从redis中取出的相等。相等则证实解锁人就是加锁人,执行delete释放锁的操做。
// 释放redis锁
public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {
try {
// 若是该锁的id 等于identifierValue 是同一把锁状况才能够删除
if (jedis.get(lockKey).equals(identifierValue)) {
jedis.del(lockKey);
}
} catch (Exception e){
e.printStackTrace();
}
}复制代码
看着好像没啥问题哈。然而仔细想一想又总感受哪里不对。
若是在执行jedis.del(lockKey)操做以前,恰好锁的过时时间到了,而这个时候又有别的客户端取到了锁,咱们在此时执行删除操做,不是又不符合一致性的要求了吗。
而后咱们修改成下述方案:
修改后的代码为:
public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifierValue));
//0释放锁失败。1释放成功
if (1 == result) {
//若是你想返回删除成功仍是失败,能够在这里返回
System.out.println(result+"释放锁成功");
}
if (0 == result){
System.out.println(result+"释放锁失败");
}
} catch (Exception e){
e.printStackTrace();
}
}复制代码
实现思路:
咱们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为identifierValue。eval()方法是将Lua代码交给Redis服务端执行。
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与identifierValue相等,若是相等则删除锁(解锁)。那么为何要使用Lua语言来实现呢?由于要确保上述操做是原子性的。
那么为何执行eval()方法能够确保原子性?源于Redis的特性,由于Redis是单线程,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,而且直到eval命令执行完成,Redis才会执行其余命令。
若是想免费学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。
本文对Redis实现分布式锁作了比较详细的总结。我我的也对上述代码作了实践检验。其实我在使用时,一直用的错误的案例。直到看到园友Ruthless的一篇文章才晓得稀疏日常的写法居然漏洞百出。下一篇准备再研究研究ZooKeeper的实现。