当前位置: 首页 > news >正文

缓存的三大问题分析与解决


概览:三大问题一览

  • 缓存穿透(Cache Penetration):查询一个不存在的数据,既不在缓存也不在数据库,导致每次请求都穿透到数据库,压力直达 DB。
  • 缓存击穿(Cache Breakdown / Cache Miss Storm):某个热点 key 在短时间内失效,瞬时大量并发请求同时击穿缓存去加载 DB,产生雪崩式 DB 压力。
  • 缓存雪崩(Cache Avalanche):大量缓存 key 在同一时间失效(或缓存服务宕机),导致大量请求同时落到后端 DB,引起不可用或崩溃。

1. 缓存穿透(Cache Penetration)

1.1 问题描述

客户端频繁请求不存在或被伪造的 key(例如:/product?id=99999999),缓存里没有,DB 也没有,每次都打到 DB,造成 DB 压力或被恶意刷。

1.2 造成原因

  • 用户或攻击者查询随机/不存在 id(漏洞、爬虫、恶意攻击)。
  • 接口缺乏有效参数校验(比如 id 格式校验不严格)。

1.3 检测指标

  • 某些 key 的 DB 查询命中率极高但缓存命中率为 0。
  • DB 慢查询增加且来源于相似参数(同一接口大批不存在 id)。

1.4 常见解决方案(按实用度排序)

A. 参数校验 + 白名单
  • 在业务层先做合法性校验(id 格式、范围、签名校验等),对非法请求直接拒绝或返回错误,不查询缓存/DB。
  • 适用于可以通过规则识别的大量无效请求。
B. 布隆过滤器(Bloom Filter)
  • 在请求到达缓存/DB 前,先在布隆过滤器里判断是否可能存在(布隆过滤器误判为存在,但不会误判为不存在)。
  • 当布隆过滤器判断“不存在”时直接响应空/错误,不查询 DB。
  • 适合高 QPS 场景且数据集合较稳定(例如用户 id、商品 id 集合)。

实现示例(伪代码)

if not bloom.mightContain(id):return 404 / empty
# 否则再查缓存 -> 查 DB -> 填充缓存

注意:布隆过滤器需要定期与 DB 同步(新增/删除)或采用可增量更新的方案。

C. 缓存空值(Negative Caching)
  • 对 DB 确认不存在的 key,在缓存中写入一个特殊空值(例如 NULL__NULL__)并设置较短 TTL(如 1~5 分钟)。
  • 对后续相同请求直接返回空值而不触发 DB。

Redis 示例

SET key "__NULL__" EX 60  # 缓存 60s

权衡:

  • 优点:实现简单,能快速阻止重复穿透请求。
  • 缺点:空值也占内存;若大量不同不存在 key 被缓存,会导致缓存被污染(建议把负缓存 TTL 设置短一些)。
D. 接口限流 / 认证
  • 对短时间内的某些 IP 或某些接口进行限流或验证码校验(防止恶意探测)。

2. 缓存击穿(Cache Breakdown / Hot Key Miss Storm)

2.1 问题描述

某个热点 key(例如商品详情页、热门排行榜)在到期时或被主动删除时,瞬间有大量并发请求同时去读取 DB 重建缓存,造成 DB 瞬时高并发访问,产生高延迟或宕机风险。

2.2 造成原因

  • 热点 key 的 TTL 相同且到期集中。
  • 热点 key 被批量清理或缓存失效策略不合理。
  • 单点热点(某个 key 请求量 >> 其他)。

2.3 检测指标

  • 某单 key 的 QPS 高且缓存 miss 突增。
  • DB 连接池瞬间耗尽或慢查询率暴涨。
  • 同一 key 同一时刻大量缓存重建日志。

2.4 常见解决方案(组合使用更可靠)

A. 互斥锁(Mutex / Lock)
  • 在缓存未命中时,某个请求获取到重建缓存的锁,其他请求等待(或短时间返回空)。
  • 推荐使用 Redis 的 SET key value NX PX ttl 获取分布式锁,并用 Lua 脚本安全释放锁(以防删除别人的锁)。

获取锁示例(Redis)

SET lock_key token NX PX 30000
-- 成功表示获取到锁

安全释放(Lua)

