大厂 难题: redis 突然变慢,如何定位? 如何止血 ? 如何 根治?
本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面
在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
- redis 突然变慢,如何定位? 如何止血 ? 如何 根治?
最近有小伙伴在面试 阿里,又遇到了相关的面试题。
小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
Redis 变慢 带来 致命 影响
Redis 是 高性能核心组件, 单个实例的 QPS 可达 10W 左右。
当 使用 Redis 时,若出现 变慢, 会 带来 致命 影响。
比如下面场景:
- 在 Redis 上执行相同命令,为何有时响应极快,有时却很慢?
- 为什么 Redis 执行 SET、DEL 这类命令也会耗时很久?
- 为什么我的 Redis 突然变慢一阵,之后又恢复正常了?
- 为什么我的 Redis 稳定运行许久,突然从某个时间点开始变慢了?
- ...
倘若不了解 Redis 内部的实现原理,那么在排查这类延迟问题时就会毫无头绪。
这篇文章提供一个全面的问题排查思路,并且针对这些导致变慢的场景,还会给出高效的解决方案。
‘紧急止血+架构治根’ 双重思路,整合成的五步根治方案
通过 「精准定位→快速止血→降级保命→性能压榨→架构根治」 五步闭环,实现Redis性能问题的标本兼治。
第1步:定位原因(精准诊断)
核心工具
问题类型 | 诊断命令 |
---|---|
高复杂度命令 | redis-cli slowlog get + redis-cli config get slowlog-log-slower-than |
BigKey扫描 | redis-cli --bigkeys + redis-cli memory usage <key> |
Key集中过期 | `redis-cli info stats |
持久化阻塞 | `redis-cli info persistence |
内存碎片 | `redis-cli info memory |
SWAP使用 | `redis-cli info memory |
CPU绑定问题 | `redis-cli info server |
第2步:紧急止血包(5分钟生效)
场景 | 操作 |
---|---|
大Key问题 | redis-cli config set maxmemory-policy volatile-ttl (优先淘汰过期Key) |
慢查询堆积 | ① 禁用高危命令:rename-command KEYS "" ② 调整阈值:config set slowlog-log-slower-than 10000 |
集中Key过期卡顿 | config set active-expire-effort 1 (降低过期扫描强度) |
AOF刷盘阻塞 | config set appendfsync no (临时关闭AOF刷盘) |
内存大页拖累 | echo never > /sys/kernel/mm/transparent_hugepage/enabled |
Swap占用过高 | swapoff -a + 调整/proc/sys/vm/swappiness=0 |
第3步:弃卒保帅(降级方案)
具体参见后面详情
第4步:性能爆破(扛高并发)
具体参见后面详情
第5步:架构治根(长效方案)
具体参见后面详情
第一步、如何定位Redis变慢问题
(1)链路追踪定位问题
如果 发现 业务服务 API 响应延迟变长,首先需要先排查服务内部,究竟是哪个环节拖慢了整个服务。
比较高效的做法: 在服务内部集成链路追踪,也就是在服务访问外部依赖的出入口,记录下每次请求外部依赖的响应延时。
首先, 还是应该把其它因素都排除完了,再把焦点关注在业务服务到 Redis
这条链路上。
如以下的火焰图, 就可以很肯定的说问题出现在 Redis 上了:
(2)基准性能测试
首先要确定是不是Redis 本身问题。
需要对 Redis 进行基准性能测试,了解 Redis 在生产环境服务器上的基准性能。
只有了解了 Redis 在生产环境服务器上的基准性能,才能进一步评估,当其延迟达到什么程度时,才认为 Redis 确实变慢了。
为了避免业务服务器到 Redis 服务器之间的网络延迟,需要直接在 Redis 服务器上测试实例的响应延迟情况。
执行以下命令,就可以测试出这个实例 60 秒内的最大响应延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.
1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.
从输出结果可以看到,这 60 秒内的最大响应延迟为 72 微秒(0.072毫秒)。
还可以使用以下命令,查看一段时间内 Redis 的最小、最大、平均访问延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range
...
以上输出结果是,每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。
了解了基准性能测试方法,那么就可以按照以下几步,来判断 Redis 是否真的变慢了:
(1) 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
(2) 找到认为可能变慢的 Redis 实例,测试这个实例的基准性能
(3) 如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了
确认是 Redis 变慢了,那如何排查是哪里发生了问题呢?
(3)慢日志分析工具
Redis 的慢查询日志 (Slow Log) 是一个内置的、非常重要的诊断工具,专门用于记录那些执行时间超过预设阈值的 Redis 命令。
它帮助识别和分析哪些操作可能导致了 Redis 服务器的性能瓶颈或延迟。
1)慢日志的核心作用
(1) 捕获性能问题:记录执行缓慢的命令(如复杂查询、大键操作)。
(2) 定位瓶颈:分析耗时操作的原因(如KEYS *
、大范围HGETALL
)。
(3) 监控优化:定期检查慢日志,优化数据结构和算法。
2)关键配置参数
通过 redis.conf
或运行时 CONFIG SET
调整:
参数 | 含义 | 默认值 | 建议 |
---|---|---|---|
slowlog-log-slower-than |
执行时间阈值(微秒) | 10000 μs (10ms) | 根据业务调整(如生产环境设为 5 ms) |
slowlog-max-len |
慢日志最大条目数 | 128 | 适当调大(如 1000),避免日志丢失 |
3)动态配置示例:
CONFIG SET slowlog-log-slower-than 5000 # 阈值设为 5ms
CONFIG SET slowlog-max-len 1000 # 最多存 1000 条
4)存储方式:
- 慢查询日志存储在 Redis 内存中。
- 它是一个固定长度 (FIFO - First In First Out) 的队列。
- 最大长度由配置参数
slowlog-max-len
设定。当队列满了之后,添加的新记录会导致最老的记录被丢弃。 - 默认长度通常为 128。需要根据需求调整这个大小(通常在
redis.conf
文件或运行时用CONFIG SET
命令调整)。
5)操作慢日志的命令
命令 | 作用 | 示例 |
---|---|---|
SLOWLOG GET [n] |
获取最近 n 条记录(默认全部) |
SLOWLOG GET 5 |
SLOWLOG LEN |
当前慢日志条目数 | SLOWLOG LEN |
SLOWLOG RESET |
清空慢日志 | SLOWLOG RESET |
输出示例:
1) (integer) 14 # ID
2) (integer) 1700000000 # 时间戳
3) (integer) 15000 # 耗时 15ms
4) 1) "HGETALL" # 命令
2) "user:1000:profile" # 参数(大键)
5) "127.0.0.1:37261" # 客户端地址
6) "cart-service" # 客户端名称
每条记录包含 6 个字段(Redis ≥4.0):
(1) 唯一ID:自增序号,日志被清理后重置。
(2) 时间戳:命令执行完成的 Unix 时间戳(秒)。
(3) 耗时:命令执行时长(微秒,注意单位)。
(4) 命令与参数:记录执行的命令和参数(敏感参数可能被截断)。
(5) 客户端地址:IP:端口
(如 127.0.0.1:58376
)。
(6) 客户端名称:通过 CLIENT SETNAME
设置的名称(可选)。
6)生产环境实践建议
-
高并发场景:建议
1-5 ms
(1000-5000 微秒)。 -
避免设为
0
(记录所有命令)或负数(不记录),除非调试。 -
慢日志可能记录键名或参数,确保访问权限控制。
-
用
SLOWLOG GET
导出日志,结合监控工具(如 Prometheus)分析。 -
增大
slowlog-max-len
会占用更多内存(但每条日志体积很小)。
7)注意事项
-
仅包括命令执行时间(不含网络传输、排队时间)。
-
排队时间可通过
redis-cli --latency
独立监控。 -
慢日志存储在内存中(非磁盘),重启后丢失。
-
如需持久化,需定期导出到文件。
Redis 慢日志是性能调优的关键工具。通过合理配置阈值、定期分析日志内容,并结合客户端信息定位问题源,可显著提升 Redis 性能。
对于关键服务,建议将慢日志监控集成到运维系统中,实现自动化告警。
第二步:紧急止血包(5分钟生效)
(1)大Key问题 (Bigkey问题) 的识别和解决
如果慢日志里没出现什么高复杂度命令,反而是SET、DEL这类简单命令频繁耗时,那就得留意实例中是不是写入了bigkey。
Redis写入数据时要为新数据分配内存,删除数据时要释放对应内存。要是一个key的value特别大,不管是分配内存还是释放内存,都会很耗时——这种key,我们就叫它bigkey。这时候得检查业务代码,评估写入的key值大小,尽量别让单个key存太大的数据。
Bigkey指单个Key存储的数据量过大(如String值达10MB,List含数万元素)。这类Key会导致Redis在分配/释放内存时耗时激增,表现为SET
/DEL
等简单命令出现在慢日志中,引发性能瓶颈。
(1.1 Bigkey的影响
- 内存风险:Redis内存持续增长,可能触发OOM(内存溢出),或者达到maxmemory限制后,导致写入阻塞、重要key被淘汰;
- 集群失衡:bigkey会导致访问集中在某个节点,让该节点先达到性能瓶颈,进而拖慢整个集群。而且Redis集群数据迁移有最小粒度,bigkey占用的内存没法均衡到其他节点;
- 带宽抢占:读取bigkey的请求会占用大量带宽,导致其他请求因资源不足而延迟;
- 主从同步中断:删除bigkey时,主库释放内存可能长时间阻塞,引发主从同步中断甚至主从切换。
(1.2 如何扫描bigkey
要是已经写入了bigkey,怎么找出它们在实例中的分布呢?
Redis提供了专门的扫描命令,执行后会按数据类型展示bigkey的情况:
# 扫描指定实例的bigkey,-i参数控制扫描间隔(单位:秒),降低对线上服务的影响
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
...
-------- summary -------
Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)
Biggest string found 'key:291880' has 10 bytes
Biggest list found 'mylist:004' has 40 items
Biggest set found 'myset:2386' has 38 members
Biggest hash found 'myhash:3574' has 37 fields
Biggest zset found 'myzset:2704' has 42 members
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
从结果能清楚看到:每种数据类型中最大的key(按内存或元素数)、各类key的占比和平均大小。
这个命令的原理其实很简单:内部执行SCAN遍历所有key,再根据类型调用STRLEN(字符串长度)、LLEN(列表元素数)、HLEN(哈希字段数)、SCARD(集合元素数)、ZCARD(有序集合元素数)来获取信息。
不过执行时要注意两点:
(1) 线上扫描会临时增加Redis的QPS,用-i
参数控制间隔(比如-i 0.01
表示每次扫描后休息0.01秒),减少对业务的影响;
(2) 容器类型(List、Hash等)的扫描结果只体现元素数量,元素多不代表内存一定大,需要结合业务进一步评估实际内存占用。
(1.3 优化方案
对付bigkey,核心思路是“预防为主,处理为辅”,具体可以这么做:
1)避免写入bigkey:
上游业务要杜绝在不合适的场景写入bigkey(比如用String存大型二进制文件)。
如果业务必须存大量数据,可采用“大key拆分”:
- String类型拆成多个key-value;
- Hash、List等容器类型拆成多个小容器。
2)定期清理无效数据:
对于Hash类型,用HSCAN遍历字段,结合HDEL删除无效数据,避免字段持续累积变成bigkey。
3)安全删除bigkey:
- Redis 4.0及以上版本,用
UNLINK
替代DEL
。UNLINK
会把内存释放操作交给后台线程,避免阻塞主线程; - Redis 6.0及以上版本,开启lazy-free机制(
config set lazyfree-lazy-user-del yes
),让DEL命令的内存释放也在后台执行。
4)特殊场景处理:
消息队列、生产消费场景的List、Set等,要设置过期时间或定期清理任务,同时配置监控,应对突发流量导致的消费积压(比如下游服务故障时,数据持续堆积成bigkey)。
即便有这些解决方案,也还是要尽量避免实例中出现bigkey。因为bigkey的“副作用”很隐蔽:分片集群中迁移困难、资源倾斜、数据过期/淘汰时耗时增加、透明大页场景下性能恶化等,都会受它影响。
(2)慢查询堆积 的识别和解决
通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时。
如果的应用程序执行的 Redis 命令有以下特点,那么有可能会导致操作延迟变大:
(1) 经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令
(2) 使用 O(N) 复杂度的命令,但 N 的值非常大
第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源。
第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。
列举一些常见的复杂度高的命令如下:
命令类型 | 危险命令 | 时间复杂度 | 风险场景 | 延迟影响 | 安全替代方案 |
---|---|---|---|---|---|
全局扫描 | KEYS * |
O(N) | 百万级Key库 | 完全阻塞实例 | SCAN 迭代遍历 |
FLUSHALL |
O(N) | 生产环境误操作 | 服务中断风险 | 禁用/权限控制 | |
哈希操作 | HGETALL |
O(N) | 大Hash(>5k字段) | 网络传输阻塞 | HSCAN 分批获取 |
HVALS |
O(N) | 大Hash取值 | 内存分配延迟 | HMGET 指定字段 |
|
集合操作 | SMEMBERS |
O(N) | 大Set(>10k元素) | 客户端卡顿 | SSCAN 分页获取 |
SINTER |
O(N*M) | 多大型集合交集 | CPU瞬时100% | 客户端分步计算 | |
有序集合 | ZRANGE 0 -1 |
O(logN+M) | 全量获取大Zset | 内存溢出风险 | ZRANGE 0 99 分页 |
ZUNIONSTORE |
O(N)+O(MlogM) | 大集合聚合 | 持久化阻塞 | 预计算+缓存 | |
列表操作 | LRANGE 0 -1 |
O(N) | 长List(>10k) | 主线程阻塞 | LRANGE 0 99 分段 |
LTRIM |
O(N) | 大量元素删除 | 内存释放延迟 | 分批次删除 | |
键操作 | DEL big_key |
O(N) | 删除复合大Key | 秒级阻塞 | UNLINK 异步删除 |
脚本 | 复杂Lua脚本 | 取决于逻辑 | 长时运行 | 命令队列堆积 | 拆分脚本+超时控制 |
除此之外,我们都知道,Redis 是单线程处理客户端请求的,如果经常使用以上命令,那么当 Redis 处理客户端请求时,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长。
解决方案:
(1) 尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
(2) 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回
(3) 生产环境禁用高危复杂命令
血泪教训:某电商平台因误用
KEYS *
导致集群雪崩,直接损失千万级订单。生产环境必须禁用高危命令!
(3)key集中过期 的识别和解决
如果在使用Redis时发现一个规律:
平时操作延迟很稳定,但总会在固定时间点(比如整点、每小时)突然出现一波延迟,过后又恢复正常——这种情况很可能是大量key集中过期导致的。
(3.1 Redis的Key过期策略
要弄明白集中过期为何会引发延迟,得先了解Redis是如何处理过期key的。
Redis采用“被动过期+主动过期”两种策略配合,确保过期数据及时清理。
1)被动过期:
只有当客户端访问某个key时,Redis才会检查它是否过期。如果已过期,就删除该key并返回空结果。
特点:不主动消耗资源,但可能导致过期key长期驻留内存(比如从未被访问的过期key)。
2)主动过期:
Redis内部有个定时任务,默认每100毫秒(每秒10次)执行一次,流程如下:
(1) 从全局过期哈希表中随机选出20个key;
(2) 删除其中已过期的key;
(3) 如果过期key占比超过25%,重复步骤1-2,直到占比≤25%或执行耗时超25毫秒,才停止循环。
关键注意点:
这个主动过期的定时任务,是在Redis的主线程中执行的。可能阻塞客户端请求
(3.2 集中过期为何会导致延迟?
当大量key在同一时间点过期时,主动过期机制会触发“连锁反应”:
(1) 主动过期任务在主线程执行,需要连续删除大量过期key(尤其是占比超过25%时,会循环清理);
(2) 清理过程中,主线程被占用,无法处理客户端的新请求,导致请求排队等待,延迟增加;
(3) 若过期的是bigkey,删除时释放内存耗时更长,阻塞更严重;
(4) 更隐蔽的是:主动过期的清理耗时不会记录在慢日志中(慢日志只记录命令本身的执行时间,而清理是在命令执行前完成的)。
这就是为什么有时慢日志里看不到耗时命令,但应用却能明显感知到延迟——时间都花在清理过期key上了。
(3.3 优化方案
如果业务确实需要批量设置过期时间,可通过以下两种方式优化,避免主线程阻塞:
1)打散过期时间
在固定过期时间的基础上,增加随机偏移量,让key的过期时间分散在一个时间段内,降低集中清理的压力。
伪代码示例:
# 原逻辑:所有key都在expire_time(如整点)过期
# redis.expireat(key, expire_time)
# 优化后:在expire_time基础上,加0-300秒(5分钟)的随机时间
# 使过期时间分散,避免集中清理
redis.expireat(key, expire_time + random.randint(0, 300)) # random(300)表示5分钟内的随机秒数
效果:原本集中在同一秒过期的10万个key,会分散到5分钟内,每次主动过期任务只需清理少量key,避免主线程阻塞。
2)开启lazy-free机制(Redis 4.0+)
Redis 4.0以上版本支持“惰性释放”,可将过期key的内存释放操作交给后台线程执行,避免主线程阻塞。
配置方式:
# 在redis.conf中开启,或通过命令临时生效
config set lazyfree-lazy-expire yes # 释放过期key的内存时,使用后台线程
原理:主动过期删除key时,主线程只需标记key为过期,实际内存释放由后台线程异步完成,不阻塞客户端请求。
3)建立监控体系
通过Redis的运行指标,可及时发现集中过期导致的异常:
1)执行INFO
命令,查看expired_keys
指标:
redis-cli info | grep expired_keys
# 输出示例:expired_keys:12589 (累计删除的过期key数量)
2)监控该指标的短期突增:
- 正常情况下,expired_keys增长平缓;
- 若某一时刻(如整点)突然大幅增长(比如1分钟内增长10万+),且与应用延迟时间吻合,基本可确认是集中过期导致。
3)结合业务报警:将expired_keys的突增阈值(如1分钟增长5万)配置为报警项,及时排查代码中的过期设置逻辑。
(4)持久化 fork耗时问题
实际使用中,为了保证Redis数据安全,我们常会开启后台定时RDB快照和AOF重写功能。
但可能没注意到:这两个操作都依赖一个关键步骤——主进程fork出子进程,而fork操作本身可能成为性能瓶颈,导致Redis短暂阻塞。
(4.1 哪些场景会触发fork?
fork在Redis中主要用于“后台异步操作”,常见场景有三个:
(1) 后台RDB持久化:按配置定时(如save 3600 1
)生成全量内存快照时,主进程fork子进程写入RDB文件;
(2) AOF重写:当AOF文件增长到一定大小(由auto-aof-rewrite-percentage
和auto-aof-rewrite-min-size
控制),主进程fork子进程重写AOF文件;
(3) 主从首次同步:主从节点刚建立连接时,主进程会fork子进程生成RDB,发给从节点做全量同步。
(4.2 fork为什么会耗时?
在类Unix系统中,fork的核心是“复制进程资源”,但最耗时的是复制内存页表(虚拟地址到物理地址的映射表)。
举个例子:在Linux/AMD64系统中,内存按4KB分页管理,每个页需要一个页表项(约8字节)。如果Redis实例占用24GB内存,对应的页表大小就是:
24GB ÷ 4KB × 8字节 = 48MB
fork时,主进程需要把这48MB的页表完整拷贝给子进程——这个过程会占用大量CPU,尤其是在虚拟机(如Xen)上,大内存块的分配和初始化成本更高,耗时可能是物理机的10-100倍。
更关键的是:fork操作在Redis主进程中执行。在拷贝页表期间,主进程会被完全阻塞,无法处理任何客户端请求。如果实例大、CPU忙,fork耗时可能达到秒级,直接导致业务超时。
(4.3 如何查看fork耗时?
Redis提供了直接的指标查看上一次fork的耗时,执行以下命令:
# 查看上一次fork操作的耗时(单位:微秒)
redis-cli INFO | grep latest_fork_usec
# 输出示例:latest_fork_usec:59477 (表示59.477毫秒)
这个值直接反映fork的阻塞时间。对于多数业务,若超过100毫秒就可能感知到延迟;若达到秒级(如1000000微秒),则会严重影响可用性。
(4.4 优化方案
针对fork耗时的问题,可从实例配置、部署环境、操作时机三个维度优化:
1)控制实例内存大小
fork耗时与实例内存正相关:实例越大,页表越大,拷贝越慢。建议:
- 用作缓存的Redis实例,内存控制在10GB以内;
- 超过10GB的场景,拆分为多个小实例,分散fork压力。
2)优化持久化策略
- 优先用AOF模式:AOF重写的频率可通过参数调优(如降低
auto-aof-rewrite-percentage
),单次fork的压力比全量RDB小; - 低峰期执行RDB:若必须用RDB,可在业务低峰(如凌晨)手动触发,避开流量高峰;
- 非核心业务关闭持久化:纯缓存场景(如会话存储),可关闭AOF和RDB,彻底避免fork。
3)改善部署环境
- 用物理机而非虚拟机:虚拟机的内存管理开销更高,fork耗时通常是物理机的数倍;
- 换用更快的磁盘:子进程写入RDB/AOF时若磁盘慢,会间接拖慢主进程(子进程占用CPU时,主进程的fork可能更慢),建议用SSD。
4)减少主从全量同步
主从首次同步或从库断连太久时,主库会fork生成RDB。可通过以下方式减少全量同步:
- 调大
repl-backlog-size
(默认1MB):从库断连后,主库会把新写数据存入backlog,从库重连时若backlog足够,可直接用增量同步,无需全量; - 避免从库频繁离线:确保从库网络稳定,减少断连概率。
5)合理配置fork相关参数
- 若用Redis 6.0+,可通过
server_cpulist
绑定主进程CPU,bgsave_cpulist
绑定RDB子进程CPU,避免主从进程争抢CPU; - 关闭内存大页(前文已讲):减少fork后写时复制的开销,间接降低fork的整体影响。
(5)AOF刷盘问题
RDB和AOF重写时fork操作的影响,其实数据持久化对性能的影响不止于此。
AOF(Append Only File)作为另一种主流的持久化方式,其配置是否合理直接关系到Redis的写入延迟——尤其是磁盘I/O成为瓶颈时,很可能让Redis从"毫秒级响应"变成"秒级卡顿"。
(5.1 AOF 持久化机制
AOF的核心原理是"记录所有写命令":Redis每执行一个写命令(如SET、HSET),都会把命令以文本形式保存到AOF文件中。整个过程分两步:
(1) 写内存:Redis先把命令写入内存中的AOF缓冲区(调用write
系统调用);
(2) 刷磁盘:再根据配置的刷盘策略,把缓冲区中的命令同步到磁盘(调用fsync
系统调用)。
这两步缺一不可:
-
第一步保证命令不丢失(至少在内存中),
-
第二步保证数据真正落地到磁盘。
(5.2 三种刷盘策略
为了平衡"性能"和"数据安全性",Redis提供了3种AOF刷盘策略(通过appendfsync
配置),但它们对性能的影响天差地别。
策略 | 工作机制 | 性能影响 | 数据安全 | 适用场景 |
---|---|---|---|---|
always | 每次写操作后同步刷盘 | 高延迟 | 最高 | 金融交易系统 |
everysec | 后台线程每秒刷盘 | 中等延迟 | 丢失1秒数据 | 通用业务(推荐) |
no | 依赖操作系统刷盘 | 最低延迟 | 可能丢失多秒(通常操作系统会每隔30秒刷一次) | 纯缓存场景 |
everysec
看似完美,但在磁盘I/O繁忙时,可能出现"后台线程阻塞导致主线程卡顿"的情况。具体过程如下:
(1) 后台线程按计划执行fsync
刷盘,但此时磁盘负载很高(如其他程序在大量写文件),fsync
被阻塞,迟迟无法返回;
(2) 主线程继续接收写命令,执行到"写入AOF缓冲区"(write
系统调用)时,发现后台线程的fsync
还没完成;
(3) 此时主线程会被卡住,等待后台线程fsync
结束后,才能继续执行write
——因为操作系统对同一文件的write
和fsync
可能存在锁竞争(避免数据不一致)。
最终结果:明明刷盘操作在后台线程,主线程却还是被阻塞,客户端感知到明显延迟。
当然Redis服务器上若还部署了其他服务(如日志收集、数据备份程序),这些程序大量写磁盘时,也会挤占Redis的I/O带宽,导致fsync
变慢。
(5.3 优化方案
针对AOF和磁盘I/O导致的延迟,有三个实用的优化方向:
1)避免AOF重写与刷盘冲突
Redis提供了一个配置项,可在AOF重写期间临时关闭后台线程的fsync
,避免两者争抢磁盘:
# 配置说明:AOF重写期间,后台线程不执行fsync
# 相当于临时把appendfsync改为no,重写结束后恢复原策略
config set no-appendfsync-on-rewrite yes
注意:开启后,重写期间若Redis宕机,可能丢失更多数据(最多丢失重写期间所有的写命令)。需根据业务对数据安全性的容忍度选择——对纯缓存场景可开启,对核心业务建议谨慎。
2)隔离磁盘资源
Redis服务器应"专机专用":
- 排查并迁移所有在Redis机器上执行大量写操作的程序(如日志程序、备份脚本);
- 若用云服务器,可给Redis单独挂载磁盘,避免与其他服务共享磁盘I/O。
3) 升级硬件:用SSD替代机械硬盘
机械硬盘(HDD)的随机写性能较差(通常每秒几百次fsync
),而SSD的随机写性能是HDD的10-100倍,能显著降低fsync
耗时。对于写频繁、对延迟敏感的业务,升级SSD是最直接有效的方案。
(6)内存达到上限 问题
当Redis内存触及maxmemory
阈值时,写入新数据前会触发一个关键操作:先从实例中淘汰一部分旧数据,腾出足够内存后,才能写入新数据。
这个淘汰过程是有耗时的,而延迟大小就和配置的淘汰策略直接相关。
(6.1 Redis基本淘汰策略
Redis提供了8种淘汰策略,不同策略的淘汰逻辑和耗时差异很大:
策略 | 含义 | 耗时特点 |
---|---|---|
allkeys-lru |
淘汰所有key中最近最少访问的 | 较高(需追踪访问频率,比较筛选) |
volatile-lru |
只淘汰设置了过期时间、且最近最少访问的key | 较高(同上,范围限于过期key) |
allkeys-random |
随机淘汰所有key | 较低(无需筛选,直接随机删除) |
volatile-random |
只随机淘汰设置了过期时间的key | 较低(范围限于过期key) |
allkeys-ttl |
淘汰所有key中即将过期的 | 中等(需检查过期时间) |
noeviction |
不淘汰任何key,写入直接返回错误 | 无(但会导致写入失败) |
allkeys-lfu |
淘汰所有key中访问频率最低的(4.0+) | 较高(需追踪访问次数) |
volatile-lfu |
只淘汰设置了过期时间、且访问频率最低的key(4.0+) | 较高(范围限于过期key) |
实际用得最多的是allkeys-lru
和volatile-lru
,基本步骤:
(1) 每次从实例中随机取一批key(数量可配置);
(2) 从这批key中淘汰“最近最少访问”的那个;
(3) 剩下的key暂存到一个“候选池”中;
(4) 继续随机取key,和候选池中的key比较,再淘汰最久未访问的;
(5) 重复以上步骤,直到内存降到maxmemory
以下。
关键参数:
- 抽样数量
maxmemory-samples 5
(默认值) - 单次最大耗时:25ms(与过期清理共用阈值)
(6.2 为什么内存达上限后会变慢?
当 Redis 实例达到配置的内存上限(maxmemory
)时,会出现以下特征:
- 写入延迟飙升:新数据写入前需先释放空间
- 读操作正常:已有数据的读取不受影响
- 周期性性能下降:内存压力持续时写入延迟持续高位
问题本质:内存淘汰过程发生在命令执行前,且全程在主线程完成,导致请求阻塞。
关键问题:这个淘汰过程和删除过期key一样,都是在主线程执行的——也就是说,写入新数据前,必须等淘汰逻辑跑完,主线程才能处理写入请求。写请求越频繁(QPS越高),这种延迟叠加就越明显。
如果实例中还存了bigkey,内存达上限时的延迟会更严重:
- 淘汰bigkey时,释放内存的操作本身就很耗时(需要遍历释放大量数据);
- 若用LRU等复杂策略,bigkey还可能因为“最近被访问过”被反复保留,导致需要淘汰更多小key才能腾出内存,进一步增加耗时。
这也是为什么我们一直强调要避免bigkey——它会在各种场景下放大性能问题。
(6.3 优化方案
针对内存达上限后的延迟问题,有四个实用的优化方向:
1)避免存储bigkey
这是最根本的办法。小key的内存释放速度远快于bigkey,即使触发淘汰,单次操作耗时也能控制在毫秒级以内。
2)改用随机淘汰策略
如果业务允许(对数据淘汰的“精准度”要求不高),可以把策略从LRU/LFU
换成random
(如allkeys-random
)。
随机淘汰无需追踪访问频率,也不用比较筛选,直接随机删key,耗时能降低60%以上。
3)拆分实例,分散淘汰压力
把数据分摊到多个实例,每个实例的maxmemory
阈值降低,单次需要淘汰的key数量也会减少。
比如原本1个实例存10GB数据,触发淘汰可能要删1GB;拆成10个实例后,每个存1GB,触发淘汰可能只需删100MB,压力大幅降低。
4)开启lazy-free机制(Redis 4.0+)
Redis 4.0以上支持“惰性释放”,可以把淘汰key时的内存释放操作交给后台线程,避免阻塞主线程。
配置方式:
# 开启后,淘汰key的内存释放由后台线程执行
config set lazyfree-lazy-eviction yes
效果:主线程只需标记key为“待删除”,实际释放内存的工作由后台线程异步完成,写入请求的延迟会显著降低。
(7)内存大页问题
在排查Redis性能问题时,除了fork操作本身的耗时,还有一个容易被忽略的系统配置可能引发延迟——内存大页(Transparent Huge Pages,THP)。这个机制看似是系统优化,却可能在特定场景下显著拖慢Redis。
内存大页机制对Redis而言是“弊大于利”:它通过增大内存页大小减少页表数量,但在Redis执行持久化(fork子进程)时,会因写时复制机制被迫拷贝大量内存,导致写请求延迟飙升。
对Redis服务器的建议是:彻底关闭内存大页,用常规4KB页保证内存操作的低延迟。这一优化虽简单,却能有效避免持久化期间的突发性延迟,尤其对大内存实例(10GB以上)效果更明显。
(7.1 什么是内存大页?
我们知道,应用程序向操作系统申请内存时,是以“内存页”为单位的。默认情况下,内存页的大小是4KB。而Linux内核从2.6.38版本开始引入了“内存大页”机制,允许应用以2MB为单位申请内存(是常规页的512倍)。
从系统角度看,内存大页的优势是“减少页表数量”:比如申请2MB内存,用常规页需要512个4KB页(对应512个页表项),而用大页只需要1个页表项,能降低内存管理的开销。
但对Redis这种对延迟极其敏感的数据库来说,大页的劣势更突出:单次内存申请的耗时变长了。
(7.2 内存大页为何会拖累Redis?
问题的关键出在Redis的写时复制(Copy On Write,COW) 机制上——这一机制在Redis执行RDB持久化或AOF重写时(即主进程fork子进程后)会频繁触发。
写时复制的基本逻辑
当主进程fork出子进程(用于生成RDB或重写AOF)后:
(1) 子进程需要读取主进程在fork瞬间的内存快照,用于持久化;
(2) 主进程继续处理客户端的写请求,但此时不会直接修改原内存数据,而是先拷贝该部分内存数据,再修改新拷贝的内存块(这就是“写时复制”)。
这样做的目的是:子进程能安全读取fork时的内存快照,不受主进程后续写操作的影响。
内存大页放大了写时复制的开销
当系统开启内存大页时,写时复制的成本会急剧增加:
- 若主进程需要修改10B的数据,而该数据所在的内存页是2MB大页,那么主进程必须拷贝整个2MB的大页(而不是4KB的常规页),才能进行修改;
- 拷贝2MB内存的耗时,远高于拷贝4KB(约512倍),这会直接导致写请求延迟增加;
- 若修改的是bigkey(本身占用大量内存页),需要拷贝的大页数量更多,延迟会更严重。
什么是Copy On Write
COW
是一种建立在虚拟内存重映射技术之上的技术,因此它需要MMU
的硬件支持,MMU
会记录当前哪些内存页被标记成只读,当有进程尝试往这些内存页中写数据的时候,MMU
就会抛一个异常给操作系统内核,内核处理该异常时为该进程分配一份物理内存并复制数据到此内存地址,重新向MMU
发出执行该进程的写操作。
(7.3 如何关闭内存大页?
既然内存大页对Redis性能有负面影响,建议直接关闭这一机制。操作步骤如下:
(1) 检查当前内存大页状态
执行以下命令,查看系统是否开启了内存大页:
# 查看内存大页配置
cat /sys/kernel/mm/transparent_hugepage/enabled
输出结果解读:
- 若显示
[always] madvise never
:表示已开启(always
被选中); - 若显示
always madvise [never]
:表示已关闭(never
被选中)。
(2) 临时关闭内存大页
若当前是开启状态,执行以下命令临时关闭(立即生效,重启后失效):
# 关闭内存大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
(3) 永久关闭内存大页
为避免重启机器后配置失效,需将关闭命令写入开机启动脚本(如/etc/rc.local
):
# 编辑rc.local,添加关闭命令
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >> /etc/rc.local
# 确保rc.local有执行权限
chmod +x /etc/rc.local
(8)SWAP 问题
如果发现Redis突然变得异常缓慢,每次操作耗时达到几百毫秒甚至秒级,那很可能是碰到了Swap问题——这种情况下,Redis基本丧失了高性能服务的能力。
Swap对Redis的影响是“致命”的——磁盘与内存的速度差距,会让Redis从“高性能”沦为“不可用”。解决的关键是“让数据回到内存”,要么扩容内存,要么释放资源并重启实例。更重要的是提前监控,在内存不足前及时干预,避免Swap发生。记住:对Redis来说,“Swap=性能灾难”,必须严防死守。
(8.1 什么是Swap?为什么会拖慢Redis?
了解操作系统的同学知道,当机器内存不足时,系统会把一部分暂时不用的内存数据“挪”到磁盘上,这个磁盘区域就叫Swap。这样做的目的是给其他程序腾出内存空间,起到缓冲作用。
但对Redis来说,Swap简直是“性能杀手”:
- 内存中的数据被换到磁盘后,Redis再访问这些数据时,就必须从磁盘读取——而磁盘的访问速度比内存慢几百倍(内存是微秒级,磁盘是毫秒级);
- Redis作为高性能内存数据库,对延迟极其敏感,哪怕几十毫秒的延迟都无法接受,更别说几百毫秒到秒级的卡顿了。
(8.2 如何检查Redis是否使用了Swap?
要确认Redis是否用到了Swap,可以按以下步骤操作:
1)找到Redis的进程ID
# 查找redis-server进程的ID(PID)
ps -aux | grep redis-server
# 输出示例:redis 1234 0.5 1.2 100000 20000 ? Ssl 08:00 0:05 redis-server *:6379
# 其中1234就是PID
2)查看该进程的Swap使用情况
# 替换1234为实际的Redis进程ID,查看内存和Swap使用详情
cat /proc/1234/smaps | egrep '^(Swap|Size)'
3)解读输出结果
执行后会看到类似这样的输出:
Size: 1256 kB # 该内存块的总大小
Swap: 0 kB # 该内存块中被换到磁盘的数据大小
Size: 4 kB
Swap: 0 kB
Size: 1921024 kB # 较大的内存块
Swap: 1921024 kB # 该块数据完全被换到磁盘(Size=Swap)
...
- 关键判断:如果某块内存的
Swap
值接近或等于Size
,说明该块数据已完全被换出到磁盘; - 风险阈值:累计Swap超过100MB时,Redis性能会明显下降;若达到GB级,基本处于不可用状态。
(8.3 优化方案
一旦确认Redis使用了Swap,需立即处理,核心思路是“让Redis数据回到内存中”。
1)增加机器内存(根本解决)
如果机器长期内存不足,最彻底的办法是扩容内存,确保Redis有足够的内存空间,避免数据被换出。
2) 释放内存并恢复Redis数据到内存
若暂时无法扩容,可先释放机器上的其他内存资源,再让Redis重新使用内存:
- 关闭机器上非必要的进程,释放内存;
- 释放Redis的Swap(通常需要重启实例)。
3)安全重启步骤(避免业务中断):
(1) 若Redis是主从架构,先执行主从切换,将流量切到从库;
(2) 重启旧主库(此时会重新加载数据到内存,脱离Swap);
(3) 待旧主库同步完成后,再切回主库(可选)。
4)如何预防Swap问题?
Swap问题的核心是“内存不足”,预防需从监控和容量规划入手:
(1) 实时监控:监控Redis机器的内存使用率(建议阈值≤80%)和Swap使用量,一旦Swap非零或内存接近满负荷,立即报警;
(2) 容量规划:根据业务增长趋势,提前扩容内存(如Redis内存达到机器内存的70%时,就考虑扩容);
(3) 隔离部署:Redis机器尽量专用,不部署其他耗内存的服务(如数据库、大数据组件),避免内存争抢。
(9)内存碎片问题
Redis的数据全存在内存里,要是应用程序频繁修改数据(比如频繁更新、删除key),很容易产生内存碎片。这些碎片会浪费内存空间,还可能间接导致性能问题——比如明明内存没存满,却因为碎片太多,新数据无法分配到连续内存块,不得不触发淘汰机制。
简单说,内存碎片就是“无法被有效利用的空闲内存”。
比如:
- 先存了一个100KB的key,占了一块100KB的连续内存;
- 后来删除了这个key,这块100KB内存变成空闲;
- 再存一个80KB的key,系统会分配新的80KB内存块,而之前的100KB空闲块因为没被充分利用,就成了碎片。
频繁操作后,内存中会积累大量这种“零散的空闲块”,导致Redis实际占用的内存(操作系统分配的)比实际存储数据的内存大很多。
(9.1 如何查看内存碎片率?
执行INFO
命令,查看mem_fragmentation_ratio
(内存碎片率)指标:
# 查看内存相关信息
redis-cli INFO memory
输出中关键部分:
# Memory
used_memory:5709194824 # Redis实际存储数据的内存(单位:字节)
used_memory_human:5.32G # 5.32GB
used_memory_rss:8264855552 # 操作系统给Redis分配的总内存(单位:字节)
used_memory_rss_human:7.70G # 7.70GB
...
mem_fragmentation_ratio:1.45 # 碎片率 = used_memory_rss / used_memory
碎片率计算:mem_fragmentation_ratio = used_memory_rss / used_memory
- 比值≈1:碎片少,内存利用率高;
- 比值>1.5:碎片率超过50%,需要处理;
- 比值<1:可能是内存交换(Swap)导致,需优先解决Swap问题。
(9.2 如何解决内存碎片?
根据Redis版本不同,有两种主要方案:
1)Redis 4.0以下版本:重启实例
老版本Redis没有自动碎片整理功能,只能通过重启实例解决:
- 重启后,Redis会重新加载数据,内存会按连续块分配,碎片自然消失;
- 注意:需提前做好数据备份(如RDB/AOF),避免重启丢失数据;若为集群,可逐节点重启,减少业务影响。
2)Redis 4.0及以上版本:开启自动碎片整理
Redis 4.0引入了activedefrag
(自动碎片整理)功能,可通过配置自动清理碎片。但要注意:碎片整理在主线程执行,会消耗CPU,可能增加延迟。
自动碎片整理的核心配置
# 开启自动内存碎片整理(总开关)
activedefrag yes
# 内存使用量低于100MB时,不进行碎片整理(小内存无需整理)
active-defrag-ignore-bytes 100mb
# 碎片率超过10%时,开始整理(最低触发阈值)
active-defrag-threshold-lower 10
# 碎片率超过100%时,全力整理(最高优先级)
active-defrag-threshold-upper 100
# 整理时最少占用1%的CPU(保证整理能推进)
active-defrag-cycle-min 1
# 整理时最多占用25%的CPU(避免影响业务)
active-defrag-cycle-max 25
# 整理容器类型(List/Hash等)时,每次扫描的元素数量(避免单次耗时过长)
active-defrag-max-scan-fields 1000
(10)绑定CPU问题
系统部署redis时,为了减少进程在多个CPU核心间切换的开销,运维人员一般采用“绑定CPU”的方式提升性能。
但对Redis来说,这项操作却没那么简单——如果不了解Redis的运行机制就盲目绑定,很可能适得其反,反而拖慢性能。
绑定CPU更像是“锦上添花”的优化,而非必选项。
只有在对性能有极致要求(如微秒级延迟),且已排除其他瓶颈(如bigkey、网络)后,再考虑这项操作——毕竟,理解硬件与软件的交互逻辑,比盲目配置更重要。
10.1 为什么Redis绑定CPU需要谨慎?
现代服务器通常有多颗CPU,每颗CPU包含多个物理核心,每个物理核心又可能有多个逻辑核心(如超线程技术)。
同一物理核心下的逻辑核心会共用L1、L2缓存,这对性能很重要。
而Redis的运行涉及多个“角色”:
- 主线程:处理客户端请求、执行命令;
- 子进程:负责RDB持久化、AOF重写(fork生成);
- 子线程:处理异步操作(如异步释放内存、AOF刷盘等)。
这些角色都会消耗CPU资源,若绑定不当,就会引发资源争抢。
如果把Redis进程只绑定到一个逻辑核心上,会出现严重问题:
- 子进程(如RDB持久化时)会继承父进程的CPU偏好,也在这个核心上运行;
- 子进程执行持久化时需要扫描全量数据,会占用大量CPU;
- 主线程与子进程在同一核心上争抢资源,导致客户端请求被阻塞,延迟飙升。
10.2 Redis如何绑定CPU?
若必须绑定CPU,至少要绑定到多个逻辑核心,且优先选择同一物理核心下的逻辑核心(如核心0和核心1,假设它们同属一个物理核心)。
这样做的好处是:
- 减少主线程与子进程的争抢;
- 同一物理核心的逻辑核心共用L1、L2缓存,能降低数据访问延迟。
但这种方式只能“缓解”争抢——子进程、子线程仍可能在这些核心间切换,存在一定性能损耗。
Redis 6.0版本,引入了“组件级CPU绑定”功能,可将主线程、子线程、子进程分别绑定到不同核心,彻底避免争抢。核心配置如下:
# 主线程和IO线程绑定到逻辑核心0、2、4、6(间隔2个核心,避免同物理核心冲突)
server_cpulist 0-7:2
# 后台子线程(如异步释放内存)绑定到逻辑核心1、3
bio_cpulist 1,3
# AOF重写子进程绑定到逻辑核心8-11
aof_rewrite_cpulist 8-11
# RDB持久化子进程绑定到逻辑核心1、10、11(可根据实际调整)
# bgsave_cpulist 1,10-11
配置说明:
server_cpulist
:负责处理客户端请求的主线程和IO线程;bio_cpulist
:后台异步任务线程(如lazy-free释放内存);aof_rewrite_cpulist
:AOF重写子进程专用核心;bgsave_cpulist
:RDB持久化子进程专用核心。
第三步:弃卒保帅(降级方案)
。。。。。。。。 由于平台 篇幅限制,后面 省略1000字+
原始的内容,请参考 本文 的 原文 地址
相关文章: 塔尖 redis 面试题, 毒打面试官
阿里面试:Redis 为啥那么快?怎么实现 100W并发?说出 这 6大架构,面试官跪 了
腾讯面试: 执行一条redis 命令时,底层干了什么? 小伙懵逼,挂了
京东面试: 亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)
希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)
史上最全: Redis: 缓存击穿、缓存穿透、缓存雪崩 ,如何彻底解决?