当前位置: 首页 > news >正文

Redis 典型应⽤-分布式锁

一、什么是分布式锁?

        在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题.

        ⽽java的synchronized或者C++的std::mutex,这样的锁都是只能在当前进程中⽣效,在分布式的这 种多个进程多个主机的场景下就⽆能为⼒了.

此时就需要使⽤到分布式锁

本质上就是使⽤⼀个公共的服务器,来记录加锁状态.

这个公共的服务器可以是Redis,也可以是其他组件(⽐如MySQL或者ZooKeeper等),还可以 是我们⾃⼰写的⼀个服务.

二、 分布式锁的基础实现

思路⾮常简单.本质上就是通过⼀个键值对来标识锁的状态.

举个例⼦:考虑买票的场景,现在⻋站提供了若⼲个⻋次,每个⻋次的票数都是固定的.

现在存在多个服务器节点,都可能需要处理这个买票的逻辑:先查询指定⻋次的余票,如果余票>0,则设 置余票值-=1.

 显然上述的场景是存在"线程安全"问题的,需要使⽤锁来控制.

 否则就可能出现"超卖"的情况.

此时如何进⾏加锁呢?我们可以在上述架构中引⼊⼀个Redis,作为分布式锁的管理器.

        此时,如果买票服务器1尝试买票,就需要先访问Redis,在Redis上设置⼀个键值对.⽐如key就是⻋ 次,value随便设置个值(⽐如1). 

        如果这个操作设置成功,就视为当前没有节点对该001⻋次加锁,就可以进⾏数据库的读写操作.操作完 成之后,再把Redis上刚才的这个键值对给删除掉.        

        如果在买票服务器1操作数据库的过程中,买票服务器2也想买票,也会尝试给Redis上写⼀个键值对, key 同样是⻋次.但是此时设置的时候发现该⻋次的key已经存在了,则认为已经有其他服务器正在持 有锁,此时服务器2就需要等待或者暂时放弃.

Redis中提供了setnx操作,正好适合这个场景.即:key不存在就设置,存在则直接失败.

但是上述⽅案并不完整.

 

三、引⼊过期时间

        当服务器1加锁之后,开始处理买票的过程中,如果服务器1意外宕机了,就会导致解锁操作(删除该 key) 不能执⾏.就可能引起其他服务器始终⽆法获取到锁的情况.

        为了解决这个问题, 可以在设置key的同时引⼊过期时间.即这个锁最多持有多久,就应该被释放.

可以使⽤ set ex nx 的⽅式,在设置锁的同时把过期时间设置进去.

 注意! 此处的过期时间只能使⽤⼀个命令的⽅式设置.

        如果分开多个操作,⽐如setnx之后,再来⼀个单独的expire,由于Redis的多个指令之间不存在关 联,并且即使使⽤了事务也不能保证这两个操作都⼀定成功,因此就可能出现setnx成功,但是expire 失败的情况.

此时仍然会出现⽆法正确释放锁的问题.

四、 引⼊校验id

对于Redis中写⼊的加锁键值对,其他的节点也是可以删除的.

⽐如服务器1写⼊⼀个"001":1这样的键值对,服务器2是完全可以把"001"给删除掉的.

当然,服务器2不会进⾏这样的"恶意删除"操作,不过不能保证因为⼀些bug导致服务器2把锁误删 除.

 为了解决上述问题,我们可以引⼊⼀个校验id.

        ⽐如可以把设置的键值对的值,不再是简单的设为⼀个1,⽽是设成服务器的编号.形如"001":"服务器 1".

        这样就可以在删除key(解锁)的时候,先校验当前删除key的服务器是否是当初加锁的服务器,如果是, 才能真正删除;不是,则不能删除.

逻辑⽤伪代码描述如下:

 String key = [要加锁的资源 id];String serverId = [服务器的编号]; // 加锁, 设置过期时间为 10sredis.set(key, serverId, "NX", "EX", "10s"); // 执⾏各种业务逻辑, ⽐如修改数据库数据. doSomeThing(); // 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配. if (redis.get(key) == serverId) {redis.del(key);}

