面试知识

Redis(远程字典服务)缓存一致性与缓存问题

12-Redis从基础到精通 面试知识整理。

Redis(远程字典服务)缓存一致性与缓存问题

本章的核心不是背“先更新数据库再删缓存”,而是先确定权威数据源、允许多长的不一致窗口、失败后由谁补偿,再选择缓存模式。缓存用于提高读取吞吐和降低延迟,可以丢、可以重建;支付资金事实和库存最终正确性必须由数据库事务、唯一约束、状态机、流水与对账保证。

1. 缓存角色、一致性目标与模式选择

1.1 权威数据源与四种缓存模式

Cache Aside(旁路缓存)由业务代码先查缓存,未命中再查数据库并回填;写入时先提交数据库,再删除缓存。Read Through(读穿透)把加载逻辑封装在缓存组件内部;Write Through(写穿透)由缓存组件同步写权威存储;Write Behind(异步回写)先写缓存,再异步批量落库。后三者不是 Redis(远程字典服务)自动提供的完整业务语义,通常需要代理层或应用框架实现。选型的第一问是“谁是事实来源”:若数据库是权威源,缓存丢失后必须能重建;若缓存先确认、数据库后落地,就要承担队列丢失、进程崩溃、乱序回写和冲突合并风险。

flowchart LR
    C["客户端"] --> A["应用服务"]
    A -->|"Cache Aside(旁路缓存)读写"| R["Redis(远程字典服务)"]
    A -->|"事务写入"| D["权威数据库"]
    A --> T["缓存代理层"]
    T -->|"Read Through(读穿透)"| R
    T -->|"Write Through(写穿透)"| D
    T -->|"Write Behind(异步回写)"| Q["持久事件队列"]
    Q --> D
模式写入确认点优点主要风险适用场景
Cache Aside(旁路缓存)数据库事务提交简单、数据库保持权威删除失败、回填竞态订单、商品、轨迹读模型
Read Through(读穿透)不改变写语义加载逻辑集中代理层成为复杂组件统一数据访问平台
Write Through(写穿透)缓存与数据库同步完成调用方语义简单写延迟、部分失败处理复杂写量较小的配置数据
Write Behind(异步回写)缓存或队列接收合并写、吞吐高丢失、乱序、不可审计可重算指标,不适合资金事实
多级缓存取决于权威写路径降低远程访问与热点压力多层失效和版本收敛更难商品详情、基础字典

数据演绎 1:模式的吞吐交换。 某商品查询峰值为 30,000 QPS(每秒查询率),数据库安全读能力为 2,000 QPS(每秒查询率),缓存命中率 96% 时回源约 1,200 QPS(每秒查询率),数据库仍有余量。若使用 Write Through(写穿透)并要求缓存与数据库串行完成,单次写由 4 毫秒增加到约 6 毫秒;若使用 Write Behind(异步回写),接口可在 1 毫秒返回,但队列积压 60 秒时,数据库事实最多落后 60 秒。订单和库存不能接受这种确认语义,因此选择 Cache Aside(旁路缓存),把缓存作为可丢读模型。

热门面试题

  1. 问题(基础题):四种缓存模式的核心区别是什么?

    • 考点:加载职责、写入确认点、权威数据源。
    • 回答思路:不要只描述调用顺序,要说明失败时谁负责恢复。
    • 详细答案:Cache Aside(旁路缓存)由应用显式维护缓存;Read Through(读穿透)把未命中加载封装在缓存层;Write Through(写穿透)同步更新后端;Write Behind(异步回写)延迟落库。真正差异是写入何时向业务确认,以及缓存丢失后能否从权威源恢复。
    • 进阶追问:Redis(远程字典服务)原生支持哪一种?
    • 进阶回答:Redis(远程字典服务)提供数据结构和命令,不自动理解数据库事务与业务状态机。四种模式都需要应用、代理或框架组织,其中 Cache Aside(旁路缓存)最常由业务代码实现。
  2. 问题(原理题):为什么 Write Behind(异步回写)吞吐高?

    • 考点:异步化、批处理、合并写与确认语义。
    • 回答思路:先解释分治和缓冲,再说明风险转移。
    • 详细答案:前台只写高速缓存或日志,后台把多个同键更新合并并批量写数据库,减少随机输入输出和事务次数,所以峰值吞吐更高;代价是数据还没进入权威库就已返回,崩溃、积压、乱序与重复都要有持久队列、版本和重放机制处理。
    • 进阶追问:余额能否这样做?
    • 进阶回答:不能只靠异步回写。余额变化需要事务流水、唯一业务号和审计对账先成为权威事实,缓存可以异步刷新展示值,但不能成为扣款成功的唯一依据。
  3. 问题(项目追问题):WMS(仓储管理系统)库存缓存怎样定职责?

    • 考点:库存正确性边界。
    • 回答思路:区分扣减事实、聚合读模型和前端展示。
    • 详细答案:库存流水和带条件的数据库更新负责最终不超卖;Redis(远程字典服务)可保存按仓库、货主和商品聚合的可用量,用于查询和削峰。扣减成功后携带新版本失效或刷新缓存,缓存异常时回源,不允许仅凭缓存中的旧余量完成最终出库。
    • 进阶追问:缓存显示有货但数据库扣减失败怎么办?
    • 进阶回答:以数据库结果为准,返回库存不足并删除或覆盖旧缓存,同时记录版本差异;监控出现频率并由流水重建,不能为迎合缓存结果绕过条件更新。

2. Cache Aside(旁路缓存)的并发窗口

2.1 先删后更、先更后删与事务边界

“先删除缓存,再更新数据库”存在经典旧值回填:写线程删缓存后尚未提交,读线程未命中并从数据库读到旧版本,再把旧值写回;之后数据库提交新版本,但缓存长期保留旧值。“先更新数据库,再删除缓存”把危险窗口缩短为数据库提交到删除完成之间,读请求可能短暂读旧值,却较少出现旧值在提交后长期回填。它仍不是强一致:删除可能失败,事务提交与网络调用不能组成一个原子操作,读线程也可能在提交前读到旧值并于删除后迟到回填。

sequenceDiagram
    participant W as "写线程 A"
    participant R as "读线程 B"
    participant C as "Redis(远程字典服务)"
    participant D as "数据库"
    W->>C: "删除商品缓存"
    R->>C: "查询,未命中"
    R->>D: "读取 v1"
    W->>D: "提交 v2"
    R->>C: "迟到回填 v1"
    Note over C,D: "数据库为 v2,缓存长期为 v1"
sequenceDiagram
    participant W as "写线程 A"
    participant R as "读线程 B"
    participant D as "数据库"
    participant C as "Redis(远程字典服务)"
    W->>D: "事务提交 v2"
    R->>C: "短窗口读到 v1"
    W->>C: "删除缓存"
    R->>C: "下次未命中"
    R->>D: "读取 v2"
    R->>C: "回填 v2"
顺序主要错误窗口失败后果工程判断
先删缓存后更新数据库删除到事务提交旧值被并发读回填并长期存在通常不选
先更新数据库后删缓存提交到删除短暂旧读;删除失败会长期旧读常用,必须补偿
事务提交前删除删除可能早于最终提交回滚后缓存被无谓删除,提交窗口仍在不应绑定事务体中间状态
事务提交后回调删除已确认权威事实回调丢失或进程崩溃加消息或扫描补偿

数据演绎 2:旧值回填。 初始数据库与缓存都是库存 v1=100。10:00:00.000 写线程删除缓存;10:00:00.002 读线程查库得到 v1;10:00:00.006 写事务提交 v2=99;10:00:00.009 读线程才把 v1 回填,若 TTL(存活时间)为 30 分钟,错误可能持续 30 分钟。改为先提交后删除后,若删除在 10:00:00.008 完成,旧读窗口约 2 毫秒;但删除请求超时仍需补偿。

热门面试题

  1. 问题(基础题):为什么通常先更新数据库再删除缓存?

    • 考点:错误窗口与旧值回填。
    • 回答思路:用两线程时间线比较,不把它说成绝对一致。
    • 详细答案:先删后更会给并发读留下较长的旧值回填窗口;先提交数据库让权威事实先稳定,再删缓存,后续未命中会加载新值。该顺序只降低概率,删除失败和迟到回填仍存在,所以必须配合重试、版本或事件失效。
    • 进阶追问:为什么不是更新数据库后直接更新缓存?
    • 进阶回答:并发写可能按数据库提交顺序 v2、v3,却按网络到达顺序把缓存写成 v3、v2;删除是幂等的,下一次读会从权威源重建,通常比直接更新更容易收敛。
  2. 问题(原理题):先更新后删除是否仍会回填旧值?

    • 考点:迟到回填竞态。
    • 回答思路:构造读先查库、写后删除、读最后回填的顺序。
    • 详细答案:会。读线程缓存未命中后在数据库提交前读到 v1,写线程随后提交 v2并删除缓存,最后读线程才把 v1 写入。该窗口通常较窄,但慢查询、线程暂停或网络抖动会放大,因此可在回填时携带版本并比较、使用短过期或二次删除。
    • 进阶追问:加分布式锁能彻底解决吗?
    • 进阶回答:锁只在所有读写都遵守同一协议且租约不失效时缩小并发,无法把数据库事务与缓存写成跨系统原子提交,还引入延迟和故障模型,不应作为唯一一致性方案。
  3. 问题(项目追问题):支付订单状态为何不能只更新缓存?

    • 考点:状态机、资金事实和读模型。
    • 回答思路:先落订单与渠道流水,再失效展示缓存。
    • 详细答案:支付回调先按渠道事件号幂等写订单状态和资金流水,状态机拒绝倒退;事务提交后发送事件或删除缓存。缓存只加速订单查询,即使缓存删除失败,也能由版本、短过期和补偿任务收敛,绝不据缓存值重复扣款或退款。
    • 进阶追问:用户支付后立即查询看到未支付怎么办?
    • 进阶回答:支付完成响应带订单版本,短时间读主库或权威查询;缓存记录版本和更新时间,低版本不返回,必要时展示“处理中”并主动查询渠道,而不是把未知当失败重试支付。

3. 删除失败、延迟双删与事件驱动失效

3.1 重试、binlog(二进制日志)与 CDC(变更数据捕获)

删除缓存是跨系统副作用,数据库事务已经提交而删除超时,不能回滚数据库来迁就缓存。同步重试适合短抖动,但必须有上限,避免拖垮请求线程;可靠消息、事务外盒表或订阅 binlog(二进制日志)的 CDC(变更数据捕获)可以在事务后持续重试删除。事件消费者必须处理重复、乱序和积压:按业务主键路由同一分区、携带数据库版本、删除操作保持幂等、失败进入重试和死信,并用扫描任务核对长时间未收敛的键。

延迟双删是在更新前或提交后删除一次,再等待一个覆盖“最慢旧读加回填”的经验时长后第二次删除。它能清理迟到回填,但延迟值来自分布而非魔法常数;进程崩溃会丢第二次删除,睡眠会占线程,重复更新还会互相干扰。因此更稳妥的实现是提交后立即失效,再把带版本的延迟失效任务放入可靠队列。

