该资料面向大型远程静态索引的缓存设计,核心目标是在索引体积大、条目多、偶尔更新的场景下,把日常新鲜度检查的成本降到最低。文档以 Kotlin 代码片段说明实现方式,并重点讨论正确性陷阱与验证方法。
用途:缓存一个以单个大型静态文件形式发布的目录或索引(资料描述为数百 KB、数千条目、偶尔更新),使其可被快速搜索,并在需要时与服务端校验是否更新。
主要设计决策:
- 存储为数据行而非整体 blob:只解析一次,写入数据表,使搜索成为有索引的查询,而不是每次都重新解析。
- 使用 TTL(生存时间):在向服务器询问文件是否变化之前,先在本地判断是否仍在有效期内。
- 使用服务器自身的校验器(ETag)做条件请求:发送 If-None-Match,使“未变更”只消耗响应头,不下载正文。
示例代码展示 refreshIndex(force) 的流程:读取本地元数据与条目数;若未强制刷新、元数据存在、条目数大于 0 且未超过 INDEX_TTL_MS,则直接返回 false;仅在存在条目时才回放 ETag;收到 NotModified 时更新 fetchedAt 并返回 false;收到 Updated 时替换条目并写入新的 ETag 与时间;收到 Failed 时返回 false,且不修改条目与 fetchedAt。
文档列出的陷阱:
- 空表时回放校验器会使空状态永久化:若 ETag 仍存在但数据行已丢失(事务失败、迁移不完整、手动清空),后续检查会一直得到 304,应用按协议正确却永远为空。修复方式是 takeIf { cached > 0 }。
- 200 响应解析出零条目应视为解析失败,而不是空目录:发布格式可能漂移,正则可能匹配不到内容;若此时替换缓存并保存新 ETag,会丢失原本可用的缓存。应在解析边界将零条目归类为失败。
- 失败的检查不得刷新时间戳:否则一次网络抖动会导致 TTL 阻塞重试;成功与未修改刷新时间戳,失败则两者都不动。
- 未修改(304)必须刷新时间戳:否则每次启动都会重新询问,违背 TTL 的目的。
- 数据行与校验器必须在一个事务中落地:分开写入时,崩溃可能留下描述不存在数据的 ETag,再次触发永久为空问题。示例给出 @Transaction 的 replaceIndex。
- 弱校验器仍应发送:静态主机常见的 W/"…" 同样能返回 304;跳过条件请求会让每次检查变成完整下载。应原样发送服务器给出的值,包括引号。
- 离线浏览后最后一刻失败体验更差:索引保存名称,每个条目的载荷通常是独立小文件;应按索引中的路径缓存已获取的载荷,使使用过的条目在无网络时仍可用。
- 用户输入进入 LIKE 时需转义通配符:先转义反斜杠,再转义 _ 和 %,并声明 ESCAPE '\'。示例给出 escapeForLike。
- force 应跳过 TTL 但保留校验器:手动“检查更新”若丢弃 ETag,会把小体积条件请求变成每次完整下载;解析响应才是成本所在。
验证方法:文档建议先用 curl -sIL 查看 content-length、etag、last-modified 以衡量文档大小;再证明条件请求确实节省正文。两次调用都加 --compressed,因为真实客户端会这样发送,也才会看到弱校验器;测量调用加 -L,否则重定向会显示 302 0 而看似通过。随后携带正确 ETag 应得到 304 0,携带故意错误的校验器应得到 200 和非零大小,否则说明检查对一切都返回 304,证明不了任何事。还建议做三种故障注入测试:空表但校验器存在时应重新填充;200 但正文无法解析时旧行与旧 fetchedAt 都应保留;传输失败时 fetchedAt 不变,且立即再次刷新应发出第二次请求而不是被 TTL 短路。
适用场景:大型静态目录或索引的客户端缓存、需要子串搜索的本地索引、需要离线访问已用条目的应用。限制与依赖:资料未提供具体平台、收费、价格或效果数据;文中代码为 Kotlin 示例,实际实现细节、存储引擎与网络库暂未提供。