qinghuan
← 从 Java 到只读商户 Agent

商户项目 · 独立练习

缓存补充实验:把旧值出现的顺序记下来

记录 TTL 丢失、旧读回填和删缓存失败后的恢复。

核查日期:2026-10-11。状态:三个实验均未运行;下面给出的是设计步骤、预期观察和空白记录,不能作为系统已验证的证明。

目标与前置知识

理解 cache-aside 的命中、回填与写库后失效,再亲手观察三个容易被略过的失败窗口。先会商户查询、数据库事务、Redis GET / SET / DEL / TTL,并了解同一请求可在读取和回填之间停住。保留“Redis 缓存”和“商户缓存与一致性边界”原文;本节作为延伸,不改黑马点评 Day01。

只用隔离数据库及独立 Redis 的 study:merchant:A:1001 键。1001 为虚构“测试茶铺”,初始营业时间 09:00–18:00、数据库版本 v1;v2 的营业时间可改成 09:00–17:00。版本只是观察标签,还不是防止旧值的校验协议。

先走通正常路径

  1. 删除本实验自己的缓存键,发起有权查询:缓存未命中,查数据库 v1,将 v1 与有效期一起写进 Redis,再返回 v1。
  2. 再查一次,检查缓存命中,并用数据库查询计数核实是否避免回源。
  3. 将数据库提交为 v2,提交成功后删除缓存;下一次查询读取 v2 并回填。分别留下数据库提交、删除命令结果和缓存内容的证据,不能只看一条“更新成功”日志。

实验一:普通 SET 覆盖后,TTL 去哪里了

在 Redis CLI 中按顺序执行下面命令。它们只写指定学习键,尚未实际执行:

文中示例 · text
SET study:merchant:A:1001 "v1" EX 60
GET study:merchant:A:1001
TTL study:merchant:A:1001
SET study:merchant:A:1001 "v2"
GET study:merchant:A:1001
TTL study:merchant:A:1001

设计期望是:第一次 TTL 为非负剩余秒数;若命令足够快通常接近 60,但不要断言恰好 60。第二次普通 SET 成功后,键保留 v2,TTL 为 -1,表示存在但没有过期时间。TTL 为 -2 表示键不存在,两者不可合并成“缓存失效”。再分开做 SET ... EX 60 和 SET ... KEEPTTL 两个对照;KEEPTTL 必须在键原本有有效期时才有可保留的 TTL。本实验数据库不参与改变,数据库版本列标注“不适用”,不能杜撰版本。

实验二:写库并删缓存后,旧读仍可能回填

用可控闸门、断点或测试钩子保证下面的先后顺序,避免依赖随机 sleep 猜测是否复现。这里的 v1/v2 和 TTL 都是设计状态,实际值要另记。

事件执行者与动作数据库设计状态缓存设计状态TTL 观察点
E1准备数据,缓存为空v1不存在读取 TTL,应为 -2
E2读请求 R 缓存未命中,从数据库取到 v1,暂停在回填前v1不存在记录实测
E3写请求 W 提交数据库事务v2不存在记录实测
E4W 删除缓存并收到成功回复v2不存在记录 DEL 返回及 TTL
E5放开 R,它把先前读到的 v1 写回并设 EX 60v2v1读取实际剩余 TTL
E6新请求命中缓存v2可能仍为 v1记录响应版本与 TTL

此交错显示:删除成功只说明删除那一刻的状态,挡不住随后发生的旧回填。TTL 从回填发生时计算,不能直接当成“从数据库提交时算起最多只旧 60 秒”的保证;延迟回填、再次覆盖或刷新有效期会改变窗口。单次删缓存、延迟双删都不保证强一致。

