⚡ Redis面试题分布式锁概述

在分布式系统架构中,Redis面试题分布式锁是后端开发面试中的绝对高频考点。随着微服务架构的普及,单机同步锁(如Java的synchronized或ReentrantLock)已无法满足多节点并发控制的需求。因此,基于Redis实现的分布式锁成为了互联网大厂面试中检验候选人高并发编程能力的试金石。

本文将围绕Redis面试题分布式锁的核心知识点,从基础原理到高级实现,从理论分析到代码实战,全方位拆解这一技术难点。我们不仅关注“怎么做”,更关注“为什么这么做”,帮助读者在面试中展现出深度的技术思考能力。

为什么需要分布式锁?

在集群环境下,多个服务实例同时访问共享资源(如数据库记录、文件、缓存),必须保证同一时刻只有一个实例能修改资源,防止数据不一致。

Redis锁的优势

高性能(内存操作)、简单易用(命令少)、生态丰富(Redisson等成熟框架)。相比Zookeeper,Redis在读取性能上更具优势,但需要处理网络分区问题。

核心挑战

互斥性(同一时刻只有一个客户端持有锁)、安全性(锁只能被持有者释放)、死锁避免(设置过期时间)、高可用(主从切换导致锁丢失)。

⚙️ 底层原理与SETNX命令

理解Redis面试题分布式锁的基础,必须从原生命令开始。最基础的实现方式是使用 SET key value NX PX milliseconds 命令。

1. 互斥性实现

利用 SETNX(SET if Not eXists)命令。如果键不存在,则设置键并返回1;如果键已存在,则不执行任何操作并返回0。这保证了只有一个客户端能成功获取锁。

2. 防止死锁

必须设置过期时间(TTL)。如果客户端在持有锁期间崩溃,而没有设置过期时间,锁将永远存在,导致其他客户端无法获取锁,形成死锁。使用 PX 参数设置毫秒级过期时间。

3. 原子性问题

早期的实现中,SETNX 和 EXPIRE 是分开的两条命令。如果 SETNX 成功但 EXPIRE 失败,就会导致死锁。Redis 2.6.12 之后,SET 命令支持 NX 和 EX/PX 参数,保证了设置值和过期时间的原子性。

获取锁

SET resource_name my_random_value NX PX 30000

释放锁(需使用Lua脚本保证原子性)

if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

注意:释放锁时必须校验 value 是否匹配,防止误删其他客户端持有的锁。这就是为什么获取锁时要生成一个唯一的 random_value(如UUID)。

🚀 Redisson深入解析

在生产环境中,我们通常不会手写原生命令,而是使用 Redisson 框架。它是Redis官方推荐的Java客户端,提供了丰富的分布式锁实现,是Redis面试题分布式锁中的进阶考点。

可重入锁
看门狗机制
读写锁

可重入锁 (Reentrant Lock)

Redisson的 RLock 实现了Java的 java.util.concurrent.locks.Lock 接口。支持可重入性,即同一个线程可以多次获取同一把锁,锁计数器会增加;释放时计数器减少,直到为0才真正释放锁。

实现原理:使用Hash数据结构存储锁信息。Key是锁资源名,Field是线程ID+UUID(唯一标识),Value是重入次数。获取锁时,如果Field匹配,Value+1;否则设置为1。

RLock lock = redisson.getLock("myLock");
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}
                        

看门狗机制 (Watch Dog)

这是Redisson最核心的特性之一,用于解决Redis面试题分布式锁中“业务执行时间超过锁过期时间”的问题。

当调用 lock.lock() 且不指定过期时间时,Redisson默认设置锁过期时间为30秒(lockWatchdogTimeout)。同时,启动一个后台定时任务(默认每隔10秒),检查线程是否仍持有锁,如果持有,则将锁的过期时间重新延长至30秒。

这确保了只要客户端还活着,锁就不会过期,从而避免了业务未执行完锁就失效导致的多线程并发问题。

读写锁 (ReadWriteLock)

