千机游戏提供最新游戏下载和手游攻略!

了解“分布式锁”就看这篇文章

发布时间:2024-10-20浏览:80

因此,为了在分布式环境中达到本地锁的效果,人们想出了自己的策略。今天我们就来说说一般分布式锁实现的套路。

为什么需要分布式锁

Martin Kleppmann 是英国剑桥大学的分布式系统研究员。此前他曾与Redis之父Antirez就RedLock(红锁,稍后讨论)是否安全进行过激烈讨论。

Martin认为,一般我们在两种场景下使用分布式锁:

效率:使用分布式锁可以避免不同节点重复相同的工作,浪费资源。例如,用户付款后,可能会从不同的节点发送多条短信。正确性:添加分布式锁也可以避免破坏正确性。如果两个节点对同一条数据进行操作,比如多个节点机器对同一个订单操作不同的进程,可能会导致订单的最终状态出现。错误会导致损失。分布式锁的一些特点

当我们确定不同节点上需要分布式锁时,那么我们需要了解分布式锁应该具备哪些特性?

分布式锁的特点如下:

互斥性:和我们的本地锁一样,互斥性是最基本的,但是分布式锁需要保证不同节点上不同线程的互斥性。可重入性:如果同一个节点上的同一个线程获取了锁,它还可以再次获取锁。锁超时:与本地锁一样,支持锁超时,防止死锁。高效和高可用性:锁定和解锁需要高效,并且还需要确保高可用性,以防止分布式锁失败,从而增加性能下降。支持阻塞和非阻塞:与ReentrantLock一样,支持lock、trylock和tryLock(long timeOut)。支持公平锁和非公平锁(可选):公平锁是指按照请求锁的顺序获取锁,而非公平锁则是乱序获取。一般来说,这种做法很少实施。常见的分布式锁

当我们了解了一些特性之后,我们一般通过以下方式实现分布式锁:

MySQLZKRedis自研分布式锁:如Google的Chubby。下面分别介绍这些分布式锁的实现原理。

MySQL

首先我们来说一下MySQL分布式锁的实现原理。这个比较容易理解。毕竟数据库和我们开发者的日常开发息息相关。

对于分布式锁我们可以创建一个锁表:

我们前面提到的lock()、trylock(long timeout)和trylock()方法可以使用以下伪代码来实现。

锁()

锁一般都是阻塞获取锁,也就是说不获取到锁我们就不会放弃。然后我们可以编写一个无限循环来执行其操作:

mysqlLock.lcok里面是一条sql。为了达到可重入锁的效果,首先要进行查询。如果有值,我们需要比较node_info,看是否一致。

这里的node_info可以用机器IP和线程名来表示。如果一致,则会加上可重入锁计数的值。如果不一致,则返回false。如果没有值,则直接插入一条数据。

伪代码如下:

需要注意的是,这段代码需要添加事务,并且必须保证这一系列操作的原子性。

tryLock() 和tryLock(长超时)

tryLock()是非阻塞获取锁。如果无法获取,则立即返回。代码如下:

tryLock(long timeout) 实现如下:

mysqlLock.lock同上,但是需要注意的是select.for update是阻塞获取行锁。如果同一个资源的并发量很大,仍然可能会退化为阻塞获取锁。

开锁()

对于解锁,如果这里的计数为1,则可以删除。如果大于1,则需要减1。

锁定超时

我们可能会遇到我们的机器节点挂了,那么锁就不会被释放。我们可以启动一个计划任务并计算处理该任务所需的一般时间。

比如是5ms,那么我们可以把它扩大一点。当超过20ms没有释放锁时,我们可以判断节点挂了,直接释放。

MySQL总结:

适用场景:MySQL分布式锁一般适用于数据库中不存在的资源。如果数据库存在,比如订单,就可以直接给这个数据加行锁,不需要上面很多繁琐的步骤。例如,对于一个订单,我们可以使用select * from order_table where id='xxx' for update 来添加行锁,这样其他事务就无法对其进行修改。

优点:简单易懂,不需要额外的第三方中间件(如Redis、ZK)的维护。缺点:虽然容易理解,但实现起来比较麻烦。你需要自己考虑锁超时、添加事务等。性能受限于数据库,一般比缓存性能低。不太适合高并发场景。乐观锁定