实验三:数据库已提交,但删除失败了

  1. 初始数据库和缓存均为 v1,缓存带 TTL。在测试客户端的删除调用处注入一次失败,让缓存实际保留;用明确失败注入避免把“超时”误当成已知未删除。
  2. W 将数据库提交为 v2。删除失败时,记录错误、任务状态和缓存 v1,不返回“数据库也回滚了”的假象。
  3. 设计一个可持久恢复的失效任务,记录键、目标实体、事件 ID、发生时间、尝试次数和状态。在生产设计中可研究与业务更新同事务保存的 outbox 等方式;本节只提出待实现方案,没有验证它能可靠送达。
  4. 停止并重启恢复处理器,核对任务是否仍在;恢复后重试删除,确认命令返回与后续缓存状态,再由新读回填 v2。若 DEL 返回 0,可能只是键已经不存在,不能仅因此当成恢复失败。
  5. 单独保留“删命令超时但可能已经执行”的分支,恢复逻辑需允许重复失效。重复删除虽然不会重复修改数据库,却可能增加回源;仍要限制恢复速度、观察积压,并保留最终失败的人工处理出口。

这个实验只验证删缓存失败的恢复路径。即使恢复任务成功,也没有自动消除实验二的旧读回填。不要将它写成数据库与 Redis 的原子事务。

每个事件都使用的空白记录

运行ID实验/事件号单调时间/顺序执行者操作DB版本缓存版本/不存在TTL秒返回/异常恢复任务状态证据文件

一个事件一行,不把“设计状态”复制进“实际结果”。未知留空;明确无数据库参与时填写“不适用”。交付物是三张事件表、真实命令/客户端记录、复现控制点、结果断言及未覆盖窗口。

版本和结论边界

SET说明普通 SET 的过期语义和 EX / KEEPTTL 选项;TTL说明 -1/-2。两者是滚动文档,按实际 Redis 版本复核选项。

Redis cache-aside先读 How Redis supports the solution,把“先读缓存,未命中读原始库并回填;更新原始库后失效”画成两条路径。文档中的示例性能与陈旧窗口描述不能当作本站测量或无条件保证。

Spring Redis Cache核查时为 4.1.1,先读默认设置及 Redis Cache Expiration。默认键过期时间为 None,需要按实际项目明确配置;启用缓存注解不等于自动具备 TTL 或跨系统一致性。

后面的版本下界与故障恢复章节给出候选教学设计,均未实现或运行。本站已有 Java 单进程代次模型有它自己的教学边界,不能据此声称已验证数据库与 Redis、多个服务实例、进程重启和网络乱序下的方案。

商户项目 · 延伸练习 · 未运行

版本下界:缓存被删除后,仍要记住哪些旧值不能回填

先完成本页“写库并删缓存后,旧读仍可能回填”实验设计,再读缓存三态与回填票据和Redis 事务与 Lua。前者是单进程代次模型,下面研究数据库提交版本与 Redis 下界的跨系统边界,不能直接复用其验证结论。

核查日期:2026 年 10 月 12 日 00:12 JST(UTC+9)。来源:Redis Transactions 与 Scripting with Lua。均为 latest 文档,发布日期与对应完整服务端版本未标明;事务页注明 WATCH 的过期键行为在 Redis 6.0.9 前不同。适用:本页旧读回填实验之后;前置:版本号、数据库提交、缓存失效和并发屏障。以下方案只用于隔离假商户1001的教学设计,未实现或运行。

与已有 WATCH/Lua 说明衔接:Redis 内的原子比较与更新不能把数据库提交和 Redis 更新合成跨系统原子事务。下面的协议需要另外保留版本证据。

反例一 只比较当前缓存版本不够:旧读持有 version=1;写者提交 version=2 后删除缓存;旧读看到缓存不存在,于是“允许填充空键”,版本1仍可能写回。即使比较和写回由一个原子脚本完成,也无法凭空知道已被删掉的版本2。这个反例是流程推演,不是实测结论。

待验证设计:另保存“已知最低可接受版本”标记,即版本下界,与缓存内容的生命周期分开。写侧获知已提交版本2后,在 Redis 内将下界单调推进到至少2,并使低于下界的缓存内容失效;回填侧在同一个 Redis 原子操作中读取下界、比较候选版本并决定是否写入。只允许不低于下界且不覆盖更高现存版本的候选。等版本重复回填不得换成不同业务内容;删除与重建记录也必须采用不会重用旧版本的规则。

