在分布式系统中,多个服务节点需要协同工作,而共享资源的并发访问控制是绕不开的话题。Redis 凭借其高性能和原子性操作特性,成为实现分布式锁的热门选择。本文将基于你的学习笔记,深入解析 Redis 分布式锁的核心原理、原子性保障及实现逻辑,帮你构建更完整的知识体系。
一、为什么需要分布式锁?
在单体应用中,我们可以用synchronized
或ReentrantLock
等本地锁解决并发问题。但在分布式架构下,服务部署在多个节点,本地锁只能控制单个 JVM 内的线程,无法阻止其他节点对共享资源的访问。例如:
- 多台服务器同时操作同一个数据库记录
- 分布式任务调度中避免重复执行
- 秒杀系统中防止超卖
此时就需要一种跨节点的 "全局锁"------分布式锁,而 Redis 凭借以下优势成为常用方案:
- 高性能:内存操作,响应速度快
- 原子性:单线程模型保证基本操作的原子性
- 易用性:丰富的命令支持快速实现锁逻辑
二、Redis 分布式锁的核心原理
你的笔记中提到 "基于 set nx 命令实现",这是 Redis 分布式锁的灵魂。我们从最基础的逻辑逐步拆解:
1. 锁的获取:setnx 命令的关键作用
Redis 的SETNX
命令(SET if Not Exists
)定义为:当 key 不存在时设置 key-value,返回 1;若 key 已存在则不操作,返回 0 。这个命令的核心价值在于:将 "判断 key 是否存在" 和 "设置 key" 两个操作合并为一个原子操作。
举个例子:
ruby
# 客户端1尝试获取锁,key不存在,设置成功(获取锁成功)
127.0.0.1:6379> SETNX lock:order 1
(integer) 1
# 客户端2尝试获取锁,key已存在,设置失败(获取锁失败)
127.0.0.1:6379> SETNX lock:order 1
(integer) 0
如果不使用SETNX
,而是分两步执行(先GET
判断是否存在,再SET
设置),会出现什么问题?
sql
# 高并发下的危险操作
客户端A: GET lock:order → 返回nil(锁不存在)
客户端B: GET lock:order → 返回nil(锁不存在)
客户端A: SET lock:order 1 → 成功
客户端B: SET lock:order 1 → 成功(两个客户端同时获取到锁,锁失效)
可见,SETNX
通过原子性操作从根本上避免了这种 "并发抢锁" 的问题。
2. 锁的等待与重试
当一个客户端获取锁失败时(SETNX
返回 0),它不能直接放弃,需要通过轮询重试 或阻塞等待的方式再次尝试。常见的逻辑如下:
arduino
// 伪代码:获取锁的重试逻辑
public boolean tryLockWithRetry(String lockKey, long timeout) {
long start = System.currentTimeMillis();
while (true) {
// 尝试获取锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "value");
if (success) {
return true; // 获取锁成功
}
// 超过超时时间,放弃重试
if (System.currentTimeMillis() - start > timeout) {
return false;
}
// 休眠一段时间再重试(避免频繁请求压垮Redis)
Thread.sleep(100);
}
}
这里的休眠时间需要合理设置:太短会导致 Redis 请求量激增,太长则会影响业务响应速度。
3. 锁的释放:删除 key 的注意事项
当持有锁的客户端完成业务操作后,需要释放锁 ------ 即删除对应的 key,让其他客户端可以获取锁。最简单的方式是执行DEL
命令:
ruby
127.0.0.1:6379> DEL lock:order
(integer) 1
但直接删除存在一个严重问题:如果持有锁的客户端因故障(如宕机、网络中断)未释放锁,这个 key 会永远存在,导致其他客户端永远无法获取锁(死锁) 。
补充一个关键优化:给锁设置过期时间 。在获取锁时,通过SET
命令的扩展参数(Redis 2.6.12 + 支持)同时设置过期时间,确保锁能自动释放:
ruby
# 等价于SETNX + EXPIRE,但为原子操作
127.0.0.1:6379> SET lock:order 1 NX PX 30000
OK
NX
:等价于SETNX
的效果PX 30000
:设置过期时间为 30 秒(单位毫秒)
这样即使客户端异常崩溃,30 秒后锁也会自动删除,避免死锁。
三、原子性问题:从获取到释放的全链路保障
你的笔记提到 "set 和 get 操作不是原子操作",这确实是分布式锁实现的核心挑战。但原子性问题不仅存在于获取锁阶段,释放锁阶段同样需要警惕。
1. 释放锁的原子性风险
假设一个场景:
- 客户端 A 获取锁,设置过期时间 30 秒
- 客户端 A 业务执行缓慢,30 秒后锁自动过期,客户端 B 获取到锁
- 客户端 A 执行完毕,执行
DEL
命令释放锁,此时删除的是客户端 B 持有的锁!
问题根源:释放锁时没有判断锁的归属 。解决方案是给每个锁设置唯一标识(如 UUID),释放前先检查标识是否匹配,再执行删除。
但 "判断 + 删除" 两步操作本身也不是原子的,需要用LuaLua 脚本保证原子性(Redis 会将整个 Lua 脚本作为一个原子操作执行):
vbnet
-- 释放锁的Lua脚本:如果value匹配则删除key
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
在 Java 中调用示例:
ini
String lockKey = "lock:order";
String uniqueId = UUID.randomUUID().toString(); // 唯一标识
// 释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Integer.class),
Collections.singletonList(lockKey),
uniqueId
);
四、Redis 分布式锁的核心逻辑梳理(伪源码视角)
结合上述分析,我们可以梳理出一个相对完善的 Redis 分布式锁实现逻辑(类似 Redisson 等框架的核心思路):
1. 获取锁的核心流程
arduino
public String acquireLock(String lockKey, long expireTime, long waitTime) {
String uniqueId = UUID.randomUUID().toString(); // 生成唯一标识
long start = System.currentTimeMillis();
while (true) {
// 原子性操作:设置key(NX)并设置过期时间(PX)
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uniqueId, expireTime, TimeUnit.MILLISECONDS);
if (success) {
return uniqueId; // 获取锁成功,返回唯一标识
}
// 超过等待时间,返回失败
if (System.currentTimeMillis() - start > waitTime) {
return null;
}
// 休眠后重试(可优化为随机时间,避免惊群效应)
Thread.sleep(50);
}
}
2. 释放锁的核心流程
typescript
public boolean releaseLock(String lockKey, String uniqueId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
Integer result = redisTemplate.execute(
new DefaultRedisScript<>(script, Integer.class),
Collections.singletonList(lockKey),
uniqueId
);
return result != null && result > 0;
}
五、进阶思考:Redis 分布式锁的局限性
尽管 Redis 分布式锁应用广泛,但仍有一些场景需要特别注意:
-
锁超时问题
如果业务执行时间超过锁的过期时间,锁会被自动释放,可能导致多个客户端同时操作资源。解决方案可以是:
- 合理评估业务时间,设置足够长的过期时间
- 实现 "锁续期" 机制(如 Redisson 的 Watch Dog,定期延长锁的过期时间)
-
Redis 集群的一致性问题
在主从架构中,如果主节点宕机,从节点尚未同步锁信息就被提升为主节点,可能导致多个客户端同时获取到锁("脑裂" 问题)。Redlock 算法尝试解决这个问题,但实现复杂且性能开销较大,需根据业务场景权衡。
-
非可重入性
基础的 Redis 分布式锁是 "非可重入" 的(同一客户端再次获取锁会失败)。如果需要可重入性,需在 value 中记录锁的持有次数,获取时累加,释放时递减(类似 ReentrantLock 的实现)。
六、总结
Redis 分布式锁的核心是通过SET NX PX
命令实现原子性的 "抢锁",并通过 Lua 脚本保证释放锁的安全性。总结几个关键要点:
SETNX
(或SET NX
)是实现锁的基础,解决了并发抢锁的原子性问题- 必须给锁设置过期时间,避免死锁
- 释放锁时需通过唯一标识 + Lua 脚本,防止误删其他客户端的锁
- 实际应用中需考虑超时、可重入性、集群一致性等进阶问题
掌握这些原理后,再去阅读 Redisson 等框架的源码,就能更清晰地理解其设计思路。分布式锁看似简单,但细节处理不当很容易出现隐藏 bug,建议在生产环境优先使用成熟的开源框架(如 Redisson),而非重复造轮子。