Redlock:Redis分布式锁最牛逼的实现

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介:

普通实现

说道Redis分布式锁大部分人都会想到: setnx+lua,或者知道 setkey value px milliseconds nx。后一种方式的核心实现命令如下:

 
  1. - 获取锁(unique_value可以是UUID等)

  2. SET resource_name unique_value NX PX 30000


  3. - 释放锁(lua脚本中,一定要比较value,防止误解锁)

  4. if redis.call("get",KEYS[1]) == ARGV[1] then

  5. return redis.call("del",KEYS[1])

  6. else

  7. return 0

  8. end

这种实现方式有3大要点(也是面试概率非常高的地方):

  1. set命令要用 setkey value px milliseconds nx

  2. value要具有唯一性;

  3. 释放锁时要验证value值,不能误解锁;

事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

  1. 在Redis的master节点上拿到了锁;

  2. 但是这个加锁的key还没有同步到slave节点;

  3. master故障,发生故障转移,slave节点升级为master节点;

  4. 导致锁丢失。

正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。笔者认为,Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。

Redlock实现

antirez提出的redlock算法大概是这样的:

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  • 获取当前Unix时间,以毫秒为单位。

  • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

  • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

Redlock源码

redisson已经有对redlock算法封装,接下来对其用法进行简单介绍,并对核心源码进行分析(假设5个redis实例)。

POM依赖

 
  1. <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->

  2. <dependency>

  3. <groupId>org.redisson</groupId>

  4. <artifactId>redisson</artifactId>

  5. <version>3.3.2</version>

  6. </dependency>

用法

首先,我们来看一下redission封装的redlock算法实现的分布式锁用法,非常简单,跟重入锁(ReentrantLock)有点类似:

 
  1. Config config = new Config();

  2. config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")

  3. .setMasterName("masterName")

  4. .setPassword("password").setDatabase(0);

  5. RedissonClient redissonClient = Redisson.create(config);

  6. // 还可以getFairLock(), getReadWriteLock()

  7. RLock redLock = redissonClient.getLock("REDLOCK_KEY");

  8. boolean isLock;

  9. try {

  10. isLock = redLock.tryLock();

  11. // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。

  12. isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);

  13. if (isLock) {

  14. //TODO if get lock success, do something;

  15. }

  16. } catch (Exception e) {

  17. } finally {

  18. // 无论如何, 最后都要解锁

  19. redLock.unlock();

  20. }

唯一ID

实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。入口在redissonClient.getLock("REDLOCK_KEY"),源码在Redisson.java和RedissonLock.java中:

 
  1. protected final UUID id = UUID.randomUUID();

  2. String getLockName(long threadId) {

  3. return id + ":" + threadId;

  4. }

获取锁

获取锁的代码为redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),两者的最终核心源码都是下面这段代码,只不过前者获取锁的默认租约时间(leaseTime)是LOCKEXPIRATIONINTERVAL_SECONDS,即30s:

 
  1. <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {

  2. internalLockLeaseTime = unit.toMillis(leaseTime);

  3. // 获取锁时向5个redis实例发送的命令

  4. return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,

  5. // 首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)

  6. "if (redis.call('exists', KEYS[1]) == 0) then " +

  7. "redis.call('hset', KEYS[1], ARGV[2], 1); " +

  8. "redis.call('pexpire', KEYS[1], ARGV[1]); " +

  9. "return nil; " +

  10. "end; " +

  11. // 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间

  12. "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +

  13. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +

  14. "redis.call('pexpire', KEYS[1], ARGV[1]); " +

  15. "return nil; " +

  16. "end; " +

  17. // 获取分布式锁的KEY的失效时间毫秒数

  18. "return redis.call('pttl', KEYS[1]);",

  19. // 这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]

  20. Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

  21. }

获取锁的命令中,

  • KEYS[1] 就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;

  • ARGV[1] 就是internalLockLeaseTime,即锁的租约时间,默认30s;

  • ARGV[2] 就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:


释放锁

释放锁的代码为redLock.unlock(),核心源码如下:

 
  1. protected RFuture<Boolean> unlockInnerAsync(long threadId) {

  2. // 向5个redis实例都执行如下命令

  3. return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

  4. // 如果分布式锁KEY不存在,那么向channel发布一条消息

  5. "if (redis.call('exists', KEYS[1]) == 0) then " +

  6. "redis.call('publish', KEYS[2], ARGV[1]); " +

  7. "return 1; " +

  8. "end;" +

  9. // 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回

  10. "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +

  11. "return nil;" +

  12. "end; " +

  13. // 如果就是当前线程占有分布式锁,那么将重入次数减1

  14. "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +

  15. // 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除

  16. "if (counter > 0) then " +

  17. "redis.call('pexpire', KEYS[1], ARGV[2]); " +

  18. "return 0; " +

  19. "else " +

  20. // 重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息

  21. "redis.call('del', KEYS[1]); " +

  22. "redis.call('publish', KEYS[2], ARGV[1]); " +

  23. "return 1; "+

  24. "end; " +

  25. "return nil;",

  26. // 这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]

  27. Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));


  28. }

参考:https://redis.io/topics/distlock


原文发布时间为: 2018-12-02
本文作者:阿飞的博客
本文来自云栖社区合作伙伴“Java技术驿站”,了解相关信息可以关注“Java技术驿站”。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
打赏
0
0
0
3
73529
分享
相关文章
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
486 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
1月前
|
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
182 83
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
分布式爬虫框架Scrapy-Redis实战指南
Redis分布式锁如何实现 ?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。 只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。 这个命令的返回值如下。 ● 命令在设置成功时返回1。 ● 命令在设置失败时返回0。 假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行S
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
71 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
27天前
|
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
48 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
58 29
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
Redis应用—8.相关的缓存框架
Redis缓存设计与性能优化
Redis缓存设计与性能优化涵盖缓存穿透、击穿、雪崩及热点key重建等问题。针对缓存穿透,可采用缓存空对象或布隆过滤器;缓存击穿通过随机设置过期时间避免集中失效;缓存雪崩需确保高可用性并使用限流熔断组件;热点key重建利用互斥锁防止大量线程同时操作。此外,开发规范强调键值设计、命令使用和客户端配置优化,如避免bigkey、合理使用批量操作和连接池管理。系统内核参数如vm.swappiness、vm.overcommit_memory及文件句柄数的优化也至关重要。慢查询日志帮助监控性能瓶颈。
44 9
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)