Redis常见问题解答
前言
本文将深入剖析缓存三大经典故障(雪崩、穿透、击穿)以及两大生产痛点(热点 Key、大 Key),从场景分析到底层原理,全方位解析解决方案。
一、 缓存雪崩
1. 场景分析
现象:在某一时间段,缓存中大量的 Key 集中过期失效,或者 Redis 服务直接宕机。此时,原本应该命中缓存的请求,全部变成了请求数据库。
思考(结合业务架构): “如果我做了分库分表,是不是就不怕雪崩了?”
- 结论:风险依然存在,但取决于 Key 的分布。
- 分析:如果同时过期的 Key 属于不同的业务,或者虽然属于同一业务但分布在不同的分库分表节点上,那么压力会被分散,风险较低。
- 核心痛点:我们真正担心的是 “同库同表” 的海量 Key 同时失效。例如:某张核心大表的 100 万行数据缓存,在同一秒全部过期。这会瞬间打挂同一个数据库实例。
2. 解决方案
方案一:随机过期时间(预防)
- 策略:在设置过期时间时,在基础时间上叠加一个随机值(如
3600s + random(0, 600s))。 - 目的:让 Key 的过期时间分散开,避免同一秒集体失效。
- 适用:适用于对数据一致性要求不是秒级严苛的业务。
方案二:构建高可用架构(redis服务宕机兜底)
- 策略:搭建 Redis Sentinel(哨兵)或 Redis Cluster(集群)。
- 目的:即使 Redis 挂了,也能快速恢复,保证服务不中断。
方案三:限流与熔断(紧急止损)
- 策略:当检测到数据库压力过大时,触发熔断(如 Sentinel/Hystrix),直接拒绝请求或返回降级页面。如果配置有扩容能力的,可以触发自动扩容机器,避免数据库崩掉
二、 缓存穿透
1. 场景分析
现象:客户端请求的数据在缓存和数据库中都不存在。这样每次请求都会穿过缓存,直接查询数据库。 危害:如果有人恶意发起海量攻击(如请求 ID 为 -1 的商品),Redis 里没有,MySQL 里也没有,所有请求全打在 MySQL 上。
2. 解决方案
方案一:缓存空对象
- 策略:当查询数据库发现数据不存在时,向 Redis 存一个
null值,并设置较短的过期时间(如 5 分钟)。 - 副作用与应对:
- 内存膨胀:如果攻击者使用
大量不同的随机 ID,Redis 会存满空值。此时只能依赖限流。 - 数据一致性:如果后续数据库插入了该数据,Redis 里的
null值就成了脏数据。解决办法:在数据库写入成功后,主动删除 Redis 中对应的null缓存。
- 内存膨胀:如果攻击者使用
方案二:布隆过滤器
- 原理:
- 布隆过滤器是一个很长的二进制位数组和若干个 Hash 函数。
- 写入:将 ID 通过多个 Hash 函数算出位置,将数组对应位置设为 1。
- 查询:如果算出的位置有一个是 0,说明一定不存在;如果全是 1,说明可能存在。
- 特性:它极其节省空间,且有极小的误判率(可能把不存在的判为存在,但绝不会把存在的判为不存在)。
- 部署位置:通常使用 RedisBloom 模块集成在 Redis 中,或者是应用本地的 Guava 布隆过滤器。
- 局限性:
- 只支持精确匹配,不支持模糊查询。
- 不支持删除。如果商品下架,无法从布隆过滤器中移除(因为会影响其他 Key)。可以通过定时重建来解决。
- 流程:请求 -> 布隆过滤器(问:存在吗?) -> 不存在(直接拦截) -> 存在(查 Redis -> 查 DB)。
三、 缓存击穿
1. 场景分析
现象:某个极度热点 Key(如秒杀商品、热门新闻),在缓存中突然过期。在这个瞬间,海量并发请求同时访问这个 Key,全部穿透缓存,并发查询数据库。 区别:雪崩是大面积 Key 过期;击穿是某一个 Key 过期,但并发量极大。
2. 解决方案
方案一:互斥锁
- 策略:当缓存失效时,使用 Redis 的
SETNX获取分布式锁。 - 流程:只有拿到锁的线程去查数据库并回写缓存,
其他线程等待或返回旧数据【取决于策略】。
方案二:逻辑过期—— 高性能首选
- 策略:Redis 中不设置 TTL(永不过期),在 Value 中存储一个 字段。
- 流程:
- 线程获取数据,检查该字段。
- 如果未过期,直接返回。
- 如果已过期:
- 尝试获取锁。
- 拿到锁:开启异步线程去查数据库、更新缓存,然后释放锁。当前线程直接返回旧数据。
- 没拿到锁:直接返回旧数据。
- 核心优势:所有请求永远不会被阻塞,虽然可能读到短暂的超时脏数据,但性能极高,适合高并发读场景。
方案三:Binlog 异步更新
- 策略:不设置过期时间,完全依赖数据库变更的 Binlog 来驱动 Redis 更新。数据一致性好,适合更新频率低的场景。
四、 热点 Key 问题
1. 场景分析
现象:某明星官宣,海量请求瞬间集中在一个 Key 上。 原理误区: Redis 是单线程的,处理 1 万次请求,无论打在 1 个 Key 还是 1 万个 Key,总 QPS 似乎一样? 纠正: 在 Redis Cluster(集群)环境下,热点 Key 会导致流量倾斜。
- 日常 Key:流量均匀分布在不同节点。
- 热点 Key:流量全部打到同一个节点(如 Node1)。Node1 的 CPU 飙升至 100%,导致节点拒绝后续请求链接,对后续在该节点上其他正常 Key 也被堵死(连坐效应)。
- 读写分离无效:Redis Cluster 默认读写都在主节点,从节点无法分担读压力。
2. 解决方案
方案一:本地缓存—— 最有效
- 策略:在 JVM 本地(如 Caffeine、Guava Cache)缓存一份热点数据。
- 效果:请求还没出应用服务器就被拦截,根本不打 Redis。
- 代价:需要维护本地缓存的一致性(如广播更新)。
方案二:Key 拆分
- 策略:在 Key 后面加随机后缀(
hot_key_1,hot_key_2...)。 - 效果:将流量分散到 Redis 集群的不同节点上。
- 代价:写入时要多写几次,数据冗余。
方案三:监控与发现
- 如何知道哪个是热点 Key?
- 客户端 SDK 埋点统计。
- 代理层监控(如 Twemproxy)。
- Redis
hotkeys命令。
五、 大 Key 问题
1. 定义与危害
定义:
- String 类型:Value 大小超过 10KB。
- 集合类型:元素数量超过 5000 个(如一个 Set 存了 10 万粉丝 ID)。
危害(核心原理):
- 阻塞主线程:Redis 是单线程的。
- 读:读取 Big Key 时,Redis 需要序列化数据打包发给网卡。这个过程是纯 CPU 计算,如果数据大,主线程会被长时间占用,无法响应其他请求。
- 删:
DEL一个 Big Key,Redis 需要一次性释放所有内存对象,导致主线程卡顿。
- 网络拥塞:瞬间写出大流量,占满带宽。
2. 解决方案
方案一:拆分—— 预防
- 策略:将大 Key 拆成多个小 Key。
- 例如:
user_fans:1,user_fans:2...
- 例如:
- 原理分析:
- 拆分后总耗时变了吗?基本没变。
- 为什么解决了阻塞?拆分将“连续的长时间阻塞”打碎成“离散的短时间阻塞”。
- 核心价值:在处理拆分后的小 Key 间隙,Redis 主线程可以插队处理其他简单的
get请求,保证系统不卡顿。
方案二:安全删除
- 误区:直接
DEL-> 导致瞬间卡顿甚至宕机。 - 正确做法(Redis 4.0+):使用
UNLINK。UNLINK会在后台异步释放内存,主线程瞬间返回,完全无感。
- 老版本做法:使用
SCAN分批扫描 +SREM分批删除。
总结
| 问题 | 核心场景 | 关键解决方案 |
|---|---|---|
| 缓存雪崩 | 大量 Key 同时失效 | 随机过期时间、高可用架构、熔断降级 |
| 缓存穿透 | 缓存和 DB 都无数据 | 布隆过滤器、缓存空对象(需处理一致性) |
| 缓存击穿 | 单个热点 Key 过期 | 逻辑过期(高性能)、互斥锁、Binlog 更新 |
| 热点 Key | 流量倾斜打挂单节点 | 本地缓存、Key 拆分 |
| 大 Key | 阻塞主线程 | 拆分(避免长阻塞)、UNLINK(异步删除) |