当前位置: 首页 > 后端技术 > PHP

说说正确实现Redis分布式锁的方法

时间:2023-03-30 02:30:44 PHP

最近参加了学校安排的培训任务。我们的团队需要完成一套分布式&微服务的跨境电商,虽然这个话题看起来有点老套,而且有很多队友都是Java技术栈,所以有幸(被迫)做前端,用PHP的Swoole帮负责服务器的同学写了几个微服务模块。在小组成员之间的合作中,还是擦出了很多有趣的火花。昨天在review队友代码的过程中,发现我们组的分布式锁的写法好像有问题。实现代码如下:加锁部分和解锁部分的主要原理是利用redis的setnx插入一组key-value,其中key需要是锁的ID(在项目中,就是被加锁的用户userId),如果加锁失败,返回false。但是按照二段锁的思想,仔细想想就会出现这样一个有趣的现象:假设微服务A的一个请求锁定了userId=7的用户,那么微服务A的请求就可以读取到用户的信息,并且其内容是可以修改的;其他模块只能读取用户信息,不能修改其内容。假设微服务A当前请求解锁了userId=7的用户,所有模块都可以读取用户的信息并修改其内容。这样:如果微服务模块A又收到一个userId=7需要修改的用户请求时,假设用户还是锁着的,这个请求能修改吗?(对,解锁就行)如果微服务模块B收到另外一个用户的请求,需要修改userId=7,假设用户还是锁着的,这个请求能修改吗?(是的,解开锁即可。)如果微服务模块A在执行加锁请求的过程中不小心崩溃了,其他用户还能修改信息吗?(对,解锁就行)很明显,这三点都不是我们想要的。那么如何实现分布式锁才是最佳实践呢?一个好的分布式锁需要实现什么?它被某个模块的某个请求锁定,只有本模块的这个请求解锁(互斥,一个微服务只有一个请求可以持有锁)。如果锁模块的锁请求超时执行,应该自动解锁并恢复其所做的修改(容错,即使一个持有锁的微服务宕机,也不会影响其他模块最终加锁).我们应该做什么?上面说到,我们组的分布式锁在实现模块互斥时,忽略了一个重要的问题就是“请求互斥”。我们只需要在加锁的时候把key-value值保存为当前请求的requestId,解锁的时候再多加一个判断,看是不是同一个请求。那么这样修改之后,是不是可以高枕无忧了呢?是的,足够了。因为我们的开发环境Redis是单机单机,上面的方式实现的分布式锁是没有问题的,但是当我们准备部署到生产环境的时候突然意识到一个问题:如果master-slave写分离,redis多机主从同步数据时,采用的是异步复制,即向我们的reids主库发送一个“写”操作后,立即返回成功(不会等到同步完成到从库然后返回,如果这是同步完成后的同步复制),这会产生一个问题:假设我们模块A中id=1的请求被成功锁定,主库被我们打在同步到从库之前如果坏了(宕机),redissentinel会从从库中选择一个新的主库。此时如果模块A中id=2的请求重新请求加锁,就会成功。技不如人,只能借助搜索引擎划桨(迷雾),发现还真有解决这种情况的通用方法:redlock。Redlock分布式安全锁如何实现首先redlock是redis官方文档推荐的实现方式。它不使用主从架构。它使用多态主库逐个获取锁。假设这里有5个主数据库,整体流程大致是这样的:应用层请求加锁,依次向5个redis服务器发送请求。如果超过一半的服务器返回加锁成功,则加锁完成。如果没有,解锁将自动执行,然后等待一段随机的时间再试一次。(锁定失败的客观原因:网络状况不佳、服务器无响应等,随机等待一段时间再重试可以避免出现“蜂拥而入”导致服务器资源占用骤增的情况)servers如果已经持有锁,则锁会失败,等待一段随机的时间后重试。(主观原因解锁失败:被别人锁了)解锁直接向5台服务器发送请求即可,不管这台服务器是否已经有锁。整体思路很简单,但是在实现的过程中还是有很多值得注意的地方。在向这5台服务器发送锁请求时,会加上一个过期时间,以保证上述的“自动解锁(容错)”。考虑到延迟等原因,这5台服务器自动解锁。时间不完全一样,就出现了锁定时差的问题。一般来说是这样解决的:加锁之前,必须在应用层(或者单独把分布式锁封装成一个全局的通用微服务)2。记录最后一次redis主库加锁后请求加锁的时间戳T1,记录时间戳T2,加锁所需时间为T1-T2假设资源10秒后自动解锁,则资源为真可用时间为10–T1+T2。如果可用时间不符合预期,或者是负数,你懂的,再试一次。如果对锁过期时间控制比较严格,可以记录下T1到第一个服务器加锁成功的时间,然后把这个时间加上最后可用的时间,得到更准确的值现在再考虑一个问题,如果锁某个时间的requested保存在三台服务器上,三台都宕机了(怎么这么倒霉..TAT),这时候又来一个request来请求锁,岂不是又回到问题中了我们小组一开始面对的是什么?很遗憾的说是,官方文档对这个问题给出的答案是:启用AOF持久化功能后情况会不会有所改善?关于性能处理,一般来说不仅需要低延迟,还需要高吞吐量,根据官方文档,我们可以使用多路传输同时与5个redis主库通信,以减少整体时间消耗,或者将socket设置为非阻塞模式(这样做的好处是发送命令时不等待返回,因此可以一次发送所有命令,等待整体运行结果。虽然我认为一般情况下,如果网络延迟极低,是不会生效的,等待服务器处理的时间会比较长。)有问题可以移步我的博客:http://www.zzfly.net/redis-re...留言讨论