sequenceDiagram
    participant A as "业务服务"
    participant D as "数据库"
    participant O as "事务外盒表"
    participant P as "CDC(变更数据捕获)发布器"
    participant M as "持久消息队列"
    participant C as "缓存失效消费者"
    participant R as "Redis(远程字典服务)"
    A->>D: "事务更新 v8"
    A->>O: "同事务写失效事件 v8"
    D-->>A: "提交成功"
    P->>O: "读取未发布事件"
    P->>M: "发送 key 与版本 v8"
    M->>C: "至少一次投递"
    C->>R: "幂等删除或版本比较"
    C-->>M: "成功确认"
方案能解决不能解决关键工程条件
同步删除重试短暂网络失败进程崩溃、长故障超时、退避、次数上限
延迟双删迟到旧值回填延迟任务丢失、参数失准可靠调度、延迟基于观测
事务外盒消息提交与事件记录一致消费端永久失败发布扫描、幂等消费、告警
binlog(二进制日志)订阅对业务代码侵入低解析延迟、模式变更、乱序位点保存、同键有序、重放
版本比较拒绝旧事件覆盖新值无版本的历史数据单调版本、原子比较

数据演绎 3:删除失败与补偿。 订单 v7 更新为 v8,事务在 12:00:00.000 提交;同步删除因网络超时失败,缓存仍是 v7。事务外盒发布器在 80 毫秒后发送 v8 失效事件,消费者第一次处理失败,按 100、500、2000 毫秒退避,第三次在 2.68 秒删除成功。一致性窗口为 2.68 秒。若消息重复到达,删除无副作用;若 v9 事件先到,v8 后到,删除仍安全,但直接覆盖值必须比较版本。

热门面试题

  1. 问题(基础题):缓存删除失败怎样补偿?

    • 考点:最终一致闭环。
    • 回答思路:同步重试、可靠事件、死信和扫描四层回答。
    • 详细答案:请求内有限退避重试,仍失败则由与业务事务一致记录的事件继续驱动删除;消费者至少一次处理并保持幂等,超过阈值进入死信和告警,后台扫描用数据库版本与缓存版本核对并修复。不能只打印日志等待自然过期。
    • 进阶追问:消息队列也可能丢消息怎么办?
    • 进阶回答:用事务外盒表或 binlog(二进制日志)作为可重放事实,发布器保存位点并核对未发布记录;消费者保存处理状态,定期对账业务版本,消息只是传输通道而不是唯一事实。
  2. 问题(原理题):延迟双删的延迟时间如何确定?

    • 考点:最慢旧读、回填时间和调度可靠性。
    • 回答思路:根据监控分位而非背固定秒数。
    • 详细答案:延迟应覆盖数据库旧读耗时、应用暂停、网络传输和缓存回填的高分位,并留安全余量。例如最慢查询 120 毫秒、线程暂停 80 毫秒、回填 20 毫秒,可从 300 至 500 毫秒验证;还要覆盖长尾并监控二删前后的版本差。
    • 进阶追问:延迟越长越好吗?
    • 进阶回答:不是。太长会扩大错误驻留和多次更新互相删除的窗口,太短清不掉迟到回填。它是概率治理,应结合版本、可靠事件和短过期,而非单点保证。
  3. 问题(项目追问题):跨境轨迹事件乱序怎样避免旧轨迹覆盖新轨迹?

    • 考点:业务时间、版本、幂等与迟到事件。
    • 回答思路:原始事件先持久化,读模型按单调规则更新。
    • 详细答案:以承运商、运单号和事件标识去重,为标准化事件生成可比较序号;数据库只接受更高版本或合法状态迁移,缓存更新使用原子版本比较。迟到旧事件可保留审计,但不能覆盖最新节点,删除事件重复消费也必须安全。
    • 进阶追问:两个承运商时间戳不可靠怎么办?
    • 进阶回答:不能只按外部时间排序,应按渠道序号、接收位点、节点优先级和人工规则生成内部版本,并把冲突送到异常队列,避免伪造全局时间顺序。

4. 版本号、回填保护与多写者冲突

4.1 版本化缓存与消息补偿

当业务存在慢读回填、多个写者和异步消息时,可把缓存值设计为 {payload, version, expireAt}。version(版本号)来自数据库行版本、单调事件序号或权威日志位点,写缓存时通过 Lua(脚本语言)脚本原子比较:仅当新版本大于或等于当前版本时覆盖。删除事件也携带目标版本:若缓存已经是更高版本,旧失效事件不应误删高版本热点值;若只执行无条件删除,正确性通常仍能收敛,但会造成额外回源。版本不能用各应用机器的本地时间生成,否则时钟回拨与并发提交无法保证单调。

flowchart TD
    E["收到版本 v 的更新或失效事件"] --> G["读取缓存当前版本 c"]
    G --> J{"事件类型"}
    J -->|"更新"| U{"v 大于等于 c?"}
    U -->|"是"| S["原子写入 v"]
    U -->|"否"| I["忽略旧更新并记录"]
    J -->|"失效"| D{"c 小于等于 v?"}
    D -->|"是"| X["删除缓存"]
    D -->|"否"| K["保留更新版本"]
版本来源单调范围优点风险
数据库行版本单行或聚合根与事务提交绑定聚合多表需定义版本传播
自增事件序号单业务键或分区易比较和重放生成器与分区要稳定
binlog(二进制日志)位点日志流可追溯跨分片不可直接全序
业务状态等级状态机内防止状态倒退不能表达同状态多次变化
本地时间戳单机近似实现简单时钟漂移,不可作为可靠全序

数据演绎 4:乱序消息。 数据库先提交 v41,再提交 v42。由于网络分区,v42 缓存更新在 20 毫秒到达,v41 在 90 毫秒到达。无版本覆盖会把缓存从 v42 降回 v41;原子比较发现 41<42,拒绝旧写。若 v41 是删除事件而缓存已是 v42,条件删除会保留 v42,减少下一次回源;扫描任务仍以数据库 v42 对账。

热门面试题

  1. 问题(基础题):为什么缓存值要带版本号?

    • 考点:乱序、迟到回填和状态倒退。
    • 回答思路:说明版本提供的是拒绝旧写的依据。
    • 详细答案:异步网络只保证消息最终可能到达,不保证多个生产者和重试后的到达顺序。缓存带权威版本后,脚本可原子拒绝低版本覆盖,读请求也可发现缓存落后于客户端已知版本并回源,从而把隐蔽旧读变成可观测的版本差。
    • 进阶追问:版本号能实现强一致吗?
    • 进阶回答:不能。它避免旧版本覆盖,却不能让数据库提交与缓存更新同时成功,也不能消除传播延迟;仍是带有明确收敛规则的最终一致方案。
  2. 问题(原理题):为什么版本比较要用 Lua(脚本语言)脚本?

    • 考点:读改写原子性。
    • 回答思路:构造客户端先读再写的竞态。
    • 详细答案:若客户端先读取当前版本,再决定写入,两次命令之间可能插入更高版本更新,随后旧客户端仍覆盖新值。Lua(脚本语言)脚本在 Redis(远程字典服务)主执行线程内把读取、比较和写入作为一个原子命令序列,避免该竞态。
    • 进阶追问:脚本执行很久会怎样?
    • 进阶回答:脚本会阻塞同实例其他命令,因此只做常数复杂度的版本比较与赋值,禁止扫描和大对象编码;脚本延迟需进入慢日志与超时监控。
  3. 问题(项目追问题):支付状态版本如何设计?

    • 考点:状态机与并发回调。
    • 回答思路:业务状态约束与数据库行版本结合。
    • 详细答案:订单表维护行版本和合法状态迁移,渠道回调按事件号幂等;事务成功后发布订单号、状态和版本。缓存只接受更高版本,终态不能被处理中覆盖。退款与支付最好使用不同子状态机,展示模型再聚合,避免用一个整数粗暴比较无关状态。
    • 进阶追问:回调先于前台支付响应到达呢?
    • 进阶回答:两条路径都竞争同一数据库幂等记录和状态机,以事务结果决定唯一版本;前台响应只查询结果,不得凭本地先后顺序覆盖回调确认。

5. 缓存穿透与不存在数据治理

5.1 参数校验、空值缓存与 Bloom Filter(布隆过滤器)

缓存穿透是请求的键在缓存和数据库都不存在,每次都回源。治理顺序应是入口参数与权限校验、限流、空值缓存,再根据数据规模评估 Bloom Filter(布隆过滤器)。空值缓存要用短 TTL(存活时间),并在对象创建时主动删除,避免真实数据已存在但空值仍遮挡。Bloom Filter(布隆过滤器)通过多个哈希位判断“可能存在”或“一定不存在”,存在假阳性但没有假阴性这一结论只在位图未丢失、更新流程正确且不随意删除位的前提下成立;它不能替代数据库查询,也不能阻止恶意请求集中命中假阳性。

flowchart TD
    Q["查询商品编号"] --> V{"参数、租户、权限有效?"}
    V -->|"否"| F["快速拒绝并计数"]
    V -->|"是"| B{"Bloom Filter(布隆过滤器)判断可能存在?"}
    B -->|"否"| N["返回不存在"]
    B -->|"是"| C{"缓存命中?"}
    C -->|"是"| R["返回值或空值标记"]
    C -->|"否"| D["查询数据库"]
    D --> E{"真实存在?"}
    E -->|"是"| P["回填并返回"]
    E -->|"否"| Z["写短期空值"]
方案拦截位置优点代价与边界
参数与权限校验应用入口成本最低只能拦截可识别非法请求
空值缓存Redis(远程字典服务)精确、简单占空间;创建数据时需失效
Bloom Filter(布隆过滤器)缓存前空间效率高假阳性、重建和同步复杂
接口限流网关与应用控制最坏回源牺牲部分可用性
黑名单风控层针对恶意源误伤、绕过与维护成本

数据演绎 5:穿透预算。 现有 5,000 万个有效商品编号,目标假阳性率 1%,Bloom Filter(布隆过滤器)约需 4.79 亿位,即约 57 MiB(兆字节),最佳哈希次数约 7。攻击流量 100,000 QPS(每秒查询率)时,若全部为随机不存在键,理论仍约有 1,000 QPS(每秒查询率)因假阳性回源,因此数据库前还要有请求合并、限流和容量隔离。空值 TTL(存活时间)设为 30 秒,可吸收对同一假键的重复请求,但键空间随机时帮助有限。

热门面试题

  1. 问题(基础题):缓存穿透和缓存击穿有什么区别?

    • 考点:不存在键与热点失效。
    • 回答思路:从数据是否存在和请求集中度回答。
    • 详细答案:穿透通常查询数据库也不存在的键,缓存无法形成有效命中;击穿是一个真实热点键恰好失效,大量并发同时回源。穿透重在校验、空值和过滤器,击穿重在请求合并、互斥重建与逻辑过期。
    • 进阶追问:两者能同时发生吗?
    • 进阶回答:能。攻击者可集中请求一个不存在键,形成“热点穿透”;需同时使用空值缓存、请求合并和限流,不能靠单一标签决定方案。
  2. 问题(原理题):Bloom Filter(布隆过滤器)为什么有假阳性?

    • 考点:位图共享与哈希碰撞。
    • 回答思路:解释多个元素共同设置位。
    • 详细答案:每个元素通过多个哈希函数设置若干位;查询时若这些位都为一,可能只是被其他元素分别设置,因此“可能存在”不可靠;只要插入过程完整,任一所需位为零则可判定一定不存在。位图丢失或同步漏写会产生工程上的假阴性。
    • 进阶追问:删除元素怎么办?
    • 进阶回答:普通位图不能直接清位,因为该位可能被其他元素共享。可周期重建、使用计数型过滤器或接受删除后仍可能判断存在,具体取决于空间和一致性要求。
  3. 问题(项目追问题):支付订单查询是否适合用过滤器?

    • 考点:安全边界与隐私。
    • 回答思路:先做用户权限和订单归属,再谈过滤。
    • 详细答案:订单接口必须先校验身份、租户和订单归属,不能让过滤器返回差异泄露订单是否存在。对内部批量查询可用过滤器减少随机无效编号回源,但最终仍以订单库为准,并对假阳性流量限流。
    • 进阶追问:订单创建后过滤器尚未更新怎么办?
    • 进阶回答:关键的创建后立即查询应绕过或同步更新过滤器;异步更新场景允许短窗口时,客户端携带已知订单版本直接读权威库,避免工程假阴性挡住真实订单。

