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

18 KiB
Raw Permalink Blame History

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,不提交归档和个人路径。