Redis常见问题解答

2 阅读7分钟

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 中存储一个 字段。
  • 流程
    1. 线程获取数据,检查该字段。
    2. 如果未过期,直接返回。
    3. 如果已过期:
      • 尝试获取锁
      • 拿到锁:开启异步线程去查数据库、更新缓存,然后释放锁。当前线程直接返回旧数据
      • 没拿到锁:直接返回旧数据。
  • 核心优势:所有请求永远不会被阻塞,虽然可能读到短暂的超时脏数据,但性能极高,适合高并发读场景。

方案三: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(异步删除)