Files
UHD_Media_Tool/docs/runbook/cache.md
T
2026-09-29 17:16:50 +08:00

5.4 KiB

缓存与限流运行手册(C2-A)

不引入 Redis / Deno KV。跨进程共享的状态都放 PG:站点限流令牌桶、TMDB 缓存。缓存语义照搬 MoviePilot, 对照与偏离见 docs/porting/moviepilot-tmdb-cache.md。

跨进程站点限流

  • 表 rate_limit_bucket(迁移 0041_rate_limit):key site:<站点 id>,tokens / capacity / refill_per_sec / blocked_until。
  • @umt/db 的 PgTokenBucket.acquire(key, cost, {capacity, refillPerSec, waitMs}):事务里 select … for update, 按 DB 时钟补令牌后扣减;拿不到按需要等待的时间加抖动轮询,超过 waitMs 返回 ok:false(站点限流器会重新排队)。
  • 站点适配器用 createSharedSiteRateLimiter:进程内先串行排队,再到 PG 扣令牌;429 的 Retry-After 写 blocked_until。 站点限流间隔为 0(不限流)时只走本地;PG 出错时本次退回进程内令牌桶并记 warn。
  • 容量 = 站点并发(burst),速率 = 1000 / 最小间隔毫秒。改站点限流设置即时生效(每次扣令牌带上最新参数)。

排查:select * from rate_limit_bucket where key = 'site:<id>';blocked_until 不为空说明站点要求暂停。

PG 缓存(kv_cache)

  • 表 kv_cache(迁移 0042_cache,UNLOGGED:不写 WAL,崩溃后清空):(region, key) 主键,value jsonb, expires_at 每条独立,accessed_at 为最近读写时间。
  • @umt/db 的 PgCache(对照 MP TTLCache):set 带 ttl(秒,≤ 0 删除)与 maxsize(按 region 计条数,超出先删过期再删最久未用); get 命中刷新最近使用;cachedCall 对照 MP @cached(skip_none / skip_if / empty_ttl / fresh)。
  • 定时任务 cache.sweep(维护组,每 10 分钟)物理删除过期行。
region 内容 键 TTL 条数上限
tmdb.request TMDB 请求响应(MP TMDb.request) path?query(不含凭证) 搜索 / find:storage.cacheTtl.tmdbSearchDays(7 天);详情 / 季:tmdbDetailDays(30 天,追踪中的作品命中续期);results 为空:tmdbMissDays(1 天) cache.tmdbMaxSize
tmdb.recognize TMDB 识别结果(MP TmdbCache) [类型][语言]{tmdbid 或名称}-{年份}-{季} tmdbSearchDays;未识别 {id:0} 按 tmdbMissDays cache.tmdbMaxSize
site.detail 站点详情页扩展内容 source_release.id max(站点详情缓存期, storage.cacheTtl.siteDetailDays)(7 天) 5000
websearch 网页搜索结果 参数 sha256 storage.cacheTtl.webSearchDays(1 天) 1000

TTL 设置(CACHE-2 起):settings.storage.cacheTtl,见 storage.md。MP 的 META_CACHE_EXPIRE 只有一个值, 我们按请求类别拆成搜索 / 详情两档,EMPTY_RESULT_CACHE_TTL 对应 tmdbMissDays;缓存语义不变。 条数上限仍是 cache.tmdbMaxSize(缺省 256,MP CONF.tmdb)。旧字段 cache.tmdbTtlHours、cache.emptyResultTtlSec 不再读取。

API:GET /api/v1/cache/tmdb?kind=recognize|request&limit= 列出条目(键 / 过期 / 大小,识别条目带 tmdbId / 标题 / 海报); POST /api/v1/cache/tmdb/clear 清空两类(仅 admin)。

识别缓存何时读写(sources-identity/service/tmdb-recognize-cache.ts):

  • 识别资源时先读:命中且有标题 → 按 id 取 TMDB 详情作为唯一远端候选;命中 {id:0} → 不再搜索;强制复查(force)不读。
  • 写:自动识别到 TMDB 条目写正常条目;确认无匹配写 {id:0};TMDB 出错或有歧义不写。
  • 人工认定资源身份后回填(MP update_recognize_cache),覆盖名称负缓存。
  • "元数据是剧集、缓存是电影"视为类型冲突:读时丢弃,写时拒绝。

图片代理与海报

  • GET /api/v1/img/tmdb/{size}/{file}(会话或 API Key read):只代理 image.tmdb.org,size 走白名单(w92…original), file 只允许 [A-Za-z0-9_-]+.(jpg|jpeg|png|webp)。
  • 缓存路径对照 MP sanitize_url_path:DATA_DIR/cache/images/t/p/<size>/<file>。
  • cache.globalImageCache(缺省关闭,MP GLOBAL_IMAGE_CACHE)开启才读磁盘缓存;拉到的合法图片总会写盘。
  • 按文件头识别图片类型,非图片 / SVG 拒绝;ETag = md5(内容),Cache-Control: public, max-age=604800,If-None-Match 相同回 304。
  • 上游超时(20s)/ 非 200 / 非图片 → 502,前端回退无彩占位。
  • 清理:定时任务 cache.images_clean(每天)删除 mtime 早于 cache.globalImageCacheDays(缺省 7)天的文件。
  • 容量 LRU(CACHE-2):cache.images_lru(30 分钟)在总量超过 storage.budgets.imagesGb(缺省 20 GB)时按最久未访问删到 90%; 读盘命中会刷新文件 atime。见 storage.md。
  • 用量 / 清空:GET /api/v1/cache/images、POST /api/v1/cache/images/clear(仅 admin);设置 → 元数据 → 图片代理与缓存。

前端:作品库列表、作品详情、搜索面板的 TMDB 候选在有 posterPath(metadata.posterPath / TMDB 候选)时显示海报, 加载失败回退原来的无彩占位。TMDB 详情刷新会把 posterPath / backdropPath 写进 metadata_field。

常见问题

  • TMDB 缓存不生效:确认 select region, count(*) from kv_cache group by 1 有 tmdb.request;PG 读写失败时按未命中处理并记 TMDB 缓存读写失败,按未命中处理。
  • 改了 TMDB 缓存设置:新写入的条目按新 TTL / 上限;已有条目保持原过期时间,必要时清空。
  • TTL 拉长后条目更早被条数上限挤掉:cache.tmdbMaxSize 缺省只有 256,追踪作品多时调大。
  • 数据库重启后缓存为空:UNLOGGED 表的正常行为。