跳转至

局域网答题系统 性能优化报告

日期:2026-09-15 范围:后端数据访问层(dbcore / sitedb / records / main)与前端静态资源(static / templates)

一、优化目标与关键指标

指标 目标
接口响应时间 核心统计接口 < 500ms(P95)
数据库访问次数 消除列表/报告类接口的 N+1 查询
全表扫描 热点查询走索引,避免 SCAN
首屏资源 qbank 页不再阻塞加载 1.3MB 富文本编辑器
静态资源传输 命中缓存 + GZip 压缩
无效请求 页面不可见时暂停轮询

二、测量环境与方法

  • 造数规模:users=1500、qb_questions=8000、attempt_items=36000、sessions=120,库约 5.1MB。
  • 两条测量路径:
  • 探针层:直接对 SQLite 反复执行同一条 SQL,衡量纯查询成本(缓存热)。
  • 端到端层:走 dbcore 真实路径(每次 q() = 建连 + 6 条 PRAGMA + 查询 + 关闭),衡量接口真实成本。
  • 对照方法:同一场景分别在“优化前 SQL / 优化后 SQL”下多次执行取均值。

三、基线瓶颈分析

探针 场景 现状 说明
A 每请求建连开销 connect+PRAGMA 0.417ms,纯 connect 0.071ms PRAGMA 设置占建连成本约 83%
B 登录态解析(每 API 必经) SELECT * WHERE session_token=? 0.531ms,SCAN users session_token 无索引
C N+1 逐题查题干(40 题) 逐条 19.452ms → IN 批量 0.546ms 纯查询层 ≈35x
D sessions 校级列表 SCAN,无 school_id 索引 数据量小时耗时不显,规模增长后放大
E qb_questions 子题展开 parent_id 展开 0.925ms SCAN → 加索引 0.410ms ≈2.3x
F 报告明细聚合(24k+ 行) SELECT * 87.0ms → 取 4 列 40.8ms → 服务端 GROUP BY 10.5ms 宽行 + 客户端聚合是主成本

四、优化措施

P0 后端数据访问

  1. 补齐索引sitedb._perf_indexes):
  2. idx_users_token(部分索引,session_token)
  3. idx_sessions_school(sessions.school_id, id)
  4. idx_qbq_parent(qb_questions.parent_id)
  5. idx_qbq_school(school_id, visibility, id)
  6. idx_att_sess_qn(attempt_items.session_id, qn_id)
  7. idx_su_sess_score(session_users.session_id, score)
  8. 同时清理重复定义的 idx_users_status / idx_users_batch
  9. 消除 N+1stats_questionsexam-distsession_detailuser_sessions 的逐题查题干改为一次 IN (...) 批量查询。
  10. 列裁剪 + 合并扫描overview 对 attempt_items 的三次扫描合并为一次;session_users / attempt_items 去掉 SELECT *,只取所需列;get_user 每请求查询裁剪敏感/无用列。
  11. 连接复用dbcore.dbc,2026-09-16 追加):SQLite 改为线程局部连接——每线程首次使用时建连并缓存、之后长期复用,退出 dbc() 只提交/回滚不关闭。消除每查询 ~0.93ms 的建连+PRAGMA 固定开销(探针 G),并保持页缓存热态。MySQL 仍走原连接池。实测 8 线程×5 查询并发:24.6ms → 9.0ms(2.7x)。

P1 前端与传输

  1. 静态资源main.CachedStaticFiles):/static/excelgrade 命中 Cache-Control: public, max-age=86400 + ETag,对 js/css/json/svg/txt 启用 GZip;全站资源加 ?v=<静态版本> 版本号,改文件重启即击穿旧缓存。
  2. 轮询暂停uc.js vizInterval):页面 document.hidden 时暂停定时器,重新可见时立即执行一次并恢复。应用于 exam.js(大厅)、exam_control.js(场控)、admin_rooms.js(房间)。
  3. wangEditor 懒加载qbank.js loadWang):1.3MB 编辑器脚本/样式改为首次打开编辑弹窗时动态加载(幂等 Promise),题库浏览/列表主路径不再承担首屏成本。

