什么是分布式锁

在分布式架构中,Java的锁无法管理多个实例,因此需要有一个类似于统一管理锁的架构模式,即分布式锁。

目前比较常见的分布式锁实现方案有以下几种:

  • 基于数据库乐观锁,如MySQL,增加字段版本标识version控制实现
  • 基于缓存,如Redis,使用set命令实现
  • 基于Zookeeper、etcd等,借助ZooKeeper的临时节点实现

例如两个订单服务,对要更新数据库的数据,如果能获取到相应的锁才能进行后续操作。

在这里插入图片描述

分布式锁的几种实现方式

基于MySQL实现分布式锁

当我们想要获得锁时,可以插入一条数据;当需要释放锁的时,可以删除这条数据

CREATE TABLE `database_lock` (
	`id` BIGINT NOT NULL AUTO_INCREMENT,
	`resource` int NOT NULL COMMENT '锁定的资源',
	`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
	PRIMARY KEY (`id`),
	UNIQUE KEY `uiq_idx_resource` (`resource`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

在表database_lock中,resource字段做了唯一性约束,这样如果有多个请求同时提交到数据库的话,数据库可以保证只有一个操作可以成功(其它的会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_idx_resource’),那么那么我们就可以认为操作成功的那个请求获得了锁。

基于Redis实现分布式锁

简单实现

使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。

在这里插入图片描述

Redis的缺点与改进

Redis的缺点

使用redis做分布式锁的缺点在于:如果采用单机部署模式,会存在单点问题,只要redis故障了,加锁就不行了。

采用master-slave模式,加锁的时候只对一个节点加锁,即便通过sentinel做了高可用,但是如果master节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

RedLock算法

基于以上的考虑,其实redis的作者也考虑到这个问题,他提出了一个RedLock的算法,这个算法的意思大概是这样的:

假设redis的部署模式是redis cluster,总共有5个master节点,通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒
  • 轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒
  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  • 要是锁建立失败了,那么就依次删除这个锁
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。
在这里插入图片描述

使用Redission

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

Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持,比较推荐使用,它帮我们考虑了很多细节

  • redisson的“看门狗”逻辑保证了没有死锁发生:redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s,这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
  • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
  • 提供了对redlock算法的支持

在这里插入图片描述

基于Zookeeper实现分布式锁

Zookeeper特性

zookeeper有一些特性使得原生支持分布式锁:

  • 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点。
  • 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,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实现

Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现,底层原理和上面分析的是差不多的

在这里插入图片描述

两种方案的优缺点比较

关于Redis

redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题,关于redlock的讨论可以看How to do distributed locking

但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”

所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。

关于Zookeeper

zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。

如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

但是zk也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