之前我们介绍过的是悲观锁。这里我们要提一下乐观锁。我们实际项目中经常会实现乐观锁,因为行锁的性能消耗比较大。通常我们在一些比赛中不会那么激烈。

但它还需要确保我们的并发顺序执行是使用乐观锁来处理的。我们可以在表中添加版本号字段。

那么我们查询到一个版本号之后,在更新或者删除的时候就需要依靠我们查询到的版本号来判断当前数据库和查询到的版本号是否相等。如果相等,则可以执行。如果不相等,则无法执行。

这样的策略和我们的CAS(Compare And Swap)非常相似。比较和交换是原子操作。这样我们就可以避免为更新行锁添加select * 的开销。

动物园管理员

ZooKeeper也是我们实现分布式锁的常用方法。与数据库相比,如果你没有了解过ZooKeeper,上手可能会比较困难。

ZooKeeper是基于Paxos算法的分布式应用程序协调服务。 ZK的数据节点类似于文件目录,所以我们可以利用这个特性来实现分布式锁。

我们把一个资源当作一个目录,这个目录下的节点就是我们需要获取锁的客户端。未获取锁的客户端注册需要向之前的客户端注册一个Watcher,可以用下图表示:

/lock是我们用于锁定的目录,/resource_name是我们锁定的资源,其下面的节点按照我们锁定的顺序排列。

馆长

Curator封装了ZooKeeper的底层API,让我们操作ZooKeeper更加简单方便,而且它封装了分布式锁功能,让我们不需要自己去实现。

Curator 实现了可重入锁(InterProcessMutex)和不可重入锁(InterProcessSemaphoreMutex)。读写锁也是以可重入锁的方式实现的。

进程间互斥体

InterProcessMutex是Curator实现的可重入锁。我们可以通过下面的代码来实现我们的可重入锁:

我们用acuire来锁定,用release来解锁。

加锁过程如下:

首先,进行可重入判断:这里的可重入锁记录在ConcurrentMap中。如果threadData.get(currentThread)有值,则证明是可重入锁,然后记录会加1。

我们之前的MySQL其实可以通过这个方法来优化。它不需要计数字段的值。在本地维护它可以提高性能。

然后在我们的资源目录下创建一个节点:比如这里创建一个/0000000002节点。该节点需要设置为EPHEMERAL_SEQUENTIAL,这是一个临时节点并且是有序的。获取当前目录下的所有子节点,并判断自己的节点是否为第一个子节点。如果是第一个,则获得锁,可以返回。如果不是第一个,说明之前已经有人获得了锁,那么就需要获取自己节点的上一个节点。 /0000000002 的前一个节点是/0000000001。我们获取到这个节点后,在上面注册一个Watcher(这里的Watcher实际上是调用object.notifyAll()来解除阻塞的)。

object.wait(timeout) 或object.wait():阻塞等待,对应我们第5步中的Watcher。解锁的具体过程:

首先判断可重入锁:如果有可重入锁,则将锁数量减1即可。如果减1后锁数量为0,则继续执行以下步骤。如果不为0,则直接返回。删除当前节点。删除threadDataMap中的可重入锁数据。读写锁

Curator提供了一个读写锁,它的实现类是InterProcessReadWriteLock。这里的每个节点都会有前缀:

私有静态最终字符串READ_LOCK_NAME='__READ__';私有静态最终字符串WRITE_LOCK_NAME='__WRIT__';不同的前缀区分是读锁还是写锁。对于读锁,如果发现前面有写锁,那么Watcher需要注册并拥有最近的写锁。写锁的逻辑和我们之前4.2中分析的一样。

锁定超时

ZooKeeper不需要配置锁超时。由于我们将节点设置为临时节点,因此我们每台机器都维护一个ZK Session。通过这个Session,ZK可以判断机器是否宕机。

如果我们的机器挂了,相应的临时节点就会被删除,所以我们不需要关心锁超时。

ZK总结:

优点:ZK不需要关心锁超时。有现成的第三方包实现,比较方便,支持读写锁。 ZK按照加锁的顺序获取锁,因此是公平锁。高可用方面,采用ZK集群来保证高可用。缺点:ZK需要额外维护,增加维护成本。其性能与MySQL相差不大,但还是比较差。并且需要开发者了解什么是ZK。雷迪斯

