【Redis 毁灭计划】7 大高危操作打崩线上服务!从缓存雪崩到数据库宕机,90% 程序员都踩过的坑

文章标签

Redis 后端开发 Java 缓存 线上故障 面试

文章摘要

Redis 作为后端高并发架构的核心组件,用好了能扛住百万 QPS,用错了就是线上的「定时炸弹」。本文拆解了 7 个 Redis「毁灭级」高危操作,从底层原理、故障复现、线上危害到根治方案全流程讲解,结合可视化图解与实战代码,帮你避开 90% 的 Redis 线上坑,杜绝从删库到跑路的悲剧。


前言

你有没有遇到过这种情况:

  • 线上 Redis 突然阻塞,服务全量超时告警?
  • 数据库莫名被打满,排查后发现是缓存失效导致的雪崩?
  • 一行看似正常的 Redis 代码,上线后直接引发全线业务中断?

很多程序员对 Redis 的使用,只停留在set/get的基础 API,却忽略了其单线程模型、过期策略、内存淘汰机制等核心特性,不经意间的一行代码,就可能成为压垮系统的最后一根稻草。

本文就来揭秘 7 个堪称「Redis 毁灭计划」的高危操作,从根上讲透故障发生的原因,给出可落地的避坑方案,看完就能直接用到线上项目中。

先给大家上一张「Redis 高危操作总览图」,本文要讲的所有坑,都在这张图里了:

一、连接攻击:频繁建连 / 关连,耗尽 Redis 连接数

1.1 高危操作描述

放弃 Redis 连接池,每次业务请求都新建一个 TCP 连接,用完立即关闭,循环往复。

1.2 底层原理与故障危害

Redis 是基于 TCP 的客户端 - 服务端架构,每次新建连接都要经历 TCP 三次握手,关闭连接要经历四次挥手,本身就有巨大的性能开销。

  • 连接数耗尽 :Redis 默认maxclients最大连接数为 10000,频繁新建连接会快速占满连接池,导致新的合法请求被 Redis 直接拒绝;
  • CPU 开销剧增:服务端和 Redis 都要频繁处理连接的创建与销毁,会占用大量 CPU 资源,导致业务逻辑处理能力骤降;
  • TCP 资源浪费:大量 TIME_WAIT 状态的连接会占用服务器的 TCP 端口资源,最终引发网络层面的故障。

1.3 故障复现(反面代码示例)

java

运行

复制代码
// 反面示例:每次请求都新建Jedis连接,用完关闭
public String getValue(String key) {
    // 每次新建连接
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    String value = jedis.get(key);
    // 用完立即关闭
    jedis.close();
    return value;
}

高并发场景下,这段代码会快速耗尽 Redis 的连接数,引发服务不可用。

1.4 正确方案与避坑指南

  1. 强制使用连接池:无论是 Java 的 JedisPool、Lettuce,还是 Python 的 redis-py 连接池,都必须使用连接池复用连接;
  2. 合理配置连接池参数

java

运行

复制代码
// 正确示例:Jedis连接池配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 最大连接数
poolConfig.setMaxTotal(200);
// 最大空闲连接
poolConfig.setMaxIdle(50);
// 最小空闲连接
poolConfig.setMinIdle(10);
// 连接耗尽时等待超时
poolConfig.setMaxWaitMillis(3000);
// 初始化连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

// 业务代码复用连接
public String getValue(String key) {
    try (Jedis jedis = jedisPool.getResource()) {
        return jedis.get(key);
    }
}
  1. 监控连接数 :通过info clients命令监控 Redis 的当前连接数,设置阈值告警。

二、批量改单条:循环单条写入,拖垮 Redis 性能

2.1 高危操作描述

放弃 Redis 的MSETPipeline等批量操作 API,改用for 循环逐条执行 SET 命令写入大量数据。

2.2 底层原理与故障危害

Redis 的命令执行分为 3 步:「客户端发送命令→服务端执行命令→客户端接收结果」,每一次命令执行都有一次网络往返(RTT)。

  • 网络开销爆炸:循环单条写入 10 万条数据,就要经历 10 万次网络 RTT,哪怕单次 RTT 只有 1ms,10 万次也要 100 秒,效率极低;
  • Redis QPS 被无效占满:单线程的 Redis 把大量时间浪费在网络 IO 上,无法处理正常的业务请求,引发业务超时;
  • 带宽被打满:大量小包传输会占用服务器带宽,甚至引发网络拥塞。

