分布式下必备神器之分布式锁
今天这篇文章我们来聊聊在分布式环境下的一个神兵利器——分布式锁!在看这篇文章的时候,默认大家对锁已经了解了,如果不了解的朋友可以去翻翻公号前面的文章,有很多篇详细介绍了锁的一些知识。
写这篇文章的主要原因是之前星球中有朋友说面试中被问的频率有点高,虽然知道分布式锁是什么,但是还是不能很好的说出来,这篇文章就是帮助大家好好梳理一下分布式锁的原理,希望对大家有帮助。
另外欢迎到 Java 极客技术知识星球中,我们一起煮酒论技术~
什么是分布式锁
首先我们先来简单了解一下什么是分布式锁(关于什么是锁,可以翻翻之前公号的文章或者到我们的网站 http://www.justdojava.com/ 上看看之前的文章)。
在引入分布式锁之前大家应该都知道经典的 CAP 理论提到任何一个系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)三者的,同一时刻只能满足两个,在这种情况下分布式锁就出现了,分布式锁就是用来解决数据一致性问题的。
在以前单体应用的环境下,Java 的 API 提供了很多控制并发的接口,包括 synchronized
以及 JUC 下面的一些实现,但是在分布式环境下这些 API 就没有用武之地了,因为应用是多实例部署的,很多实例甚至都不在同一台主机上,根本无法使用 Java 中的 API,这个时候分布式锁就诞生了。
所以简单说什么是分布式锁,分布式锁就是在分布式环境下用来解决多实例对数据访问一致性的一种技术方案。
使用场景
在实际环境中我们有很多场景会用到分布式锁,例如全局计数器,只要涉及到多个实例进程对同一份数据进行修改等操作都会需要分布式锁。在比如在下单,更新缓存,减少库存等场景下也会用到分布式锁的。
分布式锁的特性
在看分布式锁的实现之前,我们先了解下一个分布式锁应该具备哪些特性:
- 在分布式环境下同一时刻只能被单个线程获取;
- 可重入,意思是已经获得锁的线程在执行的过程中不需要再次获得锁;
- 异常或者超时自动删除,避免死锁;
- 高性能,分布式环境下必须要性能好;
实现方式
分布式锁的实现方式流行的主要有三种,分别是基于缓存 Redis 的实现方式,基于 ZK 临时顺序节点的实现以及基于数据库行锁的实现。这里简单提供下实现思路,不重复造轮子因为网上已经有很多开源的很好的解决方案了。
基于 Redis 缓存的实现
首先我们来看下基于 Redis 缓存实现的分布式锁,Redis 支持SETNX
命令,表示设置一个key
的值当且进度Key
不存在的时候才能设置成功。例如执行如下命令:set ziyou 18 NX PX 10000
表示将名叫ziyou
的 key 的值设置为 18,当且仅当不存在名为ziyou
的 key 的时候才能设置成功,并且过期时间设置为 10 秒钟。
setnx
命令是 Redis 实现分布式锁的核心,这个命令操作是原子操作的,千万不能分两步先用set
再用expire
,这样分开操作不是原子性的,无法实现效果。
然后 Redis 分布式锁在网上有开源实现 Redission,具体的实现可以参考。
百度也有一个开源的分布式 Redis 锁叫 dlock,我们采用就是这个,目前使用这么久还没出现什么问题。使用方式类似下面:
优缺点
优点:
- 实现简单;
- 理解逻辑简单;
- 性能好,毕竟是缓存。
缺点:
- Redis 容易单点故障,集群部署;
- key 的过期时间设置多少不明确,只能根据实际情况调整。
基于 ZK 的实现
前面提到 Redis 的核心的是SETNX
命令,那么对于 ZK 来说,实现分布式锁的核心是临时顺序节点。首先关于 ZK 的知识我们后面有机会再跟大家介绍,目前我们只要知道 ZK 的节点种类中有一种叫做临时顺序节点,两个关键词:临时,顺序。
临时表示在客户端创建某节点后,如果客户端经过一段时间跟服务端之间失去了心跳,说明客户端已经掉线了,那么这个节点就会被自动删除(这一点跟 Redis key 的过期时间类似);顺序的意思是在一个 node 下面生成的子节点是按顺序的,每个子节点都有一个唯一编号,并且这个编号是按顺序自增的。
临时顺序节点再加上 ZK 的监听机制就可以实现分布式锁了,Curator 是一个 ZK 的开源客户端,也提供了分布式锁的实现,这个我没用实际用过,但是网上用的人也很多,大家可以自己去研究一下。
优缺点
优点:
- ZK 本身就是集群部署,避免单机故障;
- 顺序节点所以不用考虑过期时间设置问题;
缺点:
- 实现较为复杂;
- 非缓存机制,大量频繁创建删除节点会影响 ZK 集群性能;
基于数据库的实现
基于数据库的分布式锁个人觉得性能不是很好,在高并发的情况下对数据库服务器的压力过大,会影响业务,不建议使用。不过从学习的角度来看,我们还是有必要了解下具体的实现方式。基于数据库的分布式锁的实现大致有两种方式,这里的数据库我们以 MySQL 为例。两种方案的实现都需要一个额外的表,并且要有一个唯一索引字段。
- 阻塞式语句
select xxx for update
- 非阻塞试
insert into xxx ; delete from
解释下:
第一种方案在实施的时候,需要关闭事务的自动提交,然后执行 SQL 去获得锁,如果获得锁成功,执行下面的业务逻辑,如果这里没有获取到锁,则会阻塞,一直等待。业务执行结束后,手动提交事务。这里如果程序在执行提交事务失败,异常或者服务宕机后,数据库会自动释放锁,从而导致死锁。但是这里有个问题就是如果在高并发的情况下,很多线程都没有获得到锁,都在阻塞等待,这样会导致数据库的服务器压力过大,会影响数据库的服务。这个是要注意的,这也是我不建议的地方,容易出现瓶颈,毕竟没有缓存高效。
第二种方案跟第一种类似,不一样的地方是这里通过第一步向指定的表中插入一条唯一索引的数据,插入成功则表示获得锁,插入失败则未获得到锁,成功获得的锁后就可以执行业务逻辑,在执行完业务逻辑后就可以删除执行的记录。如果插入失败就需要重新触发获取锁的动作。但是这种方案存在的问题是无法设置锁的失效时间,需要其他手段来清理超时数据,而且为了支持可重入,需要将主机和服务的信息一起保存。
优缺点
优点
- 容易理解和实现,但是细节要注意;
缺点:
- 高并发的情况下性能不好,阻塞式的情况下很多链接不释放会拖垮数据库服务;
- 需要定时清理超时数据,麻烦;
- 数据库的行锁会因为 MySQL 的查询优化而失效
小结
这篇文章主要跟大家介绍了一下分布式锁的使用场景和实现逻辑,知道了具体的逻辑,代码实现可以参考很多开源的实现,理解了原理再造轮子或者修改轮子会深刻很多。在现在的分布式环境下,很好的理解分布式锁是一个很重要的点,希望能帮助到大家,最后欢迎到我们 Java 极客技术的知识星球中探讨技术,如果想了解其他方面的技术也可以跟我们提,我们互相学习共同进步。知识星球期待你的加入。