当人们在网上搜索分布式锁的时候,恐怕最流行的实现就是Redis了。 Redis因其良好的性能和简单的实现而受到很多人的青睐。

Redis分布式锁的简单实现

熟悉Redis的同学一定熟悉setNx(set if not exit)方法。如果不存在,则会更新。可以很好的用来实现我们的分布式锁。

要锁定某个资源我们只需要:

setNx resourceName 值存在问题。加锁后,如果机器宕机了,锁也不会被释放,所以会加上一个过期时间。添加过期时间需要与setNx相同的原子操作。

在Redis 2.8之前,我们需要使用Lua脚本来实现我们的目标,但是在Redis 2.8之后,Redis支持nx和ex操作作为相同的原子操作。

设置资源名称值ex 5 nxRedission

Java 人都知道Jedis。 Jedis是Redis的Java实现的客户端。其API对Redis命令提供了比较全面的支持。

Redission也是Redis的客户端,功能比Jedis简单。 Jedis只是简单地使用阻塞I/O与Redis交互,而Redission通过Netty支持非阻塞I/O。

Jedis的最新版本2.9.0发布于2016年,已经近3年没有更新了,而Redission的最新版本是在2018年10月更新的。

Redission封装了锁的实现,继承了java.util.concurrent.locks.Lock接口,让我们可以像操作本地Lock一样操作Redission的Lock。

下面介绍一下分布式锁的实现方法:

Redission不仅提供了Java自带的一些方法(lock、tryLock),还提供了异步锁,更加方便异步编程。

由于内部源代码很多,我就不贴源代码了。这里用文字描述来分析一下它是如何锁定的。下面对tryLock方法进行分析:

尝试锁定:首先,您将尝试锁定。由于需要兼容老版本的Redis,所以不能直接使用ex、nx原子操作API。那么就只能使用Lua脚本了。相关Lua脚本如下:

可以看到它并没有使用我们的sexNx来操作,而是使用了hash结构。我们每一个需要加锁的资源都可以看做是一个HashMap。锁定资源的节点信息为Key,锁定数量为Value。

这样就可以很好的达到可重入的效果。只需要Value加1即可实现可重入锁。当然,我们之前提到的本地计数也可以用于这里的优化。

如果锁尝试失败,判断是否超时,超时则返回false。

如果锁失败后没有超时,则需要在名为redisson_lock__channel+lockName的通道上订阅解锁消息,然后阻塞直到超时,或者直到有解锁消息。

重试步骤1、2、3,直到最终获取到锁,或者获取锁的某个步骤超时。

我们的解锁方法比较简单,也是通过lua脚本解锁。如果是可重入锁,只是减1。如果是非加锁线程解锁,则解锁失败。

Redission 还实现了公平锁。对于公平锁来说,它使用链表结构和哈希集结构来保存我们排队的节点以及节点的过期时间。这两个数据结构帮助我们实现了公平锁,这里不再讨论。简介已展开。如果有兴趣可以参考源码。

红锁

让我们想象这样一个场景。 A机器申请锁后,如果Redis master宕机了,此时slave机器没有同步到这个锁,那么B机器再次申请时会再次申请这个锁。锁。

为了解决这个问题,Redis作者提出了RedLock算法,RedLock也在Redission中实现。

通过上面的代码,我们需要实现多个Redis集群,然后对红锁进行加锁和解锁。

具体步骤如下:

首先,为多个Redis集群创建Rlock,并将其构建为RedLock。

按顺序锁定三个簇。锁定过程与5.2相同。

如果循环加锁过程中加锁失败,需要判断加锁失败次数是否超过最大值。这里的最大值基于簇的数量。例如,如果有3个,则仅允许失败1次,如果有5个,则仅允许失败1次。允许两次失败,并且必须保证大部分成功。

加锁过程中,需要判断加锁是否超时。有可能我们把锁定时间设置为只有3ms,而第一次集群锁定已经消耗了3ms。那么就认为是锁失败。

如果步骤3、4加锁失败,则进行解锁操作。解锁将要求一次性解锁所有集群。

可见,RedLock的基本原理是利用多个Redis集群成功锁定大部分集群,从而降低某个Redis集群出现故障而导致分布式锁问题的概率。

Redis总结:

