1. 主页 > 用户投稿

分布式锁的实现方式redis(redis实现分布式锁的几种方式)

关于这个redis实现分布式锁的几种方式(分布式锁的实现方式redis)很多人还不知道,现在让我们一起来看看吧!

1、为什么需要分布式锁?在谈论分布式锁之前,有必要解释一下为什么需要分布式锁。与分布式锁相比,单机锁则相反。当我们编写多线程程序时,我们避免了同时操作一个共享变量所导致的数据问题。

2、通常,我们使用一个锁来互斥以保证共享变量的正确性,其使用范围是在同一个进程中。如果有多个进程需要同时操作一个共享资源,它们怎么会互斥呢?现在的业务应用通常是微服务架构,这也意味着一个应用会部署多个流程。如果多个进程需要修改MySQL中的同一行记录,为了避免乱序操作导致的脏数据,这时候就需要引入分布式锁。

3、要实现分布式锁,我们必须使用一个外部系统,在这个系统中所有的进程都申请锁。这个外部系统必须实现互斥,即当两个请求同时进来时,只有一个进程被成功锁定,另一个会失败。这个外部系统可以是数据库,可以是Redis,也可以是Zookeeper,但是为了追求性能,我们通常会选择用Redis或者Zookeeper来做。

4、Redis本身可以被多个客户端共享访问,恰好是一个共享存储系统,可以用来存储分布式锁。而且Redis读写性能高,可以应对高并发锁操作场景。本文主要讨论如何基于Redis实现分布式锁以及实现过程中可能出现的问题。

5、如何实现分布式锁?Redis作为实现分布式锁过程中的共享存储系统,可以使用键-值对存储锁变量,接收和处理不同客户端发送的加锁和释放锁的操作请求。那么,键值对的键和值是如何确定的呢?我们需要给锁变量一个变量名,用这个变量名作为键-值对的键,而锁变量的值就是键-值对的值。这样Redis可以保存锁变量,客户端通过Redis的命令操作实现锁操作。

6、要实现分布式锁,Redis必须是互斥的。可以使用SETNX命令,意思是SETIFNOTEXIST,即如果键不存在,就设置它的值,否则什么都不做。两个客户端进程可以执行这个命令实现互斥,可以实现分布式锁。

7、下面展示了Redis如何使用键/值对来保存锁变量,以及两个客户机如何同时请求锁。锁定操作完成后,成功锁定的客户端可以对共享资源进行操作,比如修改MySQL中的一行数据。操作完成后,要及时释放锁,给后来者操作共享资源的机会。

8、怎么开锁?使用DEL命令直接删除该键。这个逻辑很简单,整个过程写成伪代码如下。//加锁SETNXlock_key1//业务逻辑DOTHINGS//释放锁DELlock_key但是,上面的实现有一个很大的问题。

9、当客户端1获得锁时,如果发生以下情况,将会导致死锁。程序处理业务逻辑异常,没及时释放锁进程挂了,没机会释放锁以上情况会造成已经获取锁的客户端一直占用锁,其他客户端永远不会获取锁。如何避免死锁为了解决上面的死锁问题,最简单的解决方案是在申请锁并在Redis中实现时为锁设置一个到期时间。

10、假设操作共享资源的时间不会超过10s,那么在锁定的时候,设置密钥过期10s。但是,上述操作仍然存在问题。锁定和设置到期时间是两个命令。

11、有可能只执行了之一个,而第二个失败了,例如:SETNX执行成功,执行EXPIRE时由于 *** 问题,执行失败SETNX执行成功,Redis异常宕机,EXPIRE没有机会执行SETNX执行成功,客户端异常崩溃,EXPIRE没有机会执行简而言之,如果这两个命令不能保证是原子操作,则存在到期时间设置失败的潜在风险,死锁仍可能发生。好在Redis12以后,Redis扩展了SET命令的参数,在SET的同时指定了到期时间。这个操作是原子的。

12、例如,以下命令将锁的过期时间设置为10秒。SETlock_key1EX10NX到目前为止,死锁问题已经解决,但仍然存在其他问题。想象以下场景:

13、客户端1锁定成功,开始操作共享资源。

14、客户端1操作共享资源时间过长,超过锁到期时间,锁失效(锁自动释放)。客户端2锁定成功,开始操作共享资源。客户端1操作共享资源,手动释放finally块中的锁,但此时释放客户端2的锁。

