首页 > 编程知识 正文

redis分布式锁代码,redis不适合做分布式锁

时间:2023-05-04 21:04:48 阅读:153074 作者:2865

在谈论分布式锁为什么需要分布式锁之前,必须先解释为什么需要分布式锁。

对于分布式锁是独立锁。 编写多线程程序时,同时操作共享变量以避免出现数据问题。 通常使用锁来保证共享变量的正确性。 其使用范围在同一流程内。 如果是多个进程,则必须同时操作一个共享资源,但如何是互斥的呢? 目前的业务APP应用通常是微服务架构。 这意味着在一个APP应用程序中部署多个进程。 如果多个进程需要修改MySQL中的同一行记录,则在这种情况下必须引入分布式锁,以避免操作顺序不正确和出现脏数据。

要实现分布式锁定,必须利用外部系统,所有进程都向该系统申请解锁。 该外部系统必须实现互斥能力,即两个请求同时进入,只锁定一个进程成功,另一个失败。 此外部系统可以是数据库,也可以是Redis或Zookeeper,但为了追求性能,通常使用Redis或Zookeeper。

Redis本身可以由多个客户端共享和访问。 这意味着它是一个共享存储系统,拥有分布式锁。 另外,Redis读写性能高,能够应对高同时的锁定操作场景。 本文介绍了如何基于Redis实现分布式锁,以及在实现过程中可能遇到的问题。

分布式锁是如何在分布式锁实现时作为共享存储系统实现的? Redis可以使用键-值对保存锁定变量,并接收和处理从不同客户端发送的锁定和解锁操作请求。 那么,键值对的键值具体是如何确定的呢? 给锁定变量赋予变量名,将该变量名作为键值对的键,锁定变量的值作为键值对的值。 这样,Redis可以保存锁定变量,客户端也可以通过Redis的命令操作实现锁定操作。

要实现分布式锁定,Redis需要互斥的能力。 可以使用SETNX命令。 这意味着SET IF NOT EXIST。 也就是说,只有在key不存在时才设置值,否则什么都不做。 两个客户端进程可以通过执行此命令并达到互斥来实现分布式锁定。

以下过程说明了Redis如何使用key/value保存锁定变量,以及两个客户端同时请求锁定。

锁定操作完成后,成功锁定的客户端可以对共享资源进行操作,例如更改MySQL数据行。 操作完成后,立即解除锁定,让后来者有操作共享资源的机会。 如何解除锁定? 直接使用DEL命令删除这个key就可以了。 这个逻辑非常简单,整体流程变成伪代码的情况如下。

//锁定SETNX lock_key 1//业务逻辑DO THINGS//解锁DEL lock_key。 但是,上述安装存在很大的问题,客户机1取得锁定后,如果发生以下情况,就会出现死锁。

程序处理了业务逻辑的异常,锁定过程没有立即解除就挂起了。 既然没有机会解锁,获得锁定的客户端就会继续占用锁定,其他客户端将无法获得锁定。

为了解决以上死锁问题,在申请上锁时、用Redis实现时,对上锁设定有效期限,如果假设操作共享资源的时间不超过10s,则在上锁时,对该key设定10s的有效期限

但是,上述操作还存在问题。加锁、设置过期时间是2条命令,有可能只执行了第一条,第二条却执行失败,例如:

SETNX运行成功,运行EXPIRE时由于网络问题,运行失败SETNX运行成功,Redis异常关闭,EXPIRE运行SETNX成功,客户端异常崩溃,EXPIRE暂且不论幸运的是,从Redis 2.6.12开始,Redis扩展了SET命令的参数,可以在设置的同时指定EXPIRE时间。 这个操作是原子的。 例如,以下命令将锁定的过期时间设置为10秒:

SET lock_key 1 EX 10 NX现在已解决死锁问题,但存在其他问题。 请想象以下情景。

1锁定成功。 已开始操作共享资源。 超过了1锁的有效期限。 锁已失效。 (锁定自动解除了。 )客户端2锁定成功。 已开始操作共享资源。 1共享资源操作已完成。 在finally块上手动解除了锁定,此时解除锁定的是客户端2的锁定。 这里有两个严重的问题:

锁过期释放他人锁的第一个问题是如果不能保证是原子操作,就有潜在的风险导致过期时间设置失败,依旧有可能发生死锁问题

第二个问题是释放了别人的锁。 解锁操作是一种没有大脑的操作,因为没有检查此锁定的归属,所以解锁并不严格。 怎么解决?

当客户端锁定时,释放锁的解决方案是评估操作共享资源的时间不准确导致的,如果只是一味增大过期时间,只能缓解问题降低出现问题的概率,依旧无法彻底解决问题。原因在于客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景,如果是redis实现,则为SET key unique_value EX 10 NX。 之后,解锁的时候,首先判断这个锁是否自己有,只有自己才能解锁。

//解锁比较unique_value是否相等,以避免意外释放ifredis.get(key ) )==unique_value

then return redis.del("key")

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

客户端1执行GET,判断锁是自己的客户端2执行了SET命令,强制获取到锁(虽然发生概念很低,但要严谨考虑锁的安全性)客户端1执行DEL,却释放了客户端2的锁

由此可见,以上GET + DEL两个命令还是必须原子的执行才行。怎样原子执行两条命令呢?答案是Lua脚本,可以把以上逻辑写成Lua脚本,让Redis执行。因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成,这样一来GET+DEL之间就不会有其他命令执行了。