6. 缓存击穿、热点重建与逻辑过期

6.1 互斥重建、请求合并与可用性取舍

缓存击穿发生在少数热点键失效或被删除时,成千上万请求同时回源。互斥重建只允许一个执行者查询数据库,其他请求等待、返回旧值或快速失败;锁必须有持有者标识、租约和原子释放,但这里的锁只保护重建成本,不承担业务最终一致性。请求合并可在单进程用共享任务实现,也可在分布式环境用 Redis(远程字典服务)互斥键;等待方必须有超时和降级,防止重建线程卡死形成请求堆积。

逻辑过期不让热点键物理消失,而是在值中保存 expireAt(逻辑过期时间)。发现过期的第一个请求异步重建,其余请求继续读旧值,因此用陈旧性换可用性。它适合商品详情、轨迹摘要等允许短期旧读的场景,不适合支付最终状态和库存放行判断。

sequenceDiagram
    participant U as "并发请求"
    participant C as "Redis(远程字典服务)"
    participant L as "重建协调器"
    participant D as "数据库"
    U->>C: "读取热点键,逻辑已过期"
    C-->>U: "返回旧值与过期标记"
    U->>L: "尝试成为重建者"
    alt 获得重建资格
      L->>D: "查询权威新值"
      D-->>L: "返回 v18"
      L->>C: "原子写 v18 与新过期时间"
      L->>L: "释放资格"
    else 未获得资格
      L-->>U: "继续使用旧值或短暂等待"
    end
策略回源并发用户看到旧值失败风险适合场景
无保护回填通常不旧数据库被打穿非热点键
互斥重建并等待约 1等待堆积、锁超时强调新鲜度的配置
互斥重建返回旧值约 1旧值窗口商品详情、轨迹摘要
逻辑过期异步刷新约 1重建失败后持续旧超高热点读模型
后台主动刷新可控预测失误、资源浪费已知固定热点

数据演绎 6:击穿峰值。 某爆款键平时 20,000 QPS(每秒查询率),数据库查询 25 毫秒。物理过期后若 25 毫秒内请求全部回源,约 500 个并发查询同时压向数据库;连接池只有 100,剩余请求排队并触发超时重试。请求合并后只有 1 次查询,499 个请求最多等待 25 毫秒;采用逻辑过期后,499 个请求直接读取 2 秒内的旧值,重建失败则每 500 毫秒退避重试并告警。

热门面试题

  1. 问题(基础题):如何治理缓存击穿?

    • 考点:请求合并、互斥重建、逻辑过期。
    • 回答思路:根据新鲜度要求选择等待还是旧值降级。
    • 详细答案:热点键物理失效前可预热或主动刷新;失效后由单一重建者回源,其他请求有限等待或读取旧值。必须限制重建并发、设置超时、失败退避,并监控热点键回源率,不能让等待请求无限占用线程。
    • 进阶追问:互斥锁过期但查询未完成怎么办?
    • 进阶回答:可能出现多个重建者,正确性依赖版本比较而不是锁本身;租约按查询长尾设置,可谨慎续期,最终写入要拒绝旧版本,等待方也要有独立超时。
  2. 问题(原理题):逻辑过期为什么能抗击穿?

    • 考点:物理存在与异步刷新。
    • 回答思路:说明读路径不因过期变成全部未命中。
    • 详细答案:键始终保留,过期只表示数据需要刷新。请求仍能得到旧值,只有获得重建资格的请求访问数据库,因此回源并发从流量规模降到约一个;代价是明确接受陈旧数据,需要定义最大陈旧时间和失败告警。
    • 进阶追问:重建服务永久失败会怎样?
    • 进阶回答:旧值会无限陈旧,所以要记录最近成功刷新时间,超过硬上限时降级到权威库、返回暂不可用或隐藏敏感字段,并触发告警与人工处理。
  3. 问题(项目追问题):跨境轨迹首页怎样使用逻辑过期?

    • 考点:业务可接受陈旧性。
    • 回答思路:摘要可旧,关键节点查询回源。
    • 详细答案:运单列表中的最新节点可保存数据版本、轨迹时间和逻辑过期时间,过期后先展示带更新时间的旧摘要并异步刷新;用户主动点击详情或已知有新事件时,按版本读取轨迹库。签收、异常和清关等关键状态设置更短硬上限。
    • 进阶追问:如何防止旧轨迹一直展示?
    • 进阶回答:监控距最近成功刷新时长,超过阈值不再静默返回旧值;消费轨迹事件主动失效,后台扫描热点运单,刷新失败进入重试和异常队列。

7. 缓存雪崩、预热与多级缓存

7.1 过期离散、限流降级与层级收敛

缓存雪崩是大量键在同一时间失效、实例故障或网络隔离,使回源量骤增。随机 TTL(存活时间)只能解决“过期时间集中”,不能解决节点整体不可用。完整治理包括容量冗余、过期离散、发布预热、请求合并、数据库隔离、限流、熔断、返回旧值和静态降级。预热不是把全部数据盲目装入缓存,而是按访问频率和业务事件加载热点,并验证缓存容量、淘汰和数据库回源曲线。

多级缓存常见为进程内缓存加 Redis(远程字典服务)加数据库。进程内层延迟最低,但每个实例都有副本,失效通知可能丢失;因此值应带短 TTL(存活时间)与版本,消息通知用于加速失效,版本校验和自然过期负责兜底。层级越多,命中越快,一致性和内存放大也越复杂。

flowchart LR
    U["请求"] --> L1{"进程内缓存命中?"}
    L1 -->|"是且版本可接受"| O["返回"]
    L1 -->|"否"| L2{"Redis(远程字典服务)命中?"}
    L2 -->|"是"| F["回填进程内缓存"] --> O
    L2 -->|"否"| G["请求合并与回源限流"]
    G --> D["数据库"]
    D --> B["逐层回填带版本值"] --> O
    E["变更事件"] --> X["失效 Redis(远程字典服务)与广播本地缓存"]
故障类型仅随机过期是否有效主要措施降级结果
大批键同秒过期有效基础时长加随机抖动、分批预热少量旧值或限流
Redis(远程字典服务)节点故障无效高可用、客户端隔离、回源闸门返回本地旧值
网络分区无效超时、熔断、就近缓存有界陈旧或部分不可用
热点发布后未预热部分有效按流量预热、灰度放量逐步增加容量
数据库也过载无效独立连接池、优先级和静态降级保护核心交易

数据演绎 7:雪崩容量。 100 万个商品键都设置整点 30 分钟过期,平均每个键每分钟被访问 0.2 次。整点后第一分钟理论约有 20 万次回源,即约 3,333 QPS(每秒查询率);数据库安全能力仅 1,500 QPS(每秒查询率)。把过期时间调整为 25 至 35 分钟均匀分布后,回源分散到约 600 秒,平均约 333 QPS(每秒查询率);再配合每实例 200 QPS(每秒查询率)回源闸门和旧值降级,可将最坏压力限制在预算内。

热门面试题

  1. 问题(基础题):缓存雪崩有哪些来源?

    • 考点:集中失效与系统性故障。
    • 回答思路:区分键级、节点级和依赖级故障。
    • 详细答案:大量键集中到期、缓存实例或集群故障、网络分区、发布后冷缓存、错误清空和热点迁移都可能让回源突然放大。随机过期只治理第一类,其他场景需要高可用、限流、熔断、旧值和数据库隔离。
    • 进阶追问:缓存高可用后还会雪崩吗?
    • 进阶回答:会。故障切换期间的超时重试、热点迁移、复制旧读和客户端拓扑未刷新都能放大流量,高可用降低停机但不等于无限容量与强一致。
  2. 问题(原理题):多级缓存怎样失效?

    • 考点:广播不可靠与最终收敛。
    • 回答思路:事件加速、版本拒旧、短过期兜底。
    • 详细答案:数据库提交后发布带版本的失效事件,远程缓存删除,应用实例收到广播后删除本地值;广播可能丢失,因此本地缓存使用较短 TTL(存活时间),读取时可比较客户端已知版本,低版本回源。不能承诺所有实例瞬间同步。
    • 进阶追问:本地缓存是否会造成内存放大?
    • 进阶回答:会。实例数乘单实例热集是总内存,扩容会线性增长;必须限制条目数和权重,观测命中率与淘汰,避免把大对象复制到每个进程。
  3. 问题(项目追问题):WMS(仓储管理系统)大促前如何预热?

    • 考点:热点识别、容量和正确性。
    • 回答思路:按历史与活动清单分批加载,不预热扣减事实。
    • 详细答案:根据仓库、货主、商品访问高分位和活动清单确定热集,分批从库存读库加载带版本聚合值,控制每批回源和缓存写入速率;灰度放流观察命中率、淘汰与数据库连接池。最终扣减仍走条件更新或原子库存协议。
    • 进阶追问:预热数据加载过程中发生库存变化怎么办?
    • 进阶回答:预热值携带数据库版本,实时变更事件用更高版本覆盖;加载脚本只允许高版本写入,完成后抽样对账,不能让批量旧快照覆盖实时新值。

8. 过期删除与 maxmemory(最大内存)淘汰

8.1 惰性过期、主动过期与淘汰策略

Redis(远程字典服务)过期删除与内存淘汰是两套机制。惰性过期在访问键时发现过期并删除;主动过期周期性抽样带 TTL(存活时间)的键,若过期比例高则继续处理,在处理器时间和内存释放之间折中。过期键可能在逻辑到期后短时间仍占内存,但正常读取不会返回。maxmemory(最大内存)达到阈值后,写命令按策略淘汰:noeviction(不淘汰)直接报错;allkeys-lru(所有键近似最近最少使用)、allkeys-lfu(所有键近似最不常用)等在全键空间选候选;volatile(仅带过期键)系列只处理设置过期的键。近似 LRU(最近最少使用)和 LFU(最不常用)通过采样选择,不是维护全局精确排序。

flowchart TD
    W["写命令申请内存"] --> M{"当前内存超过 maxmemory(最大内存)?"}
    M -->|"否"| S["执行写入"]
    M -->|"是"| P{"淘汰策略"}
    P -->|"noeviction(不淘汰)"| E["返回内存不足错误"]
    P -->|"allkeys(所有键)系列"| A["从全键空间采样候选"]
    P -->|"volatile(仅过期键)系列"| V["从带过期键中采样"]
    A --> X["按 LRU(最近最少使用)、LFU(最不常用)或随机淘汰"]
    V --> X
    X --> M
