Files
lionwebsite-backend/docs/GALLERY_INDEX_REWRITE.md
T

7.4 KiB
Raw Blame History

图片索引缓存(GalleryIndex)— 重写模块说明

问题

在线阅读的索引是「gid → 每一页 imgkey」,存在 cache.db 的两张表里:

表 列 含义
gidToKey gid, key, pages 画廊头部:gid、画廊 key、总页数
ImageKeyCache gid, imgkey, page 每页的 imgkey

旧实现(GalleryManageService.cacheImagesKey)依次做四件事:查缓存、判断是否需要重建、 外呼抓 mpv 页、逐页 INSERT。由此产生的实际问题:

  1. 没有唯一约束。实测生产缓存库有 4 组重复 gid、97 组重复 (gid,page)。
  2. 整组替换不是原子的。先写头部再逐页写,中途失败会留下「头部命中、页 key 缺失」的 半截索引;后续请求直接判定为已缓存,永远取不到图。
  3. 用行数判断完整性。count(*) == pages 无法发现「页码 1、3 而 pages=2」这种跳号。
  4. 没有并发合并。同一本画廊被并发打开时,两个构建者会互相删除对方刚写入的行。
  5. 逐页写入放大写事务,抓取与写入之间没有事务边界。

交付物与职责

文件 职责
Service/GalleryIndexService.java 索引的唯一读写入口:同 gid 合并、抓取在事务外、校验后整组短事务替换
Exception/GalleryIndexException.java 分类失败原因:INVALID_LINK / UNAVAILABLE / MALFORMED / TRANSIENT
Configuration/GalleryIndexConfiguration.java 缓存库的事务管理器与读写模板(独立文件,未改 SqlConfiguration)
Dao/cache/ImageCacheMapper.java 新增页码查询、脏行计数、批量写入(原有方法全部保留)
Maintenance/GalleryIndexMaintenance.java 离线体检与修复工具(只删不健康 gid 的整组索引)
scripts/migrate_image_index_cache.{sh,sql} 迁移:默认 dry-run、备份、清理、建唯一索引
Service/GalleryIndexServiceTest.java 服务编排的行为约束(无 Spring、无网络)
Maintenance/GalleryIndexMaintenanceTest.java 工具在真实临时 SQLite 上的判定与删除范围
Dao/cache/ImageCacheMapperSqlTest.java 注解 SQL 在真实 MyBatis + SQLite 上的验证

接口

GidToKey ensureIndex(String url) throws GalleryIndexException;

返回该画廊的 gid / key / pages。三条不变量:

  • 抓取在事务外:upstream.fetchPageKeys(url) 先于任何写事务执行;写事务只在 publish(...) 里开启,删除旧索引与批量写入在同一个事务内整体生效。
  • 校验先于写入:行必须满足 gid 与请求一致、页码恰好为 1..N、不重复、imgkey 非空, 否则抛 MALFORMED,此时尚未开启写事务,库中旧索引原样保留。
  • 读在快照内:lookup 在同一个读事务里读头部与页码,避免读到整组替换的中间状态。

失败原因:

Reason 触发条件 isPermanent() 建议处置
INVALID_LINK 链接里取不到 gid 或 key true 直接回「链接错误」
UNAVAILABLE 上游返回空页列表(下架/删除) true 回「该图片已下架或已被删除」
MALFORMED 上游页 key 不自洽 false 可重试,回「网络波动或其他异常」
TRANSIENT 上游异常或写库失败 false 可重试,回「网络波动或其他异常」

接入方式(由主模型完成 GalleryManageService 侧改动)

构造函数注入:

final GalleryIndexService galleryIndexService;

cacheImagesKey 改为:

public String cacheImagesKey(String url) {
    Response response = Response.generateResponse();
    try {
        GidToKey gidToKey = galleryIndexService.ensureIndex(url);
        return response.success(objectMapper.valueToTree(gidToKey)).toJSONString();
    } catch (GalleryIndexException e) {
        response.failure(e.getReason() == GalleryIndexException.Reason.INVALID_LINK
                ? "链接错误"
                : e.isPermanent() ? "该图片已下架或已被删除" : "网络波动或其他异常");
        return response.toJSONString();
    }
}