这一设计的必要前提:版本来自数据库受控提交顺序,所有写入和回填路径遵守协议;下界不能被迟到事件倒退,不能随普通缓存删除而丢失。数据库提交到下界推进之间仍有窗口;传播失败、重启或淘汰丢失下界时,不能将“下界缺失”默认等价为“任何旧值都可写入”。教学策略可选择跳过缓存回填并从权威主存储重新读取,但仍需重新建立协议,单次重读也不是跨系统原子保证。

三个可控验收场景:

  • 先提交版本2并确认下界已到2,再放行持有版本1的旧读;预期拒绝其回填,并保留可解释的拒绝原因

  • 先收到版本3、后收到迟到的版本2通知;预期下界与现存版本不倒退。重复版本3通知应保持相同结果,不延长旧数据寿命

  • 人为延迟下界通知,或只在本地模拟标记过期、淘汰与丢失;检查是否触发预定保守路径,并记录仍可能读旧值的窗口。不能只跑正常顺序后宣称已解决一致性

记录数据库提交版本、通知到达顺序、下界前后值、候选版本、回填决策和最终响应;把“下界已推进之后拒绝旧候选”与“提交后立刻读到最新值”分成两个不同验收目标。WATCH 冲突要重新读取并有界重试,不能用旧比较结果直接重放写入;Lua 方案需限制执行时间,不能把外部数据库查询塞进 Redis 原子区。

限制:没有部署或测试本方案,无线性一致性、故障恢复或性能保证。版本下界的可靠保存、通知恢复、数据库读一致性、Redis故障切换和集群键布局仍须专门设计。仅将版本数字放入缓存值中不等于协议完整;不提供未经验证的可直接上线脚本。

↑ 返回延伸练习目录

商户项目 · 延伸练习 · 未运行

版本下界丢失后的恢复检查与观察记录

接在本页版本下界设计之后。先读Redis 持久化与恢复,理解副本确认、数据恢复与连接可达的区别;本节只给故障适配器验收设计。

核查日期:2026 年 10 月 12 日 06:06 JST(UTC+9)。来源:Redis replication 与 WAIT。均为 latest 文档,发布日期与完整适用版本未标明;WAIT 自 Redis 3.0.0 提供,文档另说明 Redis 7.0 起不可阻塞上下文中的行为。适用阶段:版本下界方案的故障测试;前置为隔离假商户数据、数据库权威版本及可控制的故障适配器。实验未运行。

文档边界:前置文章已解释默认异步复制与 WAIT 的连接范围、确认数量。这里还需注意,WAIT 超时 0 表示无限等待;脚本等不可阻塞上下文不能当作同步等待屏障。WAIT 不使 Redis 成为强一致存储,故障切换仍可能丢失已确认写入。INFO replication 或 ROLE 的状态本身也不是数据库与缓存业务版本一致的证明。

应用推论:缓存重新连通、复制链路正常和版本下界已恢复是三个不同结论。若候选实现引入 WAIT,应绑定正确写入连接、有限等待并记录实际确认数;不能把确认不足解释成先前写入已回滚,也不能盲目重做数据库业务写入。本节只研究其边界,不要求为原项目增加 WAIT 或修改复制配置。

恢复前检查清单:

  • 确认当前读写目标、实例角色与测试版本,避免仍向旧主节点写回;连接成功只证明可达

  • 从权威数据或可靠恢复记录核对业务版本、下界及缓存内容;下界缺失或落后时走预设保守路径,先暂停该键回填,不能把缺失当版本0

  • 查清仍在途的旧读与迟到通知;在恢复完成后放行旧候选,确认版本约束仍生效,而非清空测试进程后掩盖问题

  • 只有恢复依据足够、下界已按协议重建且受控负例通过,才在实验中标记恢复回填;这不等于证明所有故障场景安全

两个未运行实验:一是在故障适配器中让数据库保留版本3而缓存侧下界恢复成版本2或缺失,保留在途版本1读;检查保守路径与后续拒绝旧回填。二是模拟缓存写入已完成但确认响应丢失,将客户端结果标“执行结果未知”,先核对当前版本再按幂等协议恢复。两个实验均只模拟症状,不实际重启、切换或破坏任何线上Redis。

