Java程序员面试不懂分布式锁?那得多吃亏啊

时间:2019-07-26 来源:www.ppdig.com

为何使用分布式锁?

在讨论这个问题之前,让我们看一下业务场景:

系统A是电子商务系统。目前,它是一个机器部署。用户可以使用界面下订单。但是,用户必须在下订单之前检查库存,以确保在用户订购之前库存充足。

由于系统具有一定程度的并发性,因此货物的库存将预先存储在redis中,并且当用户下订单时将更新redis的库存。

系统架构如下:

如果在某些时候,redis中某个项目的库存为1,则两个请求同时到达。其中一个请求执行到上图中的第三步,更新数据库的库存为0,但第四步尚未执行。

另一个请求在步骤2中执行,如果库存仍为1,则继续步骤3。

结果是已售出两件商品,但实际上只有一件库存。

显然错了!这是典型的库存超卖问题

此时,我们可以轻松地想到一个解决方案:使用锁定锁定2,3,4步骤,让它们执行,之后另一个线程可以进入执行步骤2.

根据上图,执行步骤2时,使用Java提供的synchronized或ReentrantLock进行锁定,然后在执行步骤4后释放锁。

通过这种方式,2,3和4的3个步骤被“锁定”,并且多个线程只能被序列化。

但好景不长,整个系统都在飙升,一台机器无法阻挡。现在添加一台机器,如下所示:

添加机器后,系统如上图所示,天啊!

假设两个用户的请求同时到达,但是它们落在不同的机器上,那么这两个请求可以同时执行,或者超卖库存会出现问题。

为什么?由于上图中的两个A系统在两个不同的JVM中运行,因此它们添加的锁仅对属于其自己的JVM的线程有效,对其他JVM线程无效。

因此,这里的问题是Java提供的本机锁机制在多机部署方案中失败

这是因为两台机器添加的锁不是同一个锁(两个锁在不同的JVM中)。

然后,只要我们保证两台机器添加的锁是同一个锁,问题就解决了吗?

此时,分布式锁将隆重推出。分布式锁的想法是:

提供一个全局且独特的“东西”来获取整个系统中的锁,然后每个系统要求“东西”在需要锁定时获得锁定,以便可以将不同的系统视为同一个锁。

至于这个“东西”,它可以是Redis,Zookeeper或数据库。

文字说明不是很直观,让我们看看下面的图片:

通过以上分析,我们知道在使用Java本机锁机制的分布式部署系统的情况下,库存超卖情况不能保证线程安全,因此我们需要使用分布式锁定方案。

那么如何实现分布式锁?然后往下看!

基于Redis的分布式锁

以上分析是使用分布式锁。在这里,我们将了解如何处理分布式锁。

最常见的是使用Redis作为分布式锁

使用Redis执行分布式锁定的想法可能是:在redis中设置一个值以指示已添加锁定,然后在释放锁定时删除该键。

具体代码如下:

以这种方式有几个要点:

务必使用SET键值NX PX毫秒命令

如果不是,请先设置该值,然后设置到期时间。这不是原子操作。在设置到期时间之前可能会关闭,这将导致死锁(密钥是永久性的)

价值必须是唯一的

这是在需要验证值与锁相同时删除密钥。

这避免了这种情况:假设A获得锁定并且过期30秒。此时,35秒后,锁定已自动释放。 A去释放锁定,但此时B可能获得锁定。客户端无法删除B的锁。

除了考虑客户端应如何实现分布式锁之外,还需要考虑redis的部署。

Redis有3种部署方法:

独立模式

主奴+哨兵选举模式

Redis集群模式

使用redis进行分布式锁定的缺点是,如果使用独立部署模式,只要redis失败就会出现单点问题。锁定不起作用。

在主从模式下,锁定时只锁定一个节点,即使它通过标记高度可用,但如果主节点发生故障,则会发生主从切换,并且存在锁定丢失的可能性。

基于以上考虑,实际上redis的作者也考虑过这个问题,他提出了一个RedLock算法,这个算法的含义大概是这样的:

假设redis部署模式是redis集群,则总共有五个主节点。按照以下步骤获取锁定:

获取当前时间戳(以毫秒为单位)

尝试依次在每个主节点上创建锁,将到期时间设置为短时间,通常为几十毫秒

尝试在大多数节点上创建锁定,例如需要3个节点(n/2 +1)的5个节点

客户端计算建立锁定的时间。如果锁定时间小于超时时间,则建立成功。

如果锁创建失败,则依次删除锁

只要其他人创建了分布式锁,您就必须不断轮询以尝试获取锁

然而,这样的算法仍然存在很大争议,并且可能存在许多问题。锁定过程不能保证是正确的。

另一种方式:Redisson

另外,Redis分布式锁的实现,除了自己基于redis客户端的原生api外,还可以使用开源框架:Redission

Redisson是一个企业级开源Redis客户端,它还提供对分布式锁的支持。我也强烈推荐大家使用,为什么?

回想一下,如果你编写自己的代码通过redis设置一个值,它由以下命令设置。

SET anyLock unique_value NX PX 30000

此处设置的超时时间为30秒。如果我没有完成业务逻辑超过30秒,密钥将过期,其他线程可能获得锁定。

在这种情况下,第一个线程尚未执行业务逻辑,进入第二个线程的线程也会出现线程安全问题。所以我们仍然需要额外保持这个过期时间,太麻烦了

让我们看看如何实现redisson。首先要感受使用重新分配的冷静:

就这么简单,我们只需要通过锁定完成分布式锁定并在其api中解锁,他帮助我们考虑了很多细节:

Redisson所有指令都由lua脚本执行,redis支持lua脚本原子执行

Redisson将密钥的默认到期时间设置为30秒。如果客户持有锁超过30秒怎么办?

Redisson有一个看门狗概念,转化为看门狗。获得锁定后,它会将键超时设置为每10秒30秒。

在这种情况下,即使始终保持锁定,密钥也不会过期,其他线程将获得锁定。

Redisson的“看门狗”逻辑保证不会发生死锁。

(如果机器关闭,看门狗就会消失。此时,密钥到期时间不会延长.30秒后,它会自动失效,其他线程可以获得锁定)

这是它的实现代码的一点粘贴:

此外,redisson还支持redlock算法,

它的用法也很简单:

总结:

本节使用redis作为分布式锁来分析特定的登陆计划

还有一些局限性

然后介绍了redis客户端框架redisson,

这是我建议大家使用的,

与编写自己的代码相比,它不需要太多细节。

基于zookeeper的分布式锁

在常见的分布式锁实现中,除了使用redis实现外,还可以使用zookeeper实现分布式锁。

在介绍zookeeper(以下改用zk)实现分布式锁机制之前,让我简要介绍一下zk是什么:

Zookeeper是一种集中式服务,提供配置管理,分布式协作和命名。

zk模型如下所示:zk包含一系列称为znodes的节点,就像文件系统一样。每个znode代表一个目录,然后znode具有一些功能:

有序节点:如果当前有父节点/锁,我们可以在此父节点下创建子节点;

Zookeeper提供可选的有序功能。例如,我们可以创建子节点“/lock/node-”并指定顺序,然后zookeeper将在生成子节点时根据当前子节点数自动添加整数。 >

也就是说,如果它是创建的第一个子节点,则生成的子节点是/lock/node-0000000000,下一个节点是/lock/node-0000000001,依此类推。

临时节点:客户端可以建立临时节点。会话结束或会话超时后,zookeeper将自动删除该节点。

事件监控:在读取数据时,我们可以同时为节点设置事件监控。当节点数据或结构发生变化时,zookeeper将通知客户端。目前的zookeeper有以下四个事件:

节点创建

节点删除

节点数据修改

子节点更改

基于上面的一些zk功能,我们可以轻松地提出一个使用zk实现分布式锁的计划:

使用zk的临时和有序节点,每个线程通过在zk中创建临时有序节点来获取锁,例如在/lock /目录中。

