高并发帖子列表分页加载优化
高并发下帖子列表分页的性能瓶颈,99% 不在「分页」本身,而在每次请求都做了一次全表 `COUNT(*)` 加一次深翻 `OFFSET`;把这两件事拆掉——热门页走缓存、深翻页改游标、列表查询走覆盖索引——单机撑住几千 QPS 的列表页并不难。
先干掉 COUNT(*),它比你想的贵得多
结论:帖子列表页最大的隐形开销是分页总数统计,而不是取那 20 条数据。
`SELECT COUNT(*) FROM posts WHERE fid=3 AND status=1` 在百万级表上就是一次全索引扫描,而它每次翻页都要跑一遍。三种处理方式,按性价比排序:
- 估算 + 封顶:页码只显示到前 N 页(比如 100 页),超过就提示「请用搜索或时间筛选」,避免用户翻到第 8000 页。这是社区场景最实用的做法——没人真的翻到第 8000 页。
- 缓存计数:把每个版块的帖子数写进缓存,发帖/删帖时增减,而不是每次 `COUNT`。
- 游标分页(keyset pagination):用 `WHERE created_at < ? ORDER BY created_at DESC LIMIT 20` 替代 `LIMIT 20 OFFSET 40000`。深翻时 `OFFSET` 会先扫过并丢弃前 4 万行,游标分页直接走索引定位,第 1 页和第 1000 页耗时几乎一样。
注意点:游标分页拿不到「第几页」,只能给「下一页」,所以通常和上面的「页码封顶」配合用——前台保留页码,深翻入口收敛。
热的永远是第一页,把它缓存起来
结论:论坛列表流量高度集中在第 1-3 页,缓存这几页的收益远大于优化深翻页。
Clara BBS 的插件公共设施里有 `Cache::remember`,思路就是它:
Cache::remember('forum_list_fid3_page1', 60, function () {
// 查 20 条帖子 + 作者信息 + 分类名
});
三个必须注意的点:
- 缓存键要带维度:版块 ID、页码、分类筛选、用户组(权限不同看到的帖子不同)都要进键名,否则会把隐藏版块的帖子缓存给游客。
- TTL 别太长:列表页 30-120 秒足够,发帖后靠主动清缓存或短 TTL 自然过期。Clara BBS 本身是「保存即生效、无需清缓存」的设计,缓存属于你额外加的层,失效逻辑要自己写清楚。
- 别缓存整页 HTML 给登录用户:登录态、头像、未读通知都在页面里,缓存的应该是数据层,不是渲染结果。
想做首页/版块页的「伪静态化」,可以配 `Cron::register` 注册一个懒触发的定时任务,每隔几分钟把那几页渲染成静态文件,请求直接吐文件。
索引和组织结构,决定查询是 5ms 还是 500ms
结论:帖子列表的黄金索引是「过滤字段在前、排序字段在后」,让它能同时完成 WHERE 和 ORDER BY。
典型列表查询是「某版块 + 正常状态 + 按时间倒序」,对应索引大致是:
ALTER TABLE posts ADD INDEX idx_fid_status_time (fid, status, created_at);
-- 需要置顶排前时:(fid, status, is_top, created_at)
几个容易踩的坑:
- 软删除要进条件:Clara BBS 前台删除是软删除(进回收站保留 30 天,天数可配),列表查询必须带上状态条件,否则回收站里的帖子会漏出来,而且 `COUNT` 也会算多。
- **慎用 `SELECT *`**:列表只需要标题、作者、时间、回复数。字段越少,越可能走覆盖索引、免回表。要正文摘要就单独查,或用延迟关联(先用索引查出 20 个 ID,再用主键查详情)。
- 多表 JOIN 要控制:作者名、版块名、分类名如果每次 JOIN,压力会翻倍。作者信息可以整表缓存在内存里按 ID 取。
页面层也有优化空间,而且改起来最省事
结论:减少每页条数、限制最大页码、引导用户少翻页,这三件在后台和产品层就能做,成本最低。
- 后台「显示分页」相关设置里把每页条数从 20 调到 15-20 之间即可,别为了「看起来内容丰富」设成 50。
- 用好全文阅读模式(带目录与进度条)和长文折叠:用户在一个页面里读完,比翻五页更省资源。
- URL 走 `.html` 伪静态,服务器只需把非静态请求转发给 `index.php`(Nginx `try_files` 或 Apache `.htaccess`),无需额外规则;配合 CDN 时静态化收益更明显。
- 版块内的子分类筛选、置顶/加精/推荐等筛选条件,都会改变查询组合,索引设计要把常用组合覆盖到,冷门组合宁可慢一点。
最后提醒两件事
一是权限优先于性能:任何缓存和静态化输出,都必须只包含游客可见内容,隐藏版块、权限版块绝不能进公共缓存或 GEO 输出——Clara BBS 的 GEO 输出本身就是这个口径,你加缓存时要守住同样的边界。
二是先量后调:上线前用慢查询日志找出真正慢的那条 SQL,再决定是加索引、加缓存还是改游标分页。绝大多数站点根本到不了「高并发」那一步,先扛住第 1 页的缓存,就能解决八成的列表页压力。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





