分布式锁
1.理解分布式锁
编程语言往往都会提供锁来保证线程并发访问共享变量,比如,Java 语言给我们提供了线程锁,开放了处理锁机制的 API,比如 Synchronized、Lock 等。当一个锁被某个线程持有的时候,另一个线程尝试去获取这个锁会失败或者阻塞,直到持有锁的线程释放了该锁。
在单台服务器内部,可以通过线程加锁的方式来同步。但是在分布式场景下就需要分布式锁来保证多实例并发安全访问共享变量。

分布式场景下解决并发问题,需要应用分布式锁技术。如上图所示,分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。
2.分布式锁应该具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
- 高可用的获取锁与释放锁;
- 高性能的获取锁与释放锁;
- 具备可重入特性;
- 具备锁失效机制,防止死锁;
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的常用实现方式
实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案
- 基于关系型数据库
基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。
以唯一索引为例,创建一张锁表,定义方法或者资源名、失效时间等字段,同时针对加锁的信息添加唯一索引,比如方法名,当要锁住某个方法或资源时,就在该表中插入对应方法的一条记录,插入成功表示获取了锁,想要释放锁的时候就删除这条记录。
下面创建一张基于数据库的分布式锁表:
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法或者资源',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='对方法加锁';
当希望对某个方法加锁时,执行以下 SQL 语句:
insert into methodLock(method_name) values ('method_name');
在数据表定义中,我们对 method_name 做了唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么就可以认为操作成功的那个线程获得了该方法的锁,可以执行后面的业务逻辑。
当方法执行完毕之后,想要释放锁的话,在数据库中删除对应的记录即可。
基于数据库实现分布式锁操作简单,但是并不是一个可以落地的方案,有很多地方需要优化。
缺点:
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
- 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
2.基于Redis实现
使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
实现方法
伪代码如下:
if(setnx(key,value)==1){
expire(key,expireTime) //设置过期时间
try{
//业务处理
}finally{
//释放锁
del(key)
}
}
缺点:
- setnx和expire不是原子操作,所以存在设置了setnx 之后突然崩溃,导致锁没有设置过期时间,那么这个锁就会一直存在,无法被其他线程获取。此时可以用Redis 2.8以后支持set和expire的原子操作
> SET key value expireTime nx
2. Redis集群下可能会出现竟态
客户端 A 从 Master 节点获取锁;
Master 节点宕机,主从复制过程中,对应锁的 key 还没有同步到 Slave 节点上;
Slave 升级为 Master 节点,于是集群丢失了锁数据;
其他客户端请求新的 Master 节点,获取到了对应同一个资源的锁;
出现多个客户端同时持有同一个资源的锁,不满足锁的互斥性。
关于集群下如何实现分布式锁,Redis 的作者 Antirez(Salvatore Sanfilippo)提出了 Redlock 算法
Redlock 算法的流程
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作:
客户端记录当前系统时间,以毫秒为单位;
依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。
3.基于zookeeper
ZooKeeper 有四种节点类型,包括持久节点、持久顺序节点、临时节点和临时顺序节点,利用 ZooKeeper 支持临时顺序节点的特性,可以实现分布式锁。
1.持久节点 (PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在
2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号
3.临时节点(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除
4.临时顺序节点(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
当客户端对某个方法加锁时,在 ZooKeeper 中该方法对应的指定节点目录下,生成一个唯一的临时有序节点。

判断是否获取锁,只需要判断持有的节点是否是有序节点中序号最小的一个,当释放锁的时候,将这个临时节点删除即可,这种方式可以避免服务宕机导致的锁无法释放而产生的死锁问题。
下面描述使用 ZooKeeper 实现分布式锁的算法流程,根节点为 /lock:
客户端连接 ZooKeeper,并在 /lock 下创建临时有序子节点,第一个客户端对应的子节点为 /lock/lock01/00000001,第二个为 /lock/lock01/00000002;
其他客户端获取 /lock01 下的子节点列表,判断自己创建的子节点是否为当前列表中序号最小的子节点;
如果是则认为获得锁,执行业务代码,否则通过 watch 事件监听 /lock01 的子节点变更消息,获得变更通知后重复此步骤直至获得锁;
完成业务流程后,删除对应的子节点,释放分布式锁。
在实际开发中,可以应用 Apache Curator 来快速实现分布式锁,Curator 是 Netflix 公司开源的一个 ZooKeeper 客户端,对 ZooKeeper 原生 API 做了抽象和封装,若感兴趣可自行查询资料了解。
通过ZooKeeper实现起来比较方便,但是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。
文章主要整理自以下课程,如果想系统学习分布式技术原理与实战,可点击:
https://skaygo.blog.youkuaiyun.com/article/details/113850871
7638



被折叠的 条评论
为什么被折叠?



