| 项 | 值 |
|---|---|
| 版本 | V1.0.18(version.php,末位数字 18 = 目录号 18) |
| 主题 | 收敛「同一请求内对 users / settings / notifications / messages / level_config 的重复查询」,降低每页数据查询次数 |
| 涉及静态资源 | 否(不含 sw.js,CACHE_VERSION 无需递增;本包不含任何 CSS / JS) |
| 数据库结构变更 | 无(无表结构、无索引、无数据迁移;不改动任何业务数据) |
| 部署关键点 | 覆盖 10 个代码文件 + 根目录 version.php;纯文件覆盖,无配置项、无升级 URL。若站点已存在 protected/level_config_cache.php 且此前改过等级配置,建议删除该文件让其重建(见第六节) |
一、变更文件清单
| # | 文件 | 变更类型 |
|---|---|---|
| 1 | app/Helpers/UserPrefs.php |
新增(请求级用户偏好缓存:一次查询取 theme / night_theme_auto / hide_right_sidebar / home_mode / list_excerpt / list_excerpt_len) |
| 2 | app/Helpers/Theme.php |
修改(getCurrent() / getHomeMode() / hideRightSidebar() 改读 UserPrefs 缓存,删除 3 处 users 单查) |
| 3 | app/Controllers/ThemeController.php |
修改(readPref() 改读 UserPrefs 缓存,消除逐 key 查询;.settings() 连续 5 次调用共享一份数据) |
| 4 | app/Helpers/Settings.php |
修改(reconcileWithDb() 复用已加载的全量 KV + 60s 对账节流;新增 getUnreadBadgeCounts() / clearUnreadBadgeCache();getUnreadMessageCount() 改为委托;clearUnreadMessageCache() 同步清合并缓存) |
| 5 | app/Helpers/Notification.php |
修改(getUnreadCount() 当前登录用户走合并缓存;markAllRead() 后清合并缓存) |
| 6 | app/Helpers/Permission.php |
修改(groupIdOf() 当前登录用户直接复用 Auth::getCurrentUser()['group_id']) |
| 7 | app/Helpers/Points.php |
修改(新增 loadAllConfigs() / writeConfigCache() / clearConfigCache();getLevelConfig() 与 getAllConfigs() 共用同一份数据源;ensureTable() / saveLevelConfig() / deleteLevelConfig() 写后失效) |
| 8 | app/Models/Thread.php |
修改(decorate() 新增 $decorateUserCache / $decorateCatCache,同一请求同一 uid/cid 只查一次) |
| 9 | app/Models/User.php |
修改(getStats() 的 COUNT(*) blogs 按 uid 请求级缓存) |
| 10 | plugins/medal/Plugin.php |
修改(自动发章评估:当前登录用户复用 Auth 的 points / created_at) |
| 11 | version.php |
修改(V1.0.13 → V1.0.18) |
未触碰:
app/SplitDB/、列表页的WHERE id IN (...)批量补用户/分类、投票的UNION ALL合并、游客前置静态缓存。以上均为既有正确写法,本次刻意保持原样。
二、优化前现状与根因
页脚「数据查询 N 次」= Database::getQueryCount(),仅统计 business.sqlite 业务库的真实数据语句(不含 sessions 库、分片桶、PRAGMA/DDL)。优化前实测(登录态):首页 24 次、论坛 23 次、帖子详情 29 次、主题设置 21 次。根因共 4 条,全部属于「公共骨架重复查询」,而非业务查询本身:
- 用户偏好字段被 4 个方法各自查 users 表,且都没有请求级缓存
Theme::getCurrent()查theme, night_theme_auto(同一请求 4~5 次)、Theme::getHomeMode()查home_mode、Theme::hideRightSidebar()查hide_right_sidebar、ThemeController::readPref()按 key 逐次查(一次 5 条)。而Auth::getCurrentUser()本就有请求级缓存,只是这 3 个字段不在其 SELECT 列表内,才各自回源。 - 同页两次读 settings 表
Settings::reconcileWithDb()每请求执行SELECT "key" FROM settings做孤儿键对账,而loadAllFromDb()已读过全量 key/value,两者未复用。 - 导航未读数两条 COUNT 各查一次
Notification::getUnreadCount()(通知)与Settings::getUnreadMessageCount()(私信)每页各执行一条 COUNT。 - 详情页单行装饰被重复调用
Thread::decorate()内部逐条查 users + categories;帖子详情里同一主题被装饰两次(主题行 + 首帖),users / categories 各查 2 次。 另有Permission::groupIdOf()、medal 插件评估、Points::getLevelConfig()对同一用户的重复读取。
三、逐项改动细则
| # | 项目 | 做法 | 每页省 |
|---|---|---|---|
| 1 | 用户偏好缓存 | 新增 UserPrefs::all()(静态缓存 + 一次查询取 6 字段,游客直接返回默认值不查库);Theme 3 处 + ThemeController::readPref() 改读它 |
3~4(主题设置页 10) |
| 2 | settings 双查合一 | reconcileWithDb():①已从 DB 全量加载时零成本跳过(缓存侧键集合 = DB 键集合,不存在孤儿键);②其余情况 60s 节流戳(protected/settings_reconcile.stamp),窗口内不再查键 |
1 |
| 3 | 未读数合并 | 新增 Settings::getUnreadBadgeCounts():一条 UNION ALL 取通知 + 私信未读数(两表过滤条件均为 is_read = 0,同库同连接);Notification::getUnreadCount()(当前用户)与 getUnreadMessageCount() 均委托它 |
1 |
| 4 | 当前用户元数据复用 | Permission::groupIdOf() 当前登录用户取 Auth 缓存的 group_id(含 <=0 → 1 归一);medal 插件评估取 Auth 的 points / created_at(非当前用户仍回源) |
1~2 |
| 5 | level_config 缓存 | 新增 Points::loadAllConfigs():请求级静态 → protected/level_config_cache.php(含 1 小时 TTL 安全网)→ DB;两条读取路径(getLevelConfig / getAllConfigs)共用一份数据源;三个写路径写后立即失效 |
1 |
| 6 | 详情页去重 | Thread::decorate() 的 users / categories 子查询按 uid/cid 请求级复用(行本身仍各自组装,因详情页两次装饰的 $content 不同,按行缓存会串内容);User::getStats() 的 COUNT(*) blogs 按 uid 请求级缓存 |
1~3 |
正确性配套(必须一起发布,否则同请求内可能读到旧值):
Notification::markAllRead()更新后调用Settings::clearUnreadBadgeCache();Settings::clearUnreadMessageCache()同时清$unreadBadgeCache(私信已读路径MessageController已调用该方法);Points::ensureTable()/saveLevelConfig()/deleteLevelConfig()写后调用clearConfigCache()。
四、优化效果(CLI 探针跑完整 index.php 管线实测)
4.1 登录态
| 页面 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
/ 首页 |
24 | 14 | −10 |
/forum 论坛 |
23 | 14 | −9 |
/thread/70 帖子详情 |
29 | 18 | −11 |
/blog/16 博客详情 |
24 | 15 | −9 |
/tags 标签 |
16 | 8 | −8 |
/profile 个人主页 |
17 | 8 | −9 |
/post/new 发帖 |
16 | 8 | −8 |
/search 搜索 |
15 | 7 | −8 |
/theme-settings 主题设置 |
21 | 7 | −14 |
4.2 游客态
| 页面 | 优化前 | 优化后 |
|---|---|---|
/ |
11 | 11(游客走 cookie,本包对游客渲染路径零改动) |
/forum |
16 | 11 |
/thread/70 |
17 | 13 |
五、验证方式与结果
- 语法:
php -l全部变更文件通过(No syntax errors detected); - 编码:所有文件 UTF-8 无 BOM + LF(新增
UserPrefs.php与修改后的Notification.php行尾已校正为 LF); - 渲染一致性:同批复测页面输出字节数与优化前基线逐字节比对——
/75324、/forum81165、/thread/70164951、/blog/1647367 完全一致;/tags、/post/new、/search、/theme-settings与上一轮完全一致;游客/70466、/forum76064、/thread/70107522 完全一致; - 未读数语义等价:改造前后同一时刻逐值比对 → user 1:通知 2 / 私信 0(独立查询与
UNION ALL完全一致);user 2:0 / 0(空表边界,messages表 0 行同样正确);notifications表 1640 行规模下亦一致; - 缓存内容一致性:
level_config文件缓存 11 行 vs DB 逐字段比对一致;settings 缓存 107 键 vs DB 107 键,无孤儿键、无缺失键(故 60s 对账节流不改变任何渲染结果); - SQL 明细核对:
Theme.php偏好查询 ×4、Theme.phphome_mode / hide_right_sidebar、ThemeController::readPref()×5、Settings.php对账、Points.php等级表、Permissiongroup_id、medal 评估等重复项均已从明细中消失。
已知观察项(非缺陷):
/profile输出字节数在两次跨越时间点的测量间出现 ±1 字节(31030 → 31029),该页为唯一承载用户会话态/在线态动态显示的页面;同批其余页面逐字节一致,且上述第 3~5 项已排除代码原因。/blog/16仍有 2 条COUNT(*) FROM blogs WHERE user_id = :uid,经明细核对为两个不同 uid(博主 + 当前登录用户)的正常统计,按「去重仅针对同一请求同一 id」的约定不做合并。
六、部署与回滚
部署:按本目录结构覆盖到站点根目录即可:
app/Helpers/UserPrefs.php ← 新增文件(必须上传)
app/Helpers/Theme.php
app/Helpers/Settings.php
app/Helpers/Notification.php
app/Helpers/Permission.php
app/Helpers/Points.php
app/Controllers/ThemeController.php
app/Models/Thread.php
app/Models/User.php
plugins/medal/Plugin.php
version.php
- 无数据库迁移、无配置项、无需访问升级 URL;
- 站点会在首次请求时自动生成两个运行时缓存文件:
protected/level_config_cache.php(等级配置)与protected/settings_reconcile.stamp(对账节流戳)。二者均非必需,删除后自动重建; - 若站点此前已存在
protected/level_config_cache.php且其后改过等级配置,建议在覆盖后删除该文件一次,确保等级档位与数据库一致(此后由写路径自动失效,无需再手动处理)。
回滚:还原上述 11 个文件到旧版本即可;删除 protected/level_config_cache.php 与 protected/settings_reconcile.stamp 以清除新增缓存产物。
七、未纳入本包的观察项(供后续评估)
- 博客详情页
SELECT b.*, u.username … FROM blogs WHERE id = ?存在同参数重复 2 次,可加请求级缓存再省 1 次; Thread::decorateList()与decorate()可共享同一份 uid/cid 缓存,列表与详情混合渲染的页面可能再省 1 次;/blog/16的两条COUNT(*) blogs(不同 uid)如需合并为一条IN (uid1, uid2),属业务语义变更,需单独确认后再做。