三个所需位均为一时返回“也许”;含零时表示不在所代表集合中。位置为示意,假定过滤器正确维护。

布隆过滤器能廉价地说一句“也许”

布隆过滤器能廉价地排除某个元素属于它所表示的集合,而肯定答案只能解释为“可能存在”。这种不对称依赖更新正确、哈希规则保持一致。对于已经插入的元素,普通只增不减算法没有假阴性;但过时或损坏的过滤器仍可能把数据库里真实存在的键判断成不存在,不能把算法性质无限扩大成应用保证。 位图保存的是候选线索,不是记录 先准备一张包含 m 个零位的位图,并为每个键计算 k 个哈希位置。插入时将这些位置全部置一;查询时,只要任意所需位置为零,就返回否定,否则返回“也许”。不同键可能共用位置,所以全部位置都是一,并不能证明查询的键真的被插入过。 再次插入同一个键,不会改变已经置好的位。过滤器没有保存原始键和值,也不能枚举原集合。如果误判会影响正确性,精确集合或数据库仍然是权威答案;不能仅凭“也许存在”就授予权限、扣款,或者丢弃必须处理的工作。少一次查询的收益不能替代业务判断。 按预计元素规模分配空间 在哈希近似均匀且相互独立的常用模型下,插入 n 个不同元素后的假阳性概率近似为 p = (1 − exp(−kn/m))^k。固定 m 和 n 时,k 接近 (m/n) ln(2) 可以使这个近似值最小。哈希次数并不是越多越好:除了增加计算,它也会更快地把位图填满。 选择合适的哈希数量后,百分之一的误判目标大约需要每个元素九点六位,尚未计算实现开销。一千万个元素因此需要大约一千二百万字节的位图,并不是几百万个位就够用。讨论空间时必须区分位、字节、位图本体以及整个进程的内存,避免把不同口径混在一起。 假阳性率针对的是确定不存在的元素查询,不是所有肯定答案中错误答案所占的比例;后者还取决于调用方有多大概率查询真实存在的元素。应使用已知非成员测量,并将有限样本的观察值与概率模型区分开来,不能看到一次误判较少就宣称达到了严格概率保证。 让表示的集合跟上权威数据 如果准备根据否定答案跳过数据库查询,过滤器就必须包含所有相关数据库插入。先发布新行、稍后才更新过滤器,会产生可见性缺口。重建过程也需要定义快照和增量更新如何交接;过滤器的一次查询成功,并不能证明应用层这些步骤已经原子完成。 写入方和读取方还必须保持位数、哈希构造、种子或域前缀以及字节编码一致。改变这些输入就会改变位置。普通布隆过滤器不能通过清除某个元素对应的位来安全删除,因为其他元素可能依赖同一位;计数型或其他支持删除的结构,使用的是不同契约,不能把接口混为一谈。 坏掉的样子 向固定大小的位图插入过多元素,会提高假阳性率,逐渐失去减少昂贵查询的收益。正确插入的元素仍不会被误判为不存在,但查询越来越难得到否定答案。扩大固定过滤器需要重建或采用相应增长设计,不能只修改取模用的位数,否则已有位的位置含义就变了。 真实产品不一定像下面的固定大小模型。Redis 的 BF.RESERVE 在允许扩展时可以增加子过滤器,禁止扩展时则在达到容量后拒绝插入。子过滤器会增加内存和查询工作;应检查实际配置与实现,不能假定产品会像没有容量检查的插入循环一样静默变得饱和。 更新丢失、编码不一致、快照落后和错误删除,都可能破坏应用期望的否定答案安全性。除了观察假阳性,还要记录插入失败与过滤器新鲜度。过滤器是一项优化,它的维护仍属于数据生命周期的一部分,不是创建一次之后就可以忽略的永久索引。 布隆:也许并不等于证明。依据原文绘制机制;模型值与原文报告的观察不代表本次运行测试。 快速验证 保存为 bloom_check.py,用 Python 3.10 以上版本执行 python3 bloom_check.py。实验已在 Python 3.14.4 上运行,仅用标准库。七个带域前缀的 SHA-256 摘要、明确的 UTF-8 编码和固定键序列保证测试可重现,不需要外部服务、网络访问或文件写入。 ...

十二月 15, 2027 · 2 分钟 · XB Field Notes
收到的 Age 为 20,共享缓存到 30、私有缓存到 60 即变陈旧。独立陈旧窗口允许响应与异步验证并行,不代表重新新鲜。