if redis.call("GET",KEYS[1]) == ARGV[1] thenreturn redis.call("DEL",KEYS[1])
elsereturn 0
end

伪代码 flow

  1. 请求查缓存,miss。

  2. 尝试获取锁:

    • 成功:从 DB 加载,写入缓存,释放锁。
    • 失败:短轮询(sleep+retry)或直接返回旧值/降级。

注意:锁要设置合理 TTL,且重建耗时不可超过锁 TTL(或可采用续期)。

B. 单飞机制 / Request Coalescing(Singleflight)
  • 把并发请求合并为一次后端加载(Go 的 singleflight 就是这个思想)。
  • 多个请求等待同一次加载结果,加载完成后将结果广播给等待者。
C. 永不过期 / 主动刷新(Cache Refresh / Refresh-Ahead)
  • 对热点数据不使用短 TTL,而是:在 TTL 到期前由后台线程异步刷新(refresh-ahead),保证缓存持续存在。
  • 或者把热点 key 设为长期 TTL(或不失效),并通过消息/事件在数据更新时主动刷新或删除对应缓存。
D. 限流 + 降级(Fallback)
  • 在重建期间,对请求进行限流(固定窗口、令牌桶等),超额请求返回降级数据(如上次快照、空页面、错误码或静态页面)。
E. 本地缓存 + 多级缓存(L1 + L2)
  • 对于单机热点,可以在应用内维护本地 LRU/LFU 缓存(例如 Guava Cache、Caffeine),减少对 Redis 的并发访问,再以 Redis 作为集中缓存层。对热点 key,本地缓存命中可大幅降低到 Redis 的并发。

2.5 实践建议

  • 热点检测:定期统计 top-N key 的 QPS,针对热点采取“永不过期 + 后台刷新”策略。
  • 使用单独的缓存策略(不同 TTL、不同刷新策略)来隔离热点 key。
  • 锁与单飞机制要注意超时与异常情况下的容错(避免死锁或缓存永远不生效)。

3. 缓存雪崩(Cache Avalanche)

3.1 问题描述

大批量缓存 key 在同一时间失效(例如统一 TTL、Redis 宕机重启导致数据丢失、批量清理),导致大量请求同时落到 DB,引发 DB 宕机或服务不可用。

3.2 造成原因

  • 大量 key TTL 一致且同时到期(常见于统一设置 TTL 的批量写入)。
  • 缓存服务宕机/重启/网络抖动(缓存瞬间不可用)。
  • 在扩容/缩容或运维过程中误操作批量清空缓存。

3.3 检测指标

  • 大量缓存 miss 同时出现,DB QPS 突增。
  • Redis 主从切换或重启日志与 DB 请求峰值时间一致。

3.4 常见解决方案

A. TTL 随机化(Jitter)

为 key 设置 baseTTL + 随机值,避免大量 key 同一时刻过期。

示例(Python)

import random, redis
ttl = base_ttl + random.randint(0, jitter_seconds)
redis.set(key, value, ex=ttl)
B. 分批过期 / 保持短平替换

把一批写入的 key 分批设置不同 TTL 或在写入后异步按批刷新,避免集中过期。

C. 预热(Pre-warming / Cache Warming)
  • 在服务启动或维护后,预先把热点数据加载到缓存(异步批量加载)。
  • 适用于可预估热点集合。
D. 多级缓存与降级(Graceful Degradation)
  • 应用端 fallback:当缓存不可用时,限制请求速率访问 DB 或返回降级内容(静态页面、默认值)。
  • 使用本地缓存作为短期降级缓存(L1),在 Redis 不可用时仍能应对一部分流量。
E. 利用读副本与限流
  • 将读请求在 DB 的只读副本中分摊,或在缓存击穿时先在副本上读取。
  • 对突发流量做保护,采用限流、熔断策略。
F. 异步填充 / 队列
  • 在缓存失效高峰时,通过队列对 DB 的请求做排队,单线程或少量线程逐步填充缓存,避免直接并发打爆 DB。

3.5 运维/治理措施

  • 不要在高峰期做批量清除缓存或重启缓存集群。
  • 对关键业务增加多可用区(AZ)架构,缓存使用持久化(RDB/AOF)与高可用方案(主从 + 哨兵 / 集群)。
  • 定期演练“缓存不可用”的降级方案(chaos engineering 风格)。