策略候选范围适用前提风险
noeviction(不淘汰)不允许静默丢键写失败需业务处理
allkeys-lru(所有键近似最近最少使用)所有键近期访问代表未来热度扫描型流量污染热度
allkeys-lfu(所有键近似最不常用)所有键长期频率更重要新热点升温有过程
volatile-ttl(仅过期键按剩余时间)带过期键所有可淘汰键均设过期无过期键时无法释放
volatile-lru(仅过期键近似最近最少使用)带过期键永久键必须保留永久键可能挤占全部空间

数据演绎 8:容量水位。 实例上限 32 GiB(吉字节),业务键 24 GiB(吉字节),复制和客户端缓冲 2 GiB(吉字节),内存碎片比率 1.35 时进程常驻内存约为 35.1 GiB(吉字节),已经超过容器限制,即使键空间未到 32 GiB(吉字节)也可能被系统终止。生产应把 maxmemory(最大内存)设在容器限制以下,预留缓冲、碎片、重写写时复制和增长空间,例如只给键空间 20 至 22 GiB(吉字节),并压测淘汰稳定性。

热门面试题

  1. 问题(基础题):过期删除和内存淘汰有什么区别?

    • 考点:触发条件与候选对象。
    • 回答思路:一个依据时间,一个依据内存压力。
    • 详细答案:过期删除处理已经超过 TTL(存活时间)的键,由访问和主动周期触发;内存淘汰在达到 maxmemory(最大内存)时根据策略选择仍可能有效的键释放空间。键没有过期也可能被 allkeys(所有键)策略淘汰。
    • 进阶追问:过期键为何不在到期瞬间全部删除?
    • 进阶回答:为每个键维护精确定时器成本高,集中到期还会长时间阻塞。抽样主动删除加访问时惰性删除,把处理器时间分摊并避免瞬时风暴。
  2. 问题(原理题):为什么 LRU(最近最少使用)只是近似?

    • 考点:采样与元数据成本。
    • 回答思路:对比全局链表排序的开销。
    • 详细答案:为数亿键维护精确访问链表会增加每次访问写放大、锁和内存。Redis(远程字典服务)从有限候选中采样,根据访问时间近似选择较冷键,以小成本获得足够好的淘汰结果;采样数越高越接近精确但消耗更多处理器时间。
    • 进阶追问:缓存命中率下降如何判断是策略问题?
    • 进阶回答:同时看淘汰速率、键空间、访问频率分布、热键被淘汰情况和内存水位;对比不同采样与策略压测,不能仅凭总命中率归因。
  3. 问题(项目追问题):支付订单缓存应选哪种策略?

    • 考点:缓存可丢与写失败处理。
    • 回答思路:按数据角色选择,不把缓存当账本。
    • 详细答案:订单查询缓存可设置有限 TTL(存活时间)并允许淘汰,丢失后从订单库重建;幂等窗口若被淘汰会改变重复请求保护,不能与普通缓存混放或必须由数据库唯一键兜底。资金流水不进入可淘汰缓存作为唯一事实。
    • 进阶追问:使用 noeviction(不淘汰)是否更安全?
    • 进阶回答:它避免静默丢键,却会在满内存时让写入失败;应用必须识别错误并降级,且数据库约束仍是最终保障。安全来自完整协议,不是单一策略名。

9. big key(大键)、hot key(热键)与内存碎片

9.1 识别、拆分、惰性释放与热点隔离

big key(大键)既可能是单个大字符串,也可能是包含百万成员的集合。风险包括网络传输、序列化、命令执行、删除释放和复制重同步放大;判断阈值必须结合延迟预算,不存在万能字节数。hot key(热键)是访问量高度集中到单个键,即使值很小也会让一个分片、网卡或主线程饱和。治理分别是控制单值大小、分页或按业务维度拆键、增量删除、lazy free(惰性释放)、本地缓存、只读副本、热点复制和应用层分片;拆分后必须处理聚合、顺序与一致性。

flowchart LR
    T["流量识别"] --> K{"问题类型"}
    K -->|"big key(大键)"| B["测量字节、成员数和命令耗时"]
    B --> S["分页、分桶、增量迁移、惰性释放"]
    K -->|"hot key(热键)"| H["测量单键 QPS(每秒查询率)和分片占比"]
    H --> R["本地缓存、热点副本、请求合并、隔离分片"]
    S --> V["双读校验与灰度切换"]
    R --> V
问题关键指标错误处理推荐治理
大字符串字节数、带宽、延迟只提高超时分块、压缩、对象存储
大集合成员数、单命令复杂度全量读取或删除游标分页、分桶、异步释放
热读键单键 QPS(每秒查询率)只扩 Redis Cluster(Redis 集群)节点本地缓存、复制热点
热写键单键写入率多副本并发写业务分片、批处理、削峰
内存碎片常驻内存与已用内存比只看键空间分配器分析、主动整理、重启迁移

数据演绎 9:热键与大键。 集群有 6 个主分片,总流量 120,000 QPS(每秒查询率),理论每片 20,000 QPS(每秒查询率);一个活动配置键独占 45,000 QPS(每秒查询率),它所在分片达到 60,000 QPS(每秒查询率),扩容到 12 片仍无法把单键拆开。将该键放入进程内缓存 1 秒并通过版本广播失效,假设 100 个应用实例,每秒仅各回源一次,远程流量降至约 100 QPS(每秒查询率)。另一个 80 MiB(兆字节)集合删除时若同步释放数百万对象会阻塞,应先分桶迁移,再异步删除旧键。

热门面试题

  1. 问题(基础题):big key(大键)和 hot key(热键)的区别是什么?

    • 考点:体积与访问集中度。
    • 回答思路:分别从空间和时间维度定义。
    • 详细答案:big key(大键)强调单键占用空间或成员过多,影响传输、遍历、复制和释放;hot key(热键)强调访问频率集中,值可能很小却压垮单分片。一个键可以同时既大又热,但检测指标和治理手段不同。
    • 进阶追问:增加集群分片为何不能解决 hot key(热键)?
    • 进阶回答:单键由哈希槽映射到一个主分片,增加节点只分散不同键,不能自动把该键请求拆开;需要应用本地缓存、热点副本或业务拆键。
  2. 问题(原理题):lazy free(惰性释放)解决什么问题?

    • 考点:逻辑删除与内存回收解耦。
    • 回答思路:说明主线程移除键后后台释放对象。
    • 详细答案:大集合释放需要遍历并归还大量内存,若在主线程同步执行会阻塞所有命令。惰性释放先从键空间断开引用,使业务不可见,再由后台线程回收对象,降低主线程停顿;实际内存不会瞬间下降,后台队列也可能积压。
    • 进阶追问:能否无脑开启惰性释放?
    • 进阶回答:不能。持续大量删除会让待释放内存积压并抬高常驻内存,仍需限制大键、监控后台队列和容量水位,必要时分批迁移。
  3. 问题(项目追问题):WMS(仓储管理系统)库存热键如何拆分?

    • 考点:业务分片与聚合一致性。
    • 回答思路:按仓库、库区、货主和商品组合拆分。
    • 详细答案:避免把全局商品库存放在一个键,可按仓库和商品形成独立聚合,必要时再按库区或批次分桶;查询总量从多个分桶聚合,扣减在明确仓库分桶内完成。跨仓调拨由数据库事务和流水协调,不能用缓存求和代替最终库存。
    • 进阶追问:拆分后读取放大怎么办?
    • 进阶回答:维护可重建的异步总量读模型,常用维度预聚合,详情按需分页;聚合值携带版本与更新时间,最终放行仍检查权威分桶。

10. 项目落地、可观测性与正确性边界

10.1 WMS(仓储管理系统)、支付与跨境轨迹方案

项目设计要同时给出正确性路径和加速路径。WMS(仓储管理系统)中,数据库库存流水、条件更新和唯一业务号保证不超卖,Redis(远程字典服务)保存查询聚合与热点预判;支付中,订单状态机、渠道流水、幂等键和对账保证资金正确,缓存只服务查询;跨境轨迹中,原始回执和标准化事件是可审计事实,缓存保存最新摘要。三类缓存都应带版本、更新时间和来源,变更后通过事务事件失效,删除失败重试,缓存全失可重建。

观测指标不能只有命中率。至少包括分层命中率、回源 QPS(每秒查询率)、单键访问高分位、缓存版本落后量、删除失败和重试次数、消息积压时间、空值比例、淘汰速率、过期速率、内存碎片、重建耗时、数据库连接池等待和降级比例。排障先保护权威库,再按“流量变化、键分布、缓存节点、网络、数据库、最近变更”收集证据。

flowchart TD
    A["缓存命中率骤降告警"] --> P["先限制回源并保护数据库"]
    P --> T{"全局还是单键?"}
    T -->|"单键"| H["检查 hot key(热键)、过期与大对象"]
    T -->|"全局"| N["检查节点、网络、淘汰、发布与批量失效"]
    H --> D["比较数据库版本与缓存版本"]
    N --> D
    D --> C{"权威数据正确?"}
    C -->|"是"| R["重建、补偿、灰度恢复"]
    C -->|"否"| S["停止业务推进,按流水修复"]
    R --> V["验证命中、回源、延迟和版本差归零"]
sequenceDiagram
    participant U as "用户"
    participant A as "订单服务"
    participant D as "权威订单库"
    participant O as "事务外盒事件"
    participant C as "缓存消费者"
    participant R as "Redis(远程字典服务)"
    U->>A: "查询支付订单"
    A->>R: "读取状态与版本"
    R-->>A: "v12 处理中"
    Note over D,O: "渠道回调事务提交 v13 成功"
    O->>C: "投递失效事件 v13"
    C->>R: "删除 v12"
    U->>A: "再次查询,携带已知 v13"
    A->>D: "缓存缺失,读取 v13"
    A->>R: "回填 v13"
    A-->>U: "支付成功"
项目权威事实缓存内容一致性策略缓存不可用时
WMS(仓储管理系统)库存流水、条件更新、冻结记录可用量聚合、热点商品提交后失效、版本拒旧、对账限流回源,扣减仍走权威路径
支付订单订单状态机、渠道与资金流水订单查询状态、幂等短窗口事务事件、状态不倒退、补偿读订单库,禁止据缓存扣款
跨境轨迹原始回执、标准事件表最新节点、预计到达时间事件序号、逻辑过期、迟到处理回源轨迹库或展示更新时间
商品详情商品主数据多级热点详情随机过期、预热、版本广播返回旧值或静态降级

数据演绎 10:一致性服务等级。 支付查询允许 99.9% 在 2 秒内看到新状态,订单事务到缓存删除链路的 P99(99 分位响应时间)为 420 毫秒,最大重试积压 1.6 秒,因此满足常态目标;超过 2 秒的事件触发版本差告警,用户携带更高已知版本时直接回源。库存扣减不使用该 2 秒窗口作为正确性承诺,数据库条件 available >= quantity 决定是否成功。轨迹摘要允许 60 秒陈旧,超过硬上限时展示更新时间并主动刷新。

