Redis分布式锁实现方式(附有正解及错误示例)
一、前言
本文内容主要来自博客:https://wudashan.com/2017/10/23/Redis-Distributed-Lock-Implement/,本文用于归纳总结及笔记用途,如有需要请看原博客
分布式锁一般有三种实现方式:
① 数据库乐观锁
② 基于Redis的分布式锁
③ 基于ZooKeeper的分布式锁
本篇文章主要介绍基于Redis实现的分布式锁,且只考虑Redis单机部署的场景,如果是多机部署可以尝试使用Redisson实现(Redis官方提供的Java组件)
二、分布式锁的可靠性
为了确保分布式锁可用,至少要确保锁的实现满足以下4个条件:
① 互斥性。在任意时刻,只有一个客户端能持有锁。
② 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
③ 解铃还须系铃人。加锁和解锁必须是同一个客户端,不能把别人加的锁给解了。
④ 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
三、加锁实现方式
① 错误示例一
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, Integer expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
//若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
}
这是常见的一种实现方式,使用jedis.setnx及jedis.expire组合实现加锁,setnx就是set if not exist,expire就是给锁设置一个过期时间。
然而因为是两条命令,不具备原子性;如果在程序执行完setnx后突然崩溃,会导致锁没有设置过期时间,最终导致死锁。
网上之所以有很多人这么实现,是因为低版本的jedis不支持多参数的set方法。
② 错误示例二
public static boolean wrongGetLock2(Jedis jedis, String lockKey, Integer expireTime) {
long time = System.currentTimeMillis() + expireTime;
String expireStr = String.valueOf(time);
//如果锁不存在则返回加锁成功
if (jedis.setnx(lockKey, expireStr) == 1) {
return true;
}
//如果锁存在则获取锁的过期时间
String currentValueStr = jedis.get(lockKey);
if (null == currentValueStr) {
return false;
}
//锁未过期直接返回成功
if (Long.parseLong(currentValueStr) > System.currentTimeMillis()) {
return true;
}
//锁已过期,获取上一个锁的过期时间并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, expireStr);
//多线程并发的情况,只有一个线程的设置值和当前值相同,才有权利加锁
if (oldValueStr.equals(currentValueStr)) {
return true;
}
return false;
}
实现思路:使用jedis.setnx方法实现加锁,其中key是锁,value是锁的过期时间
实现解析:
- 通过setnx方法尝试加锁,如果当前锁不存在则返回加锁成功
- 如果锁已存在则获取锁的过期时间,和当前时间比较,如果锁已经过期则设置新的过期时间并返回加锁成功
问题解析:
- 由于是客户端自己生成的过期时间,所以需要强制要求分布式下每个客户端的时间必须同步
- 当锁过期的时候,如果多个客户端同时执行getSet方法,虽然最终只有一个客户端能加锁,但是这个客户端的锁过期时间可能被其他客户端覆盖
- 锁不具备拥有者标识,即任何客户端都可以解锁
③ 正解
public static boolean rightGetLock(Jedis jedis, String lockKey, String requestId, Integer expireTime) {
//传requestId的原因:这样可以知道这把锁是哪个请求加的,在解锁的时候就有依据,只能解锁自己加的锁,requestId可以使用UUID.randomUUID().toString()方法生成
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
实现解析:
- 加锁就一行代码,一共5个形参
- 第一个为key
- 第二个为value,这里传的是requestId;它的意义在于可以区分这把锁是哪个请求加的,解锁的时候就有据可依满足“解铃还须系铃人”的条件
- 第三个为nxxx,这里传的是NX
- 第四个为expx,这里传的是PX,意思是给key加一个过期设置,具体时间由第5个参数决定
- 第五个为time,与第四个参数相呼应,代表key的过期时间
上面这种实现方式满足了可靠性里描述的三个条件:
- 首先,set加入NX参数可以保证如果key已存在则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性
- 其次,对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间自动解锁,不会发生死锁
- 最后,将value赋值为requestId,代表加锁的客户端请求标识,在客户端解锁的时候就可以校验是否是同一个客户端,满足“解铃还须系铃人”
四、解锁实现方式
① 错误示例一
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
//没判断锁的拥有者,导致任何客户端都能随时进行解锁,即使这把锁不是它的
jedis.del(lockKey);
}
最常见的解锁代码就是直接使用jedis.del方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时解锁,即使这把锁不是它的。
② 错误示例二
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
//判断加锁与解锁是否为同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
//在执行del方法前原有锁过期且被其他客户端重新加锁成功,则会导致误解锁,解除了不属于当前客户端的锁,本质上还是由于两步操作不具有原子性
jedis.del(lockKey);
}
}
这种解锁代码乍一看也是没问题,与正确姿势差不多,唯一区别是分成两条命令执行,不具有原子性。
问题解析:
- 如果调用jedis.del方法时,这把锁已经不属于当前客户端的话,就会解除其他客户端加的锁
- 例:客户端A加锁,一段时间后客户端A解锁,在执行jedis.del方法前锁突然过期了,此时客户端B尝试加锁成功,然后客户端A在执行del方法将客户端B加的锁解除了
③ 正解
public static boolean rightReleaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEY[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
实现解析:
- 解锁只需要两行代码就搞定了,该方式可以确保上述操作是原子性的
- 第一行是一个简单的Lua脚本代码,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁
- 第二行是将Lua代码传到jedis.eval的方法里,并使参数KEY[1]赋值为lockKey,ARGV[1]赋值为requestId,然后交给Redis服务端执行
那么为什么执行eval方法可以确保原子性,源于Redis的特性,官网对eval命令的部分解释如下:
简单来说就是在eval命令执行Lua代码时,Lua代码将被当成一个命令去执行,直到eval命令执行完成Redis才会去执行其他命令
五、总结
① 上文介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个经典的错误示例
② 现在网上的一些答案并没有被实际场景验证过,而且在遍地转载的今天更容易将错误答案扩散出去误导别人,所以在网上查资料尽量找一些比较资深博主写的文章避免被误导
Redis分布式锁实现方式(附有正解及错误示例)的更多相关文章
- 基于Redis分布式锁的正确打开方式
分布式锁是在分布式环境下(多个JVM进程)控制多个客户端对某一资源的同步访问的一种实现,与之相对应的是线程锁,线程锁控制的是同一个JVM进程内多个线程之间的同步.分布式锁的一般实现方法是在应用服务器之 ...
- redis分布式锁练习【我】
package redis; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis全方位详解--数据类型使用场景和redis分布式锁的正确姿势
一.Redis数据类型 1.string string是Redis的最基本数据类型,一个key对应一个value,每个value最大可存储512M.string一半用来存图片或者序列化的数据. 2.h ...
- Redis(十三):Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁的正确实现方式(Java版)
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis 分布式锁 - 分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis 分布式锁的正确实现方式(转)
_ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各 ...
随机推荐
- A - 最大子矩阵 HYSBZ - 1084 (DP)
题目链接:https://cn.vjudge.net/contest/281963#problem/A 题目大意:中文题目 具体思路:观察到m<=2,所以我们可以对两种情况进行单独讨论,当m== ...
- web前端最全各类资源
链接:http://www.sohu.com/a/157593700_132276
- 原生JS给元素添加class属性
有下面这三种简单语句. document.getElementsByTagName('body')[0].className = 'snow-container'; //设置为新的 document ...
- SpringMVC集成MongoDb
(1)pom添加相关依赖 <dependency> <groupId>org.springframework.data</groupId> <artifact ...
- linux系统下安装tomcat及配置
一.下载TOMCAT压缩包 apache-tomcat-6.0.44-client.zip 或 apache-tomcat-7.0.69.tar.gz 点击进入官网:http://tomcat.apa ...
- 【bzoj2653】【middle】【主席树+二分答案】
Description 一个长度为 n 的序列 a ,设其排过序之后为 b ,其中位数定义为 b[n/2] ,其中 a,b 从 0 开始标号 , 除法取下整. 给你一个长度为 n 的序列 s .回答 ...
- lnmp环境下piwiki网站流量分析工具的安装及配置
piwiki统计网站的安装 Piwik是一个PHP和MySQL的开放源代码的Web统计软件. 它给你一些关于你的网站的实用统计报告,比如网页浏览人数, 访问最多的页面, 搜索引擎关键词等等- Piwi ...
- 源码编译安装nginx1.4.7
传统上基于进程或线程模型架构的web服务通过每进程或每线程处理并发连接请求,这势必会在网络和I/O操作时产生阻塞,其另一个必然结果则是对内存或CPU的利用率低下.生成一个新的进程/线程需要事先备好其运 ...
- 程序打jar包
打包的时候,平常选择第二个选项 jar包中的内容为: 一般运行没有问题. 在部分情况,使用quartz时,在eclipse中不报错,使用第二种方式打包,运行会报错. 选择第一种打包方式,就不会报错了( ...
- OCM_第三天课程:Section1 —》表空间的操作和管理、服务配置
注:本文为原著(其内容来自 腾科教育培训课堂).阅读本文注意事项如下: 1:所有文章的转载请标注本文出处. 2:本文非本人不得用于商业用途.违者将承当相应法律责任. 3:该系列文章目录列表: 一:&l ...