flock()不适合做分布式互斥锁,因其仅限单机进程间有效、多机不可见、默认阻塞易拖垮接口、NFS等场景行为不可靠;Redis SETNX+EX才是可靠方案。

PHP 缓存击穿问题,本质是高并发下缓存失效瞬间大量请求穿透到数据库。用互斥锁(Mutex)能有效缓解,但关键不在于“加锁”,而在于锁的粒度、生命周期和失败回退逻辑是否合理。
为什么 flock() 不适合做分布式互斥锁
本地文件锁 flock() 在单机 PHP-FPM 进程间能起作用,但遇到多台 Web 服务器或容器化部署时完全失效——各机器操作的是不同文件句柄,锁彼此不可见。更严重的是,flock() 默认阻塞,若缓存重建耗时长,后续请求会卡在 flock() 上,拖垮整个接口响应。
- 仅适用于单机 CLI 脚本或极简单机环境
- 未设置超时会导致请求堆积,形成雪崩前兆
- 锁文件权限、NFS 挂载等场景下行为不可靠
Redis SETNX + 过期时间才是可靠方案
用 SET key value EX seconds NX 命令实现带自动过期的原子加锁,是 PHP 中最常用且可落地的互斥方式。它规避了先 GET 再 SET 的竞态,也避免了死锁风险(EX 确保锁必释放)。
-
NX保证只有 key 不存在时才写入,天然互斥 -
EX时间应略大于缓存重建耗时(如重建平均 800ms,设为 2s),防锁残留 - 锁 value 必须是唯一标识(如
uniqid()),用于解锁时校验所有权,防止误删 - 解锁必须用 Lua 脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key lock_value
PHP 实现中容易漏掉的三个细节
很多代码只写了加锁和查缓存,却忽略重试、降级和锁清理的衔接。真实业务里,锁获取失败不能直接报错,而要走兜底逻辑。
立即学习“PHP免费学习笔记(深入)”;
- 加锁失败后,不应立即返回错误,而是
usleep(50000)后重试 2–3 次,再 fallback 到直连 DB(避免全部请求排队) - 缓存重建成功后,必须先
SET缓存,再DEL锁;顺序颠倒会导致新请求读到空缓存又触发重建 - 如果重建过程异常退出(如 DB 查询失败),需主动
DEL锁,否则该 key 将被锁死直到 EX 过期——这正是“假成功真阻塞”的根源
互斥锁不是银弹。锁太粗(如整个商品类目共用一把锁)会降低并发度;锁太细(如每个 SKU 都独立锁)又增加 Redis 压力。真正难的,是结合业务流量特征去压测锁等待时间与缓存命中率的平衡点。











