
1) 【一句话结论】基于Redis SETNX的分布式锁在单命令层面具备线程安全性,但未考虑超时、异常和重入场景,易导致死锁或锁未释放,需结合TTL、Lua脚本原子化操作及重试策略改进。
2) 【原理/概念讲解】老师口吻:Redis的SETNX(Set if Not eXists)是原子命令,当键不存在时设置键值并返回1,否则返回0。这保证了“加锁”操作的单线程原子性——同一时间只有一个线程能成功获取锁。但在分布式锁场景中,锁的完整流程是“加锁-业务执行-解锁”三步,若仅依赖SETNX,未处理超时或业务中断,会导致死锁。类比:SETNX像一把“单门锁”,能保证同一时间只有一个线程能进入(原子性),但如果门没锁(没设置过期时间),其他线程可能永远等不到,或者业务执行超时后锁没释放,导致死锁。此外,递归调用时若不处理重入,会因计数错误引发死锁,需通过线程本地变量记录锁次数。
3) 【对比与适用场景】
| 方案 | 定义 | 特性 | 使用场景 | 注意点 |
|---|---|---|---|---|
| SETNX | Redis原子命令,若键不存在则设置 | 单命令原子性,无锁竞争时高效 | 低并发、简单场景 | 未设置过期时间易死锁,无重入支持 |
| 改进方案(Lua脚本) | 结合Lua脚本实现加锁-业务-解锁原子流程 | 通过脚本保证原子性,支持重入、异常处理 | 高并发、业务复杂场景 | 脚本复杂度影响性能,需确保Lua版本兼容 |
4) 【示例】
伪代码(带重入与异常处理):
// 线程本地变量记录当前线程持有的锁次数
ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
Integer currentCount = lockCount.get();
lockCount.set(currentCount + 1);
// 加锁逻辑(Lua脚本保证原子性)
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
" redis.call('incr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList("lock-key"), "lock-value", 10);
if (result == 1) {
try {
// 业务逻辑
process();
} finally {
// 解锁逻辑(Lua脚本原子性删除)
luaScript = "if redis.call('decr', KEYS[1]) == 0 then " +
" redis.call('del', KEYS[1]) " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Void.class),
Collections.singletonList("lock-key"));
// 重入支持:减少锁计数
lockCount.set(currentCount);
}
} else {
// 未获取锁,简单重试
Thread.sleep(100);
executeWithLock("lock-key");
}
5) 【面试口播版答案】
“面试官您好,基于Redis的SETNX实现分布式锁的核心是利用其单命令原子性,但存在缺陷。首先,SETNX本身是原子操作,能保证同一时间只有一个线程成功加锁,这是线程安全的基础。不过,分布式锁的关键是锁的持久性和业务流程的完整性,若不设置过期时间或业务执行异常,会导致死锁。改进方案是在加锁时设置TTL,同时通过Lua脚本实现加锁-业务-解锁的原子流程,支持重入,并确保异常时锁能被正确释放。总结来说,SETNX适合低并发场景,但需注意超时、异常和重入等工程细节。”
6) 【追问清单】
7) 【常见坑/雷区】