缓存的新鲜度要对着时钟判断

响应经过另一个缓存,不代表自动获得一份全新的新鲜度额度。缓存必须计入交付路径上已经积累的年龄,再增加自己的本地停留时间。否则,每一层看起来都只缓存六十秒,串在一起却可能把旧内容直接复用远超源站预期的时间。判断新鲜度需要明确时间来源,不能只把配置中的秒数当成最终交付保证。 收到的 Age 为 20,共享缓存到 30、私有缓存到 60 即变陈旧。独立陈旧窗口允许响应与异步验证并行,不代表重新新鲜。 仅为来源示意;本任务未执行示例。 先算清年龄 Age 估计从响应生成或成功验证以来经过的秒数,其中包括缓存停留与网络传输。收到的 Age 是计算当前年龄的输入,不是在本地存放期间保持不变的计数器。Date 提供响应生成时间;把这些信息联系起来时,时钟同步和实际测得的请求、响应时刻都可能影响结果。只截取一组响应头,不能看到它随后在缓存里又停留了多久。 RFC 9111 给出两条年龄估计路径:接收时刻减去 Date,负数归零,得到表观年龄;收到的 Age 加响应延迟,得到校正年龄值。保守算法取两者较大值,再加本地停留时间。例如请求时刻为一百秒,接收为一百零二秒,Date 为九十五秒,收到的 Age 为十秒,当前时刻为一百零七秒,表观年龄是七秒,校正值是十二秒,当前年龄最终为十七秒。只读原始 Age 就漏算了七秒。 新鲜度寿命也有优先顺序。共享缓存存在 s-maxage 时先用它,否则 max-age 优先于 Expires 减 Date;私有缓存忽略 s-maxage。没有显式过期信息时,在允许的条件下可能使用启发式寿命,但并不存在统一固定时长。只有适用的新鲜度寿命严格大于当前年龄,响应才是新鲜的;恰好相等时已经到达陈旧边界,而不是再多送一次的许可。 ...

十二月 3, 2027 · 2 分钟 · XB Field Notes
文章受控样例中,同一键的十二个重叠调用,不合并时执行十二次加载,一个共享 Group 执行一次,两个独立 Group 执行两次。这轮结束后,下一轮仍会重新加载。

缓存集中失效会放大回源工作

缓存雪崩或击穿所造成的一个关键风险,是并发未命中对同一依赖发起大量重复工作。同时到期只是触发条件之一:容量淘汰、刚部署的空缓存、缓存服务不可用,都可能造成类似突发。即使只有一把热门键,也足以压垮后端。判断风险时应数清楚:一份结果尚未计算完的这段时间内,到底有多少次重建真正抵达数据源。 文章的重叠调用样例在明确的 Group 边界内统计工作量。一个 Group 合并一轮在途工作,两个独立 Group 各自加载,下一轮仍重新执行。完成结果的存储仍由应用负责。 在真实边界内合并工作 Go 的 singleflight Group 对同一键,在这个 Group 内最多保持一次在途函数执行;重复调用者共享结果,也共享错误。不同 Group 之间不会协调,即使它们位于同一个进程;不同服务实例通常也各有自己的 Group。因此它既不是分布式锁,也不是保存已完成结果的缓存。把 Group 放在每个请求内部重新创建,无法实现跨请求合并。 实际使用时,应让请求共享同一个 Group,并使键准确表示等价工作。不同租户、权限、币种或响应表示如果错误地使用同一个键,可能拿到不属于自己的结果。对于缓存回填,在共享函数内部应再次检查缓存,确实仍缺失才访问数据源,并在函数返回前完成成功结果的缓存写入。这些存储操作都不是 singleflight 自动提供的能力。 错开时间不能改变新鲜度承诺 随机抖动可以打散不同条目或独立副本的到期时间,却不能阻止访问同一过期热键的人一起未命中。如果最大 TTL 是业务规定的新鲜度上限,在有界范围内随机缩短寿命,不会偷偷越过这个上限;任意增加正向抖动则改变了原有承诺。不能脱离请求量、计算成本和允许陈旧程度,宣称几个百分点或几秒钟就普遍足够。 ...

十一月 22, 2027 · 2 分钟 · XB Field Notes
从旧到新的三行依次为 ABC、访问 A 后的 BCA,以及加入 D 后的 CAD;被淘汰的是 B。

