直接缓存SELECT...LIMIT结果易出错,因缓存键若未包含完整分页上下文(如查询条件哈希、排序字段、状态标记、版本号等),会导致页数据混用或漏/重;底层数据变更后缓存未同步失效,总数与列表key必须分离并主动更新。

为什么直接缓存 SELECT ... LIMIT 查询结果容易出错
因为分页参数(如 $page、$per_page)变化时,缓存键若没包含完整分页上下文,就会混用不同页的数据。更隐蔽的问题是:当底层数据增删后,第2页的原始记录可能已变成第1页末尾——但缓存里还固执地返回旧的“第2页”,导致漏数据或重复。Memcached 本身不感知 SQL 语义,它只认 key-value,所以关键不在“存什么”,而在“key 怎么算”。
memcached_get() 前必须构造带业务维度的缓存键
不能只拼 "page_{$page}_{$per_page}",得把影响分页结果的所有变量都塞进去:查询条件哈希、排序字段、数据状态标记(比如是否含软删除)、甚至数据库主从延迟容忍度。推荐做法:
- 用
md5()对 SQL 查询主体(不含LIMIT和OFFSET)做哈希,例如md5("SELECT id,name FROM user WHERE status=1 ORDER BY created_at DESC") - 把分页参数转为标准化字符串:
"p{$page}_l{$per_page}",避免10,20和010,020被当成不同 key - 最终 key 形如:
"pg_{$sql_hash}_{$page_spec}_v2",版本号v2用于后续逻辑变更时批量失效
缓存未命中时,要原子性写入并防穿透
多个并发请求同时发现缓存为空,若都去查库再写缓存,会引发雪崩。PHP 中没有原生 CAS 操作,得靠 Memcached 的 add() 配合短时锁:
- 先尝试用
$memcached->add($lock_key, 1, 3)获取 3 秒锁(失败说明别人正在生成) - 获取成功则执行真实查询 +
$memcached->set($data_key, $result, 3600) - 无论成败,最后都调用
$memcached->delete($lock_key)清锁(注意不是remove) - 如果锁被占满 3 秒还没释放,可降级为直查数据库,不等缓存
总数统计不能和列表共用一个缓存 key
分页必须知道总条数才能算页码,但 COUNT(*) 和实际 SELECT 的条件可能因权限、状态过滤产生差异。常见错误是缓存了 count 却忘了更新——比如用户删了一条记录,列表缓存过期了,但总数还挂着旧值,导致最后一页显示空内容。
立即学习“PHP免费学习笔记(深入)”;
稳妥做法是:
- 总数 key 单独生成,例如
"cnt_{$sql_hash}_v2",过期时间比列表略长(如 7200 秒) - 在数据变更时(如
INSERT/UPDATE/DELETE),主动$memcached->delete()对应的 count key 和所有相关 page keys - 绝不依赖“查一次列表顺带 COUNT”来省事——网络延迟、事务隔离级别都可能导致两者不一致
缓存分页最麻烦的从来不是怎么存,而是怎么让 key 的生命周期和业务语义对齐;少一个条件哈希,多一个版本号,差的可能就是线上查不到最新数据。



















