ThinkPHP 面试精讲:Redis 分布式锁与 Redlock 思路

在高并发业务场景中,分布式锁是面试中绕不开的高频考点,尤其是在 ThinkPHP 技术栈的岗位中,面试官往往会从「你怎么加锁」一路追问到「Redlock 到底解不解决问题」。本文从实战角度出发,梳理 Redis 分布式锁的核心要点与 Redlock 的设计思路,帮助你在面试中答出深度。

一、为什么需要分布式锁

单机环境下,使用文件锁或 swoole_lock 就能解决并发问题。但在多台 PHP-FPM 服务器同时处理请求时,本地锁无法跨进程、跨机器生效。典型场景包括:

  • 秒杀扣库存,防止超卖
  • 订单重复提交
  • 定时任务在多节点重复执行
  • 缓存击穿时的互斥重建

分布式锁的本质是:在分布式系统中,通过一个共享的存储节点,保证同一时刻只有一个客户端能持有锁。

二、Redis 分布式锁的基本实现

2.1 加锁:SET NX PX

在 ThinkPHP 中,通常通过 think-redis 扩展或原生 Redis 类操作。核心命令只有一个:

$redis->set($key, $uniqueValue, ['nx', 'px' => 10000]);

这里有三个关键点,面试时务必说清楚:

  1. NX(Not eXists):只有 key 不存在时才设置成功,保证互斥。
  2. PX(毫秒过期):必须设置过期时间,防止客户端宕机导致死锁。
  3. uniqueValue:每个客户端生成唯一值(如 uniqid() 或 UUID),用于安全释放锁。

很多候选人只答「用 SETNX」,却忽略了原子性。SETNX + EXPIRE 是两条命令,中间宕机会导致锁永不过期。必须使用 SET key value NX PX 这一条原子命令,这是面试的第一个加分点。

2.2 解锁:Lua 脚本保证原子性

释放锁时不能简单 DEL,因为可能误删别人的锁。正确做法是先比对 value,再删除,且这两步必须原子执行:

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

在 ThinkPHP 中通过 $redis->eval($lua, [$key, $value], 1) 调用。面试官常追问:「为什么不用 GET 再 DEL?」答案就是:两步之间锁可能过期并被其他客户端获取,导致误删。

2.3 锁续期与看门狗

如果业务执行时间超过锁的过期时间,锁会提前释放,其他客户端就能乘虚而入。解决方案有两种:

  • 预估足够的过期时间,简单但不可靠。
  • 看门狗机制:后台定时任务在锁未释放时自动续期。Redisson 的看门狗默认每 10 秒续期到 30 秒。PHP 中没有现成的 Redisson,可以用 Swoole 定时器或独立进程实现。

三、单机 Redis 锁的局限

上面的方案在单节点 Redis 下工作良好,但存在两个致命问题:

  1. 主从切换丢锁:客户端 A 在主节点加锁成功,主节点还没同步到从节点就宕机了,从节点升为主节点,客户端 B 就能加锁成功,此时两个客户端同时持锁。
  2. 脑裂场景:网络分区导致旧主仍可写,锁信息不一致。

这正是 Redlock 想要解决的问题。

四、Redlock 算法思路

Redis 作者 antirez 提出的 Redlock,核心思想是不依赖单一节点,而是向多个独立 Redis 节点(通常 5 个)申请锁。

4.1 加锁流程

  1. 记录当前时间 T1。
  2. 依次向 N 个独立 Redis 节点用相同的 key、value、过期时间申请锁。
  3. 统计成功获取锁的节点数,只有**超过半数(N/2 + 1)**才算加锁成功。
  4. 记录加锁完成时间 T2,计算总耗时 T2 - T1。
  5. 如果加锁成功,但有效时间 = 过期时间 – 加锁耗时已经很小或为负,则认为加锁失败,需要向所有节点释放锁。
  6. 如果加锁失败,同样向所有节点发起释放请求。

4.2 为什么是 5 个节点

5 个节点允许容忍 2 个节点故障,同时保证任意两个客户端不可能同时获得多数派。节点之间完全独立,不做主从复制,避免主从切换带来的不一致。

4.3 释放锁

向所有节点发送 Lua 脚本释放请求,无论之前是否加锁成功,都要尝试释放,避免残留。

五、Redlock 的争议与面试应对

Redlock 并非银弹。分布式系统专家 Martin Kleppmann 曾撰文质疑其安全性,主要论点包括:

  • 依赖系统时钟:如果某个节点时钟发生跳跃,锁可能提前失效。
  • GC 停顿:客户端拿到锁后发生长时间 GC,锁过期后才恢复执行,此时业务已不再受锁保护。
  • 无法提供 fencing token:即使锁有效,也无法阻止旧客户端继续写入共享资源。

antirez 则回应称,时钟跳跃可以通过运维手段规避,GC 停顿属于客户端问题,且 Redlock 的设计目标本就不是强一致。

面试中的正确姿势是:不要一味吹捧 Redlock,而是分场景回答——

  • 对效率要求高、能容忍极小概率错误的场景(如防止缓存击穿),单机 Redis 锁足够。
  • 对正确性要求极高的场景(如金融扣款),应使用 ZooKeeper、etcd 等基于共识算法的方案,或引入 fencing token 机制,在存储层做版本校验。

六、ThinkPHP 落地建议

在实际项目中,可以封装一个 RedisLock 服务类,统一处理:

  • 使用 SET NX PX 原子加锁
  • Lua 脚本安全释放
  • 可选的自动续期
  • 加锁失败的重试与退避策略(如自旋 + 随机等待)

同时注意:锁的粒度要尽量小,只锁关键资源,不要把整个业务逻辑包在锁里,否则会严重拖累吞吐。

七、总结

回答 Redis 分布式锁,可以按这个脉络展开:

  1. 从 SETNX 讲到 SET NX PX 的原子性;
  2. 从 DEL 讲到 Lua 脚本的 value 校验;
  3. 从单机锁讲到主从丢锁问题;
  4. 从 Redlock 的多节点多数派讲到它的争议;
  5. 最后落到「按一致性要求选型」的工程判断。

能把这条链路讲清楚,基本就能在面试中脱颖而出。分布式锁考的不是背命令,而是对并发、一致性、故障场景的理解深度。

未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:Redis 分布式锁与 Redlock 思路

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