LRU 缓存淘汰最久未用的条目

LRU 按最近使用顺序选择淘汰对象,而不是按历史热度、存放年龄或业务重要程度选择。有界缓存需要腾出空间时,精确的最近最少使用策略会移除最后一次有效访问发生得最早的条目。它利用的是时间局部性,既不保证命中有价值的数据,也不保证留下来的数据仍然新鲜。 沿访问顺序找出淘汰对象 假设缓存只能存三个条目,并按从旧到新的方向列出。依次加载 A、B、C 后,顺序为 A、B、C;再次读取 A,顺序变成 B、C、A;此时加载 D,就淘汰 B,留下 C、A、D。普通字典即使保留插入顺序,也不会因为某个条目被读取就自动调整它的位置。 实现必须明确哪些操作算作一次访问。常见精确方案把查找表与双向链表结合起来,以较低成本完成查找、移到最近端和从最旧端移除。维护顺序需要额外元数据,也让缓存命中变成状态更新;并发实现还必须协调这些更新,不能把读取缓存理解成永远没有写入开销。 精确顺序与采样近似不是同一契约 Redis 使用近似 LRU:抽取候选并维护较好的淘汰候选池,而不是为每个键保存完整的精确访问顺序。采样数量可以配置,更多采样用额外计算换取更接近精确 LRU 的结果。它不承诺下面小模型的逐个淘汰顺序,也不能由此推导所有淘汰操作都有固定耗时。 候选资格同样重要。Redis 的 allkeys-lru 可以淘汰任意键,volatile-lru 则只考虑带过期信息的键。设置了有效期,并不保证键在截止时间之前不会被淘汰;如果没有符合条件的带过期时间的键,volatile 策略在内存压力处理上会表现得像 noeviction。 容量约束不是新鲜度约束 限制三个条目,不等于限制三千字节。不同键、值和维护结构占用不同空间,按数量限制的缓存仍可能持有很大的对象。Python 的 lru_cache 会持有参数与返回值的引用,直到条目被淘汰或缓存被清空。选择容量单位时,应对应真正需要控制的资源,而不是只看条目数是否稳定。 最近访问也不能说明源数据有没有变化。频繁读取一个过时答案,反而可能让它一直留在缓存中。有效期、显式失效和版本化键分别处理新鲜度问题;淘汰规则既不是一致性协议,也不是备份策略,不能因为缓存命中就跳过业务对数据有效性的判断。 坏掉的样子 一次扫描大量只用一遍的条目,可能挤掉本来有用的工作集。容量为三时循环访问四个不同键,即使这些键都在被反复请求,也可能每次都未命中。LRU 不会因为过去访问次数很多就永久保留一个键。LFU、插入前的准入策略或调整容量可能适合某些负载,但不能仅凭“读多”就自动选出正确方案。 应在有代表性的时间段一起观察命中、未命中、淘汰、保留字节数以及未命中的代价。很高的命中率也可能掩盖少量特别昂贵的回源;单看淘汰数量,不知道被移除的数据是否有价值。修改策略前,先还原实际访问轨迹,再解释缓存为什么做出了这些选择。 缓存命中还必须允许安全复用。带副作用的函数,或每次调用都必须创建独立可变对象的函数,不适合直接做记忆化。Python 的 lru_cache 能保持多线程下内部结构一致,但同时发生的未命中仍可能让被包装函数执行多次。线程安全不等于抑制同一项工作被重复执行。 访问 A 后,淘汰对象变成 B。依据原文绘制机制;模型值与原文报告的观察不代表本次运行测试。 快速验证 将代码保存为 lru_check.py,用 Python 3.10 以上版本执行 python3 lru_check.py。实验已在 Python 3.14.4 上运行,只使用标准库:OrderedDict 展示精确访问顺序,真正的 functools 装饰器另行验证记忆化行为。不启动 Redis,不发送网络请求,也不写文件。 ...

十一月 12, 2027 · 2 分钟 · XB Field Notes
顺时针环标出 A20、新增 D35、B50 与 C80;高亮 (20,35] 从 B 转给 D,其余归属保持不变。

一致性哈希减少重新映射

