# 浅析分布式锁
由于使用 Java 提供的 Synchronized 或者 ReentrantLock 只能锁住当前机器的线程,在一瞬间的两个请求进来,分发到不同的服务器上,存在某些业务只能执行一次,或者需要按顺序执行,保证数据正确性,所以引出了分布式锁的概念
实现方式
- DB: 一般使用都是乐观锁,悲观锁不考虑
- Memcached: 使用 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁
- Redis: 使用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功
- Zookeeper: 使用顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的
- Chubby: Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法
比较成熟主流常用的一般是 Redis 和 Zookeeper,这里说明下这两种的实现方式
# 1. Redis
原生实现和 Redisson 框架
# 1.1. 原生实现
需要使用 setnx 命令,key 是锁的唯一标识,按业务来决定命名,还得设置过期时间防止死锁,但是 setnx 指令本身是不支持传入超时时间的,而 setnx 和 expire 两个操作合并是非原子性的,怎么解决呢
- 可以使用 lua 脚本,两个命令组合在一起就是原子的
- Redis 从 2.6.12 版本开始 set 指令增加了可选参数:set(key, value, "NX", "PX", 1000 * 60)
业务执行完成的时候,del 导致误删其他线程的锁,value 需要设置为当前线程的唯一值,del 的时候判断是不是当前线程的锁,是的话才进行删除,这样又是两个操作组合不是原子性了,怎么解决呢,这里只能依赖 lua 脚本解决
-- 获取锁
-- NX 是指如果 key 不存在就成功,key 存在返回 false,PX 可以指定过期时间
SET anyLock unique_value NX PX 30000
-- 释放锁:通过执行一段 lua 脚本
-- 释放锁涉及到两条指令,这两条指令不是原子性的
-- 需要用到 redis 的 lua 脚本支持特性,redis 执行 lua 脚本是原子性的
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
# 1.2. Redisson
Redisson 配置好直接使用即可,配置文件参考
- Github: https://github.com/dolyw/ProjectStudy/tree/master/SpringBoot/DelayTask (opens new window)
- Gitee(码云): https://gitee.com/dolyw/ProjectStudy/tree/master/SpringBoot/DelayTask (opens new window)
详细使用参考 RedisLockHelper
工具类
简单使用
// 注入RedisLockHelper
@Autowired
private RedisLockHelper redisLockHelper;
// 声明RedisKey
String redisLockKey = XXX;
// 无返回值,这段业务会锁住所有线程,必须一个个顺序执行
redisLockHelper.lock(redisLockKey, () -> {
// 业务逻辑
// ...
});
redisLockHelper.tryLock(redisLockKey, () -> {
// 业务逻辑
// ...
});
// 有返回值,返回值是泛型,支持任何对象
String result = redisLockHelper.lock(redisLockKey, () -> {
// 业务逻辑
// ...
return "";
});
String result = redisLockHelper.tryLock(redisLockKey, () -> {
// 业务逻辑
// ...
return "";
});
lock
和 tryLock
的区别,lock
会一直阻塞等待锁,不推荐使用,一般使用tryLock
详细使用
/**
* 加锁业务处理
*
* @param lockKey
* @param fair 是否公平锁 公平锁会尽量将锁优先分配给先进来的请求
* @param time 自动释放锁时间(最长锁住时间)
* @param unit 时间单位
* @param handle 处理业务方法函数
* @return void
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:10
*/
public void lock(String lockKey, boolean fair, long time, TimeUnit unit, Handle handle) throws Exception {}
// 简单调用,默认非公平锁,5S自动释放锁
public void lock(String lockKey, Handle handle) throws Exception {
lock(lockKey, false, 5, TimeUnit.SECONDS, handle);
}
// 设定是否公平锁,默认5S自动释放锁
public void lock(String lockKey, boolean fair, VoidHandle handle) throws Exception {
lock(lockKey, fair, 5, TimeUnit.SECONDS, handle);
}
// 设定自动释放锁时间(最长锁住时间),默认非公平锁
public void lock(String lockKey, long time, TimeUnit unit, VoidHandle handle) throws Exception {
lock(lockKey, false, time, unit, handle);
}
/**
* 尝试加锁业务处理
*
* @param lockKey
* @param fair 是否公平锁 公平锁会尽量将锁优先分配给先进来的请求
* @param wait 等待获取锁时间
* @param release 自动释放锁时间(最长锁住时间)
* @param unit 时间单位
* @param handle 处理业务方法函数
* @return void
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:40
*/
public void tryLock(String lockKey, boolean fair, long wait, long release, TimeUnit unit, Handle handle) throws Exception {}
// 简单调用,默认非公平锁,3S等待获取锁,5S自动释放锁
public void tryLock(String lockKey, VoidHandle handle) throws Exception {
tryLock(lockKey, false, 3, 5, TimeUnit.SECONDS, handle);
}
// 设定是否公平锁,默认3S等待获取锁,5S自动释放锁
public void tryLock(String lockKey, boolean fair, VoidHandle handle) throws Exception {
tryLock(lockKey, fair, 3, 5, TimeUnit.SECONDS, handle);
}
// 设定等待获取锁时间,自动释放锁时间(最长锁住时间),默认非公平锁,3S等待获取锁,5S自动释放锁
public void tryLock(String lockKey, long wait, long time, TimeUnit unit, VoidHandle handle) throws Exception {
tryLock(lockKey, false, wait, time, unit, handle);
}
RedisLockHelper
package com.example.redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* RedisLockHelper
*
* @author wliduo[i@dolyw.com]
* @date 2021/9/15 15:33
*/
@Component
public class RedisLockHelper {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(RedisLockHelper.class);
@Autowired
private RedissonClient redissonClient;
/**
* 获取锁 - 默认非公平锁
*
* @param lockKey
* @return
* @throws Exception
*/
public RLock getLock(String lockKey) throws Exception {
return getLock(lockKey, false);
}
/**
* 获取锁
*
* @param lockKey
* @param fair 公平锁
* @return org.redisson.api.RLock
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 14:47
*/
public RLock getLock(String lockKey, boolean fair) throws Exception {
if (StringUtils.isEmpty(lockKey)) {
throw new RuntimeException("分布式锁lockKey为空");
}
if (fair) {
// 当多个客户端线程同时请求加锁时,公平锁优先分配给先发出请求的线程
return redissonClient.getFairLock(lockKey);
}
return redissonClient.getLock(lockKey);
}
/**
* 加锁业务处理 - 默认非公平锁,5S自动释放锁
*
* @param lockKey
* @param handle
* @throws Exception
*/
public void lock(String lockKey, VoidHandle handle) throws Exception {
lock(lockKey, false, 5, TimeUnit.SECONDS, handle);
}
public void lock(String lockKey, boolean fair, VoidHandle handle) throws Exception {
lock(lockKey, fair, 5, TimeUnit.SECONDS, handle);
}
public void lock(String lockKey, long time, TimeUnit unit, VoidHandle handle) throws Exception {
lock(lockKey, false, time, unit, handle);
}
/**
* 加锁业务处理
*
* @param lockKey
* @param fair
* @param time
* @param unit
* @param handle
* @return void
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:10
*/
public void lock(String lockKey, boolean fair, long time, TimeUnit unit, VoidHandle handle) throws Exception {
RLock rLock = getLock(lockKey, fair);
try {
rLock.lock(time, unit);
handle.execute();
} finally {
rLock.unlock();
}
}
/**
* 返回值加锁业务处理 - 默认非公平锁,5S自动释放锁
*
* @param lockKey
* @param handle
* @throws Exception
*/
public <T> T lock(String lockKey, ReturnHandle<T> handle) throws Exception {
return lock(lockKey, false, 5, TimeUnit.SECONDS, handle);
}
public <T> T lock(String lockKey, boolean fair, ReturnHandle<T> handle) throws Exception {
return lock(lockKey, fair, 5, TimeUnit.SECONDS, handle);
}
public <T> T lock(String lockKey, long time, TimeUnit unit, ReturnHandle<T> handle) throws Exception {
return lock(lockKey, false, time, unit, handle);
}
/**
* 返回值加锁业务处理
*
* @param lockKey
* @param fair
* @param time
* @param unit
* @param handle
* @return void
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:10
*/
public <T> T lock(String lockKey, boolean fair, long time, TimeUnit unit, ReturnHandle<T> handle) throws Exception {
RLock rLock = getLock(lockKey, fair);
try {
rLock.lock(time, unit);
return handle.execute();
} finally {
rLock.unlock();
}
}
/**
* 尝试获取锁 - 默认非公平锁
*
* @param lockKey
* @return
* @throws Exception
*/
public Boolean getTryLock(String lockKey) throws Exception {
return getTryLock(lockKey, false);
}
/**
* 尝试获取锁
*
* @param lockKey
* @param fair 公平锁
* @return java.lang.Boolean
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 16:05
*/
public Boolean getTryLock(String lockKey, boolean fair) throws Exception {
if (StringUtils.isEmpty(lockKey)) {
throw new RuntimeException("分布式锁lockKey为空");
}
RLock rLock = null;
if (fair) {
// 当多个客户端线程同时请求加锁时,公平锁优先分配给先发出请求的线程
rLock = redissonClient.getFairLock(lockKey);
} else {
rLock = redissonClient.getLock(lockKey);
}
return rLock.tryLock();
}
/**
* 尝试加锁业务处理 - 默认非公平锁,3S等待获取锁,5S自动释放锁
*
* @param lockKey
* @param handle
* @throws Exception
*/
public void tryLock(String lockKey, VoidHandle handle) throws Exception {
tryLock(lockKey, false, 3, 5, TimeUnit.SECONDS, handle);
}
public void tryLock(String lockKey, boolean fair, VoidHandle handle) throws Exception {
tryLock(lockKey, fair, 3, 5, TimeUnit.SECONDS, handle);
}
public void tryLock(String lockKey, long wait, long time, TimeUnit unit, VoidHandle handle) throws Exception {
tryLock(lockKey, false, wait, time, unit, handle);
}
/**
* 尝试加锁业务处理
*
* @param lockKey
* @param fair
* @param wait
* @param release
* @param unit
* @param handle
* @return void
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:40
*/
public void tryLock(String lockKey, boolean fair, long wait, long release, TimeUnit unit, VoidHandle handle) throws Exception {
RLock rLock = getLock(lockKey, false);
if (!rLock.tryLock(wait, release, unit)) {
throw new RuntimeException("获取锁" + lockKey + "异常");
}
try {
handle.execute();
} finally {
rLock.unlock();
}
}
/**
* 返回值尝试加锁业务处理 - 默认非公平锁,3S等待获取锁,5S自动释放锁
*
* @param lockKey
* @param handle
* @throws Exception
*/
public <T> T tryLock(String lockKey, ReturnHandle<T> handle) throws Exception {
return tryLock(lockKey, false, 3, 5, TimeUnit.SECONDS, handle);
}
public <T> T tryLock(String lockKey, boolean fair, ReturnHandle<T> handle) throws Exception {
return tryLock(lockKey, fair, 3, 5, TimeUnit.SECONDS, handle);
}
public <T> T tryLock(String lockKey, long wait, long time, TimeUnit unit, ReturnHandle<T> handle) throws Exception {
return tryLock(lockKey, false, wait, time, unit, handle);
}
/**
* 返回值尝试加锁业务处理
*
* @param lockKey
* @param fair
* @param wait
* @param release
* @param unit
* @param handle
* @return T
* @throws
* @author wliduo[i@dolyw.com]
* @date 2022/2/22 15:51
*/
public <T> T tryLock(String lockKey, boolean fair, long wait, long release, TimeUnit unit, ReturnHandle<T> handle) throws Exception {
RLock rLock = getLock(lockKey, false);
if (!rLock.tryLock(wait, release, unit)) {
throw new RuntimeException("获取锁" + lockKey + "异常");
}
try {
return handle.execute();
} finally {
rLock.unlock();
}
}
/**
* ReturnHandle
*
* @author wliduo[i@dolyw.com]
* @date 2021/9/15 15:32
*/
public interface ReturnHandle<T> {
/**
* 业务处理
*/
T execute() throws Exception;
}
/**
* VoidHandle
*
* @author wliduo[i@dolyw.com]
* @date 2021/9/15 15:32
*/
public interface VoidHandle {
/**
* 业务处理
*/
void execute() throws Exception;
}
}
- Redisson分布式锁封装 (opens new window)
- Redisson分布式锁方法介绍 (opens new window)
- Redisson分布式锁注解封装 (opens new window)
# 1.3. 锁靠谱吗
Redis 分布式锁不是完全靠谱的,如果使用的是 Redis 集群,不是强一致性,可能存在异常
# 2. Zookeeper
Zookeeper 在 Java 中客户端有三种,Zookeeper 原生和 Apache Curator 框架,还有一个开源的 zkclient(使用率很低),一般使用的都是 Curator,原生 API 使用起来没 Curator 方便
# 2.1. 原生实现
ZooKeeper 可以创建 4 种类型的节点,分别是:持久性节点,持久性顺序节点,临时性节点,临时性顺序节点。Zookeeper 分布式锁的实现是基于临时性顺序节点来实现的,监听节点
原理举例说明
假设服务器1,创建了一个节点 /zk1,成功了,那服务器1就获取了锁,服务器2再去创建相同的锁,就会失败,这个时候就只能监听这个节点的变化。等到服务器1处理完业务,删除了节点后,他就会得到通知,然后去创建同样的节点,获取锁处理业务,再删除节点,后续的 100 台服务器与之类似。注意这里的 100 台服务器并不是挨个去执行上面的创建节点的操作,而是并发的,当服务器1创建成功,那么剩下的 99 个就都会注册监听这个节点,等通知,以此类推。
但是大家有没有注意到,这里还是有问题的,还是会有死锁的情况存在,对不对?当服务器1创建了节点后挂了,没能删除,那其他99台服务器就会一直等通知,那就完蛋了,这个时候就需要用到临时性节点了,我们前面说过了,临时性节点的特点是客户端一旦断开,就会丢失,也就是当服务器1创建了节点后,如果挂了,那这个节点会自动被删除,这样后续的其他服务器,就可以继续去创建节点,获取锁了。
但是我们可能还需要注意到一点,就是惊群效应:举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到。就是当服务器1节点有变化,会通知其余的 99 个服务器,但是最终只有1个服务器会创建成功,这样 98 还是需要等待监听,那么为了处理这种情况,就需要用到临时顺序性节点。大致意思就是,之前是所有 99 个服务器都监听一个节点,现在就是每一个服务器监听自己前面的一个节点。
假设 100 个服务器同时发来请求,这个时候会在 /zkjjj 节点下创建 100 个临时顺序性节点 /zkjjj/000000001,/zkjjj/000000002,一直到 /zkjjj/000000100,这个编号就等于是已经给他们设置了获取锁的先后顺序了。当 001 节点处理完毕,删除节点后,002 收到通知,去获取锁,开始执行,执行完毕,删除节点,通知 003~100 以此类推
# 2.2. Curator
Curator
# 2.3. 锁靠谱吗
因为 Zookeeper 集群的写操作是线性一致性的(读是顺序一致性),所以同时多个客户端进行写操作的话,出现异常的情况很低,比 Redis 更靠谱一些
# 3. 总结
- 从理解的难度角度
Zookeeper > 缓存 > 数据库
- 从实现的复杂性角度
Zookeeper >= 缓存 > 数据库
- 从性能角度
缓存 > Zookeeper >= 数据库
- 从可靠性角度
Zookeeper > 缓存 > 数据库
# 4. 封装
可以在锁的基础上再加一层封装,这样我们在从 ZK 替换到缓存,或者反过来的时候,改动点非常小
参考
- 什么是分布式锁 (opens new window)
- spring boot redis分布式锁 (opens new window)
- Redisson实现分布式锁(1)---原理 (opens new window)
- Redisson实现Redis分布式锁的原理 (opens new window)
- 分布式锁用 Redis 还是 Zookeeper? (opens new window)
- 分布式锁用Redis坚决不用Zookeeper? (opens new window)
- 02.ZooKeeper的Java客户端使用 (opens new window)
- 阿里面试官:分布式锁到底用Redis好?还是Zookeeper好? (opens new window)
- 通俗讲解分布式锁,看完不懂算作者输 (opens new window)
← 浅析分布式Session 浅析分布式ID →