7.4 KiB
图片索引缓存(GalleryIndex)— 重写模块说明
问题
在线阅读的索引是「gid → 每一页 imgkey」,存在 cache.db 的两张表里:
| 表 | 列 | 含义 |
|---|---|---|
gidToKey |
gid, key, pages |
画廊头部:gid、画廊 key、总页数 |
ImageKeyCache |
gid, imgkey, page |
每页的 imgkey |
旧实现(GalleryManageService.cacheImagesKey)依次做四件事:查缓存、判断是否需要重建、
外呼抓 mpv 页、逐页 INSERT。由此产生的实际问题:
- 没有唯一约束。实测生产缓存库有 4 组重复 gid、97 组重复
(gid,page)。 - 整组替换不是原子的。先写头部再逐页写,中途失败会留下「头部命中、页 key 缺失」的 半截索引;后续请求直接判定为已缓存,永远取不到图。
- 用行数判断完整性。
count(*) == pages无法发现「页码 1、3 而 pages=2」这种跳号。 - 没有并发合并。同一本画廊被并发打开时,两个构建者会互相删除对方刚写入的行。
- 逐页写入放大写事务,抓取与写入之间没有事务边界。
交付物与职责
| 文件 | 职责 |
|---|---|
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,因此读模板只设超时、不设只读标志。