每日八股——Redis(4)

缓存雪崩、击穿、穿透

缓存穿透------数据根本不存在

现象

  • 查询一个根本不存在的数据,比如id=-1
  • Redis查不到,请求穿透到数据库
  • 数据库也查不到,所以无法回写缓存

**后果:**每次请求都会打到数据库,如果有人利用大量不存在的ID发起攻击,数据库瞬间就挂掉

解决方法:

  • 缓存空值
    • 数据库查不到,我就在Redis里存一个null,并设置一个较短的过期时间
    • 缺点:浪费内存;如果这时数据真加进去了,会有一个短暂的不一致
  • 布隆过滤器
    • 请求到达Redis之前,先过一层布隆过滤器
    • 认为不存在时就一定不存在,直接拦截,不查Redis和DB
    • 认为存在时不一定存在,需要放过去查一下Redis
    • 优点:内存占用小,效率高

缓存击穿------单个热点Key过期

现象:

  • 一个超级热点Key扛着每秒几万的并发
  • 在他过期的那一瞬间,有几万个请求同时发现Redis没有数据

**后果:**几万个数据同时冲向数据库,数据库瞬间被压垮

解决方法

  • 互斥锁
    • 发现缓存没有,不是所有人都去查库,只有抢到锁的哪一个线程去查库、写缓存
    • 其他线程等待并重试读缓存
    • 优点:强一致性
    • 缺点:性能下降,查库的线程突然挂了会导致死锁
  • 逻辑过期:
    • 不设置Redis的TTL(永不过期)
    • 但在Value的内容里包含一个expire_time
    • 查询时发现逻辑时间过期了,后台开一个线程去更新数据,当前请求直接返回旧数据
    • 优点:高可用,性能好
    • 缺点:会有短暂的数据不一致

缓存雪崩------大量Key同时过期/Redis挂了

现象:

  • 场景A:你设置缓存时,给一大批数据(比如首页推荐商品),设置了相同的过期时间,时间到了后他们同时过期
  • 场景B:Redis宕机了

后果:海量请求全部涌向数据库,数据库立刻崩了

