基于redis的分布式锁(redlock)
在分布式系统逐渐变为主流的情况下,开发者会遇到很多在单机系统中不会遇到的问题,而如何实现一个健壮的分布式锁就是很多开发者要面临的一个问题。
分布式锁的实现方式有很多种,本文将讲述一种基于redis实现的分布式锁(至于网上博客上说的那种仅仅利用SETNX命令去实现锁的,这种在单机下还行,如果需要在分布式环境下使用,呵呵~)。
PS:本锁的实现是官方提供的Redlock算法(其实就是翻译了一下,相较于谷歌翻译就是更易读,虽然这看起来没什么,但是很多时候这将最终影响你是否能够理解该算法)
首先,我们列举出满足我们需求的分布式锁的基本要素:
1、安全特性:相互排斥。也就是说在任何时刻,只有一个客
户端可以锁定而不会发生锁被两个客户端同时锁定。
2、活性A:无死锁。也就是说,无论什么情况,例如锁定资源 的客户端崩溃或者网络被分裂也总是可以获取锁的。
3、活性B:容错,只要大多数redis节点启动,客户端就可以获 取并释放锁。
然后我们看为什么基于故障切换(master-slave模型,master故障时slave升级为master)的实现是不够的:
使用redis锁定资源最简单的方法是在redis实例中创建一个密 钥,关键是通常使用redis过期功能实现在客户端崩溃后锁的自 动释放。
看起来该实现很好,但是存在这样一个问题:该实现存在单点故 障。考虑这样一个场景,如果redis-master挂掉会怎么样呢?为 了防止redis-master挂掉,我们添加一个slave,如果master挂 掉就切换到slave节点,但很不幸的是,这样是不可行的,通过 这样做无法实现上边的互斥安全属性,因为redis复制是异步 的。
上述模型有一个明显的竞争条件:
1、客户端A获取master设备中的锁定;
2、在将密钥写入slave节点前master崩溃;
3、slave升级为master;
4、客户端B使用同样的密钥获取客户端A本已获取到的资源 锁;
这样就发生了互斥安全问题!
那么如何解决上述问题呢?我们可以这样做:
在尝试克服单实例的节点故障问题前,让我们在这种简单的情况 下检查如何正确的执行它,因为这实际上是一个可执行的方案, 在 应用程序中不时会出现争用情况,并且实现单实例redis的 锁定是我 们描述分布式算法的基础;
要获得锁定,要走的路线如下:
SET resource_name my_random_value NX PX 30000
该命令只在密钥不存在时才会成功设置(NX选项),并且过期 时间为30000毫秒(PX选项)。该键被设置为一个随机值,该随 机值在所有的客户端和所有对该资源的锁定请求中必须唯一(否 则会出现互斥问题,下面会介绍)。
基于随机值是为了以安全的方式释放锁,使用随机值,通过脚本 告诉redis:只有当密钥存在并且存储在密钥中的值恰好与我期 望的值(随机值)一样时,才能移除该密钥。该过程是很重要 的,主要是为了防止一个客户端移除其他客户端创建的锁。例 如:客户端A当前获取到了锁,但是客户端A并不是因为崩溃而 超时,只是被阻塞超时,例如进行数据库操作时被阻塞,所以等 到客户单A阻塞结束后,他会认为他还持有锁,然后会通过DEL 命令尝试释放锁,但是实际上该锁此时是客户端B所有的,而客 户端A并不知道,这样就会造成客户端A将客户端B的锁删除而造 成客户端B锁失效,并且后续客户端B可能还会重复该操作,此 时锁实际上已经失去了作用,而如果加入随机数后,客户端A删 除的时候会校验该随机数,如果发现该随机数与客户端A设置的 不一致时则表示该锁已经不是A所有的,则A不会执行DEL操作, B仍然会持有锁继而保持互斥安全,而这也是为什么要保证随机 数唯一的原因。
现在我们有了一个很好的方法来获得和释放锁,该系统对于由一 个总是可用的redis实例组成的非分布式系统(redis是非分布式 的)是安全的,现在我们尝试将该概念扩展到一个分布式系统。
在上述算法的分布式版本中,我们假设我们有N个redis-master 节点,这些节点完全独立,所以我们不使用复制或任何其他隐式 协调系统。我们已经描述了如何在单个实例中安全的获取和释放 锁,我们理所当然的认为该算法将使用此方法在单个示例中获取 并释放锁。在我们的例子中,我们设置N=5,这是一个合理的 值,所以我们需要在不同的计算机或虚拟机上运行5个redis-ma ster节点,以确保他们以最独立的方式失败(单个节点挂掉不会 影响其他节点)。
为了获得锁,客户端执行以下操作:
1、它以毫秒为单位获取当前时间;
2、它试图依次获取所有N个实例中的锁,在所有实例中使用 相同的密钥名和随机值。在步骤2中,当在每个实例中设置锁 时,客户端使用与锁自动释放时间相比较小的超时以获取它, 例如:如果自动释放时间是10秒,则超时可能在5至50毫秒范 围内。这样当某个redis挂掉后可以防止客户端长时间阻塞在 尝试与该redis实例创建连接的状态,如果redis实例不可用, 那么我们应该尽快尝试与下一个redis实例通讯。
3、客户机通过从当前时间中扣除步骤1中获得的时间戳来计 算获取锁定所花费的时间。当且仅当客户机能获得大多数redi s实例的锁定(至少N/2+1个redis实例的锁定),并且获取该 锁的总时间小于锁有效时间,则认为该锁被获取;
4、如果获得锁,则其有效时间被认为是初始有效时间减去经 过时间,该经过时间如步骤3中计算的那样。(因为如果超过 该时间那么获取的第一个redis实例的锁将会超时自动释 放),该锁将有可能不满足[至少N/2+1个redis实例的锁定] 条件;
5、如果客户端由于某种原因未能获得锁(或者无法锁定N/2 +1个实例或者获得的锁有效时间为负),他将尝试解锁所有 实例(即使他认为他没有获取到锁,方便后续其他客户端重新 获取锁);
算法是异步的吗?
上述算法依赖于这样一个假设:即虽然跨进程没有同步时钟,但 每个进程中的本地时间仍以大约相同的速率步进,与锁的自动释 放时间相比误差较小。这个假设与真实世界的计算机非常相似, 每台计算机都有一个本地时钟,我们通常可以依靠不同的计算机 来产生很小的时钟漂移。
在这一点上,我们需要更好地指定我们的互斥规则:即锁的有效 期只有持有锁的客户端在有效时间(有效时间为步骤3中获得 的)减去某个时间(仅几毫秒以补偿不同主机之间的时钟漂 移)。
参考文献
[2]H. Berenson, P. Bernstein, J. Gray, J.Melton, E. O’Neil,and P. O’Neil. A critique of ANSI SQL isolation levels. InProceedings of the SIGMOD International Conference on Management of Data, pages1–10, May 1995.
[3]Michael J. Cahill, Uwe Röhm, and Alan D.Fekete. 2008. Serializable isolation for snapshot databases. In SIGMOD ’08:Proceedings of the 2008 ACM SIGMOD international conference on Management of data, pages 729–738, New York, NY, USA. ACM.
[4]Michael James Cahill. 2009. Serializable Isolation for Snapshot Databases. Sydney Digital Theses. University of Sydney, School of Information Technologies
[5] A. Fekete, D. Liarokapis, E. O’Neil, P.O’Neil, andD. Shasha. Making snapshot isolation serializable. www.codexueyuan.com In ACM transactions on database systems, volume 39(2), pages 492–528, June 2005.
版权声明:本文内容