想象一下游泳馆的更衣室:几百个储物柜排成两排,每个柜子一把挂锁。你游泳时想把手机和衣物锁进柜子,但如果两个人同时盯上同一个空柜子,就会争抢。更麻烦的是,如果你游完泳忘了拿钥匙就走人,那把锁会永远锁着,后来的人就只能干瞪眼。更恐怖的是,如果有人粗心大意,打开别人的锁,把别人的东西当成自己的取走——全场都得乱套。 这就是分布式系统里的“抢锁”难题。在计算机世界里,多个服务器进程就像多个游泳的顾客,共享的数据库记录、缓存、文件就是那一个个“储物柜”。为了避免大家同时写入造成数据错乱,我们需要一种可靠机制:同一时刻只允许一个进程操作资源,用完必须归还,万一中途意外退出,锁也要自动解开。 Redis 分布式锁就是这一套“储物柜管理体系”的数字化实现。早期版本用一条简单的 SETNX 命令充当“挂锁”,后来发现挂锁不够安全,于是陆续加上过期时间、唯一标识、Lua 脚本校验、可重入计数器,最终演进到多节点共识的 Redlock 算法。这篇文章就用储物柜的比喻,把这条演进线讲透。
分布式锁是后端开发绕不开的硬核技能。掌握 Redis 锁的演进逻辑,能让你在生产环境中避免并发事故、正确设计数据一致性方案,这是从“会写 CRUD”到“能扛流量”的分水岭。
可视化演示
动态演示Redis分布式锁实现的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Redis分布式锁实现:分布式锁 分布式锁的核心需求与基本原语 锁的安全释放与Lua脚本原子性 核心机制可视化 Redis分布式锁实现整体架构与关键组件
锁的基础概念
游泳馆的更衣室里,储物柜的管理员立了一条铁规矩:任何一个储物柜,同一时刻只允许一个人使用。这就是“互斥”——你锁上柜门的时候,别人不可能再往里面塞东西。计算机世界里,多个进程同时修改同一份数据时,也需要这样一条铁规矩,否则就会出现“两个人同时把衣服塞进同一个柜子”的混乱局面。
“互斥”在单机时代很容易实现:一个内存标志位、一把操作系统锁就够了。但在分布式系统中,进程跑在不同的机器上,没有共享内存,这时候就需要一个所有进程都能访问的“中间人”来裁决——Redis 就是这样的中间人。它维护一把“虚拟锁”:锁存在时,其他进程必须等待;锁不存在时,第一个来的人可以获取它。
用数学语言描述互斥,最核心的判断只有一个:同一时刻最多只有一个进程持有锁。
回到比喻里:互斥是储物柜的铁规矩,但光有规矩还不够。如果一个人拿了钥匙却猝不及防地消失了,柜门永远打不开——这就是死锁。所以真正可靠的分布式锁,既要保证互斥,还要保证“意外发生时锁能自动解开”,这也是后续所有演进的需求来源。
L_i(t)=1),那么任何其他进程 j 都必然不持有锁(L_j(t)=0)。这就是锁的“互斥保证”。- 锁状态变量
L_i(t)表示进程 i 在时刻 t 的锁状态。值为 1 表示该进程持有锁,0 表示未持有。 - 持有锁判定当
L_i(t) = 1时,说明进程 i 此刻拥有操作共享资源的资格。这是获取锁成功的标志。 - 互斥推导对于任意两个不同进程 i 和 j,i 持有锁的同一时刻,j 的锁状态必须为 0。这就是“同一时刻只允许一个持有者”的数学表达。
SETNX初代锁
最早的 Redis 分布式锁非常朴素:储物柜管理员手里有一个登记本,上面写着“当前被占用的柜号”。顾客来存东西时,管理员先翻一眼登记本——如果柜号没被登记,就把名字写上去,把钥匙交给顾客;如果已经被登记,就摇摇头说“换一个吧”。这个“翻一眼、写一笔”的动作,在 Redis 里对应一条天然原子的命令:SETNX(SET if Not eXists,不存在才设置)。
SETNX 的本质是“只有键不存在时才能写入”。这恰恰是“先查再占”的安全版:它把“检查柜子有没有被占”和“写登记本”两个动作合并成一步,中间不会插入其他顾客。完整语义可以写成下面的分段函数:
初代锁的用法很直接:抢锁就是 SETNX lock_key owner_id,释放锁就是 DEL lock_key。在游泳馆里,这相当于“登记本上写名字”对应挂锁,“划掉名字”对应开锁。可是问题很快暴露了:如果顾客拿着钥匙突然晕倒在泳池边,登记本上的名字永远不会被划掉,那把锁也永远打不开。SETNX 不是原子操作的一部分吗?确实,但原子性救不了“忘记释放”的人为失误——进程崩溃了,DEL 命令永远不会执行。这就是初代锁最经典也最致命的缺陷:没有过期时间,死锁风险极高。
k∉D)时,SETNX 写入键值对并返回 1,代表加锁成功;当键 k 已存在(k∈D)时,什么都不做并返回 0,代表加锁失败。- 判空查询首先判断键 k 是否已经存在于数据库。这个判断与写入操作一起被封装在 SETNX 内部,不会被其他命令打断。
- 加锁成功若键不存在,则写入键值对 (k, v),并返回 1。v 通常存当前进程的唯一标识(比如 UUID),用于后续校验。
- 加锁失败若键已存在,说明锁已被其他进程持有,SETNX 不覆盖原值,直接返回 0。
超时与过期
为了解决“人消失、锁还在”的问题,储物柜管理员想出一个新办法:每把锁都装一个倒计时器。顾客挂锁时设定一个时间,比如 10 分钟;10 分钟后不管人有没有回来,锁自动弹开。这样即使顾客晕倒、跑路、彻底失联,柜子也一定能被重新使用。这个“倒计时器”对应 Redis 键的 过期时间(TTL)。
在 Redis 里,加锁从“先 SETNX 再单独 EXPIRE”演变为一条命令完成:SET lock_key owner_id NX EX 10。其中 NX 保证“只有键不存在时才设置”,EX 10 保证“锁在 10 秒后自动过期”。为什么不能用两个独立的命令?因为“加锁”和“设过期”若分开执行,中间进程一旦崩溃,锁照样没有过期时间,死锁问题依旧。合并成一条原子命令,删除了这个致命的“空窗期”。
锁的剩余存活时间,就是一个简单的减法:
有了过期时间,死锁问题迎刃而解,但新的矛盾又浮出水面:TTL 设多长合适?设太短,业务还没执行完锁就自动弹开了,别人会闯进来抢资源;设太长,一旦进程崩溃,其他进程要等待很久才能拿到锁。更麻烦的是,锁可能到了“该释放”的时候,而持锁人业务还没做完——柜门“咔哒”一声弹开,另一个顾客立刻把东西塞了进去。那时候原来的顾客回来,迷迷糊糊地以为柜子还是自己的,伸手就去开锁——他会打开谁的门?
t_exp 是锁设定的过期时刻,t_now 是当前时刻。当 t_left ≤ 0 时,Redis 自动删除该键,锁被释放。- 记录过期时刻加锁时把过期时刻设为当前时间加 TTL(存活时长)。比如当前是 12:00:00,TTL 是 10 秒,
t_exp就是 12:00:10。 - 计算剩余时间任意时刻用过期时刻减当前时刻,得到锁还剩余多少秒。这个值会被返回给客户端,用于判断锁是否即将失效。
- 到期自动释放当剩余时间小于等于 0 时,Redis 主动删除锁键,相当于倒计时归零、储物柜自动弹开。
防误删优化
想象一个更具体的场景:顾客 A 锁了 5 分钟的柜子,结果超时锁自动弹开;顾客 B 正好过来,锁上了同一个柜子。这时 A 回来了,他伸手用钥匙一拧——柜门开了,里面是 B 的衣物!A 一脸懵地把 B 的东西取走了。问题出在哪?A 的钥匙(锁值)没有标识主人身份,任何一把“通用钥匙”都能打开这把锁。
在 Redis 锁里,这个问题的根源是:释放锁时直接 DEL lock_key,完全不检查这个锁是不是当前进程加的。解决方案是给每把锁配一把“专属钥匙”——加锁时存入一个唯一标识(通常是 UUID),释放前先取出锁的值与自己的标识对比,一致才允许删除:
但“先 GET 再 DEL”存在一个新的安全隐患:从 GET 到 DEL 之间,锁可能刚好过期,另一个进程抢到了锁,这时再去 DEL 就会把别人的锁误删。要根治,必须把“比较”和“删除”合并成一个不可拆分的原子操作。Redis 原生支持 Lua 脚本,脚本内的多条命令在单线程中连续执行,中间不会插入任何其他命令——这个概念相当于管理员把“验钥匙、开柜门”设计成一个机械联动装置:钥匙插进去的一瞬间,验证和开锁同步完成,中间没有停顿,别人插不进来。
有了这个防误删的升级,Redis 锁在“释放安全”上才算是完备了。但故事还没结束:A 在业务逻辑里写了一个递归函数,外层调用了加锁的方法,内层又调用了同一个加锁方法——同一个进程想重复获取同一把锁,却被自己挡在了门外,这就引出了下一个问题:锁能不能被同一个持有者多次获取?
V(k) 等于本次操作的持有者标识 owner 时,才执行删除。否则说明锁已被别人持有,不能删。- 读取锁值先执行
GET lock_key,拿到当前锁的持有者标识。 - 身份比较把这个值与本地保存的 owner 标识进行比对。相等说明锁确实是自己加的,不相等说明锁已被别人抢走。
- 条件删除只有相等时才执行
DEL。这一条是兜底规则,不满足条件时什么都别做。
可重入设计
游泳馆里还有一种尴尬的场景:顾客 A 锁好柜子后,忽然想起外套还在泳池边,于是拿了钥匙去取,回来发现钥匙落在储物柜里了——他不得不再找管理员“再开一次”。有些高级储物柜支持“同一把钥匙可以多次打开同一把锁”,只要第一次锁上后,钥匙的主人不换人,再开门、再锁上都不会抵触。计算机里的可重入锁就是这个意思:同一个线程可以多次获取同一把锁,而不会阻塞自己。
为什么需要可重入?因为业务代码常常层层嵌套:方法 A 加锁,A 内部调用方法 B,B 又加同一把锁;如果锁不可重入,B 就会被自己挡在外面,直接死锁。解决方案是给锁加一个计数器:第一次获取时计数器从 0 变 1;同一个线程再次获取时,计数器继续加一;每次释放时减一,只有减到 0 才真正删除锁键:
在 Redis 中实现可重入锁通常有两种方式:一是将计数器和 owner 存在一个 Hash 结构里,用 HINCRBY 做自增操作;二是在应用层用线程本地变量维护计数器,只有首次加锁和最后一次释放才真正操作 Redis。无论哪种方式,核心都是“计数器归零才释放”。可重入设计解决了“自己拦自己”的问题,但到目前为止,我们讨论的仍然是单节点 Redis 上的锁——如果这台 Redis 宕机了,整个储物柜体系就瘫痪了。要不要给储物柜做“多区域备份”?
- 首次加锁进程第一次获取锁时,计数器从 0 变为 1。同时写入锁值和过期时间。
- 重入累加同一线程再次调用加锁方法时,发现当前锁的 owner 正是自己,于是计数器继续加一,不重复设置锁键。
- 释放递减每次释放锁时计数器减一。如果减到 0,说明所有嵌套层都已退出,此时才真正删除锁键。
Redlock算法
单节点的 Redis 锁有一个致命软肋:Redis 如果挂了,锁就全丢了;即使有主从架构,主节点故障时从节点尚未同步锁数据,也会导致“旧锁丢失、新锁产生”的竞态。就像你只有一间更衣室,屋顶漏水或者管理员请假,系统就崩溃。为了提升可用性,Redis 官方提出了 Redlock 算法——在多台独立部署的 Redis 实例上同时加锁,只有超过半数的实例都同意,才真正算拿到锁。
Redlock 的思路非常像“多个独立投票区”:假设有 5 间互不相通的更衣室(5 个 Redis 实例),顾客要存东西时,必须同时去 5 间更衣室挂锁,每间都挂锁成功打了勾。如果 5 间中有 3 间以上成功,那么即使某个更衣室的管理员撒谎或睡觉,剩下的柜子仍然能保证互斥。这个“过半数”的判定,在数学上就是多数派原则:
具体流程是:客户端先记录当前时间 t1,然后依次向 N 个 Redis 实例发送 SET key value NX EX ttl 命令(带很短的响应超时,比如 50ms),加完所有实例后记录 t2,Δt = t2 - t1。如果 N_ok ≥ ⌊N/2⌋+1 且 Δt < TTL,就把锁的有效时间定为 TTL - Δt。失败时,需要向所有实例发送释放锁命令——包括那些没加成功的实例,防止“部分成功”残留垃圾锁。
Redlock 用多数派换来了高可用,但它并非完美无瑕。它的主要争议点在于时钟漂移:如果某个实例的服务器时钟跳变,锁的过期时间计算就会出错。比如一台 Redis 的时钟突然往后拨了几分钟,它认为锁还有效,但实际早就过期了,另一个客户端就能趁机拿到锁。此外,Redlock 假设各实例之间完全独立,但在实际部署中,多个 Redis 可能运行在同一批物理机上,一旦宿主机宕机,多数派也会同时失效。因此 Redlock 适用于“需要更高可用性”的场景,但不能视为绝对安全的银弹。回到比喻中:多间更衣室确实让“管理员偷懒”的风险大大降低,但如果在同一座楼上,一场火灾就能烧掉全部更衣室——真实世界的工程抉择,永远是平衡的艺术。
N_ok 是成功加锁的实例数。当成功数达到多数派门槛时,锁才被认为获取成功。例如 5 个实例需要至少 3 个成功。- 计算多数门槛对 N 个实例,门槛值是
⌊N/2⌋+1。N=5 时门槛是 3,N=3 时门槛是 2。 - 校验总耗时客户端向所有实例依次发送加锁命令,记录整个过程消耗的时间 Δt。如果 Δt 超过了锁的 TTL,说明加锁过程太慢,此时即使多数派成功也不能算数。
- 计算锁的有效时长真正可用的锁时长必须扣除加锁消耗的时间,即
t_valid = TTL - Δt。否则锁还没用就已经过期了。
AI 应用场景
秒杀与库存扣减
阶段:并发控制阶段
解决的问题:电商秒杀时海量请求同时扣减库存,数据库行锁在极高并发下吞吐骤降,且多实例同时扣减会导致超卖。用 Redis 分布式锁把“扣库存”变成互斥操作,同一时刻只允许一个线程执行关键路径。
具体案例:某电商平台的大促秒杀活动中,后端服务有 20 个实例,所有实例共享同一个 Redis 锁队列。每次扣减库存前先获取 Redis 分布式锁,成功者执行数据库操作并释放锁,失败者直接返回“已抢光”。上线后超卖率降为 0,接口 QPS 峰值达 12000。
分布式定时任务防重
阶段:任务调度阶段
解决的问题:分布式部署的定时任务在每个节点都会触发同一任务,导致重复计算、重复发消息。用 Redis 锁保证同一时刻仅一个节点执行任务,其他节点检测到锁已存在便直接跳过。
具体案例:某 AI 数据平台每天凌晨对用户特征表执行全量更新,任务在 3 台机器上同时被调度器触发。通过 Redis 分布式锁实现“抢到锁的机器执行任务,抢不到的机器立即退出”,避免了重复计算造成的数据错乱,任务耗时从 2 小时降到 40 分钟。
AI 推理服务缓存重建
阶段:缓存更新阶段
解决的问题:AI 模型推理结果的缓存过期后,大量并发请求同时回源计算,造成缓存击穿和 GPU 算力浪费。用 Redis 分布式锁让只有一个请求去重建缓存,其余请求等待或返回旧值。
具体案例:某推荐系统对用户特征做模型推理,结果缓存 5 分钟过期。高峰期缓存失效瞬间有 3000 个并发请求同时触发重建,压垮推理服务。引入 Redis 锁后,仅一个请求获取锁执行重建,其余请求直接打上“缓存重建中”标记并快速返回旧值,缓存击穿问题彻底解决。
Python 演示
import timeimport uuidimport redis class SimpleLock: """基础版 Redis 分布式锁:SETNX + EXPIRE + Lua 防误删""" def __init__(self, client, key, ttl=10): self.client = client self.key = key self.ttl = ttl self.owner = str(uuid.uuid4()) def acquire(self): # SET key value NX EX ttl:原子完成加锁与过期设置 return bool(self.client.set( self.key, self.owner, nx=True, ex=self.ttl )) def release(self): # Lua 脚本:先校验持有者,再删除,保证原子性 lua = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """ ok = self.client.eval(lua, 1, self.key, self.owner) return bool(ok) class ReentrantLock(SimpleLock): """可重入版:加计数器,同一线程可多次获取锁""" def __init__(self, client, key, ttl=10): super().__init__(client, key, ttl) self.count = 0 def acquire(self): cur = self.client.get(self.key) if cur is None or cur.decode() == self.owner: self.client.set(self.key, self.owner, ex=self.ttl) self.count += 1 return True return False def release(self): if self.count <= 0: return False self.count -= 1 if self.count == 0: return super().release() self.client.expire(self.key, self.ttl) return True if __name__ == '__main__': client = redis.Redis(host='127.0.0.1', port=6379, db=0) lock = ReentrantLock(client, 'order:lock', ttl=5) if lock.acquire(): print('获取锁成功,执行临界区操作...') if lock.acquire(): print('再次获取锁成功(可重入)') print('释放锁:', lock.release()) print('释放锁:', lock.release())
return bool(self.client.set(self.key, self.owner, nx=True, ex=self.ttl))SET 命令的 NX 和 EX 选项把“加锁”和“设过期”合并成一步原子操作,避免死锁。ok = self.client.eval(lua, 1, self.key, self.owner)把 Lua 脚本整体发给 Redis 执行,Redis 单线程特性保证比较与删除之间不会有其他命令插入。if redis.call('get', KEYS[1]) == ARGV[1] thenLua 脚本中先读取锁值,和当前线程的 owner 标识比对,只有相等才继续删除。cur = self.client.get(self.key); if cur is None or cur.decode() == self.owner可重入判断:锁不存在或锁值属于自己时才允许加锁。self.count += 1可重入锁的计数器加一,记录同一线程获取锁的次数。self.client.expire(self.key, self.ttl)每次释放时刷新过期时间,防止业务未完成锁就自动过期。
常见误区
误区很多初学者认为「SETNX 获取锁」和「EXPIRE 设置过期」分成两步没关系,反正最终会设置上过期时间,锁不会死锁。
正解这两步之间存在时间窗口。如果 SETNX 执行成功后、EXPIRE 执行前进程崩溃,锁键没有过期时间,就会永久占用,死锁依旧发生。正确做法是用 SET key value NX EX ttl 一条命令原子完成,就像给储物柜挂锁的同时启动了倒计时器,中间没有任何空档。
为何常见:初学者习惯把操作拆成“先做 A 再做 B”,忽视了 Redis 命令之间可能插入崩溃或网络中断,这是分布式系统直觉还没建立时的典型错误。
误区掌握分布式锁后,很多人认为释放就是一行 `DEL lock_key`,简单粗暴,不需要多余的判断。
正解直接用 DEL 释放锁,可能误删别人的锁。典型场景:线程 A 的锁因业务超时自动过期,线程 B 抢到锁正在执行;这时 A 执行 DEL,把 B 的锁删了,另一个线程 C 又抢到锁——两个线程同时进入临界区,互斥被彻底破坏。正确做法是先校验锁值是否属于自己的 owner 标识,再执行删除,且校验与删除必须用 Lua 脚本包成原子操作。
为何常见:单机编程中“解锁”永远属于同一个持有者,不会出现“锁被偷换”的问题;分布式中锁的生命周期跨越多个进程,不校验就释放的后果被放大了。
误区既然业务可能执行很久,那把 TTL 设成 30 分钟甚至 1 小时,总不会在业务执行中途自动过期了吧?
正解TTL 越大,死锁恢复的代价越高。如果进程崩溃,其他进程要等整整 1 小时才能拿到锁。TTL 应该按业务的最长执行时间估算,并留少量余量;更成熟的做法是“看门狗”机制——后台线程在锁快过期时自动续期,保证长时间业务不被中断,同时崩溃时锁仍能较快释放。
为何常见:人们本能地怕“锁不够用”,却忽略了锁的另一面——过多保护同样会伤害系统可用性。分布式系统里每个参数都是权衡。
误区Redlock 用多数派思想解决单点故障,很多人认为它已经无懈可击,任意场景都可放心使用。
正解Redlock 仍有不少隐患。最典型的是时钟漂移——Redis 所在机器的系统时钟如果发生跳变,锁的过期时间会失真,可能让两个客户端同时持有锁。此外 Redlock 要求实例完全独立部署,如果多个实例在同一台物理机上,机器宕机会导致多数派同时失效。Redlock 只适合对可用性要求高于强一致的场景,绝不可以作为“绝对互斥”的保证。
为何常见:人们习惯把“高可用”误解为“绝对正确”。Redlock 解决的是单点故障的可用性问题,而不是分布式系统的强一致性问题,两者有本质区别。
误区实现可重入时,有人用一个布尔变量标记“是否已持有锁”,判断为 True 就允许重入。
正解布尔标记只解决了“能不能重入”的问题,却无法解决“嵌套层数”的问题。如果外层加锁、内层加锁、随后内层释放,布尔标记就会被置为 False——外层结束时想释放锁,却发现标记已经没了,无法正确释放。必须用计数器记录层数:获取加一、释放减一,归零才真正释放。这才是可重入锁的正确语义。
为何常见:“有 or 没有”的二元直觉让初学者忽略“嵌套次数”这一维度,这正是可重入设计和普通互斥设计的本质差异。
面试题
Q1. SETNX 命令在键不存在时会怎样?
- A.返回 1 并创建新键← 正确答案
- B.返回 0 并创建新键
- C.返回 1 但拒绝写入
- D.抛出键已存在的异常
答案:A。SETNX 是 "SET if Not eXists" 的缩写——键不存在时写入并返回 1,键已存在时不执行任何操作只返回 0。这就像储物柜管理员看到登记本上没有该柜号就写下名字,已经有时就不动。B 是键已存在的情况,C、D 不符合命令的真实语义。
Q2. 使用 SETNX 获取锁后,为什么必须设置过期时间?
- A.防止锁被误删
- B.防止死锁发生← 正确答案
- C.让锁能更快释放
- D.减少 Redis 内存占用
答案:B。如果锁没有过期时间,持有锁的进程崩溃后锁键将永远存在,其他进程永远无法获取锁,形成死锁。过期时间相当于给储物柜装了自动弹锁的倒计时器,即使主人消失也能自动恢复。A 是防误删解决的问题,C、D 不是关键原因。
Q3. 释放 Redis 锁时,为什么不能直接执行 DEL?
- A.直接 DEL 会阻塞 Redis
- B.可能误删别人的锁← 正确答案
- C.DEL 无法删除带过期的键
- D.直接 DEL 会触发语法错误
答案:B。锁到期后可能已被别的线程重新获取,此时直接 DEL 会把别人的锁删掉,破坏互斥。正确做法是先用唯一标识校验锁的归属,再通过 Lua 脚本原子地执行删除。比喻来说,必须确认柜门上的锁是自己那把才能打开,否则可能取出别人的东西。
Q4. 防误删操作中 Lua 脚本的关键作用是什么?
- A.加快键的删除速度
- B.保证判断和删除的原子性← 正确答案
- C.让多个 Redis 节点自动同步
- D.自动刷新锁的过期时间
答案:B。Redis 单线程执行 Lua 脚本,脚本内所有命令在执行期间不会被其他命令插入,因此“读锁值→比较→删除”三步被合并成一个原子操作,消除了判断与删除之间的竞态窗口。这不是速度问题、同步问题或续期问题,而是原子性的问题。
Q5. 可重入锁计数器的正确行为是?
- A.获取加一,释放减一← 正确答案
- B.获取减一,释放加一
- C.获取设为 1,释放设为 0
- D.获取清零,释放加一
答案:A。可重入锁用计数器记录嵌套层数:每次获取加一,每次释放减一,归零时才真正删除锁键。这对应“嵌套的储物柜使用记录”——进一层记一笔,出一层销一笔,全部销完才算结束。B、D 方向反了,C 的布尔化处理无法支持多层嵌套。
Q6. SETNX 与 EXPIRE 分两步执行的隐患是?
- A.SETNX 会覆盖已有键
- B.两步之间存在死锁窗口← 正确答案
- C.EXPIRE 会让锁立即失效
- D.SETNX 执行后无法再设过期
答案:B。SETNX 成功后、EXPIRE 执行前,如果进程崩溃,锁键将没有任何过期时间,永远无法释放,形成死锁。正确做法是 SET key value NX EX ttl 一条命令原子完成。这就好比挂锁和设定倒计时必须同时进行,不能“先挂锁再装定时器”。
Q7. Redlock 算法有效条件除了多数派成功,还需要什么?
- A.所有实例都返回成功
- B.加锁总耗时小于 TTL← 正确答案
- C.锁值必须为随机整数
- D.至少三个实例部署
答案:B。Redlock 除了要求超过半数的实例成功加锁,还要求整个加锁过程的耗时 Δt 必须小于锁的 TTL,否则锁可能在真正持有之前就已过期。有效持有时间应扣除加锁耗时,即 TTL - Δt。A 过于严苛,C 不是必要条件,D 是部署建议而非算法条件。
Q8. 仅用 SETNX 实现分布式锁存在哪些缺陷?
- A.无过期时间可能死锁← 正确答案
- B.无法区分锁的持有者← 正确答案
- C.加锁操作不是原子的
- D.释放锁可能误删他人锁← 正确答案
答案:ABD。SETNX 命令本身具备原子性,所以 C 错误。但仅用 SETNX 的锁确实存在三个问题:不设过期时间会死锁(A);锁值没有唯一标识,无法判断锁归属(B);释放时直接 DEL 可能误删他人锁(D)。这些缺陷催生了后续的 EXPIRE、Lua 脚本等演进。
Q9. 可重入锁的实现需要满足哪些条件?
- A.同一线程可多次加锁← 正确答案
- B.不同线程共享计数器
- C.每次释放计数器减一← 正确答案
- D.计数器归零才删除键← 正确答案
答案:ACD。可重入锁的核心是同一线程可以重复获取锁(A);释放时计数器递减(C);只有归零才真正删除锁键(D)。注意计数器是按持有者的身份隔离的,每个线程维护自己的计数,不同线程不共享(B 错误)。
Q10. 关于 Redlock 算法的说法哪些是正确的?
- A.需要在多个独立实例上加锁← 正确答案
- B.成功需要超过半数的锁← 正确答案
- C.完全不受时钟漂移影响
- D.锁的有效期需扣除耗时← 正确答案
答案:ABD。Redlock 在多个独立 Redis 实例上逐一加锁(A),成功数必须超过实例总数的一半(B),锁的有效时间等于 TTL 减去加锁总耗时(D)。Redlock 并非对时钟漂移免疫——时钟跳变可能导致锁过期时间失真,因此 C 错误。