热门面试题

  1. 问题(基础题):为什么缓存不能承担库存最终正确性?

    • 考点:可淘汰、异步复制与事务边界。
    • 回答思路:说明缓存设计目标与权威约束不同。
    • 详细答案:缓存可能过期、淘汰、故障切换丢最近写、删除失败或读到旧值,且无法自动与订单事务形成原子提交。库存最终正确性需要数据库条件更新、流水、幂等和对账;缓存只做读模型、预校验和削峰。
    • 进阶追问:Lua(脚本语言)原子扣减能否保证不超卖?
    • 进阶回答:它只保证单 Redis(远程字典服务)执行点内的原子性,仍有持久化、故障切换、数据库落地和消息丢失窗口;必须有权威库存协议和补偿,不把脚本结果当唯一事实。
  2. 问题(原理题):命中率高为什么数据库仍可能过载?

    • 考点:绝对流量、热点与回源相关性。
    • 回答思路:用总 QPS(每秒查询率)乘未命中率计算并看瞬时值。
    • 详细答案:100 万 QPS(每秒查询率)即使 99% 命中仍有 1 万 QPS(每秒查询率)回源;若未命中集中在同一秒或同一慢查询,数据库更危险。应观察回源绝对值、分位、键分布和连接池等待,而非只看平均命中率。
    • 进阶追问:如何设回源保护?
    • 进阶回答:按数据库安全容量为服务和租户分配令牌,热点请求合并,核心查询优先,超额返回旧值、静态数据或明确繁忙;避免客户端重试越过闸门。
  3. 问题(项目追问题):怎样向面试官讲一次缓存事故?

    • 考点:证据、止损、根因和闭环。
    • 回答思路:按现象、影响、时间线、止损、根因、修复和验证组织。
    • 详细答案:先量化命中率、回源 QPS(每秒查询率)、数据库等待和用户影响;立即限流回源、返回旧值并暂停批量失效;再根据单键分布、淘汰、发布记录和消息积压定位原因。修复后分批预热和放流,用版本差、延迟与错误率证明恢复,最后补容量演练、告警和应急手册。
    • 进阶追问:如何证明不是 Redis(远程字典服务)本身慢?
    • 进阶回答:对齐服务端命令延迟、慢日志、网络往返、客户端连接池、单键流量和数据库耗时;若服务端快而客户端慢,继续查网络、连接争用和线程池,避免凭组件名称归因。

10.2 综合题库使用说明

本节是非知识型过渡,不添加 kb:knowledge 标记。下面的每道题都给出可直接口述的完整答案,并链接回本章详细内容。复习时先用三至五分钟独立回答,再对照答案检查是否覆盖了正确性边界、失败窗口、补偿和可观测证据。

