51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

实现一个分布式锁,请说明使用Redis的SETNX命令的线程安全性,并分析其可能的缺陷(如死锁)及改进方案。

阅文集团JAVA开发工程师难度:中等

答案

1) 【一句话结论】基于Redis SETNX的分布式锁在单命令层面具备线程安全性,但未考虑超时、异常和重入场景,易导致死锁或锁未释放,需结合TTL、Lua脚本原子化操作及重试策略改进。

2) 【原理/概念讲解】老师口吻:Redis的SETNX(Set if Not eXists)是原子命令,当键不存在时设置键值并返回1,否则返回0。这保证了“加锁”操作的单线程原子性——同一时间只有一个线程能成功获取锁。但在分布式锁场景中,锁的完整流程是“加锁-业务执行-解锁”三步,若仅依赖SETNX,未处理超时或业务中断,会导致死锁。类比:SETNX像一把“单门锁”,能保证同一时间只有一个线程能进入(原子性),但如果门没锁(没设置过期时间),其他线程可能永远等不到,或者业务执行超时后锁没释放,导致死锁。此外,递归调用时若不处理重入,会因计数错误引发死锁,需通过线程本地变量记录锁次数。

3) 【对比与适用场景】

方案定义特性使用场景注意点
SETNXRedis原子命令,若键不存在则设置单命令原子性,无锁竞争时高效低并发、简单场景未设置过期时间易死锁,无重入支持
改进方案(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) 【追问清单】

  • 问题:死锁的具体场景?
    回答要点:业务执行超时或异常导致锁未释放,其他线程一直等待。
  • 问题:如何处理重入?
    回答要点:通过线程本地变量记录锁次数,加锁时检查当前线程是否已持有锁,避免死锁。
  • 问题:Redis故障时如何恢复?
    回答要点:结合TTL和重试逻辑,确保锁超时后自动释放,避免永久阻塞。
  • 问题:解锁异常如何处理?
    回答要点:使用finally块或Lua脚本确保解锁,避免业务中断导致锁未释放。
  • 问题:与Redlock相比有什么区别?
    回答要点:SETNX是单命令,Redlock是多实例协调,Redlock更可靠但实现复杂。

7) 【常见坑/雷区】

  • 忘记设置过期时间导致死锁。
  • 未考虑重入导致死锁(递归调用时计数错误)。
  • 解锁时误删其他线程的锁(锁键名错误)。
  • 业务逻辑中未确保最终解锁(try-finally未正确执行)。
  • 误以为SETNX本身解决了所有问题,忽略业务流程的原子性。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1