在分布式系统中,多个进程或服务常常需要互斥地访问共享资源——比如扣减库存、生成全局唯一订单号、执行定时任务等。这时候,分布式锁就成了一个绕不开的话题。在 PHP 面试中,面试官经常会问:“你了解分布式锁吗?PHP 里怎么实现?Redis 和 ZooKeeper 两种方案各有什么优劣?”本文将从实际面试角度出发,系统梳理 PHP 实现分布式锁的常见方式,并深入对比 Redis 与 ZooKeeper 两种主流方案。
一、为什么需要分布式锁?
在单机环境中,我们可以用文件锁、flock()、信号量或者数据库事务来保证互斥。但在多台服务器、多个 PHP-FPM 进程甚至跨机房部署的场景下,这些手段就失效了。分布式锁需要满足几个基本条件:
- 互斥性:任意时刻只有一个客户端能持有锁。
- 安全性:锁只能由持有者释放,不能误删他人的锁。
- 可用性:锁服务本身要具备一定的高可用能力,避免单点故障。
- 容错性:客户端崩溃或网络分区时,锁最终能够被释放,不能造成死锁。
二、PHP 中基于 Redis 实现分布式锁
Redis 是 PHP 项目中最常见的分布式锁载体,主要因为大多数 PHP 项目本身就在使用 Redis 做缓存或队列,引入成本低。
2.1 基础实现:SET NX EX
早期很多人用 SETNX + EXPIRE 两条命令,但这两条命令不是原子的,如果 SETNX 成功后进程崩溃,锁就永远不会过期。从 Redis 2.6.12 开始,SET 命令支持 NX 和 EX 参数,可以原子地完成“不存在则设置并设置过期时间”:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lockKey = 'lock:order:123';
$token = bin2hex(random_bytes(16)); // 唯一标识持有者
$ttl = 10; // 秒
$acquired = $redis->set($lockKey, $token, ['nx', 'ex' => $ttl]);
if ($acquired) {
try {
// 执行业务逻辑
} finally {
// 释放锁:必须校验 token,防止误删他人锁
$lua = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
$redis->eval($lua, [$lockKey, $token], 1);
}
}
关键点:
- 加锁必须原子,
SET NX EX是标准做法。 - 释放锁必须用 Lua 脚本保证“判断 + 删除”的原子性,否则可能删掉别人刚获取的锁。
- 每个客户端要有唯一 token,不能简单用固定值。
2.2 锁续期与 Redlock 争议
如果业务执行时间可能超过锁的 TTL,就需要“看门狗”机制自动续期。Redisson 等 Java 客户端内置了该能力,PHP 中一般需要自己实现定时续期,或者把 TTL 设置得足够长并做好兜底。
Redis 官方提出的 Redlock 算法要求向多个独立 Redis 节点加锁,但该算法在分布式系统领域存在较大争议(Martin Kleppmann 曾公开质疑其安全性)。在 PHP 面试中,如果能提到 Redlock 的争议点,往往是加分项。
三、PHP 中基于 ZooKeeper 实现分布式锁
ZooKeeper 是典型的 CP 系统,基于 ZAB 协议保证强一致性,天然适合做分布式协调。
3.1 实现原理
ZooKeeper 分布式锁通常利用临时顺序节点:
- 在锁目录(如
/locks/order)下创建临时顺序节点,如/locks/order/lock-000000001。 - 获取锁目录下所有子节点,按序号排序。
- 如果自己是最小节点,则获得锁;否则监听前一个节点的删除事件。
- 前一个节点被删除后,重新判断自己是否成为最小节点。
- 释放锁时删除自己的节点;客户端会话断开时,临时节点会自动删除,避免死锁。
PHP 中可以使用 ext-zookeeper 扩展:
$zk = new Zookeeper('127.0.0.1:2181');
$lockPath = '/locks/order';
// 创建临时顺序节点
$myNode = $zk->create($lockPath . '/lock-', '', [
['perms' => Zookeeper::PERM_ALL],
['flags' => Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE]
]);
// 获取子节点并排序,判断自己是否最小
// ... 省略轮询与 watch 逻辑
3.2 优势与代价
ZooKeeper 方案的优点是强一致性、无锁过期时间争议、临时节点自动释放。代价是:
- 需要额外部署和维护 ZooKeeper 集群,运维成本高。
- PHP 的 ZooKeeper 扩展使用场景相对小众,生态不如 Redis 丰富。
- 性能上,写操作需要集群多数节点确认,吞吐量低于 Redis。
四、Redis 与 ZooKeeper 方案对比
| 维度 | Redis | ZooKeeper |
|---|---|---|
| 一致性模型 | AP(主从异步复制可能丢锁) | CP(ZAB 强一致) |
| 性能 | 高,单节点可达数万 QPS | 较低,写需多数派确认 |
| 锁释放 | 依赖 TTL,可能误删或提前释放 | 临时节点,会话断开自动释放 |
| 实现复杂度 | 低,PHP 生态成熟 | 高,需额外组件和扩展 |
| 死锁风险 | TTL 到期可兜底,但需处理续期 | 基本无死锁,依赖会话机制 |
| 适用场景 | 高并发、允许极端情况下少量不一致 | 对一致性要求极高的场景 |
面试中的回答建议:
- 如果面试官问“你选哪个”,不要简单说“Redis 更好”。要结合业务:大多数 PHP 业务场景(秒杀、防重复提交、定时任务)用 Redis 足够,因为对绝对一致性要求没那么高,且团队已有 Redis 基础设施。
- 如果业务涉及金融交易、分布式选举等强一致场景,ZooKeeper 或 etcd 更合适。
- 可以补充:Redis 主从切换时可能丢失锁,如果业务不能接受,可以考虑 Redlock 或改用 ZooKeeper。
五、面试高频追问
-
Redis 锁过期了但业务没执行完怎么办?
答:看门狗续期、合理评估 TTL、业务幂等兜底。 -
释放锁时为什么要用 Lua?
答:保证“判断持有者”和“删除”两个操作的原子性。 -
ZooKeeper 的羊群效应是什么?
答:所有客户端都监听最小节点,锁释放时大量通知。解决方案是只监听前一个节点。 -
Redis 集群模式下分布式锁有什么问题?
答:主从切换可能导致锁丢失,Redlock 也有争议,需结合业务权衡。
六、总结
PHP 实现分布式锁,Redis 方案胜在简单、高性能、生态成熟,适合绝大多数业务场景;ZooKeeper 方案胜在强一致、自动释放、无过期争议,适合对一致性要求苛刻的场景。面试中,除了能写出 SET NX EX 和 Lua 释放锁的代码,更重要的是能说清楚两种方案的取舍逻辑,以及在实际业务中如何做容错设计。掌握这些,分布式锁这道题就能答得比较完整了。
未经允许不得转载:任鹏个人博客 » PHP 面试题:PHP 如何实现分布式锁及 Redis 与 ZooKeeper 方案对比