一致性哈希限制成员变化时的数据放置变化,但它本身不搬运数据、不让副本达成一致,也不把单个热键的请求自动分散。经典顺时针哈希环把键交给哈希位置之后遇到的第一个节点点位;如果位置恰好相等,就归该点,超过最大点位则绕回最小点位。理解这个查找方向,是判断迁移范围的前提。 先把归属区间画对 一个点拥有的是前驱之后到自身为止的区间,即 (前驱, 当前点]。假设 A 位于 20,B 位于 50,C 位于 80,那么 B 负责大于 20 且不大于 50 的位置;超过 80 的键绕回 A。这不是从 B 到其后继 C 的弧段,左右端点是否包含也不是可以随意改变的细节。 在 35 加入 D,它从顺时针后继 B 手中取得大于 20 到 35 的位置;移除 B,则将 B 原来负责的区间交给 C。未受影响的点位保持原位置及归属,这才是算法有用的性质。仅说“移动一小段弧”还不够,因为原来某段弧可能就占据整个环的很大一部分。 作为对照,直接用 hash(key) 对节点数量取模来选择数组下标,新增节点会改变除数,很多键随之选到别的下标。四个等容量且平衡的节点扩到五个时,理想哈希环大约需要迁移五分之一的键,但真实迁移比例取决于点位与键分布,不能把这个比例当成所有部署都成立的保证。 ...

九月 19, 2027 · 3 分钟 · XB Field Notes
源站 /manual.txt 已改为 B。缓存 0 的精确条目被移除,下一次请求获取 B;独立的缓存 1 仍保留新鲜的 A 并直接复用。一次定点移除不会让两个缓存同步。

缓存失效是主动推进,不是祈祷

源站文件改了,并不意味着每个已经保存响应的缓存都会收到通知。失效操作控制的是哪些已存副本还可以继续复用,而不只是更新磁盘上的内容。可靠的发布要区分到期、重新验证、定点移除以及使用新 URL:它们解决不同范围的问题,也没有哪一种能远程擦掉读者已经下载的所有字节。 两个独立私有缓存让移除范围变得具体:改变一个条目,另一个仍可复用。发布并请求新 URL 会建立独立条目,不会擦除旧 URL,也不会自动更新所有读者。 到期不是广播通知 源站发生变化后,一份仍然新鲜的响应可能继续被合法使用。新鲜度由缓存寿命和当前年龄共同决定,年龄计算涉及缓存元数据与经过的时间,并非只从源站的 Date 字段开始倒计时。到期改变的是直接复用时必须满足的条件,既不保证存储介质立即删除条目,也不会让无人访问的缓存主动联系源站。 验证器同样不是内容变化的推送通道。请求需要重新验证时,正确的 ETag 才能帮助源站确认表示未变,或者返回替代正文。浏览器手上的响应还新鲜、没有理由回源时,新增验证器并不能强迫它询问。把源站策略改成 no-cache 或 no-store,也无法追溯修改浏览器之前已经收到的缓存指令。 先确定失效覆盖范围 RFC 9111 要求:缓存收到不安全方法对应的成功或重定向响应时,必须使目标 URI 的已存响应失效。这个义务只涉及该请求实际经过的缓存,不是向整个网络广播。通过另一条接口修改数据库,不会自动覆盖所有派生页面、查询参数变体、不同语言表示和搜索列表,应用需要识别这些真实依赖。 ...

八月 5, 2027 · 2 分钟 · XB Field Notes
历史 nginx 实验:绕过请求返回 B,但缓存仍保存 A;下一次普通请求仍为 HIT A。

过期缓存需要明确的重新验证与陈旧响应策略

nginx 代理缓存需要分别作出几项决定:怎样识别已存响应,什么时候允许读取,什么时候允许保存新响应,以及过期副本还能怎样使用。如果把这些都看成一个 TTL 设置,故障就很难解释。验证时应同时观察请求实际经过的路径与交付字节,而不只是确认 nginx 接受了配置文件。 配合使用 bypass 与 no-cache 时,返回字节可与存储字节不同。图中 nginx 1.30.4 结果均为历史单进程观察;当前运行核验仍受阻。本图未重跑实验。 存储容量与新鲜度分开看 proxy_cache_path 声明磁盘存储位置及保存元数据的共享内存 keys_zone,proxy_cache 再为请求启用这个区域。元数据区大小不等于正文缓存容量。max_size 由缓存维护机制管理,不是瞬时硬磁盘配额;inactive 保留策略也不是响应的新鲜度寿命。已经过期的响应仍可能留在存储中,用于后续重新验证。 默认缓存键为 $scheme$proxy_host$request_uri,其中 request_uri 已经包含查询串,后面再加 $is_args$args 就重复拼接了参数。也不能随意替换成规范化后的 URI,或遗漏会改变表示的输入。默认启用 HEAD 转换,因此 GET 与 HEAD 共享相应缓存行为;关闭转换时,就需要在缓存键中考虑请求方法。 ...