但是很明显,解锁逻辑是两步操作"get"和"del",这样做并⾮是原⼦的.

五、引⼊lua

为了使解锁操作原⼦,可以使⽤Redis的Lua脚本功能.

Lua也是⼀个编程语⾔.读作"撸啊".是葡萄⽛语中的"⽉亮"的意思.(出⾃于Lua官⽅⽂档 https://www.lua.org/about.html)

Lua的语法类似于JS,是⼀个动态弱类型的语⾔.Lua的解释器⼀般使⽤C语⾔实现.Lua语法 简单精炼,执⾏速度快,解释器也⽐较轻量(Lua解释器的可执⾏程序体积只有200KB左右).

因此Lua经常作为其他程序内部嵌⼊的脚本语⾔.Redis本⾝就⽀持Lua作为内嵌脚本.

        很多程序都⽀持内嵌脚本,⽐如MySQL8⽀持JS作为内嵌脚本,⽐如Vim⽀持VimScript 和Python作为内嵌脚本....通过内嵌脚本来实现更复杂的功能,提供更强的扩展性.

        Lua除了和Redis搭伙之外,在很多场景也会作为内嵌脚本.⽐如在游戏开发领域常常作为 编写逻辑的语⾔.

 使⽤Lua脚本完成上述解锁功能

if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) 
else return 0 
end;

        述代码可以编写成⼀个.lua后缀的⽂件,由 redis-cli 或者 redis-plus-plus 或者 jedis 等客⼾端加载,并发送给Redis服务器,由Redis服务器来执⾏这段逻辑.

⼀个lua脚本会被Redis服务器以原⼦的⽅式来执⾏

redis-plus-plus 和 jedis 如何调⽤lua,咱们此处不做过多介绍.具体api的写法⼤家可以 ⾃⾏研究.

六、引⼊watchdog(看⻔狗)

        上述⽅案仍然存在⼀个重要问题.当我们设置了key过期时间之后(⽐如10s),仍然存在⼀定的可能性, 当任务还没执⾏完,key就先过期了.这就导致锁提前失效.

把这个过期时间设置的⾜够⻓,⽐如30s,是否能解决这个问题呢?很明显,设置多⻓时间合适,是⽆⽌ 境的.即使设置再⻓,也不能完全保证就没有提前失效的情况.

⽽且如果设置的太⻓了,万⼀对应的服务器挂了,此时其他服务器也不能及时的获取到锁.

因此相⽐于设置⼀个固定的⻓时间,不如动态的调整时间更合适.

         所谓watchdog,本质上是加锁的服务器上的⼀个单独的线程,通过这个线程来对锁过期时间进⾏"续约"

注意 ,这个线程是业务服务器上的,不是Redis服务器的.

举个具体的例⼦:

初始情况下设置过期时间为10s.同时设定看⻔狗线程每隔3s检测⼀次.

那么当3s时间到的时候,看⻔狗就会判定当前任务是否完成.

  • 如果任务已经完成,则直接通过lua脚本的⽅式,释放锁(删除key).
  • 如果任务未完成,则把过期时间重写设置为10s.(即"续约")

        这样就不担⼼锁提前失效的问题了.⽽且另⼀⽅⾯,如果该服务器挂了,看⻔狗线程也就随之挂了,此时 ⽆⼈续约,这个key⾃然就可以迅速过期,让其他服务器能够获取到锁了.

