列表页缓存与版本戳:发帖后立即生效的缓存设计
列表页缓存想做到"发帖后立刻可见",正解不是发帖后去挨个删缓存,而是给缓存键挂一个版本戳:发帖、回帖、置顶、删帖时只把对应作用域的版本号 +1,新请求自然算出一个全新的键,旧缓存没人再读、到点自动回收。删缓存是 O(n) 的扫荡,改版本戳是一次 O(1) 的自增。
为什么"发帖后删缓存"这条路走不通
结论:列表页缓存从来不是"一份缓存",而是一大片,靠主动删除永远删不干净。
一个帖子发出去,会同时影响首页列表、所属版块列表、版块内子分类筛选页、按分类排序页、每一页分页——十几二十个键起步。你既不可能精确枚举出该删哪些键,也不可能保证不漏。
再往下想更麻烦:删漏一个,用户看到"我刚发的帖子不见了";删多了,等于每次发帖都把整站列表缓存清零,缓存命中率直接崩盘。加上多键遍历本身有开销,还容易误删到别的模块的键,这条路在工程上基本是负收益。
版本戳具体怎么设计
结论:缓存键 = 作用域 + 版本号 + 参数,事件发生时只动版本号。
设计三句话就能说清:
- 一个作用域一个版本号。全局列表一个(`globalVer`),每个版块一个(`boardVer`)。
- 键的拼法固定:`list:v{globalVer}:home:p1`、`list:b{boardId}:v{boardVer}:cat3:p2`,分页、排序、分类筛选全部进键。
- 触发点只管自增:发帖/回帖/置顶/加精/关闭/删除 → 递增对应版本号,自增是 O(1) 的整数操作。
新帖发布 → 版块版本号 7 变 8 → 原来 `...:v7:...` 的所有键瞬间全部失效,请求直接算 `v8`。
版本号该存在哪里
结论:版本号不能只放缓存里,必须有一个持久、可原子自增的落脚点。
优先级从高到低:
- 数据库一行(最稳):一张 `cache_versions` 表,`scope` 做唯一键,`UPDATE ... SET ver = ver + 1`。事务内自增,重启不丢,多进程并发也安全。
- 文件:`flock` 写一个纯数字,够用但要处理权限和并发写。
- 只放缓存:最危险。缓存一被清,版本号回退,旧键"复活",用户会读到过期列表。所以版本号必须有比缓存更持久的底座。
读取侧可以再加一层进程内缓存(比如同一请求周期内只查一次库),避免每个分页请求都去打数据库。
和 Clara BBS 这类轻量论坛的契合点
结论:版本戳的设计哲学和 Clara BBS「保存即生效、无需清缓存」的路子是一脉相承的。
Clara BBS 是无框架轻量级 PHP 论坛(PHP 7.4-8.5 + MySQL 5.7+,不需要 Composer、不需要命令行、没有编译缓存),后台改配置保存即生效,插件放进 `content/plugins` 也是保存即生效、无需编译——本质都是"不要让用户去手工清理,让旧状态自然失效"。
如果你要给这类系统的列表页加缓存,可以直接复用它的公共设施:`Cache::remember` 提供缓存能力,`Cron::register` 可以跑懒触发的清理任务。手工清理仍然保留了一个出口,在后台「系统工具 → 缓存清理」。另外像 GEO 问答工厂插件那样"发布问答后自动清缓存进问答池",就是按需失效的典型场景。
这几个坑一定要避开
结论:版本戳写错的地方,九成出在键的设计和 TTL 上。
- 别用时间戳当版本号:同一秒内两个发帖会撞号,缓存不失效。用自增整数。
- TTL 必须设:版本戳只负责"逻辑失效",旧键的物理空间还得靠 TTL 回收,建议 5–15 分钟,千万别设永久。
- 分页、排序、筛选参数必须进键:少一个参数,翻到第 3 页给你第 1 页的内容。
- 用户私有状态别进公共列表缓存:像"我是否已购买此帖""我是否点过赞"这类,要么单独走一份个人缓存,要么在渲染时独立取,否则会把 A 的状态穿给 B。
- 并发递增是安全的:两个请求同时发帖,版本号 +2 无非多失效一次,不会读到脏数据。
一句话收尾:列表页缓存的正确姿势是"键里带版本、事件里加一、TTL 兜底",缓存只负责加速,版本戳负责正确性——两者分开,发帖后立即生效就是自然结果,而不是靠人去清。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





