Redis 分布式锁笔记

(公众号:水滴与银弹)深度剖析:Redis分布式锁到底安全吗?

一、初识分布式锁

1、什么是分布式锁

​ 分布式环境下,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用分布式锁来「互斥」,以保证共享变量的正确性。

2、具体使用

​ 想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上申请「加锁」。

​ 而这个外部系统,必须要实现「互斥」的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。

​ 这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。

二、Redis 实现分布式锁

1、实现的操作

想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示 SET if not exists,即如果 key 不存在,才会设置它的值,否则什么也不做。

两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

Redis 命令

  • 添加锁命令:SETNX lock 1
  • 释放锁命令:DEL lock

Java API

  • redisTemplate.opsForValue().setIfAbsent(lockId, "lock");
    
  • redisTemplate.delete(lockId);
    

2、如何解决死锁

当某个客户端拿到锁后,如果发生下面的场景,无法继续正常运行,就会造成「死锁」:

  • 程序处理业务逻辑异常,没及时释放锁
  • 进程挂了,没机会释放锁

可以在申请锁时,给这把锁设置一个「租期」。

在 Redis 中实现时,就是给这个 key 设置一个「过期时间」。

Redis 命令

  • 添加锁命令:SET lock 1 EX 10 NX // Redis 2.6.12 后,保证操作的原子性
  • 释放锁命令:DEL lock

Java API

  • redisTemplate.opsForValue().setIfAbsent(lockId, "lock", 10 * 1000, TimeUnit.MILLISECONDS);
    
  • redisTemplate.delete(lockId);
    

3、锁的释放问题

试想这样一种场景:

  1. 客户端 1 加锁成功,开始操作共享资源。
  2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」。
  3. 客户端 2 加锁成功,开始操作共享资源。
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)。

看到了么,这里存在两个严重的问题:

  1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有。
  2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁。

第一个问题,可能是我们评估操作共享资源的时间不准确导致的。

第二个问题在于,一个客户端释放了其它客户端持有的锁。

3.1 如何防止锁被其他客户端释放

解决方案:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。

1. 设置锁
SET lock $uuid EX 20 NX2. 释放锁
lua 脚本:// 判断锁是自己的,才释放if redis.call("GET",KEYS[1]) == ARGV[1]thenreturn redis.call("DEL",KEYS[1])elsereturn 0end

这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。

  1. 客户端 1 执行 GET,判断锁是自己的。
  2. 客户端 2 执行了 SET 命令,强制获取到锁(虽然发生概率比较低,但我们需要严谨地考虑锁的安全性模型)。
  3. 客户端 1 执行 DEL,却释放了客户端 2 的锁。

由此可见,这两个命令还是必须要原子执行才行。

lua 脚本:

​ 因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样可以保证命令的原子执行。

3.2 如何正确评估锁的过期时间

解决方案:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。

除此之外,这个 SDK 还封装了很多易用的功能:

  • 可重入锁
  • 乐观锁
  • 公平锁
  • 读写锁
  • Redlock(红锁,下面会详细讲)

三、Redlock 红锁

1、Redis 集群环境

​ 我们在使用 Redis 时,一般会采用主从集群 + 哨兵的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现「故障自动切换」,把从库提升为主库,继续提供服务,以此保证可用性。

主从切换时可能发生的问题

可能出现的场景:

  1. 客户端 1 在主库上执行 SET 命令,加锁成功
  2. 此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)
  3. 从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!

为此,Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)

2、 Redlock 实现的前提

Redlock 的方案基于 2 个前提:

  1. 不再需要部署从库哨兵实例,只部署主库
  2. 但主库要部署多个,官方推荐至少 5 个实例

也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。

注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。

3、Redlock 的具体使用

整体的流程是这样的,一共分为 5 步:

  1. 客户端先获取**「当前时间戳 T1」**。
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁。
  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳 T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)。
  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)。

我简单帮你总结一下,有 4 个重点:

  1. 客户端在多个 Redis 实例上申请加锁
  2. 必须保证大多数节点加锁成功。
  3. 大多数节点加锁的总耗时,要小于锁设置的过期时间。
  4. 释放锁,要向全部节点发起释放锁请求。

4、详解各项操作的意义

1) 为什么要在多个实例上加锁?

本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

2) 为什么大多数加锁成功,才算成功?

多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。

在分布式系统中,总会出现「异常节点」,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。

这是一个分布式系统「容错」问题,这个问题的结论是:如果只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。

3) 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?

因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。

所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。

4) 为什么释放锁,要操作所有节点?

在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。

例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致读取失败,那这把锁其实已经在 Redis 上加锁成功了。

所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。

四、Redlock 的争论 (分布式系统专家 Martin vs Redis 作者 Antirez)

