本文是大数据系列第 49 篇,系统讲解 Redis 高并发场景下的五类经典问题及其解决方案。

完整图文版(含截图):CSDN 原文 | 掘金

缓存穿透(Cache Penetration)

问题描述

请求的数据在缓存和数据库中均不存在,导致每次请求都绕过缓存直接打到数据库,形成无效的 DB 查询压力。常见于恶意攻击(使用随机/无效的 ID 大量请求)。

解决方案

方案一:缓存空值

数据库查询结果为空时,将 null 或空标记值写入缓存,设置较短 TTL(30-60 秒),防止重复打库。

方案二:布隆过滤器(Bloom Filter)

在缓存层前部署布隆过滤器,将所有合法 key 提前写入。请求到来时先查布隆过滤器,确定不存在的直接拦截,不查缓存和数据库。

布隆过滤器原理:使用长度为 m 的 bit 数组和 k 个独立哈希函数。写入元素时将 k 个哈希位置置 1;查询时若任意位置为 0,则该元素一定不存在;若全为 1,则大概率存在(存在误判率)。

误判率公式:p ≈ (1 - e^(-kn/m))^k,可通过增大 m 或调整 k 降低误判率。

方案三:参数校验 + 限流

在业务层对请求参数合法性做强校验,配合接口限流和异常请求监控,提前拦截非法请求。

缓存雪崩(Cache Avalanche)

问题描述

大量 key 同时过期,或 Redis 集群故障,导致海量请求同时穿透到数据库,极端情况引发系统级联崩溃。

解决方案

方案一:过期时间打散

避免批量写入时设置相同的 TTL,在基础时间上叠加随机偏移:

int expireTime = baseTime + ThreadLocalRandom.current().nextInt(0, 300);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);

方案二:多级缓存

构建本地缓存(Caffeine/Guava)→ 分布式缓存(Redis)→ 数据库的三级防护,Redis 故障时本地缓存可兜底。

方案三:高可用架构

  • 部署 Redis Sentinel 或 Redis Cluster,避免单点故障
  • 业务层集成熔断降级(如 Sentinel、Hystrix),Redis 不可用时降级到数据库或返回兜底数据
  • 关键数据在低峰期预热加载

缓存击穿(Cache Breakdown)

问题描述

单个热点 key 过期的瞬间,大量并发请求同时 cache miss,全部打到数据库重建缓存,形成”惊群效应”。

解决方案

方案一:互斥锁(Mutex Lock)

缓存 miss 时,只允许一个线程获取分布式锁去查询数据库重建缓存,其余线程等待或自旋重试。保证数据库只被查询一次。

方案二:永不过期(逻辑过期)

热点 key 不设置物理过期时间,而是在 value 中存储逻辑过期时间。后台异步任务在逻辑过期前提前刷新缓存,用户始终从缓存中读取(可能短暂读到旧数据)。

方案三:提前续期

监控 key 的剩余 TTL,在过期前主动刷新,从根本上消除过期间隙。

数据一致性问题

延迟双删策略

1. 更新数据库前删除缓存
2. 更新数据库
3. 等待约 200ms~2s(等其他线程的读请求完成)
4. 再次删除缓存(清理可能被回写的旧数据)
5. 为缓存设置合理 TTL 作为最终兜底

更彻底的方案是监听数据库 Binlog(如 Canal),在数据变更事件触发时精准失效对应缓存,实现准实时的缓存一致性。

热 Key 问题(Hot Key)

问题描述

大量请求集中访问同一个 key,超出单个 Redis 节点的网络带宽或 CPU 处理能力,可能导致节点宕机并引发雪崩。

检测方法

  • 线下redis-cli --hotkeys(基于 LFU 统计,需开启相应配置)
  • 线上MONITOR 命令抓取请求流量(有性能损耗,谨慎使用)
  • 流式计算:接入 Flink/Spark 实时统计访问频次,将热 key 信息写入 ZooKeeper 通知应用层

解决方案

  1. 本地缓存兜底:将热 key 复制到应用进程内存(Caffeine),接受短暂数据不一致的代价
  2. 分片读取:将热 key 复制到多个 Redis 节点(key_1、key_2…key_N),读取时随机选择节点
  3. 限流 + 熔断:对异常高频访问的 key 进行请求限流,熔断保护后端

大 Key 问题(Big Key)

问题描述

单个 key 的 value 体积过大(String 超过 10KB,集合元素超过 5000),会导致:

  • 内存不均匀,影响集群数据迁移和 rebalance
  • 读写操作阻塞时间长,其他请求延迟上升
  • DEL 删除大 key 时直接阻塞主线程(同步操作)

检测方法

  • redis-cli --bigkeys:扫描整个 keyspace(数据量大时耗时较长)
  • RDB 文件分析工具(如 rdbtools、redis-rdb-tools):离线分析,不影响生产

解决方案

  1. 拆分大 key:将大 String 拆分为多个 key(如分片存储),将大 Hash/List/Set 按 hash 分桶
  2. 外部存储:超大 value(图片、文档、序列化对象)存 MongoDB 或 CDN,Redis 中只存引用 ID
  3. 惰性删除:使用 UNLINK 替代 DEL,异步后台删除大 key,避免阻塞主线程
# 安全删除大 key
UNLINK big_key_name

总结

问题核心成因首选方案
缓存穿透请求不存在的 key布隆过滤器
缓存雪崩大量 key 同时过期过期时间随机化 + 多级缓存
缓存击穿热点 key 到期瞬间并发互斥锁 / 永不过期
热 Key流量集中单节点本地缓存 + 分片读取
大 Key单 key 数据体积过大拆分 + UNLINK 删除