4. 缓存与数据一致性(补充重要问题)

虽然用户只问三大问题,但在实际工程中缓存一致性是与三大问题并列的常见挑战,也要重视。

4.1 场景(常见的 race)

2 个并发请求同时更新 DB 和缓存,错误的更新顺序会导致旧数据回写到缓存(脏数据):

  • 流程 1(错误):客户端 A 写 DB(成功),删除缓存;客户端 B 读取旧缓存(未命中),读 DB,得到旧值并写回缓存 —— 最终缓存为旧值。
  • 常见操作顺序问题:delete cachewrite DB 的并发竞争。

4.2 常见解决策略

A. Cache Aside(旁路缓存) + DB 更新后删除缓存
  • 推荐顺序:先更新 DB, 再删除缓存。但仍可能出现并发时序问题(上例)。
  • 改良:写完成后,异步延迟删除或使用消息队列保证删除操作可见;或在写完 DB 后同时发布一条 invalidation 消息(使用消息队列或 Redis pub/sub)。
B. 使用分布式锁
  • 在写操作时对该 key 加锁,序列化读写(牺牲并发)。
C. 写入缓存(Write-Through / Write-Behind)
  • Write-Through:写缓存同时写 DB(同步),读来自缓存。优点:一致性好;缺点:写延迟高、DB/缓存耦合强。
  • Write-Behind(Write-Back):写入缓存后异步刷盘到 DB(性能好但有丢失风险,复杂)。
D. 乐观并发控制(版本号 / 时间戳)
  • 每条记录带版本号或 timestamp,读取时比较版本,写入时检查版本,避免旧值覆盖新值。
E. 事件驱动的缓存失效
  • 所有写事件都会通过 MQ 广播,订阅方负责使对应缓存失效或更新。

4.3 详细顺序建议(工程化)

  • 对于强一致要求:使用事务 + 分布式锁或 write-through。
  • 对于可弱一致场景:使用 cache-aside + publish/subscribe invalidation + 短 TTL。
  • 使用消息队列(可靠投递)来做缓存失效通知,能显著降低并发 race 的概率。

5. 常用实现细节与代码片段

5.1 Redis 获取分布式锁(安全释放示例)

# 获取锁(原子)
SET lock_key token NX PX 30000# 释放锁(Lua 脚本,防止误删别人锁)
local script = [[
if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end]]
redis.eval(script, 1, "lock_key", token)

5.2 双重检查 + 锁(伪码)

value = cache.get(key)
if value is not None:return value# 未命中,尝试获取分布式锁
if acquire_lock(lock_key):try:# 再次检查(双重检查)value = cache.get(key)if value is not None:return valuevalue = db.load(key)if value is None:cache.set(key, "__NULL__", ex=short_ttl)return Nonecache.set(key, value, ex=ttl_with_jitter)return valuefinally:release_lock(lock_key)
else:# 没拿到锁,短轮询或快速返回降级值time.sleep(0.05)return cache.get(key) or fallback_value

5.3 TTL 随机化设置示例

import random
base_ttl = 3600  # 1 hour
jitter = 600     # up to 10 minutes
ttl = base_ttl + random.randint(0, jitter)
redis.set(key, value, ex=ttl)

5.4 布隆过滤器伪代码

# 服务启动时从 DB 或同步服务初始化布隆过滤器
bloom.add(all_valid_ids)# 请求处理
if not bloom.mightContain(id):return 404
# else -> 查缓存 -> 查 DB -> 填充缓存

6. 生产实践建议与权衡

6.1 架构与策略选择

  • 读多写少:常用 Cache Aside(懒加载)+ 负缓存 + TTL 随机化 + 热点策略。
  • 强一致性:采用写穿或写回并结合分布式锁或版本控制。
  • 高并发热点:使用单飞合并/本地 L1 缓存 + 永不过期 + 后台刷新。
  • 防攻击/穿透:布隆过滤器 + 参数校验 + 接口限流。

