站长工具网站,外贸网站建设注意事项和建议,保定建设工程信息网,做海鲜代理在什么网站分布式锁在很多场景中是非常有用的原语#xff0c; 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。有很多分布式锁的库和描述怎么实现分布式锁管理器(DLM)的博客,但是每个库的实现方式都不太一样#xff0c;很多库的实现方式为了简单降低了可靠性#xff…分布式锁在很多场景中是非常有用的原语 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。有很多分布式锁的库和描述怎么实现分布式锁管理器(DLM)的博客,但是每个库的实现方式都不太一样很多库的实现方式为了简单降低了可靠性而有的使用了稍微复杂的设计。这个页面试图提供一个使用Redis实现分布式锁的规范算法。我们提出一种算法叫Redlock,我们认为这种实现比普通的单实例实现更安全,我们希望redis社区能帮助分析一下这种实现方法并给我们提供反馈。安全和活性失效保障按照我们的思路和设计方案算法只需具备3个特性就可以实现一个最低保障的分布式锁。安全属性(Safety property): 独享(相互排斥)。在任意一个时刻只有一个客户端持有锁。活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned)锁仍然可以被获取。活性B(Liveness property B): 容错。 只要大部分Redis节点都活着客户端就可以获取和释放锁.为什么基于故障转移的实现还不够为了更好的理解我们想要改进的方面我们先分析一下当前大多数基于Redis的分布式锁现状和实现方法.实现Redis分布式锁的最简单的方法就是在Redis中创建一个key这个key有一个失效时间(TTL)以保证锁最终会被自动释放掉(这个对应特性2)。当客户端释放资源(解锁)的时候会删除掉这个key。从表面上看似乎效果还不错但是这里有一个问题这个架构中存在一个严重的单点失败问题。如果Redis挂了怎么办你可能会说可以通过增加一个slave节点解决这个问题。但这通常是行不通的。这样做我们不能实现资源的独享,因为Redis的主从同步通常是异步的。在这种场景(主从结构)中存在明显的竞态:客户端A从master获取到锁在master将锁同步到slave之前master宕掉了。slave节点被晋级为master节点客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效有时候程序就是这么巧比如说正好一个节点挂掉的时候多个客户端同时取到了锁。如果你可以接受这种小概率错误那用这个基于复制的方案就完全没有问题。否则的话我们建议你实现下面描述的解决方案。单Redis实例实现分布式锁的正确方法在尝试克服上述单实例设置的限制之前让我们先讨论一下在这种简单情况下实现分布式锁的正确做法实际上这是一种可行的方案尽管存在竞态结果仍然是可接受的另外这里讨论的单实例加锁方法也是分布式加锁算法的基础。获取锁使用命令: SET resource_name my_random_value NX PX 30000这个命令仅在不存在key的时候才能被执行成功(NX选项)并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值)这个值在所有的客户端必须是唯一的所有同一key的获取者(竞争者)这个值都不能一样。value的值必须是随机数主要是为了更安全的释放锁释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1])else return 0end使用这种方式释放锁可以避免删除别的客户端获取成功的锁。举个例子客户端A取得资源锁但是紧接着被一个其他操作阻塞了当客户端A运行完毕其他操作后要释放锁时原来的锁早已超时并且被Redis自动释放并且在这期间资源锁又被客户端B再次获取到。如果仅使用DEL命令将key删除那么这种情况就会把客户端B的锁给删除掉。使用Lua脚本就不会存在这种情况因为脚本仅会删除value等于客户端A的value的key(value相当于客户端的一个签名)。这个随机字符串应该怎么设置我认为它应该是从/dev/urandom产生的一个20字节随机数但是我想你可以找到比这种方法代价更小的方法只要这个数在你的任务中是唯一的就行。例如一种安全可行的方法是使用/dev/urandom作为RC4的种子和源产生一个伪随机流;一种更简单的方法是把以毫秒为单位的unix时间和客户端ID拼接起来理论上不是完全安全但是在多数情况下可以满足需求.key的失效时间被称作“锁定有效期”。它不仅是key自动失效时间而且还是一个客户端持有锁多长时间后可以被另外一个客户端重新获得。截至到目前我们已经有较好的方法获取锁和释放锁。基于Redis单实例假设这个单实例总是可用这种方法已经足够安全。现在让我们扩展一下假设Redis没有总是可用的保障。Redlock算法在Redis的分布式环境中我们假设有N个Redis master。这些节点完全互相独立不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中我们假设有5个Redis master节点这是一个比较合理的设置所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例这样保证他们不会同时都宕掉。为了取到锁客户端应该执行以下操作:获取当前Unix时间以毫秒为单位。依次尝试从N个实例使用相同的key和随机值获取锁。在步骤2当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应客户端应该尽快尝试另外一个Redis实例。客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁并且使用的时间小于锁失效时间时锁才算获取成功。如果取到了锁key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。如果因为某些原因获取锁失败(没有在至少N/21个Redis实例取到锁或者取锁时间已经超过了有效时间)客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。这个算法是异步的么?算法基于这样一个假设虽然多个进程之间没有时钟同步但每个进程都以相同的时钟频率前进时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近每个计算机都有一个本地时钟我们可以容忍多个计算机之间有较小的时钟漂移。从这点来说我们必须再次强调我们的互相排斥规则只有在锁的有效时间(在步骤3计算的结果)范围内客户端能够做完它的工作锁的安全性才能得到保证(锁的实际有效时间通常要比设置的短因为计算机之间有时钟漂移的现象)。.失败时重试当客户端无法取到锁时应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂没有人会取到锁)。同样客户端取得大部分Redis实例锁所花费的时间越短脑裂出现的概率就会越低(必要的重试)所以理想情况一下客户端应该同时(并发地)向所有Redis发送SET命令。需要强调当客户端从大多数Redis实例获取锁失败时应该尽快地释放(部分)已经成功取到的锁这样其他的客户端就不必非得等到锁过完“有效时间”才能取到(然而如果已经存在网络分裂客户端已经无法和Redis实例通信此时就只能等待key的自动释放了等于被惩罚了)。释放锁释放锁比较简单向所有的Redis实例发送释放锁命令即可不用关心之前有没有从Redis实例成功获取到锁.安全争议这个算法安全么我们可以从不同的场景讨论一下。让我们假设客户端从大多数Redis实例取到了锁。所有的实例都包含同样的key并且key的有效时间也一样。然而key肯定是在不同的时间被设置上的所以key的失效时间也不是精确的相同。我们假设第一个设置的key时间是T1(开始向第一个server发送命令前时间)最后一个设置的key时间是T2(得到最后一台server的答复后的时间)我们可以确认第一个server的key至少会存活 MIN_VALIDITYTTL-(T2-T1)-CLOCK_DRIFT。所有其他的key的存活时间都会比这个key时间晚所以可以肯定所有key的失效时间至少是MIN_VALIDITY。当大部分实例的key被设置后其他的客户端将不能再取到锁因为至少N/21个实例已经存在key。所以如果一个锁被(客户端)获取后客户端自己也不能再次申请到锁(违反互相排斥属性)。然而我们也想确保当多个客户端同时抢夺一个锁时不能两个都成功。如果客户端在获取到大多数redis实例锁使用的时间接近或者已经大于失效时间客户端将认为锁是失效的锁并且将释放掉已经获取到的锁所以我们只需要在有效时间范围内获取到大部分锁这种情况。在上面已经讨论过有争议的地方在MIN_VALIDITY时间内将没有客户端再次取得锁。所以只有一种情况多个客户端会在相同时间取得N/21实例的锁那就是取得锁的时间大于失效时间(TTL time)这样取到的锁也是无效的.如果你能提供关于现有的类似算法的一个正式证明(指出正确性)或者是发现这个算法的bug 我们将非常感激.活性争议系统的活性安全基于三个主要特性:锁的自动释放(因为key失效了)最终锁可以再次被使用.客户端通常会将没有获取到的锁删除或者锁被取到后使用完后客户端会主动(提前)释放锁而不是等到锁失效另外的客户端才能取到锁。.当客户端重试获取锁时需要等待一段时间这个时间必须大于从大多数Redis实例成功获取锁使用的时间以最大限度地避免脑裂。.然而当网络出现问题时系统在失效时间(TTL)内就无法服务这种情况下我们的程序就会为此付出代价。如果网络持续的有问题可能就会出现死循环了。 这种情况发生在当客户端刚取到一个锁还没有来得及释放锁就被网络隔离.如果网络一直没有恢复这个算法会导致系统不可用.性能崩溃恢复和Redis同步很多用户把Redis当做分布式锁服务器使用获取锁和释放锁的响应时间每秒钟可用执行多少次 acquire / release 操作作为性能指标。为了达到这一要求增加Redis实例当然可用降低响应延迟(没有钱买硬件的”穷人”,也可以在网络方面做优化使用非阻塞模型一次发送所有的命令然后异步的读取响应结果假设客户端和redis服务器之间的RTT都差不多。然而如果我们想使用可以从备份中恢复的redis模式有另外一种持久化情况你需要考虑.我们考虑这样一种场景假设我们的redis没用使用备份。一个客户端获取到了3个实例的锁。此时其中一个已经被客户端取到锁的redis实例被重启在这个时间点就可能出现3个节点没有设置锁此时如果有另外一个客户端来设置锁锁就可能被再次获取到这样锁的互相排斥的特性就被破坏掉了。如果我们启用了AOF持久化情况会好很多。我们可用使用SHUTDOWN命令关闭然后再次重启。因为Redis到期是语义上实现的所以当服务器关闭时实际上还是经过了时间所有(保持锁)需要的条件都没有受到影响. 没有受到影响的前提是redis优雅的关闭。停电了怎么办如果redis是每秒执行一次fsync那么很有可能在redis重启之后key已经丢弃。理论上如果我们想在Redis重启地任何情况下都保证锁的安全我们必须开启fsyncalways的配置。这反过来将完全破坏与传统上用于以安全的方式实现分布式锁的同一级别的CP系统的性能.然而情况总比一开始想象的好一些。当一个redis节点重启后只要它不参与到任意当前活动的锁没有被当做“当前存活”节点被客户端重新获取到,算法的安全性仍然是有保障的。为了达到这种效果我们只需要将新的redis实例在一个TTL时间内对客户端不可用即可在这个时间内所有客户端锁将被失效或者自动释放.使用延迟重启可以在不采用持久化策略的情况下达到同样的安全然而这样做有时会让系统转化为彻底不可用。比如大部分的redis实例都崩溃了系统在TTL时间内任何锁都将无法加锁成功。使算法更加可靠锁的扩展如果你的工作可以拆分为许多小步骤可以将有效时间设置的小一些使用锁的一些扩展机制。在工作进行的过程中当发现锁剩下的有效时间很短时可以再次向redis的所有实例发送一个Lua脚本让key的有效时间延长一点(前提还是key存在并且value是之前设置的value)。客户端扩展TTL时必须像首次取得锁一样在大多数实例上扩展成功才算再次取到锁并且是在有效时间内再次取到锁(算法和获取锁是非常相似的)。这样做从技术上将并不会改变算法的正确性所以扩展锁的过程中仍然需要达到获取到N/21个实例这个要求否则活性特性之一就会失效。