15、这里有两个严重的问题:锁过期释放了别人的锁之一个问题是由于对操作共享资源的时间的不准确估计而引起的。如果只是一味的增加到期时间,只能缓解问题,降低出现问题的概率,仍然不能彻底解决问题。原因是客户端获得锁后,在操作共享资源时,遇到的场景非常复杂。

16、既然是预估时间,只能粗略计算,不可能涵盖所有导致耗时更长的场景。第二个问题是解除别人的锁,因为解除锁的操作是无脑的,没有检查锁的所有权,所以开锁不严谨。怎么解决?锁被别人打开了。

17、解决方案是,当客户端锁定锁时,它设置一个只有它自己知道的唯一标识符,例如,它可以是自己的线程ID。如果是redis实现的,则设置keyunique_valueEX10NX。之后,在解锁的时候,首先要判断锁是不是自己的,只有自己的才能解锁。

18、//释放锁比较unique_value是否相等,避免误释放ifredis.get("key")==unique_valuethenreturnredis.del("key")这里,GET+DEL命令用于释放锁。这时候又会遇到原子问题。客户端1执行GET,判断锁是自己的客户端2执行了SET命令,强制获取到锁(虽然发生概率很低,但要严谨考虑锁的安全性)客户端1执行DEL,却释放了客户端2的锁所以上面的GET+DEL命令还是要用原子来执行。

19、原子如何执行两个命令?答案是Lua脚本。可以把上面的逻辑写成Lua脚本,让Redis执行。因为Redis处理的每个请求都是在单个线程中执行的,所以在执行一个Lua脚本的时候,其他请求必须等到Lua脚本处理完,这样在GET+DEL之间就不会有其他命令执行了。

20、下面是使用Lua脚本(unlock.script)释放锁的伪代码,其中KEYS[1]代表lock_key,ARGV[1]是当前客户端的唯一ID。当我们执行Lua脚本时,这两个值都作为参数传入。//Lua脚本语言,释放锁比较unique_value是否相等,避免误释放ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end最后,我们执行下面的命令。

21、redis-cli--evalunlock.scriptlock_key,unique_value这样一路下来,整个上锁开锁的过程会更加严谨。首先,我们来总结一下基于Redis的分布式锁。一个严谨的流程如下:加锁时要设置过期时间SETlock_keyunique_valueEXexpire_timeNX操作共享资源释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁有了这个严谨的锁模型,我们需要重新思考之前的问题,如果锁的到期时间不好评估怎么办。

22、如何确定锁的到期时间如前所述,如果到期时间没有评估好,这个锁将有提前到期的风险。一种折中的解决方案是尽可能使到期时间冗余,降低锁提前到期的概率,但这种解决方案并不能完美解决问题。有可能设置这样的方案吗?锁的时候,先设置一个估计的到期时间,然后启动一个守护线程定期检测这个锁的到期时间。

23、如果锁即将到期,而共享资源的操作没有完成,那么锁将自动更新,到期时间将重置。这是一个更好的方案。已经有一个库封装了所有这些作品,它就是Redisson。

24、Redisson是一个用Java语言实现的Redisdk客户端。当使用分布式锁时,它采用自动更新方案来避免锁过期。这个守护线程通常被称为看门狗线程。

25、这个SDK提供的API非常友好,可以像操作本地锁一样操作分布式锁。一旦客户端锁成功,就会启动一个watchdog看门狗线程,这个线程是一个后台线程,每隔一段时间就会检查一次(这个时间的长短和设置锁的到期时间有关)。如果客户端在检查的时候仍然持有锁密钥(也就是说,它仍然在操作共享资源),那么它将延长锁密钥的生存期。

26、锁成功锁定后客户端宕机怎么办?停机后,看门狗任务将不存在,并且锁不能被更新。锁到期后会自动失效。Redis部署模式对锁的影响上述情况都是Redis单个实例可能出现的问题,不涉及Redis的部署架构细节。

27、到目前为止,Redis的几种常见部署架构有:单机模式;主从模式;哨兵(sentinel)模式;集群模式;我们在使用redis时,通常采用主从集群+哨兵的模式,哨兵的作用是监控Redis节点的运行状态。普通主从模式,当主崩溃时,需要手动切换从成为主。采用主从和哨兵相结合的方式,好处是当主设备非正常宕机时,哨兵可以实现自动故障切换,将从设备升级为新的主设备并继续提供服务,从而保证可用性。

