什么是分布式锁?
在单jvm中,我们可以使用语言类库提供的锁保证线程安全。
在多jvm中,我们可以使用分布式锁来保证线程安全。
分布式锁的应用场景?
在多个jvm之间同步访问共享资源,就需要分布式锁来保证安全。
比如多台机器都可以定时执行某个任务,如果限制任务每次只能被一台机器执行,不能重复执行,就可以用分布式锁来做标记。
比如下单减库存时,在集群情况下,就可以使用分布式锁来保证库存不超卖。
分布式锁的实现方案
- 基于数据库实现分布式锁
- 缺点:性能差、线程出现异常时,容易出现死锁。
- 基于redis实现分布式锁
- 缺点:锁的失效时间难控制、容易产生死锁、非阻塞式、不可重入
- 基于zookeeper实现分布式锁
- 实现相对简单、可靠性强、使用临时节点,失效时间容易控制
分布式锁三种实现方案的好坏
以上有提到实现分布式锁的三种方案:数据库,redis,zookeeper
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > Redis > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= Redis > 数据库
从性能角度(从高到低)
Redis > Zookeeper > 数据库
从可靠性角度(从高到低)
Zookeeper > Redis > 数据库
互斥锁的原则条件
一般来说,我们要实现的分布式指的是互斥锁。
互斥锁主要的原则条件是:
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 无死锁:即便持有锁的客户端崩溃或网络被分裂而没有主动解锁,锁仍然可以被获取。
- 解铃还须系铃人:加锁和解锁必须是同一个客户端。客户端自己不能把别人加的锁给解了。
Redis实现分布式锁
这是对Redis实现互斥锁的讲解。
Redis实现分布式锁的保障
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性:在任意一个时刻,只有一个客户端能持有锁。
- 无死锁:即便持有锁的客户端崩溃或网络被分裂而没有主动解锁,锁仍然可以被获取。
- 容错性:只要大部分Redis节点都活着,客户端就可以获取和释放锁.
- 解铃还须系铃人,加锁和解锁必须是同一个客户端。客户端自己不能把别人加的锁给解了。
单实例实现分布式锁
获取锁:
使用set获取锁。
通过nx设置锁不存在,才创建锁,保证互斥性。
通过ex设置锁的过期时间,超过该时间则自动释放锁,保证无死锁。
设置value值为随机生成的UUID,在释放锁时进行判断,需要上锁的客户端才能删除这把锁,保证解铃还须系铃人
设置获取锁的超时时间:
获取锁的时候还设置一个获取锁的超时时间,若超过这个时间则放弃获取锁,不要让程序一直循环的获取锁。
释放锁:
使用delte释放锁。
在释放锁的过程中,通过UUID判断是不是该客户端上的锁,如果是才释放锁,保证解铃还须系铃人
单实例实现分布式锁存在的问题
在这种场景(主从结构)中存在明显的竞态:
客户端A从master获取到锁
在master将锁同步到slave之前,master宕掉了。
slave节点被晋级为master节点
客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。(互斥性失效)!
有时候程序就是这么巧,比如说正好一个节点挂掉的时候,多个客户端同时取到了锁。如果你可以接受这种小概率错误,那用这个基于复制的方案就完全没有问题。否则的话,我们建议你使用Redlock。
Redlock(多实例实现分布式锁)
为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

注意:根据上面实现原理的分析,有些同学应该是对Redlock算法实现有一点点误解,假设我们用5个节点实现Redlock算法的分布式锁。那么要么是5个redis单实例,要么是5个sentinel集群,要么是5个cluster集群。而不是一个有5个主节点的cluster集群,然后向每个节点通过EVAL命令执行LUA脚本尝试获取分布式锁,如上图所示。
Zookeeper实现分布式锁
这是对Zookeeper实现互斥锁的讲解。
前言
zookeeper实现分布式互斥锁:
zookeeper主要是基于临时顺序节点和watch通知机制和会话机制来实现的
zookeeper实现分布式互斥锁锁可以保证互斥和控制时序
- 互斥:同一时间,只有一个客户端可以获取这把锁,其他客户端进行等待
- 控制时序:所有来获取这把锁的客户端,最终都会成功获取锁,只是有个全局时序,也就是指客户端获取锁的顺序性了。
zk实现分布式锁的思路
前提:在获取锁之前创建一个持久节点,作为父节点。列如/lockRoot
获取锁:
当客户端测试获取锁时,在父节点下创建临时顺序的子节点,列如:/lockRoot/node1。
获取父节点下的所有子节点,判断当前客户端创建的节点是否是最小的子节点,如果是,则获取锁成功;
如果不是,则监听比当前节点小一位的节点的删除事件,当删除事件被触发时,则当前客户端获取锁成功;
释放锁:
使用del,删除临时顺序节点即可。
无死锁保证:
基于会话机制,当客户端断开连接,则客户端创建的零时顺序节点自动删除,即释放锁成功。
zk实现分布式锁
不推荐使用原生Java API实现分布式锁,推荐使用Curator框架实现分布式锁。