Redisson还提供了 RedissonReadWriteLock。读锁之间不互斥,写锁与其他所有锁都互斥。适用于读多写少的场景,能显著提升吞吐量。

RReadWriteLock rwLock = redisson.getReadWriteLock("myRWLock");
// 获取读锁
rwLock.readLock().lock();
try {
    // 读操作
} finally {
    rwLock.readLock().unlock();
}
// 获取写锁
rwLock.writeLock().lock();
try {
    // 写操作
} finally {
    rwLock.writeLock().unlock();
}
                        

⚠️ Redlock算法争议

在Redis面试题分布式锁中,面试官常会追问:“Redis主从切换时,锁会不会丢失?” 这就引出了Redis作者提出的 Redlock 算法。

问题背景

在异步复制模式下,客户端A从Master获取锁,Master宕机前未将锁同步到Slave。Slave提升为Master,客户端B也能获取锁。导致A和B同时持有锁,破坏互斥性。

Redlock算法原理

客户端向N个独立的Redis节点(通常N=5)依次请求获取锁。只有当客户端在大部分节点(N/2+1)成功获取锁,且总耗时小于锁有效期时,才认为获取锁成功。释放锁时向所有节点发送释放命令。

争议与评价

Redlock在学术界和工业界存在争议。有人认为它在极端网络分区下仍不安全;也有人认为在生产环境中,Redis主从切换频率极低,且可以通过监控和重试机制缓解,Redlock过于复杂,性价比不高。目前主流观点是:对于强一致性要求极高的场景,建议使用Zookeeper;对于大多数互联网场景,单主Redis或Redisson的简单实现已足够。

🔥 高并发场景实战与避坑

在实际项目中,Redis面试题分布式锁的应用场景多种多样,以下是几个典型场景及解决方案。

1. 库存扣减(超卖问题)

电商秒杀场景中,高并发下库存扣减必须保证原子性。除了分布式锁,还可以结合Lua脚本或Redis原子操作(DECR)来实现,性能更高。

方案 优点 缺点 适用场景
Redis分布式锁 实现简单,互斥性强 性能开销较大,依赖Redis可用性 写少读多,逻辑复杂
Lua脚本原子操作 高性能,无网络往返 逻辑复杂时脚本难维护 简单计数,状态机
数据库乐观锁 数据强一致 冲突率高时性能差 低并发,强一致性

2. 接口幂等性

防止重复提交。在请求头中生成唯一ID,作为Redis锁的Key。获取锁成功后执行业务,释放锁。如果获取锁失败,说明请求正在处理或已处理,直接返回。

3. 定时任务分布式执行

在集群部署的定时任务,只需在一个节点执行。使用分布式锁,Key为任务名称,过期时间略长于任务执行时间。获取锁成功的节点执行任务,失败的节点跳过。

❓ 高频面试题Q&A

以下整理了互联网大厂关于Redis面试题分布式锁的高频问题及深度解答。

Q1: Redis分布式锁和Zookeeper分布式锁有什么区别?

A: Redis基于内存,性能高,但存在主从切换一致性风险;Zookeeper基于CP模型,强一致性,但性能略低,实现复杂。Redis适合读多写少、对一致性要求稍低的场景;Zookeeper适合对一致性要求极高的场景。

Q2: 分布式锁的粒度如何选择?

A: 锁的粒度越细,并发度越高,但竞争也越激烈。应根据业务逻辑选择最小必要粒度。例如,扣减库存时,按商品ID加锁,而不是全局加锁。

Q3: 如果持有锁的节点宕机了,怎么办?

A: 依靠锁的过期时间自动释放。但需注意,如果业务执行时间超过过期时间,需借助看门狗机制延长锁。或者在业务开始前检查锁状态,必要时重新获取。

Q4: Redisson的lock()方法阻塞等待的原理是什么?

A: 底层使用Pub/Sub机制。获取锁失败时,订阅锁通道的消息,然后等待。当锁释放时,发布解锁消息,阻塞的线程被唤醒并尝试重新获取锁。