聊聊 Redis 分布式锁的正确实现

最近在参加学校安排的实训任务,咱们小组需完成一套分布式&微服务跨境电商,虽然这题目看起来有点老套,而且队友可能是 Java 技术栈,因此我光荣(被迫)
的成为了一名前端,并顺路使用 PHP 的 Swoole 帮助负责服务器端的同窗编写了几个微服务模块。在小组成员之间的协做中,仍是出现了很多有趣的火花。前端

在昨天 review 队友代码的过程当中,发现了咱们组分布式锁的写法彷佛有点问题,实现代码以下:redis

加锁部分
image.png安全

解锁部分
image-1.png服务器

主要原理是使用了 redis 的 setnx 去插入一组 key-value,其中 key 要上锁的标识(在项目中是锁死用户 userId),若是上锁失败则返回 false。可是根据二段锁的思路,仔细思考会存在这么一个有趣的现象:网络

假设微服务 A 的某个请求对 userId = 7 的用户上锁,则微服务 A 的这个请求能够读取这个用户的信息,且能够修改其内容 ;其余模块只能读取这个用户的信息,没法修改其内容。
假设微服务 A 的当前请求对 userId = 7 的用户解锁,则全部模块能够读取这个用户的信息,且能够修改其内容
如此一来:架构

  • 若微服务模块 A 接收到另外一个须要修改 userId = 7 的用户 的请求时,假设这个用户还在被锁状态下,此次请求能够修改它吗?(能够,解个锁就行)
  • 若微服务模块 B 接收到另外一个须要修改 userId = 7 的用户 的请求时,假设这个用户还在被锁状态下,此次请求能够修改它吗?(能够,解个锁就行)
  • 若微服务模块 A 执行上锁的请求中途意外崩掉,其余用户还能修改信息吗? (能够,解个锁就行)

很明显,这三点并非咱们所但愿的。那么如何实现分布式锁才是最佳实践呐?异步

一个好的分布式锁须要实现什么

  • 由某个模块的某次请求上锁,而且只有由这个模块的此次请求解锁(互斥,只能有一个微服务的某次请求持有锁)
  • 若上锁模块的上锁请求超时执行,则应自动解锁,并还原其所作修改(容错,就算 一个持有锁的微服务宕机也不影响最终其余模块的上锁 )

咱们应该怎么作
综上所述,咱们小组的分布式锁在实现模块互斥的状况下,忽略的一个重要问题即是“请求互斥”。咱们只须要在加锁时,key-value 的值保存为当前请求的 requestId ,解锁时加多一次判断,是否为同一请求便可。socket

那么这么修改以后,咱们能够高枕无忧了吗?分布式

是的,够用了。由于咱们开发环境 Redis 是统一用一台服务器上的单例,采用上述方式实现的分布式锁并无什么问题,但在准备部署到生产环境下时,忽然意识到一个问题:若是实现主从读写分离,redis 多机主从同步数据时,采用的是异步复制,也即是一个“写”操做到咱们的 reids 主库以后,便立刻返回成功(并不会等到同步到从库后再返回,若是这种是同步完成后再返回即是同步复制),这将会形成一个问题:微服务

假设咱们的模块 A中 id=1 的请求上锁成功后,没同步到从库前主库被咱们玩坏了(宕机),则 redis 哨兵将会从从库中选择出一台新的主库,此时若模块 A 中 id=2 的请求从新请求加锁,将会是成功的。

技不如人,咱们只能借助搜索引擎划水了(大雾),发现这种状况还真的有通用的解决方案:redlock。

怎么实现 Redlock 分布式安全锁

首先 redlock 是 redis 官方文档推荐的实现方式,自己并无用到主从层面的架构,采用的是多态主库,依次去取锁的方式。假设这里有 5 台主库,总体流程大体以下:

加锁

  1. 应用层请求加锁
  2. 依次向 5 台 redis 服务器发送请求
  3. 如有超过半数的服务器返回加锁成功,则完成加锁,若是没有则自动执行解锁,并等待一段随机时间后重试。(客观缘由加锁失败:网络状况很差、服务器未响应等问题, 等待一段随机时间后重试能够避开“蜂拥而进”的状况形成服务器资源占用瞬时猛增 )
  4. 若有其中任意一台服务器已经持有该锁,则加锁失败, 等待一段随机时间后重试。 (主观缘由加锁失败:已经被被别人锁上了)

解锁

直接向 5 台服务器发起请求便可,不管这台服务器上是否是已经有锁。
总体思路很简单,可是实现起来仍有许多值得注意的地方。在向这 5 台服务器发送加锁请求时,因为会带上一个过时时间以保证上文所提到的“自动解锁(容错性) ”,考虑到延时等缘由,这 5 台机自动解锁的时间不彻底相同,所以存在一个加锁时间差的问题,通常而言是这么解决的:

  1. 在加锁以前,必须在应用层(或者把分布式锁单独封装成一个全局通用的微服务亦可)2. 记录请求加锁的时间戳 T1
  2. 完成最后一台 redis 主库加锁后,记录时间戳 T2
  3. 则加锁所需时间为 T1 – T2
  4. 假设资源自动解锁的时间为 10 秒后,则资源真正可利用的时间为 10 – T1 + T2。若

可利用时间不符合预期,或者为负数,你懂的,从新来一遍吧。
若是你对锁的过时时间有着更加严格的把控,能够把 T1 到第一台服务器加锁成功的时间单独记录,再在最后的可用时间上加上这段时间便可获得一个更加准确的值
如今考虑另外一个问题,若是刚好某次请求的锁保存在了三台服务器上,其中这三台都宕机了(怎么这么倒霉.. TAT),那此时另外一个请求又来请求加锁,岂不又回到最初咱们小组所面临的问题了?很遗憾的说,是的,在这种问题上官方文档给出的答案是:启用AOF持久化功能状况会获得好转 🙂

关于性能方面的处理, 通常而言不止要求低延时,同时要求高吞吐量,咱们能够按照官方文档的说法, 采用多路传输同时对 5 台 redis 主库进行通讯以下降总体耗时,或者把 socket 设置成非阻塞模式 (这样的好处是发送命令时并不等待返回,所以能够一次性发送所有命令再进行等待总体运行结果,虽然本人认为一般状况下若是自己网络延迟极低的状况下做用不大,等待服务器处理的时间占比会更加大)

如有任何疑问,能够移步个人博客:http://www.zzfly.net/redis-re... 留言讨论

相关文章
相关标签/搜索