php文件缓存需在fopen后、fwrite前加flock写锁,读时也应加共享锁;推荐用apcu内存缓存替代,其apcu_store/apcu_fetch为原子操作;缓存键须唯一稳定,含用户角色等上下文;防击穿需cache_lock模式,设合理超时。

用 flock() 给缓存文件加锁再读写
直接 open-write-close 会丢数据,尤其并发请求同时更新同一缓存键时。PHP 自带的 flock() 是最轻量、最可靠的文件级互斥方案。
关键不是“要不要锁”,而是“锁在哪一层”:必须在 fopen() 后、fwrite() 前加写锁,且读缓存时也建议加共享锁(避免读到写一半的脏数据)。
- 写缓存:先
fopen($file, 'c')(不截断),再flock($fp, LOCK_EX),然后fseek($fp, 0)+ftruncate($fp, 0)+fwrite() - 读缓存:用
fopen($file, 'r')后立即flock($fp, LOCK_SH),读完flock($fp, LOCK_UN) - 锁必须配合
fclose()显式释放;脚本结束时 PHP 会自动释放,但别依赖它——超时或异常可能让锁滞留
用 apcu_store() 替代文件缓存,天然避冲突
如果服务器启用了 APCu 扩展(PHP 7.0+ 默认内置),直接用内存缓存比文件安全得多:apcu_store() 和 apcu_fetch() 都是原子操作,不存在并发写覆盖问题。
注意它不跨进程持久化(重启 Web 服务就清空),适合存临时性、可重建的数据,比如模板片段、配置快照。
立即学习“PHP免费学习笔记(深入)”;
-
apcu_store('user_123', $data, 300)—— 第三个参数是 TTL 秒数,设为 0 表示永不过期(慎用) - 检查是否存在用
apcu_exists('key'),比apcu_fetch()+ 判空更高效 - 不要存资源句柄(如
mysqli对象)、闭包或含循环引用的数组,会触发警告甚至崩溃
缓存键设计不当引发逻辑冲突
缓存键重复或歧义,会导致 A 请求写入的数据被 B 请求误读——这不是并发问题,是设计漏洞。
典型错误:用 $_GET['id'] 直接拼接键名,没过滤、没标准化;或忽略用户身份(如未登录用户和管理员看到同一缓存页)。
- 键名必须唯一且稳定:推荐用
md5(sprintf('%s_%s_%s', $prefix, $id, $user_id ?: 'guest')) - 涉及权限或上下文的缓存,务必把关键维度(如
user_role、lang、is_mobile)纳入键生成逻辑 - 避免使用动态时间戳、随机数、session_id 等不可复现值作为键的一部分,否则缓存命中率归零
用 cache_lock 模式防缓存击穿与并发重建
当缓存失效后多个请求同时发现没数据,全去查数据库重建缓存,这就是击穿。更糟的是,它们还可能把不同结果写进同一缓存键,造成数据错乱。
标准解法是「占坑」:第一个请求拿到锁后查库并写缓存;其余请求等待锁释放后直接读新缓存。
- 文件锁实现:用
sys_get_temp_dir() . '/cache_lock_' . md5($key)作锁文件路径,flock()控制进入临界区 - APCu 实现:用
apcu_add('lock_'.$key, true, 10)尝试设锁(仅当 key 不存在时成功),失败则usleep(10000)后重试 - 锁超时必须设(如 10 秒),防止死锁;但也不能太短,要大于预期 DB 查询耗时
真正难的不是加锁动作本身,而是锁粒度——锁太粗(如全局锁)拖慢吞吐,锁太细(如每字段一锁)增加维护成本。生产环境建议按业务域分组锁,比如所有「用户中心」相关缓存共用一个锁前缀。











