-
Redis分布式锁
2020-03-24 01:02:25Redis分布式锁 介绍 随着微服务的兴起,比如Spring Cloud、Dubbo的等分布式解决方案地兴起,java的jvm锁某些场景下已经不在适用。特定的场景下,往往依靠分布式锁,常见的分布式锁实现方式有Redis分布式锁以及...Redis分布式锁
介绍
随着微服务的兴起,比如Spring Cloud、Dubbo的等分布式解决方案地兴起,java的jvm锁某些场景下已经不在适用。特定的场景下,往往依靠分布式锁,常见的分布式锁实现方式有Redis分布式锁以及Zookeeper分布式锁。Redis分布式锁在项目编码中更为常见,比较成熟的框架有Redisson,就实现了分布式锁。虽然项目中我们可以使用比较成熟的Redisson,但是这篇文章侧重于自己实现一个分布式锁。
原理
Redis可以作为分布式锁的关键因素有2个:1.单线程模型;2.基于内存高性能。单线程模型意味着,在任意时刻,只有一个线程可以持有并操作一个key。如果我们实现以个分布式锁,当A线程持有锁时,其他任何一个线程不能再获取同样的锁。因此,加锁的操作必须是一个原子操作。
SETNX命令
SETNX是redis的一个命令,也是一个原子操作命令,当redis中key不存在时,使用SETNX命令给key设置一个value,设值成功时返回1,若key存在,则设值失败,返回0。完美契合了我们加锁的条件,原子操作、加锁成功返回true、加锁失败返回false。命令执行如下:
问题
java jvm锁中提供tryLock(long** **time, TimeUnit unit)方法,当程序试图加锁时,可以设置持有锁的时间,超过持锁时间后,即使没有手动释放锁,锁也可以被释放掉,可以有效防止死锁。redis分布式锁如何实现这个功能呢?我们想到了EXPIRE,当SETNX获取锁成功后,在使用EXPIRE设置锁的过期时间。但是,SETNX、EXPIRE两个命令顺序执行就不是原子操作了。redis提供了lua脚本的方式,可以在脚本中执行redis命令,执行lua脚本是原子操作,因此我们可以在lua脚本中SETNX,再EXPIRE。
代码
# SETNX_EXPIRE.lua if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]) return true else return false end
# COMPARE_DELETE.lua if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) == 1 end return false
package com.github.rxyor.distributed.lock; import com.github.rxyor.common.util.FileUtil; import com.github.rxyor.common.util.RandomUtil; import com.github.rxyor.redis.util.LettuceConnectionUtil; import io.lettuce.core.RedisClient; import io.lettuce.core.ScriptOutputType; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import java.util.Objects; import java.util.Optional; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; /** *<p> *Redis 分布式锁 *</p> * * @author liuyang * @date 2019-05-14 Tue 18:51:00 * @since 1.0.0 */ public class RedisDistributedLock implements DistributedLock { /** * taskId本地线程缓存 */ private static final ThreadLocal<String> LOCAL_TASK_IDS = new ThreadLocal<>(); /** * redis key本地线程缓存 */ private static final ThreadLocal<String> LOCAL_KEYS = new ThreadLocal<>(); /** * 默认锁失效时间 */ private static final long DEFAULT_TIMEOUT = 5L; /** * 加锁LUA脚本 */ private static final String LUA_LOCK_SCRIPT; /** * 释放锁LUA脚本 */ private static final String LUA_UNLOCK_SCRIPT; static { LUA_LOCK_SCRIPT = readLuaLockScript(); LUA_UNLOCK_SCRIPT = readLuaUnlockScript(); } /** * redis 客户端 */ private RedisClient redisClient; /** * redis key前缀 */ @Getter @Setter private String keyPrefix; public RedisDistributedLock(RedisClient redisClient) { Objects.requireNonNull(redisClient, "redisClient can't be null"); this.redisClient = redisClient; } /** * 获取锁 * * @param redisKey * @param timeout 超时时间(秒) * @return 获取锁结果 */ @Override public boolean getLock(String redisKey, Long timeout) { long expire = (timeout == null || timeout <= 0L ? DEFAULT_TIMEOUT : timeout); String redisKeyWithPrefix = this.gainRedisKey(redisKey); LOCAL_KEYS.set(redisKeyWithPrefix); String taskId = this.generateTaskId(); LOCAL_TASK_IDS.set(taskId); return evalRedisScript(LUA_LOCK_SCRIPT, new String[]{redisKeyWithPrefix}, new String[]{taskId, String.valueOf(expire)}); } /** * 释放锁 * * @param redisKey * @return 释放结果 */ @Override public boolean releaseLock(String redisKey) { String redisKeyWithPrefix = this.gainRedisKey(redisKey); boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix}, LOCAL_TASK_IDS.get()); if (success) { this.clean(); } return success; } @Override public boolean releaseLock() { String redisKeyWithPrefix = this.gainRedisKey(null); boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix}, LOCAL_TASK_IDS.get()); if (success) { this.clean(); } return success; } /** * 执行lua脚本 * * @param script 脚本 * @param keys redis key * @param values redis参数 * @return 执行结果 */ private boolean evalRedisScript(String script, String[] keys, String... values) { StatefulRedisConnection<String, String> conn = null; try { conn = LettuceConnectionUtil.getConnection(redisClient); RedisCommands<String, String> commands = conn.sync(); return commands.<Boolean>eval(script, ScriptOutputType.BOOLEAN, keys, values); } finally { LettuceConnectionUtil.releaseConnection(conn); } } /** * 拼装或从缓存中取redis key * * @param key 原始redis key * @return String */ private String gainRedisKey(String key) { if (StringUtils.isNotEmpty(key)) { String redisKeyPrefix = Optional.ofNullable(keyPrefix).orElse(""); return redisKeyPrefix + key; } return Optional.ofNullable(LOCAL_KEYS.get()).filter(s -> StringUtils.isNotEmpty(s)) .orElseThrow(() -> new IllegalArgumentException("redis key can't be empty")); } /** * 生成一个唯一的TaskId(UUID) * @return UUID */ private String generateTaskId() { return RandomUtil.createUuid(); } /** * 清空ThreadLocal缓存 */ private void clean() { LOCAL_KEYS.remove(); LOCAL_TASK_IDS.remove(); } /** * 读取加锁LUA脚本 * @return String */ private static String readLuaLockScript() { return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/SETNX_EXPIRE.lua"); } /** * 读取释放锁LUA脚本 * @return String */ private static String readLuaUnlockScript() { return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/COMPARE_DELETE.lua"); } }
-
redis分布式锁
2020-08-13 18:08:532.redis的分布式锁 3.zookeeper的分布式锁,zookeeper机制规定,同一个目录下只能有一个唯一的文件名, 借助zookeeper的临时节点实现 二、Redis的分布式锁实现 1. 使用jedis的2.7.x及以上版本。 2. 获取锁: 命令:SET...一、分布式锁实现方式
1.数据库单独建表维护,使用的是数据库乐观锁
2.redis的分布式锁
3.zookeeper的分布式锁,zookeeper机制规定,同一个目录下只能有一个唯一的文件名, 借助zookeeper的临时节点实现二、Redis的分布式锁实现
1. 使用jedis的2.7.x及以上版本。
2. 获取锁:命令:SET key value [NX|XX] [EX|PX] seconds NX – 只有键key不存在的时候才会设置key的值 XX – 只有键key存在的时候才会设置key的值 EX seconds – 设置键key的过期时间,单位时秒 PX milliseconds – 设置键key的过期时间,单位时毫秒
获取锁方式:操作成功,rlt返回“OK”,否则返回null
3.释放锁:Redis+Lua实现:
Redis Lua脚本来释放锁三、注意点
1.首先,这个锁必须要设置一个过期时间,否则的话,当一个客户端获取锁成功之后,假如它奔溃了,或者由于发生了网络分割导致它再也无法和redis节点通信了,那么它就会一直持有这个锁,而其他客户端永远也无法获得这个锁了
2.第二,第一步获取锁的操作,把它实现成了两个redis命令,SETNX+EXPIRE.虽然这两个命令和前面描述中的一个SET命令执行效果相同,但SETNX+EXPIRE却不是原子的.如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE,导致它一直持有这个锁,所以最好使用SET命令,因为SET命令是原子的
3.第三,设置一个随机伺服穿randomVal是很有必要的,它保证了一个客户端释放的锁必须是自己持有的哪个锁(最好是唯一性UUID)
4.第四,释放锁的操作必须使用Lua脚本来实现,释放锁其实包含三步操作:GET,判断和DEL,用Lua脚本来实现能保证这三步的原子性(获取锁也可通过Redis+Lua实现)四、思考
还要一个问题是由failover(主备切换)引起的,基于单redis节点的分布式锁无法解决的,假如redis节点宕机了,那么所有客户端就都无法获得锁了,服务变的不可用,为了提高可用性,我们可以给这个redis节点挂一个slave,当master节点不可用的时候,系统自动切到slave上(failover),但是由于redis的主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性,针对这个问题,可以参看一下Redlock的算法https://redis.io/topics/distlock)
-
Redis 分布式锁
2020-08-18 11:16:00Redis 分布式锁Redis 分布式锁1. 是什么?底层源码: Redis 分布式锁 1. 是什么? 为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而 这个分布式协调技术的核心...Redis 分布式锁
Redis 分布式锁
1. 是什么?
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而
这个分布式协调技术的核心就是来实现这个分布式锁。 redisson框架 @RedisLock 注解 加在业务接口上底层源码:
KEYS[1]代表的是你加锁的那个key,比如说: RLock lock = redisson.getLock(“myLock”);
这里你自己设置了加锁的那个锁key就是“myLock”。ARGV[1]代表的就是锁key的默认生存时间,默认30秒。 ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
8743c9c0-0795-4907-87fd-6c719a6b4586:1第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。
如何加锁呢?很简单,用下面的命令: hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1 通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。
接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
好了,到此为止,ok,加锁完成了。2)锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢? 很简单,第一个if判断会执行“exists
myLock”,发现myLock这个锁key已经存在了。
接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
所以,客户端2会获取到pttl
myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。
此时客户端2会进入一个while循环,不停的尝试加锁。(3)watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch
dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。 -
redis 分布式锁
2019-04-24 11:02:24虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。 可靠性 首先,为了确保分布式锁可用,我们至少要...分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
代码实现
组件依赖
首先我们要通过Maven引入
Jedis
开源组件,在pom.xml
文件加入下面的代码:<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
加锁代码
正确姿势
Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
可以看到,我们加锁就一行代码:
jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:-
第一个为key,我们使用key来当锁,因为key是唯一的。
-
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用
UUID.randomUUID().toString()
方法生成。 -
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
-
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
-
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。
解锁代码
正确姿势
还是先展示代码,再带大家慢慢解释为什么这样实现:
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[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脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到
jedis.eval()
方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:
简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。
如果你的项目中Redis是多机部署的,那么可以尝试使用
Redisson
实现分布式锁,这是Redis官方提供的Java组件,具体请参考我的另一篇博客基于Spring boot 2.1 使用redisson实现分布式锁 -
redis分布式锁原理及实现
2019-04-16 23:13:29一、写在前面 现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud...说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是...
-
【数据分析实战训练营】Hive详解
-
2020-12-01-DVWA.md
-
【数据分析-随到随学】Hive详解
-
Python获取Websocket接口的数据
-
量化高频交易系统tick数据采集
-
列表与字典
-
CrashAPI.rar
-
日期类
-
基于ASP.NET+SQL学生选课系统毕业设计(源码+论文+开题+开题报告+答辩PPT).rar
-
华为 matebook D 加装硬盘过程注意事项
-
CPU缓存行学习笔记
-
python 基础知识梳理——Python协程
-
webgl室内3d场景.zip
-
TLP-Task13学习笔记
-
json.zip,用于网络编程
-
TreeView操作.txt
-
30个生涯锦囊,带你跳出迷茫,找到适合你的职业方向
-
常用类库之Java.text.SimpleDateFormat
-
【数据分析-随到随学】Hadoop数据分析
-
scrapy框架