五、基于 ZooKeeper 的锁

1、初始 ZooKeeper 分布式锁

如果你有了解过 Zookeeper,基于它实现的分布式锁是这样的:

  1. 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock
  2. 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
  3. 客户端 1 操作共享资源
  4. 客户端 1 删除 /lock 节点,释放锁

你应该也看到了,Zookeeper 不像 Redis 那样,需要考虑锁的过期时间问题,它是采用了「临时节点」,保证客户端 1 拿到锁后,只要连接不断,就可以一直持有锁。

而且,如果客户端 1 异常崩溃了,那么这个临时节点会自动删除,保证了锁一定会被释放。

2、可能发生的「失联」导致的问题

拿到锁的客户端会与 Zookeeper 服务器维护一个 Session,这个 Session 会依赖客户端「定时心跳」来维持连接。如果 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过期了,也会把这个临时节点删除。

基于此问题,我们也讨论一下 GC 问题对 Zookeeper 的锁有何影响:

  1. 客户端 1 创建临时节点 /lock 成功,拿到了锁
  2. 客户端 1 发生长时间 GC
  3. 客户端 1 无法给 Zookeeper 发送心跳,Zookeeper 把临时节点「删除」
  4. 客户端 2 创建临时节点 /lock 成功,拿到了锁
  5. 客户端 1 GC 结束,它仍然认为自己持有锁(冲突)

可见,即使是使用 Zookeeper,也无法保证进程 GC、网络延迟异常场景下的安全性。

这就是前面 Redis 作者在反驳的文章中提到的:如果客户端已经拿到了锁,但客户端与锁服务器发生「失联」(例如 GC),那不止 Redlock 有问题,其它锁服务都有类似的问题,Zookeeper 也是一样!

所以,这里我们就能得出结论了:一个分布式锁,在极端情况下,不一定是安全的。

3、优缺点

优点

  1. 不需要考虑锁的过期时间
  2. watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁

缺点

  1. 性能不如 Redis
  2. 部署和运维成本高
  3. 客户端与 Zookeeper 的长时间失联,锁被释放问题

六、Kaito 的建议

1、要不要用 Redlock?

Redlock 只有建立在「时钟正确」的前提下,才能正常工作,如果你可以保证这个前提,那么可以拿来使用。

但保证时钟正确,我认为并不是你想的那么简单就能做到的。

第一,从硬件角度来说,时钟发生偏移是时有发生,无法避免。

例如,CPU 温度、机器负载、芯片材料都是有可能导致时钟发生偏移的。

第二,从我的工作经历来说,曾经就遇到过时钟错误、运维暴力修改时钟的情况发生,进而影响了系统的正确性,所以,人为错误也是很难完全避免的。

所以,我对 Redlock 的个人看法是,尽量不用它,而且它的性能不如单机版 Redis,部署成本也高,我还是会优先考虑使用主从+ 哨兵的模式 实现分布式锁。

2、如何正确使用分布式锁?

1、使用分布式锁,在上层完成「互斥」目的,虽然极端情况下锁会失效,但它可以最大程度把并发请求阻挡在最上层,减轻操作资源层的压力。

2、但对于要求数据绝对正确的业务,在资源层一定要做好「兜底」,设计思路可以借鉴 fecing token 的方案来做。

七、面试相关

gitee 库森

1、什么是分布式锁?为什么使用?如何使用?

​ (锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,在 Java 中,我们常用 synchronized 、Lock 来保证资源的同步访问,但是 Java 的锁只能保证单机环境下有效。)因此,如果想在分布式集群环境实现共享资源的同步访问,就需要用到分布式锁。

​ 实现的思路是:在整个系统提供一个全局、唯一的获取锁的中间件,然后每个系统在需要加锁时,都去问这个中间件拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个中间件,可以是 Redis、Zookeeper,也可以是数据库。

2、分布式锁的特点

1、互斥性:在任何时刻,对于同一项数据,只有一个客户端可以获取到分布式锁;

2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署。

3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;

4、独占性:加锁解锁必须由同一个客户端进行,也就是锁的持有者才可以释放锁,不能出现非持有者解锁的情况。

3、分布式锁的解决方案

主流的有三种:关系型数据库、Redis、ZooKeeper。

关系型数据库

如 MySQL ,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。

缺点
  • 数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  • 锁没有失效时间。
  • 锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  • 锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

Redis

优点
  • Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。
  • Java 中,可以使用 Redisson 通过「自动续期」的方案来避免锁过期。
缺点
  • Redis 容易单点故障,需要集群部署,并不是强一致性的,锁的不够健壮;

ZooKeeper

优点
  • 不需要考虑锁的过期时间。
  • watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁。