2.3 故障复现(反面代码示例)

java

运行

复制代码
// 反面示例:for循环单条SET写入10万条数据
Jedis jedis = jedisPool.getResource();
long expireTime = (System.currentTimeMillis() / 1000) + 300;
for (int i = 0; i < 100000; i++) {
    // 循环单条写入,每次都有网络RTT
    jedis.set("product:" + i, data[i]);
    jedis.expireAt("product:" + i, expireTime);
}

这段代码在生产环境执行,会直接拉低 Redis 的整体性能,甚至引发业务请求超时。

2.4 正确方案与避坑指南

  1. 批量写入优先用 MSET :同批次的写入操作,用MSET一次性提交,仅需 1 次网络 RTT;
  2. 非原子批量操作用 Pipeline:对于需要设置过期时间、或者不同类型的命令,用 Pipeline 管道打包执行,减少网络往返;

java

运行

复制代码
// 正确示例:Pipeline批量写入
Jedis jedis = jedisPool.getResource();
Pipeline pipeline = jedis.pipelined();
long expireTime = (System.currentTimeMillis() / 1000) + 300;

for (int i = 0; i < 100000; i++) {
    pipeline.set("product:" + i, data[i]);
    pipeline.expireAt("product:" + i, expireTime);
    // 控制批次大小,避免Pipeline过大
    if (i % 1000 == 0) {
        pipeline.sync();
    }
}
pipeline.sync();
  1. 控制批次大小:Pipeline 单次打包的命令数建议控制在 1000 以内,避免单次请求过大引发网络阻塞。

三、大 Key 攻击:大 Key 写入 + 高峰期删除,直接阻塞 Redis

3.1 高危操作描述

写入超大体积的 Key (比如包含 500 万条数据的 List、100MB 的 String),并在业务高峰期执行DEL命令删除该 Key。

3.2 底层原理与故障危害

Redis 是单线程模型,所有命令的执行都在主线程中串行执行,任何一个耗时的命令,都会阻塞所有后续的业务请求。

  • 大 Key 删除阻塞主线程DEL命令删除大 Key 时,需要一次性回收大量内存,这个过程会持续占用主线程,耗时可达数百毫秒甚至数秒;
  • 服务假死 :主线程被阻塞期间,所有正常的get/set请求都无法执行,业务服务出现大量超时,甚至触发熔断;
  • 连锁故障:超时的请求会重试,进一步放大流量压力,最终引发服务雪崩。

3.3 故障复现(反面代码示例)

java

运行

复制代码
// 反面示例:写入500万元素的大Key,高峰期直接DEL
Jedis jedis = jedisPool.getResource();
// 写入大Key
for (int i = 0; i < 5000000; i++) {
    jedis.lpush("big_key:list", "value:" + i);
}

// 业务高峰期执行删除,直接阻塞Redis
jedis.del("big_key:list");

3.4 正确方案与避坑指南

  1. 杜绝大 Key 产生 :遵循 Redis 最佳实践,严格控制 Key 的大小:
    • String 类型的 Value 控制在 10KB 以内;
    • 集合类型(List/Hash/Set/ZSet)的元素数量控制在 10000 个以内;
    • 超大 Key 必须拆分,比如把大 Hash 拆分为多个小 Hash,大 List 拆分为多个小 List。
  2. 大 Key 删除用异步命令 :Redis 4.0 + 提供了UNLINK命令,会把 Key 的内存回收放到后台异步线程执行,不会阻塞主线程,替代DEL删除大 Key;

java

运行

复制代码
// 正确示例:用UNLINK异步删除大Key
jedis.unlink("big_key:list");
  1. 定期扫描大 Key :通过redis-cli --bigkeys命令定期扫描 Redis 中的大 Key,提前治理;
  2. 禁止高峰期操作大 Key:无论是大 Key 的查询、写入还是删除,都要避开业务高峰期。

四、KEYS * 全量扫描:单线程阻塞的重灾区

4.1 高危操作描述

在线上环境执行KEYS *命令,全量扫描 Redis 中的所有 Key。