观察记录模板:run_id、故障编号、JST时间与事件序号、数据库提交版本、当前实例角色、下界前后值、缓存版本、候选版本、写入回执(成功/失败/未知)、副本确认数(未使用则不适用)、回填决策、恢复依据和证据。连接恢复时间与重新允许回填时间分开记录;尚未采集的耗时留空,不填0。不同机器的时钟不作为唯一排序依据,使用测试屏障和关联事件确认先后。

限制:没有执行测试,没有故障恢复时长、丢失率或一致性成绩;可靠恢复记录如何产生仍是待设计项。Redis范围内的确认与版本比较仍不能替代数据库和缓存之间的完整一致性协议。

↑ 返回延伸练习目录

商户项目 · 延伸练习 · 未运行

缓存转存、刷新失败与回答中的时间依据

先读获取时间与事实适用时间,再完成本页缓存实验和商户经营状态判断。本节适用于商户查询已经接缓存之后,补充转存、到期边界和刷新失败如何影响工具证据与回答措辞。

资料原核查:2026 年 10 月 17 日 12:08 JST。来源为 RFC 9111(2022 年 6 月)的 Freshness、Age、Serving Stale Responses和 must-revalidate。下面的时间、阈值和策略均为未运行的教学夹具。

转存不会产生新的业务确认

已有时效文章解释 HTTP 缓存可复用与业务事实准确的区别。这里沿用 freshness_lifetime > current_age 的判断:Age 是自源站生成响应或成功验证以来的估计秒数;没有 Age 不能证明本次联系过源站。响应很快、Age 较小或刚取得资料,都不足以写成“刚刚核实商户现状”。

来源明确给出的业务更新时间或确认时间单独保留,未知就留空;本系统获取、成功验证与回答时间分别记录。缓存读出或转存时,不把本地写入时间替代来源确认时间。只有获取时间时可写“于某时获取,来源更新时间未知”;“资料截至某时显示……”必须有对应的来源时间依据。自定义 freshnessState 的语义与阈值由项目约定,模型不能自行扩展有效期。

刷新失败并不必然允许返回旧响应。HTTP 的 must-revalidate 要求过期后先成功验证,断网也不能忽略。HTTP 规范约束 HTTP 缓存;自行实现的 Redis 应用缓存需要另定业务时效及降级规则,不能声称 Redis 自动执行 HTTP 指令。

建议练习:从时间依据检查措辞

  1. 边界时刻。 用受控时钟和预先计算的 current_age,令 freshness_lifetime 为 60 秒,检查 59、60、61 秒;60 秒时已不满足 fresh 条件。这只验证比较边界,完整 HTTP 年龄还包含 Date、Age、传输和驻留时间,不能用当前时间减 Redis 写入时间替代。
  2. 缓存转存。 假资料业务确认于 10:00,系统 10:10 获取,10:20 转存,10:21 回答。确认时间仍须为 10:00。再去掉来源时间或 Age,检查回答没有“刚刚核实”或“实时”的无据断言;保存原始头、业务时间、缓存路径与最终工具结果,定位依据在哪一段丢失。
  3. 刷新失败。 对同一过期资料分别设置禁止复用的 HTTP 策略,以及单独允许、有最大陈旧期限的应用教学策略。前者验证失败时返回不可确认;后者只有在规则允许且未超期限时,才可附陈旧标记供参考。超期限、时间未知或来源冲突时按既定契约返回未知,不能用旧状态回答“现在营业”。

交付每个时间的生成者、原始值、缓存转换路径、适用策略、工具状态及最终措辞。前置文章的离线示例曾执行,不代表本节商户缓存与模型链路已执行,两者的运行记录分开。

限制:没有改动缓存配置、调用真实商户服务或模型,以上阈值不是生产推荐值。来源更新时间本身也可能滞后于现实,时间戳不能单独保证事实准确。

↑ 返回延伸练习目录