缺点
  • 性能不如 Redis。
  • 部署和运维成本高。
  • 客户端与 Zookeeper 的长时间失联,锁被释放问题。

4、Redlock

Redis 分布式锁笔记相关推荐

  1. 【使用Redis分布式锁实现优惠券秒杀功能】-Redis学习笔记05

    前言 本章节主要实现限时.限量优惠券秒杀功能,并利用分布式锁解决<超卖问题>.<一人一单问题>. 一.优惠券下单基本功能实现 1.功能介绍及流程图 2.代码实现 @Resour ...

  2. Redis分布式锁的原理以及如何续期

    面试问题 Redis锁的过期时间小于业务的执行时间该如何续期? 问题分析 首先如果你之前用Redis的分布式锁的姿势正确,并且看过相应的官方文档的话,这个问题So easy.我们来看 很多同学在用分布 ...

  3. [Redis 分布式锁 ]

    目录 前言: 使用场景: 基于 Redis 实现分布式锁的详细示例: 使用示例: 依赖: Redis分布式锁控制并发访问: 前言: 记录一些小笔记 , 如果对你有帮助 那就更好了 使用场景: Redi ...

  4. Redis 分布式锁2

    场景 一般电商网站都会遇到秒杀.特价之类的活动,大促活动有一个共同特点就是访问量激增,在高并发下会出现成千上万人抢购一个商品的场景.虽然在系统设计时会通过限流.异步.排队等方式优化,但整体的并发还是平 ...

  5. 电商项目实战之缓存与Redis分布式锁

    电商项目实战之缓存与Redis分布式锁 缓存失效 缓存穿透 缓存雪崩 缓存击穿 分布式缓存 分布式锁 SpringBoot整合Redisson实现分布式锁 实现过程 缓存和数据库一致性 场景分析 解决 ...

  6. redis分布式锁 在集群模式下如何实现_收藏慢慢看系列:简洁实用的Redis分布式锁用法...

    在微服务中很多情况下需要使用到分布式锁功能,而目前比较常见的方案是通过Redis来实现分布式锁,网上关于分布式锁的实现方式有很多,早期主要是基于Redisson等客户端,但在Spring Boot2. ...

  7. 快来学习Redis 分布式锁的背后原理

    以前在学校做小项目的时候,用到Redis,基本也只是用来当作缓存.可阿粉在工作中发现,Redis在生产中并不只是当作缓存这么简单.在阿粉接触到的项目中,Redis起到了一个分布式锁的作用,具体情况是这 ...

  8. Redis分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击关注公众号,Java干货及时送达 来源:juejin.cn/post/6854573212831842311 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本篇文章主要是基于我们实际项目 ...

  9. Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本 ...

最新文章

  1. 在新浪潮中,服务教育是你的竞争利器
  2. wxWidgets:wxRichTextEvent类用法
  3. JavaScript面向对象——封装及相关原理解析
  4. debian9.8与主机共享问题
  5. 机器学习案例系列教程——距离度量方法总结
  6. 贺利坚老师汇编课程35笔记:[BX+SI]和[BX+DI]寻址
  7. 自动化测试框架cucumber_10分钟学会 Cucumber+Watir 自动化测试框架
  8. Vmware Vsphere HA创建集群步骤
  9. 2022-2028年全球与中国氨(NH3)气体传感器行业发展趋势及投资战略分析
  10. 进制转换之十进制转换为十六进制
  11. 全球仅通过不到 2000 位的 Elastic 认证工程师,到底难不难?
  12. 使用数据库连接池建立数据库连接
  13. 页面中展示PDF(转成Swf文件)
  14. 【OS Pintos】Project1 项目要求说明 | 进程中止信息 | 参数传递 | 用户内存访问 | 有关项目实现的建议
  15. 搜狗推出明医搜索,您怎么看?
  16. linux下基于SMTP协议的C++邮件客户端
  17. Windows控制面板中英文对照表
  18. ubuntu 自动挂起_ubuntu 的挂起与休眠
  19. BUUCTF 爱因斯坦
  20. 四川汶川县发生7.6级地震 北京有震感

热门文章

  1. PHP笔记 17 18 19 20 21
  2. java实现斗地主发牌项目
  3. 中文汉字和常见英文数字等的unicode编码范围
  4. 【Java基础】swing-图形界面学习(下)
  5. 【网络部署】校园网的网线接入路由器的lan口与接入wan口有何区别,如何设置校园网,接入lan口后如何访问路由器设置页面
  6. 如何用C语言求两个数的较大值
  7. DBMS_LOB.SUBSTR(col1,n,pos) : 获取文本
  8. Android高手笔记-屏幕适配 UI优化
  9. 读取绘制visio文件
  10. python扇贝每日一句api_扇贝简易爬虫