4.2 底层原理与故障危害

KEYS命令的时间复杂度是O(N),N 是 Redis 中 Key 的总数量,它会遍历 Redis 整个 Key 字典,一次性返回所有匹配的 Key。

  • 主线程长时间阻塞 :如果 Redis 中有数百万个 Key,KEYS *命令会占用主线程数百毫秒甚至数秒,期间所有业务命令都无法执行;
  • 网络风暴KEYS *返回的大量结果会占用大量带宽,甚至把服务器带宽打满;
  • 业务全线超时:阻塞期间,所有依赖 Redis 的业务都会超时,引发服务不可用。

4.3 故障复现

bash

运行

复制代码
# 线上环境执行,直接阻塞Redis
KEYS *

4.4 正确方案与避坑指南

  1. 线上绝对禁止使用 KEYS 命令 :通过 Redis 配置文件的rename-command,把KEYS命令重命名为一个随机字符串,禁用业务端直接调用;

conf

复制代码
# redis.conf 禁用KEYS命令
rename-command KEYS random_str_xxxxxx
  1. 增量扫描用 SCAN 命令 :如果需要遍历 Key,使用SCAN命令,它会把全量扫描拆分为多次小的扫描,每次时间复杂度 O (1),不会阻塞主线程;

java

运行

复制代码
// 正确示例:SCAN增量扫描Key
ScanParams scanParams = new ScanParams().match("product:*").count(1000);
String cursor = "0";
do {
    ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
    List<String> keys = scanResult.getResult();
    // 处理Key
    cursor = scanResult.getCursor();
} while (!cursor.equals("0"));
  1. Key 查询走特定前缀:业务设计时,给 Key 加上规范的前缀,避免全库扫描的需求。

五、过期 Key 攻击:大量 Key 同时过期,主线程忙到无法响应

这是线上最容易踩的坑之一,很多程序员为了方便,给大量 Key 设置了完全相同的过期时间,最终引发 Redis 阻塞、缓存雪崩等一系列故障。

先给大家看两张图,分别是热点 Key 集中过期、海量 Key 集中过期的场景:

📌 【热点 Key 过期场景图】

📌 【此处插入图片 2:海量 Key 集中过期场景图】

5.1 高危操作描述

  1. 给首页、活动页等热点 Key 设置固定的过期时间(比如 300 秒),到期后大量请求同时击穿缓存;
  2. 给 10 万 + 商品 Key 设置完全相同的过期时间,到期后集中失效。

5.2 底层原理与故障危害

Redis 的过期 Key 删除采用「惰性删除 + 定期删除」双策略:

  • 惰性删除:Key 被访问时,才检查是否过期,过期则删除;
  • 定期删除:Redis 默认每秒执行 10 次过期检查,每次随机抽取 20 个设置了过期时间的 Key,删除其中过期的;如果过期 Key 占比超过 25%,就重复这个过程,直到占比低于 25% 或超时。

故障危害:

  • 主线程阻塞:大量 Key 同时过期,会触发定期删除的循环执行,长时间占用主线程,导致业务命令排队阻塞;
  • 缓存雪崩:大量 Key 同时失效,所有请求都会直接打到 MySQL 数据库,瞬间把数据库打满,引发数据库宕机;
  • 热点 Key 击穿:首页等热点 Key 过期,瞬间数十万请求绕过缓存直击数据库,直接压垮数据库。

5.3 故障复现(反面代码示例)

java

运行

复制代码
// 反面示例1:给10万Key设置完全相同的过期时间
long expireTime = (System.currentTimeMillis() / 1000) + 300;
for (int i = 0; i < 100000; i++) {
    jedis.set("product:" + i, data[i]);
    // 所有Key都在300秒后同时过期
    jedis.expireAt("product:" + i, expireTime);
}

// 反面示例2:热点Key设置固定过期时间
jedis.set("homepage", homepageData);
// 首页热点Key固定5分钟过期
jedis.expire("homepage", 300);

5.4 正确方案与避坑指南

  1. 过期时间加随机偏移量,打散过期时间:给相同过期时间的 Key,加上一个随机的时间偏移,避免集中过期;

java

运行