优点:Redis的实现简单,性能优于ZK和MySQL。如果不需要特别复杂的需求,可以使用setNx来实现。如果需要复杂的需求,可以使用或者学习Redission。 RedLock可以用于一些更严格的场景。缺点:Redis集群需要维护。如果要实现RedLock,就需要维护更多的集群。分布式锁的安全问题

上面我们介绍了红锁,但是Martin Kleppmann认为它仍然不安全。

关于Martin的反驳,我想不只限于RedLock。上面提到的算法基本上都存在这个问题。下面我们就来讨论一下这些问题。

GC 暂停时间较长

熟悉Java的同学一定对GC很熟悉。 STW(stop-the-world)将在GC 期间发生。

例如,CMS垃圾收集器将有两个阶段的STW,以防止引用继续更改。那么可能会出现如下图所示的情况(引自Martin驳斥Redlock的文章):

client1获取锁并设置锁超时,但是client1之后发生了STW。这个STW时间比较长,导致分布式锁被释放。

Client2 获取锁。此时client1恢复锁,那么client1和client2同时获取锁。这时候分布式锁不安全的问题就出现了。

这不仅限于RedLock,对于我们的ZK和MySQL也有同样的问题。

时钟跳动

对于Redis服务器来说,如果它的时间发生跳跃,肯定会影响我们锁的过期时间。

那么我们的锁过期时间就不是我们所期望的了。也会出现client1和client2获得相同的锁的情况,这也是不安全的。 MySQL 也会发生这种情况。但由于ZK没有设置过期时间,因此即使发生跳转也不会受到影响。

长网络I/O

这个问题和我们GC的STW很相似。就是我们获得锁之后,我们进行网络调用,调用的时间可能会比我们锁的过期时间还要长,那么也会出现不安全的问题。这个MySQL会有,ZK不会有这个问题。

对于这三个问题,网上已经发起了很多讨论,包括Redis作者。

气相色谱法

关于这个问题,可以看到基本上所有的事情都会有问题。马丁给出了解决方案。对于ZK来说,它会生成一个自增序列。那么我们在实际操作资源的时候,需要判断当前的序列是否是最新的。有点类似于乐观锁。

当然,Redis的作者反驳了这个解决方案。由于您可以生成自动递增序列,因此根本不需要锁定。换句话说,您可以遵循类似于MySQL 乐观锁的解决方案。

我个人认为这个解决方案增加了复杂性。我们在操作资源的时候,需要判断序列号是否是最新的。无论采用什么判断方法,复杂度都会增加。稍后我会介绍Google的Chubby的更好的解决方案。

时钟跳动

Martin觉得RedLock不安全的主要原因也是因为时钟跳变,因为锁过期时间强依赖于时间,而ZK不需要依赖时间,依赖于各个节点的Session。

Redis作者也给出了答案。时间跳跃分为手动调整和NTP自动调整:

人工调节:人工调节的影响完全可以控制。这是可控的。 NTP自动调整:通过一定的优化,可以将跳跃时间控制在可控范围内。虽然会跳,但是完全可以接受。长网络I/O

这不是他们讨论的重点。我个人认为这个问题的优化可以控制网络调用的超时时间,添加所有网络调用的超时时间。

那么我们的锁过期时间其实应该大于这个时间。当然,我们还可以优化网络调用,比如改串行为并行、异步等。

针对Chubby 的一些优化

当你搜索ZK时,你会发现他们都写ZK是Chubby的开源实现。 Chubby的内部工作原理与ZK类似。但Chubby作为分布式锁的定位和ZK有点不同。

Chubby也采用了上面的自增序列方案来解决分布式不安全的问题,但是它提供了多种验证方式:

CheckSequencer():调用Chubby的API检查此时的序列号是否有效。访问资源服务器查看并确定当前资源服务器的最新序列号以及我们序列号的大小。 lock-delay:为了防止我们的验证逻辑入侵我们的资源服务器,它提供了一种方法,当客户端失去联系时,不会立即释放锁,而是阻塞一定时间(默认1min)其他客户端获取此锁。然后给出一定的缓冲区等待STW恢复。如果我们GC的STW时间超过1分钟,那么你应该检查你的程序而不是怀疑你的分布式锁。

概括

不同的业务需要完全不同的安全级别。我们需要根据自己的业务场景,通过不同维度的分析,选择最适合自己的解决方案。