成功创建节点后,获取/lock目录中的所有临时节点,然后确定当前线程创建的节点是否是所有节点序列号最小的节点。

如果当前线程创建的节点是所有节点序列号最小的节点,则认为锁定成功。

如果当前线程创建的节点不是节点编号最小的节点,请将事件侦听器添加到节点编号的上一个节点。

例如,如果当前线程获取的节点号是/lock/003,然后所有节点列表都是[/lock/001,/lock/002,/lock/003],则向/lock添加一个事件监听器/002节点。

如果释放锁定,则将唤醒下一个序列号节点,然后将重新执行步骤3以确定其自己的节点号是否最小。

例如,/lock/001被释放,而/lock/002正在监听时间。此时,节点集为[/lock/002,/lock/003],则/lock/002是最小的序列节点,并获得锁定。

整个过程如下:

具体的实现思路是这样的。至于如何编写代码,这里发布它并不复杂。

策展人简介

Curator是zookeeper的开源客户端,还提供了分布式锁的实现。

他的使用也相对简单:

实现分布式锁的核心源代码如下:

实际上,策展人实施分布式锁的基本原理与上述分析类似。在这里,我们使用图表来详细描述原理:

总结:

本节介绍zookeeperr的分布式锁的实现以及zk的开源客户端的基本用法,并简要介绍其实现原理。

比较两种方案的优缺点

在学习了两种分布式锁的实现之后,本节需要讨论redis和zk实现的优缺点。

对于redis分布式锁,它具有以下缺点:

它获得锁的方式简单而粗鲁。如果无法获得锁定,可以尝试直接获取锁定并比较消耗性能。

此外,redis的设计位置确定其数据不一致,并且在某些极端情况下,可能会出现问题。锁的模型不够强大

即使你使用redlock算法来实现,在某些复杂的场景中,也无法保证它会100%没问题。有关redlock的讨论,请参阅如何进行分布式锁定

Redis分布式锁,实际上,您需要不断尝试获取锁,这会消耗性能。

但另一方面,在许多企业中使用Redis实现分布式锁是很常见的,在大多数情况下,它不会遇到所谓的“极其复杂的场景”

因此,使用redis作为分布式锁也是一个很好的解决方案。最重要的一点是Redis具有高性能并且可以支持高并发获取和释放锁定操作。

对于zk分布式锁:

Zookeeper的自然设计定位是分布式协调和强大的一致性。锁模型坚固,易于使用,适用于分布式锁。

如果你无法获得锁定,只需添加一个监听器,你就不必一直轮询,而且性能很小。

但是,zk也有它的缺点:如果有更多的客户经常申请锁定和释放锁,那么zk集群上的压力会相对较大。

总结:

总之,redis和zookeeper都有其优点和缺点。在进行技术选择时,我们可以将这些问题作为参考因素。

来自作者的一些建议

通过前面的分析,分布式锁的两个常见解决方案:redis和zookeeper,每个都有自己的优点。我该如何选择?

就个人而言,我更喜欢zk实现的锁:

由于redis具有潜在危险,因此可能会导致数据不正确。但是,如何选择查看公司的具体情况。

设置zk集群。

然后可以实现其实用的redis。另外,可能是系统设计者认为系统已经有redis,但又不想再引入一些外部依赖,你可以使用redis。

这是基于架构考虑的系统设计人员

最后,分享一本采访书[Java核心知识点整理]涵盖JVM,锁定,高并发,反射,Spring原理,微服务,Zookeeper,数据库,分布式等“,以及Java208面试问题(带答案) !

最后,分享一本采访书[Java核心知识点整理]涵盖JVM,锁定,高并发,反射,Spring原理,微服务,Zookeeper,数据库,数据结构等“,以及Java208面试问题(带答案) )!

加入我的粉丝群(Java Fill Road:659655594)免费!掌握了这些知识点,你可以在面试中获得很多候选人,暴击9999分。机会是为那些准备好的人保留的,只有充分准备才能从候选人中脱颖而出。