缓存下来的分类省下一次查询,也把昨天的判断冻进今天的决定里。真正的比较不是缓存与不缓存,而是观测、它的新鲜度,再加上站点选择的响应。三者之间的边界一旦丢失,优化就开始自己制定策略。要修的是记账,不是换一个更好的数据源。

三种记录,三个不同的时钟

观测是收到的字段、它来自的数据源,以及背后的数据集版本。新鲜度是缓存条目的属性:查询何时执行、条目何时不再可信、上一次刷新有没有返回结果。响应则是站点针对这次请求、这个账号或这个操作所做的处置。三者由不同的所有者产生,衰减速度也不同,所以把它们压成一行叫 blocked 的记录,就等于抹掉了这种区别。

RecordWho owns itHow long it should liveDamage when it is folded into another row
The observationthe feed's operatoruntil a newer dataset version replaces ita stale lookup becomes indistinguishable from a fresh one
Its freshnessthe applicationone request's tolerance for agea failed refresh hides behind a cache hit
The responsewhoever owns the site policyuntil that policy is revieweda feed outage turns into an indefinite block

有两个时钟需要特别小心。你的查询执行时刻并不是提供方发布数据集的时刻,事后读这行记录的人必须知道每个值来自哪一个。过期与刷新应按数据源文档化的更新模型和出错的代价来设定,绝不能照搬另一个站点的数字。

留下字段,而不是结论

MaxMind 的 匿名 IP 文档 就是一个具体例子:它把匿名 VPN、托管服务商、公共代理和 Tor 出口分成各自独立的字段;文档还说明,未登记的 VPN 子网可能反而挂在托管标记之下。把这一切压成一个布尔值,就丢掉了运营者日后唯一能核查的部分。

两个方向的推断都不安全。托管标记不能证明用过隧道,缺少 VPN 标记也不能证明客户端与服务之间什么都没有。这是两个不同的主张,一个布尔值哪个都表达不了。

把条目绑定到你验证过的地址

在反向代理之后,应用必须采用部署所批准的客户端地址处理方式。调用方提供的转发头,绝不能用来选中别人的缓存声誉。

这个失效既是正确性缺陷,也是安全缺陷:能控制该头的攻击者可以继承一条可信条目,也可以把受害者钉在坏条目上。密钥要从未被他人接管的、由你自己基础设施终结的连接推导,并记录采用了哪种推导。前面有多级代理时,要记录整条链路,而不是相信第一个看似合理的头。链路模糊时,把推导出的键和被拒绝的头都记下来,因为事后复盘问的正是后者。

没有证据不等于干净结果

超时、格式错误的响应和不可用的数据库,每一种都与“成功查询但没有匹配”含义不同。把它们表示为彼此独立的结果,并提前决定:完全没有证据时,受保护的操作该怎么办。只读页面与已登录账号的操作,在那里完全可以遵循不同的规则。

保留正常认证,或者要求走受支持的验证,通常比把故障当成一律放行或全站封禁更好。限定查询的运行时长,并用集成所支持的任何机制止住刷新风暴。故障期间使用旧值时,要在决策点标注它是旧值,并让刷新最近一次失败可见,别让长期故障冒充健康条目。无论选哪一种,都写在策略真正生效的地方,而不是查询旁边的注释里。

保留策略属于同一个决定:只留这个功能需要的字段和背后的复核痕迹,不要更多。IP 缓存不必变成浏览历史。

接受四种情形,其中包括一次纠正

测试新结果、过期结果、失败的查询,以及后来被纠正的分类。每一种走的是不同分支,只有最后一种能说明错误限制是否可以撤销。

然后确认普通用户在误封之后有申请复核的路径,因为数据源有时会错,队列也是设计的一部分。把这四种情形连同策略负责人的名字写进 runbook。只有当设计既去掉了冗余查询,又让不确定性保持完整、让决定清楚地落在具体的人身上时,它才配得上自己的位置。

相关文章