缓存键需标准化sql并拼接参数json,避免冲突;文件缓存须加锁防并发写入,嵌入逻辑过期时间而非依赖文件mtime,并按哈希分目录存储以提升效率。

直接用文件缓存是最简单可行的起点,但必须处理好过期判断和并发写入,否则上线后会出现数据错乱或缓存永远不更新的问题。
缓存键怎么生成才不容易冲突
不能直接用原始 SQL 字符串当键——空格、换行、注释稍有不同就导致键不同;也不能只用 md5($sql),因为 SELECT * FROM users WHERE id = 1 和 SELECT name,email FROM users WHERE id = 1 语义不同,但哈希后可能撞车。
- 推荐做法:对 SQL 做标准化处理——去除多余空白、统一关键字大写、把 WHERE 条件按字母序重排(如
cat=2&status=1),再拼上参数值的json_encode()结果 - 例如:
$key = 'query:' . md5($normalized_sql . json_encode($params, JSON_THROW_ON_ERROR)); - 务必排除连接选项(如
PDO::ATTR_TIMEOUT)和 fetch 模式(如PDO::FETCH_ASSOC)对键的影响,它们应单独参与 TTL 决策,而非混入键本身
文件缓存读写必须加锁
多个请求同时发现缓存失效,会并发执行数据库查询并尝试写同一个缓存文件,结果是最后写入者胜出,其他人的数据被覆盖——轻则缓存内容错乱,重则写入一半的序列化字符串导致后续 unserialize() 失败报错。
- 写入前调用
flock($fp, LOCK_EX),确保同一时间只有一个进程在写该文件 - 读取时也建议加
LOCK_SH,尤其在 PHP-FPM 多 worker 场景下,避免读到正在被写入的“半截”文件 - 不要依赖
file_put_contents(..., FILE_APPEND)自带的原子性——它不保证整个写入过程的完整性
过期检查不能只靠文件修改时间
仅用 filemtime() 判断是否过期,会带来两个问题:一是无法区分“刚写入但还没来得及设权限”的临时状态;二是无法支持“逻辑过期”(比如缓存内容仍有效,但需后台刷新)。
- 标准做法:缓存文件内容里嵌入完整结构,例如
serialize(['data' => $result, 'expire_at' => time() + 300]) - 读取时先
unserialize(),再检查$cached['expire_at'] > time(),而不是查文件时间戳 - 这样可支持运行时动态调整 TTL,也避免因系统时间回拨或 NFS 时间同步延迟导致误判
缓存失效时别忘了清理旧文件
每次写新缓存不会自动删旧文件,长期运行后缓存目录里会堆满过期文件,既占磁盘又拖慢 scandir() 类操作。
- 不要等“磁盘爆了”才处理——在
set()方法里顺手unlink()同名旧文件(加锁后) - 定期用
find /path/to/cache -name '*.cache' -mmin +60 -delete清理陈旧文件,但别在高并发请求路径里做scandir()全量扫描 - 更稳妥的方式是把缓存文件按哈希前两位分目录存储,例如
cache/ab/abcd1234.cache,避免单目录文件数过多影响 inode 查找效率
真正难的不是“怎么把数据存进文件”,而是“什么时候删、删哪些、删完之后有没有人还在读”。缓存逻辑一旦嵌入业务代码,这些边界条件就会在流量高峰时集中爆发。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