用户评论

水波映月

一直很困惑分布式系统的锁是怎么实现的,终于看到这篇博文了!讲得很通俗易懂,连我这种菜鸟都能理解。点赞!

    有9位网友表示赞同!

颓废人士

标题太吸引人了!我一直想弄明白分布式锁,搞不清那些复杂的概念。希望能通过这篇文章快速提升自己。

    有20位网友表示赞同!

聽風

看了这篇博客后,对分布式锁的原理有了更深入的了解。感觉写的人很懂技术,而且文字清晰易懂,推荐给有和我一样需求的朋友们!

    有18位网友表示赞同!

浮殇年华

说实话,这篇文章写得有点过于简单了,我以为会更详细地讲解不同的分布式锁实现方式,例如Redis、Zookeeper等。希望作者能补充一下。

    有10位网友表示赞同!

最怕挣扎

最后一段总结很棒,很清晰地总结了分布式锁的作用和优势。学习了一定的知识点,谢谢分享!

    有6位网友表示赞同!

单身i

在实际项目开发中,对于分布式系统的安全性是相当重要的考虑。这篇博文恰好切入到了这个关键点,对解决分布式锁问题很有指导意义,推荐给所有想学习分布式架构的开发者们。

    有18位网友表示赞同!

看我发功喷飞你

作为一名对高并发系统有追求的程序员,我对分布式系统很感兴趣。这篇文章刚好能让我快速了解到分布式锁的基础知识,下一步可以深入学习相关的实践案例了。

    有11位网友表示赞同!

?亡梦爱人

我觉得文章中的一些概念描述得不够具体,像“冲突”和“死锁”等等,缺少了一些实际示例和解释,这样容易让人糊弄不清。建议作者能丰富这些部分的內容。

    有7位网友表示赞同!

寻鱼水之欢

这篇博文很有帮助,我之前对分布式系统还是一知半解,看过这篇文章后,终于明白了分布式锁的作用以及一些基本原理,下一步可以去尝试学习一些相关的开源工具了。

    有17位网友表示赞同!

醉枫染墨

对于程序员来说,深入了解并掌握分布式技术是必不可少的。但这篇文章的深度不够,没有涉及到具体实现细节和优缺点比较,对我这种追求更深层次学习的人不太有帮助。

    有11位网友表示赞同!

封锁感觉

我以前一直觉得分布式锁很难理解,看了这篇文章后感觉豁然开朗!作者逻辑清晰、语言简洁,把复杂的知识点讲得通俗易懂。强烈推荐给所有对分布式系统感兴趣的朋友们观看!

    有16位网友表示赞同!

冷眼旁观i

我觉得文章的内容比较浅显,只停留在概念层面,没有提供具体的案例或代码示例来辅助理解。对于想深入学习分布式锁技术的人来说,可能需要寻找更专业、更实用的资料。

    有20位网友表示赞同!

仅有的余温

作为一名正在学习分布式系统的同学,我感到这篇文章很有用。它让我对分布式锁有了基本的认识,下一步我可以关注一些相关的开源项目和实践案例了。感谢作者的分享!

    有18位网友表示赞同!

罪歌

对于初学者来说,这篇博客能起到一个启蒙的作用,但要真正理解分布式锁机制,还需要进行更深入的研究和学习。希望作者以后可以继续撰写关于分布式系统的其他优秀文章!

    有17位网友表示赞同!

封心锁爱

一直想找一篇关于分布式锁的入门文章,结果看到这篇文章就对了!语言通俗易懂,重点清晰,很容易理解。谢谢作者的贡献!

    有18位网友表示赞同!

最迷人的危险

我觉得这篇文章对分布式系统新手来说很有用处,它能够快速帮助他们了解到分布式锁的概念和作用。但是对于更资深的开发者来说,也许不太有价值。

    有15位网友表示赞同!

断秋风

这篇文章虽然写得不错,但缺乏一些实际案例的分析和总结。如果能够增加一些具体的应用场景和解决方案,会更加实用和 informative.

    有20位网友表示赞同!

呆檬

总感觉这篇文章还差点儿能真正打动我,希望能深入探讨分布式锁的常见问题以及如何进行性能调优等等更实用的内容。

    有13位网友表示赞同!

热点资讯