复制代码
// 正确示例:过期时间打散
long baseExpireTime = 300;
// 基础300秒,加上0-60秒的随机偏移,避免集中过期
long randomOffset = new Random().nextInt(60);
for (int i = 0; i < 100000; i++) {
    jedis.set("product:" + i, data[i]);
    jedis.expire("product:" + i, baseExpireTime + randomOffset);
}
  1. 热点 Key 永不过期 + 后台续期:首页、活动页等超高并发的热点 Key,不设置过期时间,由后台定时任务提前更新缓存,避免过期;
  2. 过期 Key 监控 :通过info stats命令监控expired_keys指标,观察过期 Key 的删除速率,提前预警集中过期风险。

六、缓存失效三件套:穿透、击穿、雪崩,直接压垮数据库

Redis 作为缓存的核心价值,就是挡在数据库前面,承接高并发请求,减少数据库压力。而缓存穿透、击穿、雪崩这三个问题,会让 Redis 完全失去防护作用,所有请求直击数据库,直接把数据库打垮。

6.1 缓存穿透:查询不存在的数据,绕过缓存直连数据库

问题描述

用户大量查询数据库和缓存中都不存在的数据,比如 id 为 - 1、或者不存在的商品 id,导致每次请求都绕过 Redis,直接查询数据库。

危害

数据库被大量无效请求打满,无法处理正常的业务请求,最终宕机。

解决方案
  1. 空值缓存:对于查询结果为空的 Key,也在缓存中设置一个短过期时间(比如 60 秒)的空值,避免重复查询数据库;
  2. 布隆过滤器:把所有合法的 id 提前写入布隆过滤器,请求到来时先经过布隆过滤器过滤,不存在的 id 直接拒绝,无需访问缓存和数据库;
  3. 参数校验:接口层增加参数合法性校验,过滤掉明显不合法的请求(比如 id 为负数、格式错误)。

6.2 缓存击穿:热点 Key 过期,大量请求直击数据库

问题描述

某个超高并发的热点 Key(比如秒杀活动商品)突然过期,瞬间数十万请求同时绕过缓存,全部打到数据库,直接把数据库压垮。

解决方案
  1. 热点 Key 永不过期:超高并发的热点 Key 不设置过期时间,由后台定时任务更新缓存;
  2. 互斥锁:热点 Key 过期时,只允许一个线程去查询数据库并更新缓存,其他线程等待缓存更新完成后再重试,避免大量请求同时访问数据库;
  3. 提前预热:秒杀、大促等活动开始前,提前把热点数据写入缓存,设置足够长的过期时间。

6.3 缓存雪崩:大量 Key 同时失效 / Redis 宕机,全线请求压垮数据库

问题描述

两种场景会引发缓存雪崩:

  1. 大量 Key 同时过期,缓存集体失效;
  2. Redis 集群宕机,缓存完全不可用。所有请求全部打到数据库,数据库瞬间被打满,引发全线业务中断。
解决方案
  1. 过期时间打散:如前文所述,给 Key 的过期时间加上随机偏移,避免集中过期;
  2. Redis 高可用集群:搭建 Redis 主从 + 哨兵集群,或者 Redis Cluster 集群,避免单点故障;
  3. 服务熔断与降级:当数据库压力过大时,触发熔断,非核心业务直接返回降级数据,保护核心业务和数据库;
  4. 多级缓存:本地缓存(Caffeine/Guava Cache)+ Redis 分布式缓存结合,Redis 宕机时,本地缓存也能承接一部分请求,避免所有请求直击数据库。

七、内存攻击:不设过期 + 禁止淘汰,内存耗尽业务瘫痪

7.1 高危操作描述

  1. 给写入的 Key 都不设置过期时间,永久占用 Redis 内存;
  2. 把 Redis 的内存淘汰策略设置为noeviction(不淘汰)。

7.2 底层原理与故障危害

Redis 的内存上限由maxmemory参数配置,当内存占用达到maxmemory时,会根据配置的淘汰策略处理新的写入请求。而noeviction策略是:当内存满了之后,直接拒绝所有新的写入请求,只响应读请求

故障危害:

  • 业务写入全面报错 :不设过期时间的 Key 会持续占用内存,直到占满maxmemory,之后所有的缓存写入操作都会失败,业务数据无法缓存;
  • 缓存完全失效:新的数据无法写入缓存,所有请求都会直接打到数据库,引发数据库压力骤增,最终宕机;
  • 业务全线中断:写入失败会引发大量业务异常,最终导致全线业务不可用。