解决方法:

  • 随机TTL
    • 给过期时间加一个随机值(比如一小时+random(1-5分钟)
    • 让过期时间分散开,别凑到一起
  • 高可用架构
    • Redis哨兵或集群,保证Redis挂了能自动切换
  • 限流降级
    • 在网关层或者应用层做限流
    • Redis挂了,直接返回默认值或空值保护数据库

热Key问题

某个Key接收到的访问请求远高于其他Key,导致请求压力集中在单点上,引发Redis、网络或下游系统性能瓶颈,就叫做热Key问题。

如何识别热Key

业务感知

  • 针对业务的提前预判,知道哪些ID会是热Key

Redis4.0自带工具

  • redis-cli --hotkeys:Redis会扫描所有Key并统计访问次数
  • 缺点:要开启LFU淘汰策略才准确,且可能增加Redis负担

客户端收集(SDK)

  • 在连接Redis的客户端代码里添加计数器
  • 缺点:虽然准确,但是对客户端代码有入侵

抓包分析

  • 监听Redis端口流量,通过 tcpdump 抓取一段时间内的流量并上报,然后由一个外部的程序进行解析、聚合和计算。
  • 缺点:运维成本高,且热 key 节点的网络流量和系统负载已经比较高了,抓包可能会情况进一步恶化。

如何解决热Key

方案一:多级缓存(Local Cache)

**核心思想:**既然Redis单机扛不住,那就在Redis 前增加其他缓存层(如CDN、本地缓存),以分担 Redis 的访问压力,先在应用服务器的内存里读,没有再去查Redis。

做法:

  • 当发现某个Key是热Key时,把它缓存到Web服务器的本地内存中
  • 下次请求来时,先查内存:
    • 没有------>查Redis,回写服务器内存
    • 有------>直接返回

**优点:**性能强,理论上可以无限并发(取决于Web服务器数量)

**缺点:**数据一致性变差,如果Redis数据改了,各个Web服务器本地内存里可能还是旧数据(需要设置极短的过期时间)

方案二:热Key拆分

**核心思想:**既然一个Redis节点扛不住,那就把这个Key复制多分,分散到不同节点上去

做法:

  • 假设Key是hot_product
  • 我们在写入时会把它复制N份,改名为hot_product_1,hot_product_2...hot_product_N
  • 这N个key会被哈希算法分配到Redis集群不同分片上
  • 查询时,客户端随机生成一个后缀1---N,比如随机到3,就去查hot_product_3

**优点:**利用了整个集群的计算能力,不再有单点瓶颈

缺点:

  • 浪费内存:存了N份
  • 数据更新麻烦:更新时需要把这N个备份都更新一遍,容易出现短暂不一致

方案三:读写分离

做法:增加Redis的从节点Slave,让主节点负责写,多个从节点负责读。

缺点:成本高,且有主从同步延迟


大Key问题

大Key问题指的是单个Redis Key对应的value体积非常大,或包含的元素非常多(两个大:体积大,数量大),本质为处理时间过长,阻塞了后面的请求。

大Key的危害

阻塞主线程:

  • Redis时单线程处理命令的
  • 如果读取一个5MB的String或者删除一个100万元素的List,这个操作可能耗时几百毫秒
  • 这几百毫秒内,Redis无法处理其他请求,导致线上服务卡顿

网络阻塞:

  • 虽然Redis吞吐量高,但网卡宽带是有限的
  • 高并发下读取大Key,会瞬间打满网卡宽带,导致其他Key读不出来

内存问题:

  • 一次淘汰一个大Key,会让内存大跳水,命中率骤降

主从复制与持久化问题:

  • 复制大Key时时间就,同步慢,会造成不一致
  • AOF与RDB的恢复时间长

如何发现大Key

工具扫描redis-cli --bigkeys

  • 缺点:只能给出每种类型最大的那个 Key,不够全。

RDB 分析 (推荐) :使用 rdb-tools 等第三方工具离线分析 RDB 文件。

  • 优点:不影响线上服务,分析最彻底。

如何解决大Key问题

拆分

针对集合类(Hash/List/Set),如果元素太多,就把它拆成多个小 Key。

  • 场景 :一个 Hash 存了 100 万个用户对象 user_list
  • 做法 :利用 hash(field) % 1000,拆分成 1000 个小 Hash:user_list_0, user_list_1 ... user_list_999

压缩

针对 String 类型的大 JSON,可以通过压缩算法减小体积。

  • Go 生态 :不要只知道 gzip。在追求速度的场景下,Snappy (Google 开源) 或 LZ4 是更好的选择。
  • 做法
    • Go 结构体 -> JSON -> Snappy 压缩 -> Redis。
    • 读取时:Redis -> Snappy 解压 -> JSON -> Go 结构体。

安全删除

千万别直接用 DEL****删除大 Key!

  • 问题 :执行 DEL huge_key(比如一个百万级的 List),Redis 会在主线程 中一个个回收内存,直接导致 Redis 阻塞几秒钟。
  • 解决
    • Redis 4.0+ :使用 UNLINK 命令代替 DEL
      • UNLINK 只是把 Key 从元数据中解绑(逻辑删除),真正的内存回收交给后台线程 异步执行,不阻塞主线程
      • Redis 4.0 以下 :必须自己在 Go 代码里写脚本,分批次删除(比如用 SCAN 每次扫 100 个元素删掉,循环几万次删完)。
相关推荐
IT=>小脑虎2 小时前
AI时代下后端的出路在哪?
人工智能·后端·学习
杨了个杨89822 小时前
Redis主从复制部署
数据库·redis·缓存
YYHPLA2 小时前
【无标题】
java·spring boot·后端·缓存
Qwertyuiop20162 小时前
vs2022无法正常使用copilot的解决方案
经验分享
DBA小马哥2 小时前
金仓数据库替代MongoDB:如何高效存储复杂数据类型并实现平滑迁移
数据库·mongodb·dba
sunnyday04262 小时前
深入理解分布式锁:基于Redisson的多样化锁实现
spring boot·redis·分布式
EricLee2 小时前
2025 年终总结 - Agent 元年
前端·人工智能·后端
q***44152 小时前
C++跨平台开发挑战的技术文章大纲编译器与工具链差异
java·后端
JaguarJack2 小时前
PHP 8.5 升级生存指南:避免凌晨两点回滚的检查清单
后端·php·服务端