分布式锁常见的实现方式主要有三种:
- 数据库乐观锁
- 基于 Redis 的分布式锁
- 基于 ZooKeeper 的分布式锁
为了确保分布式锁可用,至少要满足以下四个条件:
- 互斥性:任意时刻只有一个客户端能持有锁。
- 不会死锁:即使客户端在持有锁期间崩溃而未解锁,其他客户端依然可以继续加锁。
- 容错性:只要大部分 Redis 节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人:加锁和解锁必须是同一个客户端,不能解掉别人加的锁。
正确的 Redis 分布式锁实现
1. 正确的加锁代码
java
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间(毫秒)
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}
}
2. 错误的加锁示例
arduino
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
}
❌ 这个实现的问题在于
setnx和expire不是原子操作,如果程序崩溃,就可能造成锁永远无法释放。
正确的解锁代码
typescript
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return RELEASE_SUCCESS.equals(result);
}
}
这段 Lua 脚本会先获取锁对应的
value,判断是否和requestId一致,如果一致才删除锁(解锁)。使用
eval()可以保证操作的原子性,避免误解锁。
错误的解锁示例
1. 不判断是否为同一客户端
typescript
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}
❌ 问题:直接删除锁,可能误删别人加的锁。
2. 非原子操作
typescript
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是否是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然过期,客户端B已经加锁
// 执行 del() 就可能把客户端B的锁误删
jedis.del(lockKey);
}
}
❌ 问题:
get+del分两步执行,可能导致锁被错误释放。
总结
- 加锁 :一定要保证原子操作,可以直接用
SET key value NX PX expireTime。 - 解锁:必须保证原子性,并且只允许锁的拥有者解锁,推荐使用 Lua 脚本。
- 避免错误 :不要拆分加锁和设置过期时间,不要直接
del锁。
在生产环境中,也可以使用 Redisson 这样的开源库,它封装好了 Redis 分布式锁的各种细节,避免踩坑。