PHP实现分布式锁方案:从Redis到ETCD,架构师必读的7种实战策略

目录导读
- 为什么PHP需要分布式锁?——并发场景的困境
- 分布式锁的三大核心原则(CP原则解读)
- 基于Redis SETNX的经典实现(附代码陷阱)
- Redlock算法——高可用与争议并存
- 基于数据库的乐观锁与悲观锁(适合中小项目)
- ZooKeeper/ETCD临时节点方案(强一致性首选)
- 锁的续期、防误删与可重入设计(实战高阶技巧)
- 高频问答:锁超时、主从切换、性能损耗如何解决?
为什么PHP需要分布式锁?
当多个PHP-FPM进程(或集群节点)同时操作同一资源(如秒杀库存、订单状态、缓存重建)时,本地flock() 已失效,分布式锁的本质是“跨进程互斥”,确保同一时刻只有一个执行者持有临界区权限。
分布式锁的三大核心原则
- 互斥性:任何时刻只能有一个客户端持有锁。
- 安全性:锁必须自动释放(过期时间),避免死锁。
- 容错性:当持有锁的节点崩溃时,其他节点仍能获取锁。
方案一:基于Redis SETNX的经典实现
// 加锁(PHP + Redis 扩展)
$lockKey = 'lock:order:123';
$token = uniqid('', true); // 唯一令牌,防止误删
$result = $redis->set($lockKey, $token, ['NX', 'EX' => 10]);
if ($result) {
// 执行业务代码
try {
// 业务...
} finally {
// Lua脚本原子释放锁(校验令牌)
$script = 'if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end';
$redis->eval($script, [$lockKey, $token], 1);
}
}
陷阱:必须用set原子命令(避免SETNX+EXPIRE两步);释放锁必须用Lua校验令牌,防止删除别人新获取的锁。
方案二:Redlock算法——高可用与争议
Redlock要求向5个独立Redis节点申请锁,超过半数成功即视为持有,PHP实现可用predis或redlock-php客户端。
争议点:Martin Kleppmann指出,Redis主从切换时可能丢失锁,建议仅在非强一致场景使用(如缓存击穿保护),不适用于账户扣款。
方案三:数据库乐观锁与悲观锁(按需选择)
- 悲观锁:
SELECT ... FOR UPDATE(需在事务中),适合低频操作。 - 乐观锁:
UPDATE stock SET version = version + 1 WHERE id = ? AND version = ?,适合高并发状态机流转。 PHP实现注意:PDO连接必须长期保持,且要设置超时时间(innodb_lock_wait_timeout)。
方案四:ETCD/ZooKeeper强一致性锁
使用ETCD的Lease + Create API,创建带租约的临时节点,通过Campaign实现leader选举。
// 简化版(使用 etcd-php 客户端)
$grant = $client->grant(10); // 租约10秒
$client->put('/lock/order', 'client1', ['lease' => $grant->getID()]);
// 业务完成后 revoke 租约
优势:无主从切换问题,自动续期机制完善,适合分布式事务调度。
锁的续期、防误删与可重入设计
- 续期:守护协程(
Workerman/Swoole)每3秒检查剩余TTL,若小于5秒则EXPIRE延长。 - 可重入:用
hash记录持锁线程的计数或进程ID,同进程内可再次获取。 - 防重放攻击:令牌必须包含随机数+业务标识,禁止使用固定值。
高频问答:锁超时、主从切换、性能损耗?
Q1:业务执行时间超过锁过期时间,导致锁提前被释放?
A:必须实现自动续期(如Redisson的watch dog),若业务与锁无关(如写入结果集),可设置足够大的过期时间,并在finally中释放。
Q2:Redis主从切换时锁丢失怎么办?
A:强烈要求高可靠性使用ETCD或ZooKeeper,若预算有限,可通过Redlock+双写机制缓解,但无法绝对避免。
Q3:高并发下锁性能损耗如何评估?
A:Redis锁的QPS可达百万级别,加锁逻辑性能损失<1ms;数据库锁通常耗时5-20ms(含网络+行锁),适合低频操作。
Q4:PHP-FPM多进程场景下要不要用锁?
A:单机多进程用flock+apc即可,仅跨节点部署时才需升级为Redis锁。