6.2 配置与参数建议

  • 负缓存 TTL:短(1min ~ 5min)以防空值污染。
  • 热点数据 TTL:设置较长或不失效,并异步刷新。
  • 锁 TTL:略大于数据加载最大耗时,避免死锁;使用安全释放确保不会误删锁。
  • 缓存容量:监控 maxmemory,设置合理的 eviction 策略(如 volatile-lru / allkeys-lru)根据访问模式选择。

6.3 监控与告警(必须)

  • 缓存命中率(hit ratio)— 低命中率意味着缓存策略问题或穿透。
  • Top hot keys — 发现单点热点。
  • 缓存 miss 突增 / DB QPS 突增 — 可能是击穿或雪崩。
  • Redis 内存 / evictions / keyspace misses
  • 锁等待时间 / 重建耗时 / 后端响应时间

6.4 测试与演练

  • 做压力测试(带有热点、穿透、故障情况)。
  • 演练 Redis 宕机、主从切换、批量过期等故障场景(Chaos)。
  • 验证降级逻辑(当缓存不可用或 DB 限流时的用户体验)。

7. 常见反模式(避免)

  • 把所有数据都设置同一 TTL(易雪崩)。
  • 把负缓存 TTL 设太长(易污染缓存)。
  • 不做参数校验就直接用缓存/DB(易被恶意扫描)。
  • 锁实现不安全(不使用 token 验证就删除锁,可能删到别人的锁)。
  • 写操作只删除缓存不做重试/补偿(易出现脏数据)。

8. 最佳实践清单(简短)

  • 对外接口做参数校验与限流。
  • 使用布隆过滤器或负缓存防穿透。
  • 对热点 key 使用单独策略(长期 TTL + 后台刷新 / local cache)。
  • 随机化 TTL,避免同时过期。
  • 在缓存失效期间采用降级与限流保护后端。
  • 使用安全的分布式锁和单飞合并来避免并发重建。
  • 对写操作采用事件驱动的缓存失效或版本号控制来保证一致性。
  • 完善监控与故障演练。

总结

缓存可以大幅降低后端压力与提升响应,但如果策略不慎,会带来穿透、击穿、雪崩等风险。生产环境中要把参数校验、数据结构(布隆)、锁与单飞、TTL 随机化、本地多级缓存、消息驱动的失效机制、监控与演练等结合起来,制定针对不同数据(热点/冷数据/写热点)的差异化策略,才能既保证性能又保证系统稳定性与数据一致性。


http://www.lryc.cn/news/617712.html

相关文章:

  • STM32蓝牙模块驱动开发
  • 第10节 大模型分布式推理典型场景实战与架构设计
  • 《算法导论》第 19 章 - 斐波那契堆
  • 【SpringBoot】持久层 sql 注入问题
  • 一周学会Matplotlib3 Python 数据可视化-绘制直方图(Histogram)
  • 银河麒麟V10配置KVM的Ubuntu虚机GPU直通实战
  • 梯度裁剪总结
  • 做调度作业提交过程简单介绍一下
  • Spring Cloud Gateway 路由与过滤器实战:转发请求并添加自定义请求头(最新版本)
  • 如何安装 Git (windows/mac/linux)
  • 【数据可视化-85】海底捞门店数据分析与可视化:Python + pyecharts打造炫酷暗黑主题大屏
  • Java数据库编程之【JDBC数据库例程】【ResultSet作为表格的数据源】【七】
  • NY185NY190美光固态闪存NY193NY195
  • cf--思维训练
  • 【C++语法】输出的设置 iomanip 与 std::ios 中的流操纵符
  • Dashboard.vue 组件分析
  • 基于 Axios 的 HTTP 请求封装文件解析
  • 【Redis的安装与配置】
  • ESP32将DHT11温湿度传感器采集的数据上传到XAMPP的MySQL数据库
  • loading效果实现原理
  • 【JAVA】使用系统音频设置播放音频
  • 在线代码比对工具
  • Selenium元素定位不到原因以及怎么办?
  • 机器学习 TF-IDF提取关键词,从原理到实践的文本特征提取利器​
  • Effective C++ 条款36: 绝不重新定义继承而来的非虚函数
  • Excel 连接阿里云 RDS MySQL
  • 开闭原则代码示例
  • Pytest项目_day11(fixture、conftest)
  • js数组reduce高阶应用
  • B 树与 B + 树解析与实现