28、那么,当主从切换时,分布式锁还安全吗?想象这样一个场景:客户端1在master上执行SET命令,加锁成功此时,master异常宕机,SET命令还未同步到slave上(主从复制是异步的)哨兵将slave提升为新的master,但这个锁在新的master上丢失了,导致客户端2来加锁成功了,两个客户端共同操作共享资源可以看出,当引入Redis副本时,分布式锁仍然可能受到影响。即使Redis通过sentinel高度可用,如果主节点由于某种原因从主节点切换到从节点,锁也会丢失。集群模式+红锁实现高可靠分布式锁为了避免Redis实例失效导致的锁失效,Redis的开发者Antirez提出了分布式锁算法Redlock。

29、Redlock算法的基本思想是让客户端和多个独立的Redis实例依次请求锁定。如果客户端能够成功完成半数以上实例的加锁操作,那么我们认为客户端已经成功获得了分布式锁,否则加锁失败。这样,即使单个Redis实例失败,由于锁变量保存在其他实例中,客户端仍然可以正常锁,锁变量不会丢失。

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

31、之一步是客户端获取当前时间。第二步,客户端依次对N个Redis实例执行锁定操作。这里的锁定操作与在单个实例上执行的操作相同,使用SET命令、NX、EX/PX选项以及客户机的唯一标识。

32、当然,如果Redis实例失败,为了保证这种情况下Redlock算法能够继续运行,我们需要为锁定操作设置一个超时。如果客户机在超时之前没有请求锁定Redis实例,那么客户机将继续请求锁定下一个Redis实例。锁定操作的超时需要远小于锁的有效时间,通常设置为几十毫秒。

33、第三步是,一旦客户机完成了对所有Redis实例的锁定操作,客户机将计算整个锁定过程的总时间。只有当客户端满足两个条件时,才可以认为锁被成功锁定。之一个条件是客户端已经成功地从超过一半(N/2+1)个Redis实例中获取了锁;第二个条件是客户端获取锁的总时间不超过锁的有效时间。

34、为什么大多数实例成功锁定才算成功?当多个Redis实例一起使用时,它们实际上形成了一个分布式系统。分布式系统中总会有异常节点,所以在谈分布式系统时,需要考虑有多少个异常节点,仍然不会影响整个系统的正确运行。这是一个分布式系统的容错问题。

35、这个问题的结论是,如果只有故障节点,只要大部分节点正常,整个系统仍然可以提供正确的服务。满足这两个条件后,我们需要重新计算这个锁的有效时间,计算的结果是锁的初始有效时间减去客户端获取锁所花费的总时间。如果锁的有效时间来不及完成共享数据的操作,我们可以释放锁,避免在共享资源的操作完成之前锁就过期的情况。

36、当然,如果客户端在锁定所有实例后无法同时满足这两个条件,那么客户端将向所有Redis节点发起锁释放操作。为什么释放锁的时候要操作所有节点?不能只操作那些成功锁定的节点吗?当Redis节点被锁定时,锁定可能会由于 *** 原因而失败。例如,一个客户端成功锁定了一个Redis实例,但是在读取响应结果时,由于 *** 问题导致读取失败,所以实际上已经在Redis上成功锁定了锁。

37、因此,在释放锁时,无论之前的锁定是否成功,都需要释放所有节点上的锁,以确保节点上剩余的锁被清理干净。在Redlock算法中,释放锁的操作与在单个实例上释放锁的操作是一样的,只要执行释放锁的Lua脚本。这样,只要n个Redis实例中有一半以上能正常工作,分布式锁就能正常工作。

38、因此,在实际的业务应用中,如果想提高分布式锁的可靠性,可以通过Redlock算法来实现。

好了,redis实现分布式锁的几种方式(分布式锁的实现方式redis)的知识介绍就到这里,本文到此结束!

本文内容由互联网用户自发贡献,该文观点仅代表作者本人。如发现本站有涉嫌抄袭侵权/违法违规的内容,请发送邮件至 203304862@qq.com

本文链接:https://jinnalai.com/n/189321.html

联系我们

在线咨询:点击这里给我发消息

微信号:

工作日:9:30-18:30,节假日休息