Spring 装配无需额外配置:GalleryIndexConfiguration 已把 GalleryIndexService 注册为 bean, 依赖 ImageCacheMapper(cache 数据源)与两个 TransactionTemplate。原来的 isIndexComplete / rollbackImageIndex 私有方法与 ImageCacheMapper 的 countImageKeyCacheByGid / delete*ByGid 仍保留,供旧调用方与非索引路径使用。

读取路径 getCachedImage 不需要改动:它按 (gid,page) 查 ImageKeyCache,现在会走新加的 ux_ImageKeyCache_gid_page 唯一索引。

迁移

# 1. dry-run(只读,不带 --apply 绝不写入)
scripts/migrate_image_index_cache.sh --db /path/to/copy.db

# 2. 先在 SQLite 备份副本上演练
sqlite3 -readonly /home/lionwebsite/cache.db ".backup '/tmp/cache-copy.db'"

# 3. 确认后执行(自动做 .backup 备份并校验)
scripts/migrate_image_index_cache.sh --db /tmp/cache-copy.db --apply

迁移只做两类写入:删除不健康 gid 的整组索引(重复头部、半截、跳号、空 imgkey)、 删除孤儿页 key;随后建立 ux_gidToKey_gid 与 ux_ImageKeyCache_gid_page。 若清理后仍有重复,建唯一索引会失败并整体回滚。真实的 /home/lionwebsite/cache.db 未被本次工作修改。

离线修复工具:

java -cp sqlite-jdbc.jar Maintenance/GalleryIndexMaintenance.java --db /tmp/cache-copy.db          # dry-run
java -cp sqlite-jdbc.jar Maintenance/GalleryIndexMaintenance.java --db /tmp/cache-copy.db --apply # 执行

对 /home/lionwebsite 下的库运行 --apply 会被拒绝,除非显式加 --allow-production。

测试

在 /tmp 的独立源码副本中运行(未复制任何生产 runtime config、未改生产库):

mvn -o -Djacoco.skip=true \
    -Dtest='GalleryIndexServiceTest,GalleryIndexMaintenanceTest,ImageCacheMapperSqlTest' \
    -DfailIfNoSpecifiedTests=false test

Tests run: 9,  Failures: 0, Errors: 0  -- GalleryIndexMaintenanceTest
Tests run: 21, Failures: 0, Errors: 0  -- GalleryIndexServiceTest
Tests run: 7,  Failures: 0, Errors: 0  -- ImageCacheMapperSqlTest
Tests run: 37, Failures: 0, Errors: 0
BUILD SUCCESS

覆盖的关键场景:

  • 同 gid 24 线程并发只抓取一次、只发布一次;失败结果也被等待者共享;失败后可重试。
  • 抓取发生在事务之外;所有删除与写入都在事务内;整组替换只有一个写事务。
  • 行数相同但页码跳号、空 imgkey、空 key、pages<=0 均判定为不完整并重建。
  • 写入失败整体回滚,旧索引与失败前逐字段一致。
  • 上游畸形数据(gid 不符、页码重复/跳号/越界、imgkey 为空)在开启写事务之前被拒绝。
  • 大规模索引按 BATCH_SIZE 分片写入;重复发布替换旧行而非累积。
  • 注解 SQL 在真实 SQLite 上验证,含唯一约束确实拒绝重复写入。

已知边界

  • 迁移后 gidToKey 433 条、ImageKeyCache 43704 条(原 443 / 43918)。 被删除的是 4 个冲突或不完整 gid 的整组索引与 214 行关联页 key,由下一次在线阅读重抓。
  • 未做性能压测,不承诺加索引后的具体延迟收益,只确认查询计划已由全表扫描变为索引查找。
  • lookup 的读事务只是为了拿到一致快照;实测 sqlite-jdbc 不允许在已建立的连接上设置 readOnly,因此读模板只设超时、不设只读标志。