缓存必须落在fetch()/fetchall()等取数动作上,而非prepare()/execute()阶段;键需含sql模板、类型化参数(json_encode)和fetch mode;写操作须联动清理表前缀相关缓存。

PHP执行SQL查询时,缓存结果不是“加个cache=true就行”,而是必须在数据真正取出后才可安全缓存;提前在prepare()或execute()阶段缓存,会导致fetch()分批读取失效、游标状态丢失、rowCount()返回错误等实际问题。
缓存必须落在fetch()、fetchAll()这类取数动作上
PDOStatement支持多次fetch()、游标滚动(PDO::CURSOR_SCROLL)、不同fetch mode(如PDO::FETCH_ASSOC),这些行为直接影响结果结构。若只在execute()后缓存,后续调用fetch()仍会触发数据库交互,缓存形同虚设。
- 代理对象需拦截
fetch()、fetchAll()、fetchColumn()、fetchObject()等方法,首次调用时执行真实查询 + 序列化存入缓存 - 后续相同语句+参数+fetch mode的调用,应直接从缓存还原数组或模拟游标状态,而非复用PDOStatement
- 必须重写
rowCount()——它不再反映数据库实时行数,而应从缓存结果中计算 - 若启用
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,缓存需保存完整结果集,否则fetch()带偏移时会出错
缓存键必须包含SQL模板 + 类型化参数 + fetch mode
仅对SQL字符串做md5()哈希极易冲突:SELECT * FROM users WHERE id = ?和SELECT * FROM users WHERE id = ? ORDER BY name会被视为同一键;更危险的是,传入1(int)和"1"(string)在PDO中可能触发不同字段类型转换,但serialize()对二者输出差异极小,json_encode($params, JSON_THROW_ON_ERROR)才是可靠选择。
- 缓存键组成:
md5($sql . '|' . json_encode($params, JSON_THROW_ON_ERROR) . '|' . $fetchMode) - 若SQL含
LIMIT或OFFSET,必须纳入键计算,否则分页结果互相覆盖 - 不要把连接选项(如
PDO::ATTR_TIMEOUT)塞进键里——它们不影响结果一致性,只影响缓存TTL设置 - 避免使用
var_export()或print_r()生成键,它们输出不可控、含空格与换行,破坏一致性
写操作后必须主动清理关联缓存
缓存不清理,等于给用户喂过期数据。不能依赖TTL自动过期——业务要求“强一致性”时,TTL再短也来不及。关键是要建立“表前缀 → 缓存键”的映射关系,一旦执行INSERT/UPDATE/DELETE,就清除所有命中该表名(或别名)的缓存键。
- 例如执行
UPDATE users SET name = ? WHERE id = ?,应清除所有含"users"或"u"(若SQL中用了FROM users u)的缓存键 - 推荐在PDO封装层统一拦截
exec()和query(),解析SQL提取涉及的表名(正则足够,无需完整语法树) - 不要试图精确匹配“哪条SELECT被影响”——成本高且不可靠;宁可多清,不可漏清
- 若用Redis,可用
KEYS user_*再DEL,但生产环境慎用KEYS;更稳的方式是写入缓存时用user:profile:xxx这类带命名空间的key,并维护user:keys集合记录所有活跃键
最易被忽略的一点:缓存结果必须保留原始fetch mode语义。比如fetchAll(PDO::FETCH_KEY_PAIR)返回[id => name]结构,若缓存时只存数组而没记mode,下次用fetch()取单行时无法还原对应键值关系——这会导致业务逻辑静默出错,比缓存未命中更难排查。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