7.3 故障复现

conf

复制代码
# redis.conf 配置禁止内存淘汰
maxmemory-policy noeviction

java

运行

复制代码
// 持续写入无过期时间的Key,最终占满内存
while (true) {
    jedis.set("data:" + UUID.randomUUID(), randomData);
}

7.4 正确方案与避坑指南

  1. 合理设置过期时间:除了配置类、字典类等永久有效的 Key,所有业务缓存 Key 都必须设置合理的过期时间;
  2. 选择合适的内存淘汰策略 :线上环境优先使用allkeys-lru(优先淘汰最近最少使用的 Key),Redis 4.0 + 推荐使用allkeys-lfu(优先淘汰使用频率最低的 Key),绝对禁止核心业务使用noeviction策略;

conf

复制代码
# 推荐的内存淘汰策略
maxmemory-policy allkeys-lfu
  1. 内存使用率监控:监控 Redis 的内存使用率,设置阈值告警(比如内存使用率超过 80% 就告警),提前治理;
  2. 定期清理无效 Key:定期扫描并清理长期不访问的无效 Key,释放内存空间。

八、线上 Redis 高危操作红线 & 监控体系

8.1 线上 Redis 绝对禁止的操作红线

  1. 禁止线上环境使用KEYSFLUSHDBFLUSHALL等高危命令,必须通过 rename-command 禁用;
  2. 禁止业务高峰期执行大 Key 的DELHGETALLSMEMBERS等耗时命令;
  3. 禁止使用循环单条操作替代批量操作,禁止无连接池频繁新建 / 关闭连接;
  4. 禁止给大量 Key 设置完全相同的过期时间,禁止热点 Key 无续期方案设置短过期时间;
  5. 禁止核心业务使用noeviction内存淘汰策略,禁止大量 Key 不设过期时间。

8.2 必做的 Redis 监控体系

表格

监控指标 核心作用 告警阈值
内存使用率 提前预警内存耗尽风险 超过 80% 告警
当前连接数 监控连接数耗尽风险 超过 maxclients 的 80% 告警
慢查询日志 发现耗时命令,提前治理阻塞风险 超过 10ms 的命令记录告警
过期 Key 速率 预警集中过期风险 突增 100% 以上告警
主线程 CPU 使用率 监控 Redis 阻塞风险 持续超过 80% 告警
大 Key 数量 提前治理大 Key 发现超过阈值的大 Key 告警

结尾

Redis 是一把双刃剑,它能帮我们轻松应对高并发场景,也能因为一行错误的代码引发全线业务故障。

本文拆解的 7 大高危操作,都是线上环境踩过无数坑总结出来的血泪教训,希望大家看完之后,能避开这些坑,让 Redis 真正成为系统的性能加速器,而不是定时炸弹。

如果本文对你有帮助,欢迎点赞👍 + 收藏⭐ + 评论💬 + 关注,后续会分享更多 Redis 实战干货与线上故障排查技巧!

相关推荐
Xpower 171 小时前
OpenClaw Token 优化的技术方案与实践:OpenSpace 自进化 Skill 引擎
java·开发语言·人工智能
南宫萧幕1 小时前
基于最优控制理论的 HEV 能量管理:从物理建模到 VMD-PPO 强化学习环境构建
开发语言·人工智能·matlab·simulink·控制
froginwe111 小时前
Memcached get 命令详解
开发语言
李日灐1 小时前
<4>Linux 权限:从 Shell 核心原理 到 权限体系的底层逻辑 详解
linux·运维·服务器·开发语言·后端·面试·权限
renhongxia12 小时前
计算机视觉实战:图像去噪模型训练与应用
开发语言·人工智能·机器学习·计算机视觉·prompt
HookJames2 小时前
Turnkey PCBA - Hero
前端·php
寒秋花开曾相惜2 小时前
(学习笔记)4.1 Y86-64指令集体系结构(4.1.6 一些Y86-64指令 )
linux·运维·服务器·开发语言·笔记·学习·安全
运维小斌2 小时前
麒麟v10arm使用dnsmasq部署本地DNS服务器
linux·运维·服务器·网络