七、引⼊Redlock算法

        实践中的Redis⼀般是以集群的⽅式部署的(⾄少是主从的形式,⽽不是单机).那么就可能出现以下⽐较极端的情况:

        服务器1向master节点进⾏加锁操作.这个写⼊key的过程刚刚完成,master挂了;slave节 点升级成了新的master节点.但是由于刚才写⼊的这个key尚未来得及同步给slave呢,此时 就相当于服务器1的加锁操作形同虚设了,服务器2仍然可以进⾏加锁(即给新的master写 ⼊key.因为新的master不包含刚才的key).

 为了解决这个问题,Redis的作者提出了Redlock算法.

        我们引⼊⼀组Redis节点.其中每⼀组Redis节点都包含⼀个主节点和若⼲从节点.并且组和组之间存 储的数据都是⼀致的,相互之间是"备份"关系(⽽并⾮是数据集合的⼀部分,这点有别于Rediscluster).

        加锁的时候,按照⼀定的顺序,写多个master节点.在写锁的时候需要设定操作的"超时时间".⽐如 50ms. 即如果setnx操作超过了50ms还没有成功,就视为加锁失败.

如果给某个节点加锁失败,就⽴即再尝试下⼀个节点.

当加锁成功的节点数超过总节点数的⼀半,才视为加锁成功.

如上图 , ⼀共五个节点,三个加锁成功,两个失败,此时视为加锁成功.

这样的话,即使有某些节点挂了,也不影响锁的正确性.

        同理,释放锁的时候,也需要把所有节点都进⾏解锁操作.(即使是之前超时的节点,也要尝试解锁,尽量保 证逻辑严密).

        简⽽⾔之,Redlock算法的核⼼就是,加锁操作不能只写给⼀个Redis节点,⽽要写个多个!!分布式系统 中任何⼀个节点都是不可靠的.最终的加锁成功结论是"少数服从多数的".

        由于⼀个分布式系统不⾄于⼤部分节点都同时出现故障,因此这样的可靠性要⽐单个节点来说靠谱不少.

八、其他功能

上述描述中我们解释了基于Redis的分布式锁的基本实现原理.

上述锁只是⼀个简单的互斥锁.但是实际上我们在⼀些特定场景中,还有⼀些其他特殊的锁,⽐如:

  • 可重⼊锁
  • 公平锁
  • 读写锁
  • ......

基于Redis的分布式锁,也可以实现上述特性.(当然了对应的实现逻辑也会更复杂).

此处我们不做过多讨论了.

实际开发中,我们也并不会真的⾃⼰实现⼀个分布式锁.已经有很多现成的库帮我们封装好了,我们直接 使⽤即可.

⽐如Java中的Redisson,C++中的redis-plus-plus.当然,有些⼤⼚也会有⾃⼰版本的分布式锁的实现.


http://www.mrgr.cn/news/70152.html

相关文章:

  • java:继承题练习
  • 【api】java和python联动
  • 数字字符串格式化
  • leetcode-15-三数之和
  • python printf中文乱码
  • 智慧医疗:纹理特征VS卷积特征
  • ArcGIS软件之“计算面积几何”地图制作
  • 【SSL-RL】自监督强化学习:随机潜在演员评论家 (SLAC)算法
  • Deepin 系统中安装Rider和Uno Platform
  • 《Django 5 By Example》阅读笔记:p1-p16
  • 前端-服务端渲染(SSR)和客户端渲染(CSR)的页面,在浏览器发出请求之后,分别返回的是什么
  • axios如何给某一个请求设置请求头信息
  • Python和Java就业趋势分析
  • Swift 宏(Macro)入门趣谈(一)
  • Xcode 16 中 Swift Testing 的参数化(Parameterized)机制趣谈
  • MrakDown图片
  • 关于JWT的攻击利用
  • 盘古信息赋能中小企业:数字化转型的成功实践分享
  • Deepin系统安装NET 8.0.10 运行时
  • 深入理解Python字符串:驻留机制、内存分析与同一性判断
  • 2024中国游戏出海情况
  • 【Linux系统编程】线程--控制
  • linux内核驱动心得
  • 整页添加水印的方法
  • idea插件开发-国际化调试
  • 985研一学习日记 - 2024.11.10