基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如果由于代码Bug导致缓存中存入了大量的脏数据,业务在线且不能停机,如何平滑清理并恢复数据一致性?

知识点图片

在业务在线且不能停机的情况下,处理大量缓存脏数据是一个典型的高可用架构应急场景。直接清空缓存(如 FLUSHALL 或批量 DEL)会导致缓存雪崩,巨量请求直接打垮数据库(DB)。

为了“平滑清理并恢复一致性”,必须遵循“控制爆炸半径、避免DB击穿、渐进式修复”的原则。以下是标准的操作指南和修复策略:


第一阶段:止血与评估(紧急操作)

  1. 切断污染源
    • 立即回滚代码发布Hotfix,确保新的请求不再产生脏数据。如果污染源不切断,后面的清理毫无意义。
  2. 评估影响面
    • 脏数据的特征是什么?(是有特定的 Key 前缀?还是某个时间段写入的数据?还是全局污染?)
    • 脏数据对业务的影响程度?(是只影响显示,还是会引起资损?)
  3. 监控水位
    • 检查当前 DB 的 CPU、IO负载,以及 Redis 的内存和网络情况,确定接下来修复任务的“可用性能余量”。

第二阶段:选择平滑修复策略

根据脏数据的特征和范围,选择以下三种策略之一:

策略一:缓存 Key 版本升级(适用于全局污染、无法区分脏数据)

如果几乎所有缓存都被污染,或者很难通过规则筛选出脏数据,最好的方式是废弃老缓存,启用新版本缓存

  • 做法
    1. 修改代码中的 Cache Key 生成规则,增加版本号前缀。例如从 user:info:{uid} 改为 v2:user:info:{uid}
    2. 绝对不能直接上线新代码! 否则会导致 100% 缓存未命中,瞬间击穿 DB。
    3. 后台预热:写一个后台任务,从 DB 中全量读取数据,写入到 v2 版本的缓存中(控制写入 QPS,保护 DB)。
    4. 灰度切换:预热完成后,通过配置中心(如 Apollo、Nacos)动态下发开关,将应用读取缓存的逻辑从 v1 切换到 v2(可以按用户ID哈希做灰度,比如先放量10%,观察DB压力,再逐步放到100%)。
    5. 清理老数据:切换完成后,老缓存不再被访问,等待其自然过期,或者在低峰期使用脚本异步清理。

策略二:异步扫描 + 强制覆盖(覆盖优于删除,适用于特定范围污染)

如果你能明确知道哪些 Key 是脏数据(例如特定前缀,或已知受影响的用户ID列表),应采用异步修复

  • 做法
    1. 提取脏 Key 列表:通过应用日志提取,或者通过 Redis SCAN 命令(绝不能用 KEYS)匹配出受影响的 Key。
    2. 编写修复脚本(或任务):读取 DB 中的正确数据,重新 SET 到缓存中。
    3. 为什么是“覆盖”而不是“删除”? 如果你把脏数据 DEL 掉,接下来的并发请求会发现缓存为空,同时去查 DB(缓存击穿)。直接用 DB 的正确数据覆盖 Redis 中的脏数据,可以保证前端请求始终命中缓存。
    4. 限流执行:脚本必须加入限流(Rate Limiter),例如限制每秒修复 500 个 Key,持续监控 DB 的 CPU 和慢查询,一旦 DB 压力上升立刻降低修复速率。

策略三:懒加载 + 强制失效(适用于长尾数据、非热点数据)

如果脏数据量极大,且大部分是冷数据,全量查 DB 覆盖成本太高,可以结合过期时间进行处理。

  • 做法
    1. 拦截业务读取逻辑:如果业务允许,可以在代码里加个短期的“补丁”。
    2. 当读到旧格式的脏数据时(可以通过数据结构特征判断,比如缺少某个新字段),代码主动判断其为无效数据。
    3. 触发常规的“缓存未命中”逻辑:去 DB 查最新数据,并回写缓存。
    4. 防击穿保护:如果该数据是热点,必须在代码中加入单机锁(如 Golang 的 singleflight 或 Java 的 ConcurrentHashMap + synchronized分布式锁,确保同一个 Key 并发失效时,只有一个线程去查 DB 回写。

第三阶段:执行细节与防雷指南(Crucial Details)

在执行上述策略时,必须注意以下技术细节,防止引发二次事故:

  1. Redis SCAN 操作规范

    bash
    # 错误做法:导致 Redis 阻塞
    KEYS user:info:* 
    
    # 正确做法:游标迭代,每次取少量
    SCAN 0 MATCH user:info:* COUNT 100
  2. 打散过期时间(TTL Jitter)
    在后台任务重新覆盖缓存时,千万不要给所有 Key 设置相同的过期时间。否则几个小时后,这批数据同时过期,会引发延迟的缓存雪崩。

    java
    // 伪代码:基础过期时间 + 随机打散
    int baseTtl = 3600; // 1小时
    int randomJitter = new Random().nextInt(1800); // 0~30分钟随机
    redis.set(key, value, baseTtl + randomJitter);
  3. 双写一致性保障
    在后台修复脚本运行期间,业务层可能也在正常修改数据。为了防止“修复脚本拿到的 DB 旧数据”覆盖了“业务刚刚写入缓存的新数据”:

    • 方案 A(时间戳/版本对比):缓存 Value 中带上更新时间,覆盖前检查,只允许新时间戳覆盖老时间戳。
    • 方案 B(利用 Redis 的 Lua 脚本):在修复脚本中,先查一下当前的 Redis 数据,如果已经被业务更新(数据正确),则跳过不修复。
    • 方案 C(简单粗暴延后修复):修复任务只处理发生 Bug 那个时间段之前的数据。
  4. 利用低峰期
    如果脏数据不导致严重的资损(例如仅仅是用户昵称显示错误、点赞数错误),尽量将大规模的修复任务(预热、扫表)延后到凌晨业务低峰期执行。


总结:标准处理 SBP (Standard Business Procedure)

  1. 发现问题 -> 代码止血(发版修复BUG) -> 脏数据不再增加。
  2. 评估DB抗压能力,开启 DB/Redis/网络 监控大盘
  3. 如果是核心热点数据 -> 采用策略一(Key版本升级+后台平滑预热+灰度切换)
  4. 如果是特定已知数据 -> 采用策略二(游标SCAN + DB读取 + 限流强制覆盖缓存)
  5. 在操作过程中,严格控制并发请求量,给重置的缓存加上随机TTL
  6. 修复完毕后,复盘并引入机制(如缓存写入校验、重要数据的对账机制)。
00:00
00:00