以下是使用Lua脚本(unlock.script)实现的释放锁操作的伪代码,其中,KEYS[1]表示lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是我们在执行 Lua脚本时作为参数传入的。

//Lua脚本语言,释放锁 比较unique_value是否相等,避免误释放if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end

最后我们执行以下命令,即可

redis-cli --eval unlock.script lock_key , unique_value

这样一路优先下来,整个加锁、解锁流程就更严谨了,先小结一下,基于Redis实现的分布式锁,一个严谨的流程如下:

加锁时要设置过期时间SET lock_key unique_value EX expire_time NX操作共享资源释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁

有了这个严谨的锁模型,我们还需要重新思考之前的那个问题,锁的过期时间不好评估怎么办。

如何确定锁的过期时间

前面提到过,过期时间如果评估得不好,这个锁就会有提前过期的风险,一种妥协的解决方案是,尽量冗余过期时间,降低锁提前过期的概率,但这个方案并不能完美解决问题。是否可以设置这样的方案,加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

这是一种比较好的方案,已经有一个库把这些工作都封装好了,它就是Redisson。Redisson是一个Java语言实现的Redis SDK客户端,在使用分布式锁时,它就采用了自动续期的方案来避免锁过期,这个守护线程我们一般叫它看门狗线程。这个SDK提供的API非常友好,它可以像操作本地锁一样操作分布式锁。客户端一旦加锁成功,就会启动一个watch dog看门狗线程,它是一个后台线程,会每隔一段时间(这段时间的长度与设置的锁的过期时间有关)检查一下,如果检查时客户端还持有锁key(也就是说还在操作共享资源),那么就会延长锁key的生存时间。

那如果客户端在加锁成功后就宕机了呢?宕机了那么看门狗任务就不存在了,也就无法为锁续期了,锁到期自动失效。

Redis的部署方式对锁的影响

上面讨论的情况,都是锁在单个Redis 实例中可能产生的问题,并没有涉及到Redis的部署架构细节。

Redis发展到现在,几种常见的部署架构有:

单机模式;主从模式;哨兵(sentinel)模式;集群模式;

我们使用Redis时,一般会采用主从集群+哨兵的模式部署,哨兵的作用就是监测redis节点的运行状态。普通的主从模式,当master崩溃时,需要手动切换让slave成为master,使用主从+哨兵结合的好处在于,当master异常宕机时,哨兵可以实现故障自动切换,把slave提升为新的master,继续提供服务,以此保证可用性。那么当主从发生切换时,分布式锁依旧安全吗?

想像这样的场景:

客户端1在master上执行SET命令,加锁成功此时,master异常宕机,SET命令还未同步到slave上(主从复制是异步的)哨兵将slave提升为新的master,但这个锁在新的master上丢失了,导致客户端2来加锁成功了,两个客户端共同操作共享资源

可见,当引入Redis副本后,分布式锁还是可能受到影响。即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况。

集群模式+Redlock实现高可靠的分布式锁

为了避免Redis实例故障而导致的锁无法工作的问题,Redis的开发者 Antirez提出了分布式锁算法Redlock。Redlock算法的基本思路,是让客户端和多个独立的Redis实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个Redis实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

来具体看下Redlock算法的执行步骤。Redlock算法的实现要求Redis采用集群部署模式,无哨兵节点,需要有N个独立的Redis实例(官方推荐至少5个实例)。接下来,我们可以分成3步来完成加锁操作。

第一步是,客户端获取当前时间。
第二步是,客户端按顺序依次向N个Redis实例执行加锁操作。

这里的加锁操作和在单实例上执行的加锁操作一样,使用SET命令,带上NX、EX/PX选项,以及带上客户端的唯一标识。当然,如果某个Redis实例发生故障了,为了保证在这种情况下,Redlock算法能够继续运行,我们需要给加锁操作设置一个超时时间。如果客户端在和一个Redis实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个Redis实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端完成了和所有Redis实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足两个条件时,才能认为是加锁成功,条件一是客户端从超过半数(大于等于 N/2+1)的Redis实例上成功获取到了锁;条件二是客户端获取锁的总耗时没有超过锁的有效时间

为什么大多数实例加锁成功才能算成功呢?多个Redis实例一起来用,其实就组成了一个分布式系统。在分布式系统中总会出现异常节点,所以在谈论分布式系统时,需要考虑异常节点达到多少个,也依旧不影响整个系统的正确运行。这是一个分布式系统的容错问题,这个问题的结论是:如果只存在故障节点,只要大多数节点正常,那么整个系统依旧可以提供正确服务。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成共享资源操作,锁就过期了的情况

当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端就要向所有Redis节点发起释放锁的操作。为什么释放锁,要操作所有的节点呢,不能只操作那些加锁成功的节点吗?因为在某一个Redis节点加锁时,可能因为网络原因导致加锁失败,例如一个客户端在一个Redis实例上加锁成功,但在读取响应结果时由于网络问题导致读取失败,那这把锁其实已经在Redis上加锁成功了。所以释放锁时,不管之前有没有加锁成功,需要释放所有节点上的锁以保证清理节点上的残留的锁。

在Redlock算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua脚本就可以了。这样一来,只要N个Redis实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。所以,在实际的业务应用中,如果你想要提升分布式锁的可靠性,就可以通过Redlock算法来实现。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。