11. 缓存一致性综合面试题库

  1. 问题:请系统说明 Cache Aside(旁路缓存)的读写流程,以及为什么它只能保证最终一致。

    • 口述答案:我会先确定数据库是权威数据源,缓存是可丢、可重建的读模型。读流程是先查 Redis(远程字典服务),命中直接返回;未命中时不能让大量同键请求一起回源,应先做请求合并或有限互斥,由一个执行者查数据库,把值连同业务版本和合理 TTL(存活时间)写入缓存,再返回结果。写流程通常是数据库事务先提交,提交成功后删除缓存,而不是先删缓存。原因是先删后更时,并发读可能在事务提交前读到旧值并回填,事务随后提交却没有再次失效,旧值会保留到过期;先提交后删除把主要窗口缩短为提交到删除完成之间,后续未命中能加载新值。但这仍不是强一致,因为数据库提交和缓存删除属于两个系统,无法组成原子操作:删除可能超时,读线程也可能在提交前读到旧值并于删除后迟到回填。工程上用事务外盒事件或 binlog(二进制日志)驱动的 CDC(变更数据捕获)持续重试删除,回填值带版本并原子拒绝旧版本,缓存设置有界过期,后台扫描版本差。面试中必须明确,支付资金和库存扣减由数据库状态机、唯一约束、条件更新、流水与对账保证,Cache Aside(旁路缓存)只优化查询,即使缓存全失也不能改变业务事实。上线验收要注入删除超时、迟到回填和消息停机,统计提交到版本收敛的最大值与 P99(99 分位响应时间),同时确认数据库回源没有越过安全容量。只有能量化旧读窗口、能重放补偿并能在缓存全失时恢复,方案才算闭环。
    • 进阶追问与回答:为什么不直接更新缓存?并发提交与网络到达顺序可能不同,旧写会覆盖新写;删除是幂等的,下一次读可重建。能否加锁变强一致?锁无法原子覆盖数据库事务和缓存调用,租约、进程暂停和绕过协议的读写仍存在。
    • 详细章节:缓存模式与并发窗口
  2. 问题:请用精确时序解释“先删除缓存再更新数据库”为什么危险。

    • 口述答案:假设数据库和缓存初始都是商品库存 v1=100。写线程 A 在 0 毫秒先删除缓存,准备把数据库改成 v2=99;2 毫秒时读线程 B 访问缓存未命中,于是查询数据库。此时 A 的事务尚未提交,B 在 5 毫秒读到 v1=100。6 毫秒时 A 提交 v2=99并返回成功,9 毫秒时 B 因查询或调度较慢,才把刚才读到的 v1=100回填到 Redis(远程字典服务)。最终数据库是 v2,而缓存重新变成 v1,若 TTL(存活时间)为 30 分钟,错误可能持续 30 分钟。风险的关键不是删除命令不原子,而是删除与数据库提交之间暴露了“缓存为空、权威值仍旧”的窗口。改为先提交数据库再删除缓存后,提交前读到旧缓存只形成短暂旧读,删除完成后下一次查询会加载 v2;仍有一种较窄竞态:B 先缓存未命中并读取 v1,A 再提交 v2并删除,B 最后回填 v1。对此不能宣称零概率,应使用带版本回填、短过期、可靠延迟失效或后台版本核对。分布式锁只能在所有参与者遵守协议时降低同键并发,不会让两个系统成为一个事务。库存最终扣减仍以数据库条件更新为准,缓存旧值最多影响展示或预判,不能导致绕过权威校验。测试时可用线程屏障把 B 固定在“查库完成、尚未回填”的位置,再让 A 提交和删除,最后释放 B;若版本脚本拒绝 v1,且后续读得到 v2,才证明防护有效。线上则记录回填来源版本和事务提交版本,出现倒退立即告警,而不是等用户发现。
    • 进阶追问与回答:事务中间删除行不行?若事务回滚会无谓失效,且提交边界仍未被缓存操作原子覆盖。延迟双删能否彻底解决?只能覆盖一部分迟到回填,任务丢失和延迟估算错误仍会失败。
    • 详细章节:先删后更时序
  3. 问题:数据库更新成功但缓存删除失败,完整补偿链路应怎样设计?

    • 口述答案:先承认数据库事务已经成为权威事实,不能因为缓存删除失败就反向回滚数据库。请求线程可以进行两三次带随机抖动的短退避重试,但必须受总超时约束,防止 Redis(远程字典服务)故障时所有业务线程都被拖住。可靠链路是在同一数据库事务中写一条事务外盒事件,记录业务键、数据库版本、事件标识和提交时间;独立发布器扫描未发布记录或订阅 binlog(二进制日志),把事件送入持久消息队列。消费者按业务键有序处理,删除本身保持幂等;若采用更新缓存,则用 Lua(脚本语言)原子比较版本,拒绝低版本覆盖。消费失败按指数退避重试,超过阈值进入死信并告警,同时保留位点以便重放。仅靠消息仍不够,后台核对任务应抽样或分片比较数据库版本与缓存版本,修复长时间未收敛的键。指标至少包括删除失败率、事件发布延迟、消费积压年龄、重试次数、死信量、版本差和一致性窗口分位。若缓存服务整体不可用,先打开回源闸门、限制数据库 QPS(每秒查询率)、返回有界旧值或静态降级。整个方案提供的是可观测、可重放的最终一致,不把消息中间件或重试包装成跨系统强一致。还要定义事件保留时间和重放速率,防止恢复时补偿流量再次压垮缓存与数据库。发布前关闭消费者数分钟制造积压,再恢复并验证旧值窗口、追赶时间、死信处置和告警升级,才能证明链路不依赖“平时网络正常”。
    • 进阶追问与回答:消息重复怎么办?删除天然幂等,更新则按事件号与版本幂等。发布器崩溃怎么办?事务外盒记录仍在,恢复后根据状态和位点继续发布,不能只依赖内存任务。
    • 详细章节:删除失败与事件补偿
  4. 问题:延迟双删的原理、参数和适用边界是什么?

    • 口述答案:延迟双删用于清理并发读的迟到旧值回填。常见流程是数据库事务提交后立即删除一次缓存,再投递一个可靠延迟任务,等待一段时间后第二次删除;有些实现还在更新前删除,但这会扩大旧值回填窗口,因此我更关注提交后的立即失效与延迟补偿。延迟时间不是固定的一秒或五百毫秒,而应覆盖“最慢旧读查询时间、应用线程暂停、网络往返、序列化和回填时间”的高分位,再加安全余量。例如数据库查询 P99(99 分位响应时间)为 120 毫秒、线程暂停高分位 80 毫秒、回填 20 毫秒,可从 300到500毫秒起步,通过故障注入观察。第二次删除不能用请求线程睡眠实现,否则占用线程且进程崩溃会丢任务;应放入持久延迟队列,带业务键、目标版本、幂等事件号和重试策略。延迟太短清不掉迟到回填,太长会扩大旧值驻留,还可能把后续已经回填的新版本无谓删除。因此可做版本条件失效:缓存版本不高于事件版本才删。即使如此,它也只降低竞态概率,消息积压、时钟偏差和消费者故障仍存在,必须配合短 TTL(存活时间)、版本比较、对账扫描。支付终态和库存放行不能依赖双删正确,权威数据库仍必须拒绝状态倒退和超卖。参数应按接口分别配置,普通商品查询与慢聚合查询的尾延迟不同,不能共享一个常数。监控二删任务从计划时间到实际执行的偏差、删除前缓存版本和删除后首次回填版本,持续校准延迟;偏差超过预算时应直接走版本补偿而不是继续增加等待时间。
    • 进阶追问与回答:二删是否越多越可靠?次数增加会增加无谓回源和事件压力,不能替代可重放链路。怎样验收?注入慢查询、暂停、删除超时和消息积压,统计版本不一致窗口而非只看最终能删除。
    • 详细章节:延迟双删边界
  5. 问题:如何用 binlog(二进制日志)和 CDC(变更数据捕获)实现缓存失效?

    • 口述答案:数据库事务提交后会形成可重放的变更日志,CDC(变更数据捕获)组件保存消费位点,把表、主键、变更类型和必要版本转换成缓存失效事件。优势是业务代码侵入较低,任何写入路径只要真正提交都能被观察;但它不是开箱即用的强一致。首先要处理模式变更和字段映射,不能在表结构调整后生成错误缓存键;其次是顺序,同一业务键的事件要路由到同一分区,跨分片日志不存在天然全局顺序;再次是至少一次投递导致重复,删除应幂等,直接更新缓存必须比较版本;最后是位点、积压和重放,消费者故障恢复后要从持久位点继续,不能跳过无法解析的事件。删除事件应携带数据库行版本或业务序号,缓存已经是更高版本时可保留,避免旧事件造成额外回源。对于多表聚合缓存,一行变化可能影响多个键,需要维护明确的依赖映射或发送“聚合根已变更”事件,不能只按物理表名拼键。生产指标包括日志产生到消费的延迟、位点差、失败重试、死信、每事件扇出数和缓存版本差。上线前做历史回放、重复、乱序、停机追赶与表结构变更演练,并保留扫描对账作为最终兜底。权限和数据脱敏也要纳入设计,日志中可能包含支付或客户敏感字段,失效事件只传主键、版本和必要路由信息。若一次表变更会扇出十万个缓存键,应转成分批失效任务并限速,否则正确事件也会制造缓存雪崩和数据库回源风暴。
    • 进阶追问与回答:CDC(变更数据捕获)比事务外盒好吗?前者侵入低但业务语义较弱,后者能明确记录业务事件;复杂状态迁移常用外盒,简单失效可用日志订阅。积压期间怎么办?限制回源、缩短缓存过期并按业务优先级追赶。
    • 详细章节:变更日志失效
  6. 问题:版本化缓存怎样防止乱序更新和迟到回填?

    • 口述答案:缓存值不能只存业务内容,还应保存权威 version(版本号)、更新时间和可选逻辑过期时间。版本来源优先选择数据库行版本、聚合根版本、单业务键事件序号或可比较日志位点,而不是应用机器本地时间;本地时钟会漂移和回拨,也不能表达并发事务真实提交顺序。更新缓存时用 Lua(脚本语言)脚本在 Redis(远程字典服务)内部原子读取当前版本、比较并写入,只有新版本大于或等于当前版本才覆盖。这样数据库依次提交 v41、v42,即使网络让 v42先到、v41后到,v41也会被拒绝。读回填同样携带查询得到的版本,慢查询返回旧版本时无法覆盖已存在的新值。失效事件可以无条件删除,因为删除最终会重新加载正确值;若热点回源成本高,可在缓存版本不高于事件版本时才删,避免旧失效事件删除 v42。版本化仍不能消除数据库提交到缓存传播之间的旧读,也不能把两个系统原子化;它提供的是明确的单调收敛规则。对于支付状态,还要叠加业务状态机,因为退款、支付和关闭可能不是一个简单整数全序;对于跨境轨迹,要按承运商序号和内部规则生成版本,不能盲信外部时间戳。多表聚合值还需要定义何时提升聚合版本,例如任一子表变化都写聚合根版本,避免只比较某张表的行号。上线时统计被拒绝的旧写数量和来源,如果数量突然上升,通常意味着消息分区、重试或慢查询发生异常,应追查而不是把拒绝当成正常噪声。
    • 进阶追问与回答:版本相同但内容不同怎么办?说明幂等键或版本生成有缺陷,应拒绝并告警,不能任意覆盖。脚本阻塞风险如何控制?只做常数复杂度比较和赋值,限制值大小并监控慢日志。
    • 详细章节:版本化缓存
  7. 问题:缓存穿透应如何分层治理,Bloom Filter(布隆过滤器)有哪些误区?

    • 口述答案:穿透是请求的键在缓存和权威库都不存在,治理要从最便宜的入口开始。第一层校验参数格式、租户、权限和订单归属,防止无意义或越权请求进入数据层;第二层按用户、租户和接口限流,控制随机键攻击的最坏成本;第三层对确认不存在的键写短期空值,吸收同一假键重复访问,真实对象创建时主动删除空值;第四层在海量稳定键集合前使用 Bloom Filter(布隆过滤器)。过滤器把每个元素映射到多个位,任一位为零可判定不存在,全部为一只代表可能存在,因此有假阳性。所谓没有假阴性只在位图完整、所有新增都正确写入且不随意清位的前提下成立;位图丢失、异步漏写或错误删除都会造成工程假阴性。容量按元素数量和目标假阳性率计算,例如 5,000 万元素、1%假阳性约需57 MiB(兆字节)和约7次哈希,但10万 QPS(每秒查询率)随机攻击仍可能有约1,000 QPS(每秒查询率)回源,所以数据库前仍需限流和隔离。过滤器重建可用双缓冲:构建新版本、追增量、验证后原子切换。支付订单接口不能通过返回差异泄露存在性,必须先鉴权,最终仍查询权威库。监控要把“过滤器判定可能存在但数据库不存在”的比例作为实际假阳性率,并区分随机攻击与正常删除。空值键也应限制总量和键长,防止攻击者把防护本身变成内存消耗;超过预算时按来源限流,而不是无限写入空值。
    • 进阶追问与回答:普通过滤器如何删除?不能直接清共享位,可周期重建或使用计数型方案。空值缓存时间多长?按对象创建频率和可接受遮挡窗口设置,并在创建事件中主动失效。
    • 详细章节:缓存穿透
  8. 问题:缓存击穿时,互斥重建和逻辑过期应如何选择?

    • 口述答案:先看业务能否接受旧值。互斥重建适合必须尽快看到新值的配置或状态:热点键未命中后,一个请求获得重建资格并查询数据库,其余请求在有界时间内等待,或快速失败。锁只保护重建成本,必须有持有者标识、租约和原子释放;即使租约过期出现多个重建者,最终写入仍要靠版本比较拒绝旧结果。等待方必须有超时,不能把服务线程全部堆在锁上。逻辑过期适合商品详情、列表摘要和跨境轨迹等允许有界陈旧的场景:键不物理删除,值中保存 expireAt(逻辑过期时间),发现过期的第一个请求异步刷新,其他请求继续得到旧值。这样20,000 QPS(每秒查询率)的热点在25毫秒查询窗口内不会产生约500个数据库并发,而只产生一个重建请求。代价是旧值可能持续,必须定义软过期和硬过期:软过期后可返回旧值并刷新,超过硬上限则回源、降级或明确不可用;记录最近成功刷新时间并告警。支付终态和库存放行不能静默返回旧值,应根据客户端已知版本走权威查询。无论选择哪种方式,都要结合预热、请求合并、失败退避和回源闸门,不把分布式锁当最终一致性答案。容量上还要限制同时重建的不同键数量,避免一万个冷热点各自只有一个重建者却仍压垮数据库。可以按租户和接口设置重建信号量,记录等待、返回旧值、重建成功和失败的比例,再根据业务陈旧度调整策略。 验收还要证明重建线程被暂停或超时时,等待请求能在预算内降级,恢复后缓存版本不会倒退。
    • 进阶追问与回答:重建失败后谁重试?由有退避和抖动的调度任务重试,避免每个请求都竞争。逻辑过期是否还需要 TTL(存活时间)?可设置较长物理过期作为垃圾回收和故障兜底。
    • 详细章节:缓存击穿
  9. 问题:请给出缓存雪崩的完整治理体系,而不是只回答随机过期。

    • 口述答案:雪崩是大量请求在短时间从缓存转向权威库,来源至少有五类:大批键集中到期、缓存节点或集群故障、网络分区、发布后的冷缓存、误操作批量删除。基础 TTL(存活时间)加随机抖动只分散第一类,不能解决节点和网络故障。事前要按热点而非全量做预热,过期时间按业务批次离散,缓存集群保留容量和故障域冗余,多级缓存保存可接受的旧值。事中最重要的是保护数据库:按其安全 QPS(每秒查询率)在网关和应用设置回源令牌,单键请求合并,核心支付与库存查询使用独立连接池和优先级,非核心列表返回旧值、静态数据或明确降级;缓存故障时要熔断快速失败,避免超时重试风暴。事后分批预热和灰度放流,观察回源量、连接池等待、缓存命中与错误率,不要缓存刚恢复就瞬间灌满。多级缓存的本地层通过失效事件加速删除,但事件可能丢,所以还要短 TTL(存活时间)和版本兜底。容量计算用绝对回源:100万 QPS(每秒查询率)即使99%命中也有1万 QPS(每秒查询率),平均命中率无法反映同一秒失效。演练应包括节点切换、网络延迟、批量过期和预热失败,并验证核心交易不因缓存故障绕过权威约束。降级策略应提前按数据类型分级:商品图片可静态化,轨迹摘要可返回旧值,支付与库存未知状态则明确处理中或繁忙。恢复阶段保持数据库令牌不变,分批加载热集并逐步提升流量,避免“缓存恢复”本身成为第二次雪崩。
    • 进阶追问与回答:高可用集群是否消除雪崩?故障切换超时、客户端拓扑滞后和热点迁移仍会放大。怎样避免恢复时二次冲击?限速预热、分租户放流并保持回源闸门。
    • 详细章节:缓存雪崩
  10. 问题:多级缓存如何设计失效、版本和容量边界?

  • 口述答案:常见层级是进程内缓存、Redis(远程字典服务)和数据库。读时先查进程内层,命中且版本满足客户端已知下限就返回;否则查远程层,再未命中才经请求合并访问数据库,逐层回填带版本值。写时数据库事务先提交,事务事件删除远程层并广播本地失效。广播只能加速,不能作为唯一保证,因为应用实例可能离线、消息丢失或刚扩容未订阅;所以本地层必须设置较短 TTL(存活时间)、限制条目权重,并在关键读上比较版本。客户端刚完成支付或库存变更时可携带已知版本,任何低版本缓存都不能返回。容量上,本地缓存会按实例数复制,100个实例各缓存500 MiB(兆字节)就是约50 GiB(吉字节)总内存,扩容还会线性放大和制造冷启动回源。因此应只保留真正热点,按对象权重淘汰,避免大对象进入每个进程。远程层负责较大共享热集,本地层吸收极端 hot key(热键)。失效风暴时不能逐实例同步等待确认,否则写路径被集群规模绑架;使用异步广播、版本和自然过期收敛。验收要注入单实例漏消息、滚动发布和扩容,确认最大陈旧时间、回源峰值和内存预算均在范围内。键结构和序列化版本也要兼容滚动发布,新旧实例并存时不能互相读出无法解析的数据;可在键名带结构版本并设置过渡双读。每层单独统计命中和回填来源,否则总命中率无法判断是哪一层失效,也无法估算本地副本的真实收益。
  • 进阶追问与回答:本地缓存一致性是否一定更差?传播层更多使窗口更复杂,但版本和短过期可使窗口有界。热点键本地缓存多久?按新鲜度与实例回源预算计算,不用统一常数。
  • 详细章节:多级缓存
  1. 问题:请系统解释过期删除和 maxmemory(最大内存)淘汰的关系。
  • 口述答案:过期删除依据时间语义处理已经超过 TTL(存活时间)的键,内存淘汰则在已用内存达到 maxmemory(最大内存)阈值时,为新写入释放空间,候选键甚至仍在有效期。惰性过期在访问键时检查并删除,主动过期周期性抽样带过期时间的键,若过期比例高就继续处理,在处理器时间和内存占用之间折中;因此键逻辑到期后可能短时间仍占内存,但正常读不会把它当有效值返回。达到内存阈值后,noeviction(不淘汰)拒绝需要新增内存的写命令;allkeys-lru(所有键近似最近最少使用)和 allkeys-lfu(所有键近似最不常用)从全键空间采样;volatile(仅过期键)系列只从带过期的键中选,若永久键挤满空间就可能无法释放。LRU(最近最少使用)和 LFU(最不常用)都是近似采样,避免维护全局精确队列的高开销。容量不能只看键空间:还要预留客户端与复制缓冲、内存碎片、后台释放、持久化重写的写时复制和增长空间。容器限制32 GiB(吉字节)时把 maxmemory(最大内存)也设32 GiB(吉字节)很危险,碎片和缓冲会让进程先被系统终止。策略选择必须区分普通可丢缓存和幂等窗口,后者仍由数据库唯一约束兜底。运维上同时观察过期键采样耗时、每秒淘汰数、写入错误、常驻内存与已用内存比,以及业务命中率。若淘汰持续但内存不降,要排查后台释放积压、碎片和大对象,而不是无限提高 maxmemory(最大内存)掩盖模型问题。
  • 进阶追问与回答:过期键为何不精确定时删除?每键定时器成本和集中触发停顿过高。LFU(最不常用)一定优于 LRU(最近最少使用)吗?取决于访问分布与热点变化速度,需压测。
  • 详细章节:过期与淘汰
  1. 问题:如何发现和治理 big key(大键),为什么它不仅是内存问题?
  • 口述答案:big key(大键)要从字节大小、集合成员数和单次命令处理量三个维度识别,不能只设一个万能阈值。一个80 MiB(兆字节)字符串会占用网络带宽和客户端堆内存;一个总字节不夸张但有数百万成员的集合,在全量读取、编码、删除和持久化时仍会产生长时间主线程工作。它会放大五类风险:命令执行阻塞其他请求,网络传输造成尾延迟,复制把大值一次推给从节点并扩大偏移差,快照或重写增加写时复制与磁盘峰值,同步删除需要遍历释放大量对象。发现时使用抽样键空间工具、内存用量命令、慢日志和业务对象统计,生产扫描要限速,避免诊断本身阻塞。治理优先从数据模型入手:大字符串拆成分页块或放对象存储;大集合按仓库、日期、租户或哈希桶拆分,读取用游标分页,禁止一次返回全量;删除使用异步释放并监控待释放内存。迁移不能直接改键:先双写新旧结构,历史数据分批搬迁,双读校验数量、摘要和业务结果,再灰度切换,最后异步删除旧键。拆分会引入聚合和跨桶一致性成本,因此库存最终事实仍在数据库流水,缓存分桶只服务查询。验收看 P99(99 分位响应时间)、复制差、网络峰值、后台释放积压和迁移对账,而不是仅证明键变小。 迁移期间还要限制双写失败重试和扫描速度,保留新旧键映射与回滚开关;发现数量或摘要不一致时停止切流,以权威数据重新生成目标分桶,不能继续带错迁移。
  • 进阶追问与回答:惰性释放后内存为何不马上下降?主线程只断开引用,后台仍需回收对象,分配器也可能保留页。压缩是否万能?可降网络和空间,却增加处理器成本和全量解压风险。
  • 详细章节:大键治理
  1. 问题:hot key(热键)为什么扩容分片不一定有效,应该如何治理?
  • 口述答案:Redis Cluster(Redis 集群)按键映射到哈希槽,单个 hot key(热键)仍只落在一个主分片。增加节点能分散不同键,却不会自动把同一键的45,000 QPS(每秒查询率)拆到多个主节点,因此该分片的主线程、网卡或客户端连接仍会过载。先用代理统计、客户端采样、服务端热点工具和分片流量识别单键占比,区分热读与热写。热读优先在应用进程内缓存一到数秒,变更事件广播失效并用版本和短 TTL(存活时间)兜底;100个实例每秒各回源一次可把45,000 QPS(每秒查询率)降到约100 QPS(每秒查询率)。也可为只读值建立多个物理副本键,由客户端随机读取,但更新时必须携带版本并处理副本收敛。热写不能简单复制,因为并发写会产生冲突;应按业务维度拆键、使用局部计数分片后异步汇总、批处理或消息削峰,并明确精确度。请求合并能减少同时回源,限流保护最坏情况。对 WMS(仓储管理系统)库存,按仓库、货主和商品分桶,跨仓总量是可重建读模型,最终扣减仍在明确权威分桶中做条件更新。治理后验证单键流量、分片处理器、网络、延迟和版本差,同时保留热点自动发现,避免活动切换后旧热点配置长期占内存。 若热点无法按业务拆分,应给该键单独设置容量、超时和降级策略,并通过压测确认单实例上限;达到上限前主动限流,而不是等待整个分片一起超时。
  • 进阶追问与回答:读从节点能解决吗?可分担读,但有复制陈旧和故障切换风险,关键读需版本校验。热点副本怎样失效?带版本广播更新或删除,短过期和权威回源兜底。
  • 详细章节:热键治理
  1. 问题:缓存命中率、回源量和容量预算应该怎样计算与解释?
  • 口述答案:命中率只是一种比例,容量设计必须还原绝对流量和时间分布。基础公式是回源 QPS(每秒查询率)等于总请求 QPS(每秒查询率)乘以一减命中率;30,000 QPS(每秒查询率)且96%命中时,回源约1,200 QPS(每秒查询率)。但平均值会隐藏雪崩和热点,应分别统计进程内命中、远程缓存命中、空值命中、数据库回源和错误,按租户、接口、键前缀以及秒级高分位观察。容量从热集条目数乘平均序列化大小开始,再加键、对象、过期字典等元数据;随后预留客户端与复制缓冲、内存碎片、后台释放、持久化重写写时复制和业务增长。若32 GiB(吉字节)实例中键空间24 GiB(吉字节),碎片比率1.35,单键空间换算的常驻内存已经约32.4 GiB(吉字节),再加缓冲就会超过限制,因此 maxmemory(最大内存)必须显著低于容器上限。容量还要看淘汰曲线:热集大于可用空间时,键会不断淘汰并回源,命中率出现抖动。数据库保护按安全吞吐而不是缓存预期命中分配令牌。例如数据库安全1,500 QPS(每秒查询率),应让所有实例共享或分配总回源预算,并为支付、库存等核心请求预留。最终用真实访问分布压测,记录命中、淘汰、回源、连接等待和延迟,不用静态公式代替验收。 预算要按平峰、活动峰值、节点故障后少一个分片和冷启动四种场景分别计算,并给数据增长保留余量;实际访问分布变化后周期性重算,避免历史命中率掩盖新热点。
  • 进阶追问与回答:99%命中一定好吗?在百万 QPS(每秒查询率)下仍有一万回源,而且可能集中。如何判断缓存过大?边际命中提升小于内存、失效和运维成本时应缩减冷数据。
  • 详细章节:容量与观测
  1. 问题:WMS(仓储管理系统)库存缓存如何设计,才能既快又不超卖?
  • 口述答案:我会把正确性路径和加速路径分开。数据库保存库存流水、可用量、冻结量和业务唯一号,扣减使用带条件更新,例如可用量大于等于申请量才成功,同一订单行通过唯一约束防止重复扣减;这条路径决定是否超卖。Redis(远程字典服务)保存按仓库、货主和商品聚合的可用量读模型,用于列表查询、预校验和热点削峰,但缓存显示有货不能绕过数据库条件。事务提交后写事务外盒事件,事件携带聚合键和数据库版本,消费者删除或原子更新缓存;删除失败重试,旧事件不能覆盖更高版本,后台按库存守恒公式核对。高并发读采用 Cache Aside(旁路缓存),热点失效时请求合并;活动前按历史热集预热,TTL(存活时间)随机离散。若使用 Lua(脚本语言)做缓存预扣,只能视为流量闸门,还要把预扣事件可靠落到数据库,失败补偿释放,故障切换或缓存全失后从流水重建。缓存不可用时限制非核心列表回源,核心扣减仍走数据库但按安全容量限流。指标包括缓存版本差、条件更新失败率、重复业务号、补偿积压、可用与冻结守恒、回源 QPS(每秒查询率)和重建时间。故障演练清空缓存、注入删除失败和乱序事件,验收标准是无超卖、幂等请求结果一致、缓存最终可重建,而不是缓存始终命中。 对账发现差异时按业务号冻结有风险操作,从流水重算聚合并覆盖缓存;修复过程保持幂等,记录受影响仓库、商品和订单范围,避免人工直接改缓存制造新的不可审计状态。
  • 进阶追问与回答:为什么不只用缓存原子扣减?主从切换、持久化和落库窗口会丢事实。跨仓调拨怎么做?权威事务或状态机记录出入两侧,缓存只展示聚合结果。
  • 详细章节:库存项目边界
  1. 问题:支付订单查询缓存如何处理支付成功后的旧读、幂等和资金边界?
  • 口述答案:支付订单、渠道事件、退款和资金流水必须先进入具备事务、唯一约束和审计能力的权威数据库。渠道回调按渠道事件号和商户订单号幂等,数据库状态机只允许合法方向推进,例如处理中不能覆盖成功,重复成功回调返回原结果;事务提交后产生带订单版本的失效事件。Redis(远程字典服务)仅保存订单查询状态、更新时间和短期路由信息。用户支付完成后立即查询时,客户端携带刚获知的订单版本,应用发现缓存版本低于该下限就绕过缓存读主库或主动查询渠道,不能把旧的“待支付”当作失败并再次发起扣款。普通查询使用 Cache Aside(旁路缓存),删除失败由事务外盒消息重试,缓存值带版本防止迟到事件倒退,终态可设置较长 TTL(存活时间),处理中状态较短。缓存不可用时按用户和订单限流回源,必要时返回“处理中”,禁止为了可用性伪造成功或失败。资金幂等不能只依赖可淘汰缓存键,数据库唯一业务号始终兜底;退款同样以退款流水和渠道查询收敛。监控从订单事务提交到缓存失效的延迟、版本落后、重复回调、未知状态、渠道查询和对账差异。缓存全失后可从订单与流水重建,金额守恒和渠道账单一致才是最终验收。 对于渠道返回超时的未知结果,不能依据缓存再次扣款,应按原幂等号查询渠道并延迟收敛;超过时限进入人工与自动对账队列,用户侧明确展示处理中和下一次查询时间。
  • 进阶追问与回答:缓存里加分布式锁能防重复扣款吗?锁可能过期或丢失,唯一键与状态机才是底线。支付成功缓存删除失败怎么办?高版本查询绕过、事件重试和短过期共同收敛。
  • 详细章节:支付订单缓存
  1. 问题:跨境物流轨迹缓存如何处理重复、乱序、迟到与热点查询?
  • 口述答案:承运商原始回执和标准化轨迹事件应持久化为可审计事实,缓存只是最新轨迹摘要。入口按承运商、运单号和外部事件标识幂等;由于外部时间戳可能时区错误、回拨或批量补发,不能只按事件时间覆盖,应结合渠道序号、接收位点、节点优先级和内部规则生成单运单可比较版本。数据库状态机保存完整事件并决定最新节点,事务后发布版本化失效事件。Redis(远程字典服务)保存最新节点、预计到达时间、业务版本和更新时间;异步更新通过 Lua(脚本语言)拒绝低版本,迟到旧事件可入历史但不能倒退摘要。运单列表属于高频、允许短期陈旧的场景,可采用逻辑过期:软过期后展示带更新时间的旧摘要并由一个执行者刷新,超过硬上限或用户主动打开详情时读轨迹库。热门大客户批量查询要分页、按租户限流并合并同运单请求,避免一个大集合键和 hot key(热键)。缓存全失时从标准化事件按运单重建;恢复期间可降级到轨迹库或明确展示最后更新时间。指标包括事件重复率、乱序率、最新版本差、刷新失败、热点运单 QPS(每秒查询率)和重建覆盖率,验收通过重复、乱序、延迟与承运商补发故障注入。 若某承运商持续补发历史数据,应隔离其消费速率并保持其他渠道正常推进;异常规则、人工修订和原始回执都要留痕,使缓存摘要的每次变化都能追溯到权威事件。 重建完成后抽样比对最新节点、版本和事件总数,确认恢复过程没有跳过异常运单。
  • 进阶追问与回答:多个承运商接力如何排序?按业务阶段和内部统一序号,不制造虚假全局时间。签收后又来运输中事件怎么办?状态机拒绝倒退并保留异常审计。
  • 详细章节:轨迹缓存
  1. 问题:缓存故障时如何排障并保护数据库?
  • 口述答案:第一步不是立刻清缓存或重启,而是确认影响并止损。对齐命中率下降、回源 QPS(每秒查询率)、数据库连接池等待、错误率和 P99(99 分位响应时间),立即按数据库安全容量收紧回源令牌,核心支付与库存请求保留优先级,非核心列表返回有界旧值、静态结果或明确繁忙;关闭无上限客户端重试,避免雪崩。第二步判断范围:全局下降通常检查缓存节点、网络、淘汰、批量失效和发布;单键尖峰检查 hot key(热键)、物理过期、大对象和重建失败。第三步收集证据:服务端命令延迟与慢日志、客户端连接池与超时、分片流量、内存水位和淘汰、过期速率、消息积压、数据库查询耗时和最近变更。若服务端很快而客户端慢,继续排查网络、连接争用和线程池,不能凭组件名称归因。第四步比较数据库权威版本与缓存版本,确认是缓存旧还是业务事实本身错误;后者要停止状态推进并按流水修复。第五步分批重建和灰度放流,保持回源闸门,观察命中、版本差和数据库等待恢复。复盘给出分钟级时间线、根因、放大因素和永久措施,例如过期离散、事件重试、热点发现、容量余量和演练。证明恢复要靠指标稳定和版本对账,不是接口偶尔成功。 若需要回滚最近发布,先确认键格式和序列化是否向后兼容,再分批执行;回滚后继续观察一个完整过期周期,确认没有延迟消息或旧实例再次写入错误版本。
  • 进阶追问与回答:能否直接切备用缓存?可以,但要评估数据新鲜度、客户端拓扑和冷启动回源。何时返回旧值?仅限业务定义可陈旧的读模型,支付和库存放行不能静默旧读。
  • 详细章节:排障路径
  1. 问题:为什么“缓存不承担最终正确性”不是一句空话,系统设计上如何落实?
  • 口述答案:这句话需要落实为四个可验证条件。第一,权威事实有独立存储和约束:库存有条件更新、冻结与流水,支付有订单状态机、渠道流水、唯一业务号和对账,轨迹有原始回执与标准事件。第二,任何业务决策都不把缓存值当唯一依据:库存缓存可用于预判,但最终扣减仍检查权威条件;支付缓存可展示状态,但扣款、退款和结算只根据权威事务;缓存未命中或版本不足时有回源路径。第三,缓存可以全量删除并重建:定义重建数据源、事件位点、批次速率、恢复时间和业务降级,定期在隔离环境演练并比较库存守恒、金额守恒、状态不倒退和轨迹最新性。第四,缓存故障不会破坏幂等:可淘汰的幂等键只能加速,数据库唯一约束和业务状态机仍能拒绝重复副作用。技术上使用提交后失效、可靠事件、版本拒旧、短过期和扫描对账缩短旧读窗口,但不把它们表述为强一致。监控也从正确性出发,记录版本落后、删除失败、补偿积压和对账差异,而不仅是命中率。面试时能说明缓存全失、主从切换丢最近写或消息积压后业务仍如何收敛,才算真正建立了边界,而不是在方案图旁写一句“以数据库为准”。 接口契约也要表达这种边界:响应携带数据版本和更新时间,未知状态不伪装为失败,写请求返回权威事务结果;审计日志能够关联业务号、数据库版本、缓存事件和补偿结果。 定期清空隔离环境的缓存并从权威源恢复,用恢复时间、版本一致率和业务守恒结果证明这一约束真实存在。
  • 进阶追问与回答:既然最终读数据库,缓存价值是什么?吸收绝大多数读、降低延迟和隔离峰值。数据库也出错怎么办?靠事务日志、备份、审计、对账和人工处置,正确性不是单点组件属性。
  • 详细章节:正确性边界
  1. 问题:如何设计缓存预热和发布流程,避免冷启动与旧值覆盖?
  • 口述答案:预热应围绕可证明的热集,而不是把全库复制进缓存。先从历史访问高分位、活动商品、重点仓库、活跃运单和支付查询趋势确定键集合,估算序列化后字节、实例内存、预计命中收益和数据库加载能力。加载程序按租户或分片分批读取,受统一回源令牌控制,避免预热本身打垮数据库;每条数据携带数据库版本,写缓存时原子比较,防止批量快照的旧版本覆盖预热期间实时变更。实时事务事件持续失效或更新,预热完成后抽样比较数据库与缓存版本。发布采用灰度:少量实例先承接流量,观察进程内和远程缓存命中、回源 QPS(每秒查询率)、淘汰、内存、数据库等待和错误率,再逐步放量。多级缓存的本地层随实例启动会产生并发冷加载,应加入随机启动抖动、共享请求合并和预取上限。若预热失败,服务保持回源闸门并返回有界旧值或静态降级,不能无限重试。旧版本键结构迁移时采用双写或双读校验,确认新结构覆盖率和摘要一致后再切换,旧键最后异步删除。验收必须模拟滚动重启、同时扩容、活动切换和缓存清空,证明数据库峰值、最大陈旧时间和恢复时间都在预算内。 发布完成后清理双读与旧键必须有独立观察期,先确认旧结构访问量归零,再限速释放;若命中下降或版本差升高,能立即恢复旧读取路径,而不重新全量加载。 预热任务自身也要有幂等批次号、检查点和失败续跑能力,避免重启后从头扫描并造成重复数据库压力;每批完成后记录版本覆盖率再推进下一批。
  • 进阶追问与回答:预热所有终态订单是否有意义?访问稀疏会浪费内存并增加淘汰,应按热度加载。如何避免多个实例重复预热?集中任务分片或共享进度,实例本地只预取自身热集。
  • 详细章节:预热与多级缓存
  1. 问题:如何选择 LRU(最近最少使用)、LFU(最不常用)和 noeviction(不淘汰)策略?
  • 口述答案:选择前先区分数据角色、访问分布和写失败后果。普通查询缓存可丢且近期访问能代表未来热度时,allkeys-lru(所有键近似最近最少使用)容易理解;热点长期稳定、偶发全量扫描会污染近期访问时,allkeys-lfu(所有键近似最不常用)通常更能保留高频键,但新热点需要计数升温。volatile(仅过期键)系列只有在所有可淘汰键都正确设置 TTL(存活时间)、永久键占比受控时才安全,否则永久键可能挤满内存而无候选可淘汰。noeviction(不淘汰)避免静默丢键,但达到阈值后写命令会失败,应用必须识别错误、降级或切流;它不等于数据安全,主从切换、持久化和进程故障仍可能丢数据。幂等窗口、分布式锁和普通缓存不应混在一个随意淘汰的实例里,因为淘汰会改变协议行为;即便隔离并使用 noeviction(不淘汰),数据库唯一约束仍是最终兜底。决策要用真实流量回放,比较命中率、淘汰速率、热键保留、写失败、处理器开销和尾延迟。还要把 maxmemory(最大内存)留在容器限制之下,为碎片、复制缓冲和后台任务预留空间。策略只是内存压力下的选择器,不能修复热集本身大于容量或大键模型错误。 策略变更前后要固定同一份访问回放并比较,而不是用不同流量得出结论;若任何策略都持续淘汰,应先扩容、缩小热集或拆分实例,再讨论参数微调。
  • 进阶追问与回答:为何不能只看命中率选策略?还要看写失败、核心键被淘汰和延迟。策略在线切换有风险吗?会立即改变候选行为,应灰度、监控并准备扩容或回滚。
  • 详细章节:淘汰策略选型
  1. 问题:请给出一套可落地的缓存一致性测试与故障演练方案。
  • 口述答案:测试要围绕允许的不一致窗口和业务不变量,而不是只验证命中。并发层构造精确屏障:读线程缓存未命中后暂停,写线程提交新版本并删除,再恢复读线程回填旧值,验证版本脚本拒绝;分别测试先删后更、先更后删、删除超时、事务回滚和多个写者乱序。消息层注入重复、乱序、丢失、消费者停机和位点回退,确认事务外盒可重放、旧事件不覆盖新值、死信告警和扫描对账能收敛。缓存问题层制造不存在随机键、热点键集中失效、百万键同秒过期、big key(大键)删除和 hot key(热键)突增,观察回源闸门、请求合并、逻辑过期、淘汰和数据库连接池。基础设施层执行节点重启、网络延迟、主从切换、内存达到阈值和缓存全量清空。项目验收用不变量:WMS(仓储管理系统)库存期初加收入减出库减冻结等于当前量且永不超卖;支付同一业务号只产生一次副作用、金额守恒、状态不倒退;轨迹最新节点版本单调且迟到事件不覆盖。指标记录最大与 P99(99 分位响应时间)一致性窗口、删除失败恢复时间、消息积压年龄、回源峰值、错误率和重建完成时间。每次演练保留时间线和证据,验证降级开关、告警和操作手册,才能证明方案在失败时仍可控。 演练结束还要核对是否残留限流、测试键和暂停消费者,恢复正常阈值并归档数据;后续版本若改变键结构、消息协议或数据库模型,应重新执行受影响用例。
  • 进阶追问与回答:如何自动判断缓存旧值?请求响应携带版本,与数据库抽样或事件位点比较。测试环境通过是否代表生产?需复制数据分布、热键、网络和容量比例,并定期生产受控演练。
  • 详细章节:项目与排障

12. 复习清单

  • 能画出先删后更导致旧值回填、先更后删短窗口与延迟失效的时序图。
  • 能比较 Cache Aside(旁路缓存)、Read Through(读穿透)、Write Through(写穿透)、Write Behind(异步回写)的确认点和失败模型。
  • 能说明事务外盒、binlog(二进制日志)与 CDC(变更数据捕获)的重复、乱序、积压和补偿。
  • 能计算 Bloom Filter(布隆过滤器)空间、假阳性回源和空值缓存边界。
  • 能按业务陈旧度选择互斥重建、逻辑过期、预热、多级缓存和降级。
  • 能区分过期删除与 maxmemory(最大内存)淘汰,并解释 LRU(最近最少使用)、LFU(最不常用)为近似策略。
  • 能识别和治理 big key(大键)、hot key(热键)、内存碎片与惰性释放积压。
  • 能明确 WMS(仓储管理系统)库存、支付资金和跨境轨迹的权威事实与缓存边界。
  • 能用回源 QPS(每秒查询率)、版本差、事件积压和数据库等待完成缓存事故排障。
  • 能设计并发竞态、消息异常、节点故障和缓存全失的可重复演练。