缓存雪崩、击穿、穿透
缓存穿透------数据根本不存在
现象:
- 查询一个根本不存在的数据,比如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 个元素删掉,循环几万次删完)。
- Redis 4.0+ :使用 UNLINK 命令代替