六月 24, 2027 · 3 分钟 · XB Field Notes
缓存命中判断直接返回已存值;未命中分支加载并保存结果。

缓存拥有未命中路径:示意。

读穿透缓存:未命中时取数回填

读穿透缓存给调用者提供统一的读取接口:命中时返回已存值,未命中时调用加载函数,再把结果保存起来。关键边界是由谁拥有这个 loader。把它放到缓存接口后面,可以减少重复的查找逻辑,但并不会自动解决并发、写入、访问授权或不同缓存副本之间的一致性。模式名称不是这些能力已经具备的证明。 找到真正负责加载的一层 在 cache-aside 模式中,应用显式查询缓存,未命中后访问数据源并回填;读穿透则让缓存组件拥有这条未命中路径。它可以是应用进程里的库,也可以是远程缓存服务。两种部署方式各有更新边界:修改进程内库的 loader,仍然需要把变更部署到使用它的进程,不能笼统宣称所有读者都会免部署地自动获得新逻辑。 单独一个 Redis GET 也不是读穿透加载器。字符串键不存在时,它返回 nil,不会自动发现并查询你的数据库。客户端库、缓存服务或应用适配层必须另外提供加载逻辑。下面的实验用带记忆功能的函数包装器提供统一读取接口,缓存和数据源都在当前进程内;这不是 Redis 服务端加载功能的演示。 读取模式没有定义写入模式 读穿透本身没有规定写操作经过哪里,写穿透是另一项安排。即便读写共用一个组件,仍需要定义数据源写入和缓存更新分别失败时的行为。绕过组件的其他写入方,可能让缓存保持旧值。应沿真实业务更新路径检查参与者,不能因为使用了一个模式名称,就假定所有写入都会配合清理。 TTL 限制的是某个缓存条目的复用时间,并不保证下一次重载从滞后副本或有缺陷的 loader 中取到最新真值。过期也不一定立即从内存中释放字节。寿命应依据数据需求选择,而非套用统一的几秒或几分钟;用户数据的 TTL 再短,也不构成权限隔离。命中率很高同样不能证明内容正确。 坏掉的样子 记录不存在与数据源不可用,是两个不同结果。缓存一次合法的“不存在”可以减少重复查询,但如果随后创建了记录,这个否定结果也可能继续遮住新记录,直到到期或失效。若把超时转换成一次正常返回的空值,就可能把故障写成错误的缓存内容。必须明确哪些结果允许保存,以及调用者如何区分缺失和失败。 并发加载是另一项独立契约。Cachetools 明确说明:锁保护缓存对象的访问,但被包装的函数在锁外运行,因此只有锁并不能抑制重复加载。它的 condition 选项另行处理同键的并发执行。下面的实验只运行单线程,刻意不使用这两个参数,不能把代码当成已经具备线程安全或分布式协作的实现。 失效还可能与一个已经读到旧源版本的加载操作竞争。删除某个键,并不天然阻止那个在途结果稍后重新写入。应根据真实的写入和加载协议选择协调方式,并测试操作顺序。也不要把大范围按键模式删除当作默认修复:它会让更多条目变冷,却没有证明源头缺陷已经解决。 历史精确 cachetools 证据仅覆盖单线程包装器与手动时钟。加载路径、缓存缺失结果和抛出错误分开;未测试并发或分布式一致性。 快速验证 需要启用断言的 Python 3.10 或更新版本,不能使用 -O 或 PYTHONOPTIMIZE,因为断言中含有必需调用。保留的精确代码块记录报告 Python 3.12.14 与 cachetools 7.1.8 上通过;本轮没有重跑。确认固定依赖安装成功后,用 "$readthrough_lab/venv/bin/python" 执行。库提供存储、到期与记忆功能,手动测试时钟无需睡眠或修改系统时间;不联系 Redis 或外部数据库,只有依赖安装需要联网。 ...

五月 26, 2027 · 2 分钟 · XB Field Notes