目录
一、什么是分布式锁?
分布式锁是分布式系统中的锁,用来控制同一时刻,只有一个 JVM 进程中的一个线程可以访问被保护的资源。
二、为什么需要分布式锁?
对于单机多线程来说,在 Java 中,我们通常使用 ReetrantLock类、synchronized 关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
三、如何实现分布式锁?
1. 要求
一个最基本的分布式锁需要满足:
- 互斥 :任意一个时刻,锁只能被一个线程持有;
- 高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。
2. 实现方式
实现方式 | 基于 Redis | 基于 ZooKeeper | 基于数据库 |
性能 | 高 | 一般 | 差 |
可靠性 | 较可靠(AP架构) | 可靠(CP架构) | 不可靠(CA架构) |
理解难易度 | 一般 | 困难 | 容易 |
实现复杂度 | 一般 | 简单 | 复杂 |
使用人数 | 多 | 一般 | 少 |
2.1 基于 Redis 实现分布式锁
(1)实现 1:使用 setnx 进行加锁,使用 del 命令释放锁。
存在问题:如果已获取锁的线程发生异常,无法执行 del 命令释放锁,那么这个锁就一直占用,其他线程也获取不到这个锁。
(2)实现 2:加锁之后,使用 expire 命令设置锁超时时间。
存在问题:如果已获取锁的线程发生异常,还没来得及执行 expire 命令设置锁超时时间,那么这个锁也是一直占用,其他线程也获取不到这个锁。
(3)实现 3:Redis 2.6.X 之后,官方拓展了 set 命令的参数,满足了当 key 不存在则设置 value,同时设置锁超时时间,确保了原子性。命令格式:set <lock.key> <lock.value> nx ex <expireTime>
存在问题:1)如果已获取锁的线程 A 执行的很慢(如:网络问题、发生 FullGC……),过了锁超时时间还没执行完,而锁自动释放了,然后线程 B 获取了锁;2)线程 A 执行完,线程 B 还没执行完,线程 A 释放了线程 B 的锁。
(4)实现 4:加锁的时候设置一个唯一标识(比如线程 ID)作为 value 代表加锁的线程,释放锁时,先判断 key 对应的 value 是否匹配,必须匹配才能释放锁,可使用 Lua 脚本确保原子性。
存在问题:解决了实现 3 的问题 2),但是问题 1)的锁超时自动释放依然存在。
(5)实现 5:Redisson,使用看门狗机制,获得锁的线程开启一个守护线程,用来给快要过期的锁续期。
2.2 基于 ZooKeeper 实现分布式锁
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。
2.3 基于数据库实现分布式锁
(1)基于数据库表的增删
通过唯一索引保持排他性,加锁时插入一条记录,解锁是删除这条记录。
(2)基于数据库的排它锁
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。