分布式锁
2026年2月23日大约 3 分钟
分布式锁
分布式锁就是指在分布式系统或集群模式下,多进程可见并且互斥的锁。
分布式锁需要满足以下5个特点:
- 多进程可见
- 互斥
- 高可用
- 高性能
- 安全性
分布式锁的实现方式
分布式锁的核心是实现多线程互斥。实现方式主要有三种:
| MySQL | Redis | ZooKeeper | |
|---|---|---|---|
| 互斥 | 利用MySQL的互斥锁机制 | 利用setnx这样的互斥命令 | 利用其节点的唯一和有序性实现 |
| 高可用 | 好 | 好 | 好 |
| 高性能 | 一般 | 好 | 一般 |
| 安全性 | 断开连接,自动释放锁 | 利用过期时间释放锁 | 断开连接,自动释放锁 |
Redis的分布式锁
Redis的分布式锁是通过setnx命令来实现的。setnx命令的全称是set if not exist,即只有当key不存在时才会设置成功。
而实现分布锁需要实现两个基本方法:
- 获取锁:
- 通过setnx命令来获取锁,如果获取成功则返回true,否则返回false。
- 示例:注意下面锁key和value需要根据业务进行设计
解决误删问题-获取锁
- 为了避免不同线程间出现误删锁的情况,这里将s线程Id作为锁的value。
- 同时避免集群环境下可能存在线程Id重复问题,使用UUID生成前缀,防止线程Id重复问题。
// 通过UUID生成唯一id作为前缀,防止线程ID重复问题
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(flag);
}- 释放锁:
- 手动释放:通过del命令来释放锁。
- 超时释放:设置过期时间,过期自动释放锁。
- 示例:
解决误删问题-释放锁
在释放锁时需要注意,必须要保证只有锁的持有者才能释放锁,否则可能会出现误删锁的情况。
- 这里是根据线程Id来判断是否是锁的持有者,如果是持有者才会删除锁,否则不做任何操作。
public void unlock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁存入的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadId.equals(id)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}解决误删问题-原子性问题
上面虽然解决了误删问题,但是因为两条命令不具有原子性,所以在高并发的情况下,可能会出现以下情况:
- 线程A获取锁成功,执行到释放锁的第一条命令时,线程A被挂起了,此时锁的过期时间到了,锁被自动释放了。
- 线程B获取锁成功,执行到释放锁的第一条命令时,线程B被挂起了,此时锁的过期时间到了,锁被自动释放了。
- 线程A恢复执行,执行到释放锁的第二条命令时,线程A删除了线程B的锁。
- 线程B恢复执行,执行到释放锁的第二条命令时,线程B删除了线程A的锁。
- 简单来说就是,在获取完锁标识,线程被挂起,此时正好key过期,导致删锁时删的是别人的锁。
因此在高并发的情况下,可能会出现误删锁的情况。为了解决这个问题,可以使用Lua脚本来保证获取锁和释放锁的原子性。