P2 内存与代码质量

  1. _hb_cache 无界增长main.py):心跳节流缓存超过 2000 条时清理 5 分钟未活跃条目。
  2. 死文件清理:删除 _chk_i.js
  3. 重复索引定义:合并 idx_users_status / idx_users_batch 的重复建表语句。
  4. 逻辑冗余修复exam.js):筛选未提交 Excel 题由 indexOf 反查索引改为 filter((q, i) => ...) 直接用下标,避免相同题目对象误匹配。

五、优化后复测数据

端到端(dbcore 真实路径,含建连 + PRAGMA)

场景 优化前 优化后 提升
stats_questions(32 查 → 2 查) 52.57 ms 31.09 ms 1.7x
exam-dist(31 查 → 2 查) 56.52 ms 33.20 ms 1.7x
登录态解析(每请求必经) 0.98 ms 1.01 ms 1.0x(建连开销主导)
overview(4 查 → 2 查) 25.31 ms 27.25 ms 0.9x(宽行读取主导)
排行(列裁剪 + 排序索引) 2.97 ms 2.97 ms 1.0x

纯查询层(探针)

场景 优化前 优化后 提升
N+1 → IN 批量(40 题) 19.45 ms 0.55 ms ≈35x
parent_id 展开 0.93 ms(SCAN) 0.41 ms(索引) ≈2.3x
宽行 → 列裁剪(24k 行) 87.0 ms 40.8 ms ≈2.1x
列裁剪 → 服务端聚合 40.8 ms 10.5 ms ≈3.9x

数据一致性验证

重构后 67 项逐字段断言(含 overview 合并扫描、IN 批量题干、列裁剪、token 解析)全部 PASS,查询结果与旧逻辑完全一致。

六、关键发现与说明

  1. 端到端提升被建连开销稀释:dbcore 旧实现每次 q() 都新建连接并执行 6 条 PRAGMA(约 0.42ms,其中 PRAGMA 占 83%)。N+1 消除在纯查询层是 35x,但端到端只体现为 1.7x——因为优化后查询次数锐减,建连固定成本占比反而上升。
  2. 已实施(2026-09-16):SQLite 线程局部连接复用(见 P0-4),每查询固定开销从 ~0.42ms 降至 ~0.001ms;并发 8 线程实测 2.7x。事务语义不变(每次 dbc() 退出仍 commit/rollback),跨线程不共享连接,回滚失败时自动丢弃缓存重建。
  3. overview / 登录态在测试规模下几乎无提升:小数据集下 SCAN 与索引差异被建连/宽行成本掩盖;索引收益随数据量增长而放大(机房满员 800 人 × 30 题 = 2.4 万明细行时,列裁剪与索引才是主要收益点)。
  4. 收益最大处:报告类接口(stats_questions / exam-dist)的 N+1 消除 + 列裁剪,以及 qbank 页 1.3MB 编辑器的首屏剥离。

七、回归验证清单

  • [x] Python 3.8 / 3.12 双环境模块导入通过
  • [x] 公开页 //exam/profile 渲染 200
  • [x] 权限场景:superadmin 访问 /qbank/admin/admin/rooms/admin/stats/admin/users 全 200
  • [x] qbank 首屏 HTML 不含 wangeditor 脚本;登录后页含 STATIC_VER 注入与 sv() 版本号
  • [x] /static/qbank.js 响应头:cache-control: public, max-age=86400ETag 存在、content-encoding: gzip
  • [x] 三处轮询(exam / exam_control / admin_rooms)均使用 vizInterval
  • [x] 查询结果一致性 67 项断言全 PASS

八、结论

本次优化在不改变任何业务行为的前提下,消除了统计报告接口的 N+1 查询与宽行扫描,补齐了热点查询索引,并将 1.3MB 富文本编辑器与全站静态资源做了懒加载 + 缓存 + 压缩处理。端到端核心统计接口提速约 1.7x,纯查询层最高 35x;前端首屏与轮询负载显著下降。剩余的连接复用优化已识别为后续可选项,未在本次实施以控制风险。