Files
90CNServer/pvf/docs/SQLite索引增量优化方案.md
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

185 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SQLite 索引增量优化方案
状态:文件投影、语义搜索和高级字符串引用的增量路径已实施。下述设计保留原始目标,实际采用兼容现有 fileIndex API 的身份映射方案。更新于 2026-10-13。
## 完成情况
- 文件投影:新建、删除、导入、脚本结构变更和版本物化复用活动数据库;删除在一个事务中调整全部关联表编号。普通内容变更在服务返回前同步文件大小、类型和变更状态,失败向调用者返回错误并禁用不一致投影。
- 语义搜索:通过稳定条目身份和路径变更队列定位影响范围,读取列表登记后只解析受影响文件的元数据;在 WAL 事务中替换其记录、标签、图标和依赖。保留未改动行和数据库索引,不复制完整候选库。
- 依赖:记录字符串表及缺失的本地化候选路径;字符串映射改变后刷新引用文件;NPC 改动刷新商城关系;关系配置变更按新旧登记差异更新。首次建库和用户强制重建仍走全量路径。
- 后台发布:校验归档对象、generation、batchRevision 和变更队列 epoch。过期结果重新排队,失败保留待处理范围;不会用旧结果清除新修改。
- 查询:语义未完成时普通搜索回退到文件路径,精确登记查询报告索引更新中,树中隐藏脏语义的标签与图标。分页绑定索引 generation 和查询数据源;高级搜索分页也绑定语义生成及完成状态。
- 高级字符串搜索:保留不可变引用分片,私有元数据库维护文件编号映射、修改覆盖引用和新增池字符串。只解析新增/修改文件,删除无需重写引用分片。旧池前缀指纹不兼容时,自动回退到完整构建。
- 缓存:SQLite 文件索引校验加载内容摘要;摘要按不可变打包基线缓存,保存后失效。高级搜索的保存后覆盖结果可发布成 v4 缓存,元数据库使用 backup 快照,引用分片使用不可变硬链接;恢复后再次编辑不会修改缓存。
- 全量重建优化:候选库只复制 `meta/files/dirs`,创建空语义表,避免复制后清空旧语义数据。普通未登记文件直接查询 `files`,语义表不再保存重复文件记录;按语义流、文件流分别分页,保留多列表登记。依赖路径按表编号缓存、按文件去重,128 条一批使用预编译 SQL,全量完成后建立依赖索引。文件索引 schema 升级为 5,旧缓存不直接复用;高级字符串缓存仍为 v4。
- 文件身份在解析、新增时分配,克隆、压缩删除及 TW 保存时保留,不写入 PVF。SQL 仍用现有 file_index,结构事务统一重映射关联表;这是对原“持久化 entry_id”设计的兼容替代,不改变绑定和文件格式。
仍有线性成本:条目身份扫描、暂存克隆、登记列表读取、删除编号重映射,以及首次从干净缓存分离工作区时的数据库复制。首次建库、schema 升级、损坏修复、强制重建和不兼容池代际允许全量构建。新增 NPK 增量扫描不在本方案范围。
## 第一批实现范围
- 为归档条目增加进程内稳定身份,解析、新增时分配,克隆与删除压缩时保留;变更日志同时记录身份。身份不写入 PVF,不改变文件格式。
- SQLite 文件投影保留现有 schema:使用稳定身份计算新旧编号映射,事务内同步调整 files、records、tags、visuals 的编号。持久化 `entry_id` 及语义表关联迁移留待下一阶段,当前采用兼容现有查询的过渡实现。
- 已接入新建、删除、导入,以及复用结构重建入口的脚本、撤销、恢复;历史结构切换可复用活动文件投影。打开归档时的版本工作区恢复仍使用原来的准备流程。
- 新建、删除、导入使用暂存归档,SQLite 事务失败时不发布暂存状态;对已经修改 live 归档的入口,失败后禁用旧投影并报告错误,用户可通过重建入口修复。
- 按受影响目录更新直接子项计数,修正原全量构建根目录把自身计入子项的问题。
- 首次编辑干净缓存前复制到独立会话库,后续复用会话库。保存发布缓存使用一致的 SQLite backup 快照,活动连接仍指向会话文件。
- 第一批交付时语义索引和高级字符串索引仍沿用原来的全量路径;现已由上方“完成情况”中的增量实现替代。
- 当前仍扫描条目身份数组,并在新建/删除暂存时克隆归档;后续性能优化可减少这些内存操作。此批主要消除同步重建文件数据库,不承诺整个操作为 O(变更数)。
## 目标与范围
将正常编辑、新建、删除、导入和版本恢复从“重建数据库”改为“更新受影响的行”。优先优化大归档的文件、目录及语义索引;高级字符串反向索引后续接入相同变更协议。NPK 图片索引独立处理,不纳入本次实施。
不承诺所有操作为 O(变更文件数):当前 PVF 删除会压缩整个文件表,必须处理编号变化;消除全量解析和数据库重建,不等于消除所有全量遍历。
## 当前限制
- `CreateFile`、`deleteFiles`、脚本结构提交、结构性版本恢复调用 `rebuildArchiveIndexesLocked`。
- 导入无论新增还是覆盖,都调用 `prepareArchiveDerivedIndexes`。
- `openSQLiteArchiveIndexContext` 在归档已修改时跳过缓存,重新生成 files、dirs 和数据库索引。
- SQLite 语义刷新调用 `prepareSemanticCandidate` 复制整个数据库,再清空 records、tags、visuals 并重新生成;没有使用内存索引已有的增量路径。
- `RemoveFiles` 压缩编号,当前多张表直接引用 `file_index`。仅删除目标行会导致存量记录指向错误文件。
- 高级字符串搜索在变更后整库失效,下次查询按需重新构建。
- 当前完整性标志不能表达文件投影版本、语义完成版本和待更新范围,需要扩展。
## 一致性要求
1. 操作成功返回时,归档和文件投影属于同一修订:路径、编号、类型、大小、变更状态、目录及直接子项计数正确。
2. 已删除文件不能继续出现在树、搜索或引用详情中;所有返回的文件编号必须来自当前映射,不能复用旧快照编号。
3. 同一路径允许存在多个归档条目。路径用于查找和语义关系解析,不能作为文件身份的唯一键。
4. 语义数据可以后台更新,但未完成范围必须显式标记。默认查询对脏范围使用路径回退,不展示过期名称、标签或图标为最新结果;精确语义查询报告“索引更新中”,不得把缺失结果宣称为完整结果。
5. SQL 失败不能出现“归档已成功修改、数据库仍停留在旧版本”而服务继续正常返回。必须回滚归档变更,或禁用不匹配投影并进入可见的恢复状态。
6. 后台结果只能发布到其对应的文件内容、关系配置和归档会话;旧任务不能清除新变更留下的脏标记。
7. 未保存工作区数据库不能冒充原始 PVF 的干净缓存。缓存不匹配或损坏时必须拒绝复用。
## 数据模型
原设计建议升级 schema,增加 SQLite 内部稳定身份,不修改 PVF 文件格式和 Wails 的 `fileIndex` API。最终实现保留现有 SQL 编号关联,使用进程内稳定身份生成事务映射;下表为原始目标模型,并非当前数据库完整定义。
| 数据 | 建议字段与职责 |
| --- | --- |
| files | `entry_id` 为稳定主键;`file_index` 为当前归档编号,唯一;保留路径、类型、大小等现有字段 |
| records | 使用 `entry_id` 关联文件;记录所属 `relation_key`,允许同一文件存在多个列表登记 |
| tags / visuals | 使用 `entry_id`;标签保留来源关系,避免删除一条关系时误删其他来源 |
| dirs | 保持路径主键,记录直接文件和直接子目录数量 |
| dirty_entries | `entry_id`、目标内容修订、影响掩码 |
| dirty_relations | 关系标识、目标关系修订 |
| meta | schema、会话身份、源内容身份、文件投影修订、语义完成修订、配置指纹、完成状态 |
查询通过 files 连接得到当前 `file_index`;不向前端暴露 `entry_id`。删除文件时事务内显式删除关联数据,或使用经过验证的外键级联。
`entry_id` 由索引会话分配,删除后不复用。克隆归档、导入和版本物化必须携带条目存续映射;不能仅凭规范化路径推断身份。重新打开归档可从有效缓存恢复身份,重新全量构建时允许重新分配身份。
## 统一变更协议
现有 `MutationSummary` 保留作为兼容入口,但结构操作应生成一次提交级别的 IndexDelta:
- 基础与目标归档修订、会话身份。
- 新增、删除、修改条目及前后元数据。
- 存续条目的旧编号到新编号映射,或由核心操作提供的等价压缩映射。
- 受影响目录、列表关系、字符串表及 NPC 依赖。
- 内容类型变化、路径变化和关系配置变化。
不能直接使用日志中的 `Index` 查询当前文件:它可能在多次删除之后已经过期。批量操作、脚本和恢复应保留提交前身份,按最终状态归并增删改;新增后又删除可抵消,删除后重建同路径不能误当成存续条目。
### 发布步骤
1. 捕获当前会话、修订与条目身份,准备归档暂存变更及 IndexDelta。优先复用现有隔离暂存机制,避免每次复制完整归档。
2. 在暂存归档或受保护的读取阶段计算必要元数据。耗时解析与磁盘 IO 不放在长期持有的核心写锁内。
3. 在 SQLite 写事务中校验基础修订,更新文件、目录、身份映射,清除删除条目和失效语义,写入脏范围与目标修订。
4. 最终发布阶段统一使用锁顺序 `core.mu → index.dbMu`,再校验会话与基础修订。提交 SQL 后安装对应归档状态、更新内存修订,并在释放锁后发送变更事件。
5. 读取必须遵守同一发布屏障;SQL 提交到内存切换之间不允许服务请求读取新数据库配旧归档。数据库提交后内存安装应设计为不会失败;无法做到时必须有撤销状态或明确的失效恢复分支。
6. 启动后台语义刷新。操作返回不等待语义解析,但必须已经完成文件结构一致性提交。
实现时校核 SQLite 读事务的启动时间和归档快照修订,不能只在查询末尾检查 generation。持有旧 SQL 快照的请求不能取得新的归档编号映射。
## 文件和目录的增量更新
### 新建、覆盖、导入
- 新建:插入 files;沿祖先链补齐缺失目录;只更新直接受影响目录的计数。
- 覆盖:更新对应条目的类型、大小、变更状态,标记必要语义依赖;无须生成完整目录投影。
- 导入:全部变更在一次文件投影事务中提交;新增和覆盖混合处理,不因导入入口而重建数据库。
- 创建新文件时,仅在相关列表已经登记该路径时安排关系修复;同时检查此前未解析的登记,避免新增目标后登记仍永久缺失。
### 删除与编号压缩
- 使用删除前身份删除条目及其关联记录。
- 根据核心返回的压缩映射,仅更新存续条目的 `file_index`;records、tags、visuals 无须逐表重新编号。
- 映射更新使用事务内临时映射及两阶段唯一值转换,避免新旧编号交叉导致唯一约束冲突。内部临时编号不能对读者可见。
- 删除空目录自底向上进行;每移除一个空子目录,只调整其父目录的直接子项计数;根目录保留。
- 重复路径删除后可能改变 `.lst` 目标的首选条目,需重算引用该路径的关系,不能只级联删除旧目标。
- 删除靠前条目仍可能需要 O(N) 映射更新,这是保留当前 PVF 紧凑编号的成本,但不再重新解析元数据、复制数据库或重建所有数据库索引。
不建议首轮改 PVF 核心为墓碑或永久稀疏编号:这会扩展到解析、保存、编辑器和绑定契约,超出当前优化范围。
## 语义索引的增量更新
后台只解析受影响的数据,在内存暂存小范围结果,随后用短事务替换受影响行。长期保留数据库二级索引,不再每次 DROP/CREATE,不复制完整候选数据库。
| 变更 | 刷新范围 |
| --- | --- |
| 已登记文件名称、稀有度、图标变化 | 对应条目的元数据及其全部登记记录 |
| 可识别 `.lst` 修改 | 该列表的新旧登记差异;第一版可整体替换单列表关系 |
| 新增、删除 `.lst` | 对应关系;影响不可判定时扩展到相关关系组 |
| `.str` 修改 | 引用受影响表或键的条目;第一版允许按整张字符串表扩展 |
| NPC 名称或登记修改 | 依赖对应 NPC 的商城记录;第一版允许刷新整个商城分类 |
| 搜索关系配置变更 | 新增、移除或改变的关系定义;显示注释、渲染规则单独变化不触发语义重建 |
| 普通内容变更且搜索字段未变化 | 只更新文件元数据,无须语义刷新 |
需保存列表目标依赖及字符串表、NPC 依赖。依赖包含未解析目标;依赖尚未完整时回退到单关系、单分类刷新,不能把“未找到依赖”当作“没有影响”。同一文件在多列表登记时保留所有关系来源。
每个结果携带条目内容修订、关系修订和配置指纹。发布前逐项校验;发生并发修改的部分重新排队,其余结果可提交。仅删除与本次目标修订匹配的 dirty 行,连续编辑合并为最新任务。
列表任务按关系串行或用关系修订校验;文件任务解析和发布时必须关联稳定身份。若字符串解析器不能提供隔离快照,解析阶段使用受控读锁,禁止后台直接读取正在修改的归档。
## 事务、缓存与恢复
- 活动会话数据库使用 WAL 和 `synchronous=NORMAL`,写事务有可靠回滚;禁止沿用全量临时构建使用的 `journal_mode=OFF`。索引属于可重建派生数据,NORMAL 不保证突然断电后保留最新提交,但重启必须验证缓存身份并拒绝不完整缓存。
- 首次全量构建仍可使用独立候选库和快速参数;未完成候选不能作为活动投影发布。
- 干净缓存与可变会话库分离,禁止直接修改不可变缓存或其硬链接。首次需要写入时允许一次性复制或使用文件系统支持的复制优化;后续局部刷新禁止反复复制整库。
- 保存或另存为后,按已保存内容身份与索引修订发布新的干净缓存快照。若语义尚未追平,可只发布明确有效的文件投影,重新打开后继续构建语义。
- 后台失败保留 dirty 范围,标记错误并允许重试;不得提前设置 ready。文件投影可继续使用,语义结果按前述回退规则读取。
- 查询分页游标绑定会话、结构修订及相关查询修订。结构或排序依据变化后拒绝旧游标,不能返回混合版本分页。
## 高级字符串反向索引:第二阶段
复用稳定条目身份、内容修订与 dirty 队列。普通文件编辑只替换该文件的字符串引用,新建插入、删除移除;存量引用通过映射取得当前编号。
字符串池偏移必须携带池身份或代际。确认池为追加写且旧偏移有效时才增量追加;池整体重排、重建或归档替换后偏移不兼容,需重建受影响池及其引用,必要时全量回退。占位符解析依赖变化单独更新 resolved 值。
引用底库更新与查询结果缓存失效分离:一次编辑可以废弃查询结果和游标,但不应销毁整个引用底库。现有持久缓存/硬链接必须保持不可变,写入前建立独立会话库。
## 保留全量重建的边界
- 首次建库、旧 schema 无法安全迁移、缓存损坏或源身份不匹配。
- 文件身份映射缺失、结构差异不可判定、池偏移代际不兼容。
- 关系解析语义整体变化,且无法确定受影响范围。
- 用户明确要求强制重建或修复。
大批量变更优先执行集合更新和按分类替换;是否改用全量候选由实测成本决定,不设置未经验证的固定百分比阈值。全量回退记录原因,并且不能在正常新建或单文件编辑中静默触发。
## 实施顺序
1. 引入修订、稳定身份、IndexDelta 与事务发布协议;迁移查询到稳定身份关联;完成失败注入测试。
2. 接入新建、删除和导入,替换同步文件投影重建;保留内存索引现有分支。活动 SQLite 会话即使删除后低于 10 万文件,也继续沿用,避免跨阈值立即迁移。
3. 接入脚本、撤销、单文件恢复、历史切换及备份恢复,统一结构变更映射。
4. 实现已登记文件与列表的语义增量,再补齐字符串表、NPC 和规则依赖;删除常规更新中的完整候选库复制。
5. 优化高级字符串反向索引,保留不可兼容池的重建边界。
每一步均保留带原因的全量修复入口,但新旧路径必须共享一致性检查。
## 验证与验收
- 以同一最终归档全量构建的索引为参照,对比目录、文件、语义记录、标签、图标和搜索结果;比较逻辑内容,忽略内部 ID 和行顺序差异。
- 覆盖首部/中部/尾部删除、多次压缩、重复路径、多列表登记、缺失登记目标后新增、同路径删除后重建、空目录、批量导入与类型变化。
- 覆盖撤销/历史切换、连续编辑、语义发布前再修改、构建中关闭或重新打开归档、旧分页游标、阈值穿越。
- 注入 SQL 执行、提交、后台解析、缓存发布失败,验证操作不会报告虚假成功,脏标记不丢失且恢复后与全量参照一致。
- 崩溃后重启拒绝身份不匹配或未完成缓存;独立验证活动会话不会污染干净缓存。
- 在同一真实归档和相同 Go 编译参数下测量冷/热状态至少三次,记录操作返回、语义追平、锁等待、SQL 写入行数、磁盘写入量和峰值内存;沿用《索引性能验证》的环境约束。
- 单文件新增/覆盖和局部语义编辑不得调用全量建库、整库复制、全表清空或 DROP/CREATE INDEX。删除允许映射扫描,但不得重新解析全部文件。
- 文件投影提交后立即可读且一致;语义未完成状态可辨识,完成后结果与全量参照一致。
性能目标在基线测试后确定,本方案不将未测量的毫秒级耗时作为承诺。实现阶段运行 `go test ./...`、前端构建;真实归档回归使用 `PVF_TESTFILE`,不提交归档和个人路径。