先聊一个场景:你负责的 Redis 晚上突然告警,内存从 3GB 直线飙到 9GB,GET一个 key 却要 200ms。用redis-cli --bigkeys扫一遍,发现有个LIST存了 80 万条历史记录,里面塞的全是几百字节的 JSON 字符串。这种事故我在线上见过太多次了,原因就一句话:Redis 的数据结构看着简单,底层的内存模型和编码规则你不搞清楚,它早晚给你来个深夜惊喜。
这篇文章是《7天学会 Redis》系列的第 6 天,专门聊内存和性能调优。如果你已经会基本命令、知道五种数据类型怎么用,接下来的内容可能直接决定你能不能应对生产环境的内存告警。我会从内存模型拆解、数据结构编码优化、淘汰策略选型、持久化性能取舍、慢查询与大 key 治理这几个角度,把调优这件事拆成一个一个能落地的动作。适合刚接触 Redis 运维的同学,也适合被线上 OOM 折磨过的老手。
1. 内存去哪儿了:Redis 内存模型与开销拆解
1.1 先看懂 INFO memory
调优第一件事不是改配置,是看懂 Redis 到底怎么看待自己的内存。我一直觉得INFO memory的输出是 Redis 给运维同学开的“体检报告”,但很多人根本没仔细读过。正常一个 Redis 节点,跑一遍这个命令,你会看到类似下面的字段:
# Memory used_memory: 1575787096 used_memory_human: 1.47G used_memory_rss: 1908293632 used_memory_peak: 2184968904 used_memory_peak_human: 2.03G used_memory_lua: 31744 mem_fragmentation_ratio: 1.21 mem_allocator: jemalloc-4.0.3几个核心字段,我给你逐个说清楚:
used_memory:Redis 自己统计并分配的内存总量,包含数据占用的空间、key 本身的开销、客户端缓冲区、Lua 脚本等等。单位是字节,所以看它的时候配合_human的字段更方便。used_memory_rss:操作系统层面看到的进程常驻内存,也就是你top看到 RSS 的那个数值。这个值通常比used_memory大,因为包含内存碎片、虚拟内存映射这些。used_memory_peak:历史峰值,是从 Redis 启动到现在的最高内存记录。这个字段很有用,判断内存有没有“曾经爆炸过”。mem_fragmentation_ratio:used_memory_rss除以used_memory的比值。这个就是我常说的碎片率,后面单独展开。mem_allocator:内存分配器,正常都是 jemalloc,这是 Redis 官方默认的。
我的经验是,每天定时把INFO memory的结果和监控系统打通,观察曲线。内存不是你说感觉正常就正常的,used_memory_peak如果一直高位不动,说明可能有大 key 被删除了但内存没完全还给系统,或者说碎片率已经不对劲。
1.2 内存开销拆解:不是只有数据才占内存
很多人有个错觉:我在 Redis 里存了 100MB 数据,那 Redis 占用的内存就应该是 100MB 左右。这是对 Redis 内存模型最大的误解。
我先抛一个结论:一个 key 从存进去开始,内存开销由几部分组成——数据本身、redisObject、SDS 字符串头、全局哈希表条目、过期字典条目、空闲指针对齐的填充字节。拿字符串类型举例子,一个 10 字节的 key 对应一个 20 字节的 value,实际消耗的内存可能是 70 到 80 字节,多余的这些就是所谓的“固定开销”。
具体拆分一下:
redisObject结构体:Redis 中每个 value 都是一个redisObject,包含类型、编码、引用计数、指向实际数据的指针。这个结构体在 64 位系统上大概是 16 字节。- SDS 字符串:Redis 自己实现的动态字符串,头部有长度、分配容量、标记位这些字段。一个 100 字节以内的字符串,头部还要额外占用 3 到 5 字节,加上
\0结尾。 - 全局 dict 条目:Redis 的键空间是一个大字典,每个键值对在这个字典里都有一个条目,包含 key 指针、value 指针、下一个节点的指针,大概 24 字节以上。
- 过期 key 的额外字典:给 key 设置了 TTL,Redis 还会在过期字典里维护一条记录,这又是开销。
所以你可以用一个简单的公式估算:
单个 key 内存开销 ≈ 16(redisObject) + 字符串头 + key 长度 + value 长度 + dictEntry 开销
正是因为有这些固定开销,小 value 的内存放大效应非常明显。比如你存 100 万个只有 5 字节的字符串,理论数据只有 5MB,但实际内存占用可能超过 100MB。这 20 倍的差距,没人告诉你的话,你排查内存暴涨时会一头雾水。
另一个常见坑是共享整数对象:Redis 启动时会预创建 0 到 9999 这些小整数对象。如果你大量使用小数字当 value,这部分不额外占用内存;但如果超过这个范围,每个字符串都要独立申请内存。这个知识虽然影响不大,但偶尔能解释为什么同样规模的数据,内存占用差异这么多。
1.3 内存碎片:为什么内存看着很浪费
继续看INFO memory,mem_fragmentation_ratio是调优永远绕不开的指标。这个比值是 RSS 对 used_memory 的比值,正常范围大概在 1.0 到 1.5 之间。
- 小于 1:危险信号。说明 Redis 使用了 swap,内存被换到磁盘了,性能会断崖式下跌。检查一下操作系统层面有没有做 swap 配置。
- 1 到 1.5:健康区间,内存使用效率高。
- 大于 1.5:碎片化严重。
used_memory看着不高,但是 RSS 很高,操作系统层面内存已经被撑爆了。常见原因是频繁增删 key、批量写入后大量删除、key 的 value 大小差异巨大。
处理碎片,我常用的方案有三条:
- 重启前先做主动碎片整理。Redis 4.0 以上支持
activedefrag yes配置,配合active-defrag-ignore-bytes 100mb和active-defrag-threshold-lower 10,让 Redis 在后台自动搬移内存配对碎片。生产环境开启前先压测一下,因为主动碎片整理本身会消耗 CPU。 - 如果碎片率太高(比如超过 2.0),且业务允许短暂抖动,直接
redis-cli shutdown save后重启。RDB 加载过程会重新分配内存,碎片会被“清零”。这是最暴力也是最有效的办法。 - 控制 value 大小的均匀性。虽然这个属于数据设计层面,但 value 差异大是碎片的主要来源之一,能避免尽量避免。
内存碎片这个问题,最开始学 Redis 时总觉得是玄学,后来才明白,这本质上是 malloc 和 free 的交替使用导致堆内存无法连续利用。jemalloc 已经算是个好分配器了,但你不可能完全消除碎片,只能监控它、管理它。
2. 数据结构优化:省内存从编码开始
2.1 五种类型的底层编码,你知道你的键在用哪种吗?
Redis 最良心的地方,就是每种数据类型都有不止一种底层编码。你存 10 个整数到 Set 里,和存 10 万个字符串到 Set 里,底层结构完全不同。这就是为什么同样类型的数据,内存占用量差别这么大。
用OBJECT ENCODING key可以查出某个 key 当前的编码:
127.0.0.1:6379> OBJECT ENCODING mylist "quicklist" 127.0.0.1:6379> OBJECT ENCODING myset "intset" 127.0.0.1:6379> OBJECT ENCODING myhash "listpack"把五种类型的常用编码整理成一张表,你一眼就能看明白:
| 数据类型 | 编码类型 | 触发条件 | 说明 |
|---|---|---|---|
| String | int | value 是整数且在 long 范围内 | 直接用 8 字节存整数,最省内存 |
| String | embstr | value 长度 ≤ 44 字节 | 字符串和 redisObject 连续分配 |
| String | raw | value 长度 > 44 字节 | 两段内存分配,多一次寻址 |
| List | quicklist | 默认 | 压缩列表 + 双向链表的组合体 |
| Hash | listpack | 元素少且 value 短 | 紧凑紧凑节省内存 |
| Hash | hashtable | 超过阈值 | 转换为真正的哈希表 |
| Set | intset | 全部是整数且数量少 | 有序整数数组,省内存且支持二分查找 |
| Set | hashtable | 超过阈值或含非整数 | 转成哈希表 |
| ZSet | listpack | 元素少且 value 短 | 紧凑编码 |
| ZSet | skiplist | 超过阈值 | 跳表 + 哈希表的组合 |
这里的编码名称在新版本里有变化,7.x 以后把 ziplist 换成了 listpack,但优化思想是一样的:元素少、value 小的时候,用连续内存的紧凑数组,不用指针链来链接,提高缓存命中率,降低内存占用。
举个例子,一个 Hash 只有 3 个 field,每个 field 和 value 都在 64 字节以内,listpack 编码下所有数据是连续存放的,中间没有指针,内存紧凑到极致。如果这个 Hash 用哈希表实现,光是 3 个 dictEntry 就比全部 listpack 数据本身还大。
2.2 压缩列表/紧凑型编码的最大配置阈值
既然省内存,为什么不所有场景都用紧凑编码?因为紧凑结构有性能代价:读写时间复杂度是 O(n)。元素少时无所谓,元素多了线性扫描就拖垮性能。所以 Redis 用两个参数控制转换时机。
相关配置如下,我以常见生产模板为例:
hash-max-listpack-entries 128 hash-max-listpack-value 64 set-max-intset-entries 512 zset-max-listpack-entries 128 zset-max-listpack-value 64entries是数量阈值,value是单个元素的最大字节长度。超过任意一个,编码就转换成标准结构。比如 Hash 有 200 个 field,哪怕每个 value 只有 10 字节,也会转成 hashtable。
调优思路很简单:如果你的业务特征是“字段多但值小”,Hash 的hash-max-listpack-entries可以调大,比如 256 或 512。但是注意,listpack 在HGETALL这种命令下,复杂度是 O(N),N 越大单次命令耗时越长。所以不要贪心,我见过有人把 entries 调到 1024,结果HGETALL一次返回 2MB 数据,网络传输和反序列化直接卡死。
Set 的 intset 编码是个意外惊喜:存整数时,intset 不仅省内存,查询还快,因为底层是排序数组,直接二分查找。如果你的业务信息适合用整数 ID 集合,比如“用户粉丝 ID 列表”这种,尽量全部用整数,让 intset 一直生效。
2.3 客户端序列化陷阱:为什么存个 JSON 多占 70% 内存
内存调优不止调 Redis 端,客户端怎么序列化和压缩也是大头。这是我和 Java 开发同事一起排查线上内存问题时,最常遇到的坑。
比如说我们要在 Redis 里存一个用户对象,很多人习惯性地把对象直接JSON.toJSONString(user),然后SET user:10001 "{"name":"张三","age":30}"。这个字符串看着也就 30 字节,没问题。问题是如果对象有几十个字段,一个用户对象序列化后可能 300 到 500 字节。这时候如果你有 100 万用户在线,光用户数据的裸字符串就占了 400MB 到 500MB。
有一个更好的思路:把对象先压缩再存。Redis 自带的SETRANGE和 Lua 脚本都没法自动帮你压缩,但你可以在应用层用 gzip 或 snappy 压缩后写入,读取时再解压。我实测下来,JSON 文本的压缩率通常能达到 50% 到 70%,也就是说一个 500 字节的对象,压缩后只剩 150 到 250 字节。读者可能担心压缩和解压的 CPU 开销,其实在 snappy 算法下,这个开销远比想象中的内存节省划算。
序列化框架的选择同样影响巨大。Java 里面 JDK 原生序列化不仅慢,产物还大,换成 Kryo 或 protobuf 之后,对象体积能再降一半以上。这也是网上热词“redis序列化”被反复讨论的原因。所以说,内存优化是端到端的——Redis 本身在省,你的应用层也在决定它最终的占用。
3. 过期与淘汰:让内存有进有出
3.1 内存上限:不出问题不用注意,出问题就是要命
很多 Redis 教程从来不说maxmemory,因为开发环境下 Redis 默认maxmemory 0,意思是“不限制内存”。可到了生产环境,Redis 和操作系统其他进程抢内存,一旦吃光物理内存,操作系统开始 swap,然后所有 Redis 请求都在卡,这个事故我经历过不止一次。
配置内存上限前,先列两个问题:
- 你的机器总内存多少?给操作系统和其他进程留多少余量?
- 你的 Redis 实例是单机独享还是和别的进程混部?
一般建议,单机 Redis 的maxmemory设为物理内存的 50% 到 70%。比如一台 16GB 的机器,配置maxmemory 8gb或10gb,剩余内存给操作系统 page cache 和稳定性兜底。如果你用的是 Redis Cluster,每个节点的上限按分片容量来算,规则类似。
设置完上限之后,还有一件很容易被忽略的事:maxmemory只约束 Redis 自己的 used_memory,不代表 RSS 一定不超过它。碎片率会让 RSS 超过 limit,所以最终选 limit 时,要再留出 20% 的余量给碎片和临时缓冲区。
3.2 八种淘汰策略的前世今生和选择
设了maxmemory,内存达到上限后新写入怎么办?这就轮到maxmemory-policy出场。Redis 提供的策略有:
| 策略 | 含义 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,写命令直接报错 | 不能丢任何数据的业务,比如支付状态 |
| allkeys-lru | 从所有 key 中按最近最少使用淘汰 | 缓存场景,无差异化的全局热数据 |
| volatile-lru | 从已设置 TTL 的 key 中按 LRU 淘汰 | 缓存 + 部分业务持久混合 |
| allkeys-lfu | 从所有 key 中按使用频率淘汰 | 热点集中、访问倾斜明显的缓存 |
| volatile-lfu | 从已设置 TTL 的 key 中按 LFU 淘汰 | 需要淘汰但不碰无 TTL 的 key |
| allkeys-random | 所有 key 随机淘汰 | 数据热度相似、无差别缓存 |
| volatile-random | 已设置 TTL 的 key 随机淘汰 | 热点不明显的临时数据 |
| volatile-ttl | 优先淘汰剩余 TTL 最短的 key | 接近过期的 key 先被清理 |
选择哪个,核心问题就一个:能不能丢数据?
- 如果 Redis 是纯缓存,推荐
allkeys-lru或allkeys-lfu。LFU 出现在 Redis 4.0 之后,解决的是“某个 key 曾经热过但再也不用,LRU 却总淘汰不掉它”的问题。如果你的访问曲线有明显热点且持续变化,LFU 比 LRU 更准。 - 如果 Redis 里混着缓存和持久数据,选
volatile-lru会安全些,因为它不碰那些没有 TTL 的数据。 - 绝对不能接受逐出导致丢失,那就
noeviction,但你要有完整的内存监控和告警,一旦内存打满写请求就失败,你需要立刻介入。
我个人的生产经验:绝大多数缓存场景,maxmemory-policy allkeys-lru配合合理的容量规划就足够了。过度复杂的策略组合,带来的收益和运维成本不成正比。
3.3 过期删除的两种机制与缓存雪崩关系
Redis 的过期 key 删除不是定时器实时扫的,而是两种机制配合:惰性删除和定期删除。
- 惰性删除:访问某个 key 时,发现它已经过期,顺手删除。缺点是过期 key 如果一直不被访问,就一直在内存里躺着。
- 定期删除:Redis 后台周期从设置了过期时间的 key 中随机抽一批,检查是否过期,过期就删。默认每秒运行 10 次左右,每次处理耗时上限受
hz配置影响。
这个机制会导致一个现象:大量 key 同时过期时,因为定期删除只能按批次处理,Redis 的内存并不会立刻下降,而是在几秒甚至几十秒内逐步下降。如果在这个过期内内存打满,就可能触发淘汰策略。
缓存雪崩就是这样产生的:假设你把所有缓存都设置了 1 小时过期,且过期时间集中在 12 点整。那一瞬间 Redis 需要删除海量 key,删除不是大问题,大问题是——下一次请求来了发现缓存没有,全部穿透到数据库,数据库连接数打满,整个系统雪崩。
规避办法我试过几种,比较靠谱的:
- 过期时间加随机偏移:
expire = 基础时间 + random(0, 300),让过期时间在 5 分钟内均匀散开。 - 热点数据不设过期:设置逻辑过期时间,在应用层判断字段值是否过期,再异步刷新缓存。这个方案能彻底避免集中过期,代价是实现复杂度上来了。
- 双层缓存:本地缓存 + Redis 缓存,即使 Redis 缓存雪崩,本地缓存还能顶一小段时间。
顺着“缓存雪崩”,经常一起出现的是“缓存穿透”和“缓存击穿”,这两个问题的核心不在内存,而在对数据库的压力管理。缓存穿透是说请求的 key 根本不存在于缓存和数据库,每次都绕过了缓存;缓存击穿是说一个高热 key 在过期的一瞬间,大量请求同时怼到数据库。处理穿透可以缓存空值加短 TTL,或者用布隆过滤器挡一下;击穿则可以加互斥锁(即那个著名的“Redis 分布式锁”),或者逻辑过期方案。这些都是 Day 4 讲缓存时应该细聊的,这里主要提醒你:内存管理的几个策略不是相互独立的,你需要在设计阶段就联动考虑。
4. 持久化耗性能:AOF 和 RDB 的调优路
4.1 RDB 的 fork 是谁在付钱
RDB 持久化是 Redis 通过 fork 子进程生成的快照。fork瞬间复制父进程的页表,这个过程虽然理论上是写时复制,但页表本身的大小和内存量成正比。内存越大,fork 越慢,阻塞时间越长。这就是为什么 12GB 内存的 Redis 在开启bgsave时,主进程可能卡 100ms 以上,这是我调优过程中最痛的一课。
影响 RDB 性能的配置项:
save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yessave的三行配置定义触发快照的条件:900 秒内至少 1 次写、300 秒内至少 10 次写、60 秒内至少 1 万次写。stop-writes-on-bgsave-error yes的意思是:如果上次 bgsave 失败,Redis 停止接收写命令,避免数据丢失风险扩大。线上保持 yes 更安全。rdbcompression能让 RDB 文件更小,消耗少量 CPU,一般保持默认。rdbchecksum在加载时会做校验,增加几十毫秒加载耗时,但能防止文件损坏纠错,不建议关。
我的经验是:调 RDB 不是调快照频率,而是调快照大小和 fork 时的内存冗余。在内存占比高的实例上,RDB 快照尽量放到业务低峰期,且保证机器有足够闲置内存。比如 Redis 使用了 8GB,那 fork 后写时复制可能临时多占 1 到 2GB 内存,如果机器只有 12GB,这段时间就有 OOM 风险。
4.2 AOF 三种刷盘策略怎么取舍
AOF 持久化把写命令追加到文件里。默认appendfsync everysec,这是 Redis 权衡性能和可靠性的标准答案。它的含义是一秒钟把缓冲区里的内容同步到磁盘,极端情况下可能丢最近 1 秒的数据。另两个选择:
| 策略 | 数据安全性 | 性能影响 | 适用场景 |
|---|---|---|---|
| always | 每次写命令都 fsync,基本不丢数据 | 写性能骤降,碎盘直接没法用 | 金融级严格对账场景 |
| everysec | 每秒 fsync 一次,最多丢 1 秒数据 | 性能折中,生产首选 | 绝大多数线上业务 |
| no | 由操作系统决定何时刷盘 | 最高性能 | 可接受较大丢数据的内部缓存 |
everysec是生产环境最常用的配置,但不是没坑。如果磁盘性能差,或者瞬间写入量过大,可能 AOF 同步队列积压,导致主线程阻塞。你会看到日志里出现Asynchronous AOF fsync is taking too long或者Can't recover from AOF sync error,这就是磁盘写不过来了。
另一个 AOF 相关的可口优化是aof-use-rdb-preamble yes。开启后,AOF 重写生成的混合格式文件先用 RDB 二进制快照,再追加增量命令,既保留了 AOF 的完整性,又利用了 RDB 的加载速度。最好保持默认 yes。
4.3 AOF 重写:为什么会放大内存压力
AOF 文件无限增长不是好事,所以 Redis 提供了 AOF 重写机制:把当前内存里的数据重新生成一份最小化的写命令集合。配置项长这样:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb含义是:当 AOF 文件大小超过上次重写后大小的 100%(即翻倍),且文件大于 64MB 时,触发自动重写。重写也是 fork 子进程来完成,和 RDB 一样有 fork 停顿开销。重写过程中父进程继续接收新写命令,会把新命令积攒在重写缓冲区里,这又额外占用内存。
这个特性最常翻车的场景就是:内存已经很紧张,AOF 又触发重写,fork 直接因内存不足失败,随后stop-writes-on-bgsave-error和重写失败连环爆炸。所以我现在的做法是:
- 监控
INFO persistence中的aof_rewrite_in_progress和rdb_bgsave_in_progress,不允许手动执行 debug 级别的重写。 - 高内存水位时(比如超过 70%),主动拆 key、扩大集群,而不是依赖 AOF 重写自己收拾残局。
- Redis 6.0 之后推出了
aof-timestamp-enabled这种增量支援机制,但实际生产中使用率不高,暂时不用投入太多精力。
5. 性能定位:慢日志和大 key 治理
5.1 慢日志怎么配怎么用
内存和性能调优,最先要搞清楚的是“谁拖慢了 Redis”。Redis 自带了慢日志机制,不像 MySQL 那样需要装插件,它本身就把每条命令的执行耗时记录下来,按阈值切分。
slowlog-log-slower-than 10000 slowlog-max-len 128 slowlog-log-max-len 1024slowlog-log-slower-than单位是微秒,10000 即 10ms。生产环境建议设置为 5000(5ms)甚至 3000,这样能抓到更多潜在的慢命令。slowlog-max-len控制最多保留多少条记录。查看慢日志:
127.0.0.1:6379> SLOWLOG get 5结果里每个条目包含命令执行时间戳、消耗时长、命令详情。我处理慢日志时有一个固定套路:
- 连续跑几次
SLOWLOG get,看慢命令的类型和时间分布。 - 如果是
KEYS、SMEMBERS这种 O(N) 命令频繁出现,直接考虑改SCAN替代。 - 如果是
HGETALL、LRANGE这种,看返回的数据量是不是太夸张。 - 如果是周期性出现,和 RDB 快照、AOF 重写的时间点对上,那就是 fork 阻塞,问题在持久化侧,不在命令本身。
Redis 还提供了 latency monitor 功能,开启后能监测各种事件的延迟峰值:
latency-monitor-threshold 100配合LATENCY HISTORY命令,看到某个时间点命令执行延迟飙升到多少、持续多久。这个在日常排障中比慢日志更直观,因为它能画出时间线,和 trace 系统配合定位。
5.2 Redis 自带工具:--bigkeys 和 --memkeys
大 key 是 Redis 性能怒火最主要的来源。一个 1MB 的 key,GET它和GET一个 10 字节的 key,网络传输量完全不是一个级别。而且大 key 在淘汰、过期、删除时会阻塞主线程,很多“Redis 直接卡死几秒”的事故,最后都定位到某个大 key 上。
找大 key,最省事的方式是用官方工具:
# 统计各种类型中最大的 key redis-cli --bigkeys # 按内存占用排序统计(Redis 4.0+ 支持) redis-cli --memkeys--bigkeys的实现原理是遍历所有 key,对每种类型抽样:字符串类型直接看长度,列表/哈希/集合/有序集合类型则循环取元素数量。它会输出每个类型下最大的 key,以及每个类型的元素总量统计。注意,这不是准确的内存测量,它只是给出一个“谁可能是大麻烦”的清单,单靠它还不够。
更精确的测量单 key 内存,用MEMORY USAGE:
127.0.0.1:6379> MEMORY USAGE bigkey输出的是这个 key 实际占用的字节数,会考虑编码、序列化长度、redisObject 开销等。生产环境批量排查时,用redis-cli --memkeys扫一遍,再精确追踪大 key 的详细信息。
另外,如果你会SCAN,也可以自己写脚本遍历所有 key,配合DEBUG OBJECT或者MEMORY USAGE拿到更多维度。我自己写过一个 Python 脚本,定期把大 key 清单推到日志系统,低于历史峰值就从列表移除。这是治理大 key 最有效的手段——让大 key 在出现的一周内就被发现,而不是等它撑满内存时才知道。
5.3 热 key 与大 key 的处理策略
找到大 key 之后怎么处理,取决于它的类型:
- 字符串大 key:如果是缓存数据,可以压缩后存储;如果是业务数据,考虑拆成多个 key,分散压力。例如用户的大 JSON 拆成多个 Hash field,读取时只取需要的字段。
- Hash 大 key:把一个大 Hash 按业务维度拆成多个中等 Hash,每个存较少的 field,减少
HGETALL的返回量。 - List 大 key:这是最常见的坑,尤其是消息队列场景。建议按容量或时间进行分段,比如每天一个 list,读取时只取当天的,避免一次性 LRANGE 整个队列。
- Set/ZSet 大 key:如果业务允许,用分片 key 或换用别的结构。ZSet 的按分值区间查询在大 key 下性能很差。
热 key 则是另一个维度的性能毒瘤。一个 key 一秒被访问 10 万次,单个节点单线程的特殊性,这个 key 会拖慢后面所有命令。Redis 单线程指处理命令的是一个线程,虽然 6.0 之后网络 IO 是多线程,但命令执行还是串行的。所以热 key 的优化思路通常是:在应用层加本地缓存(如 Caffeine),把热 key 的读写挡在 Redis 之前;或者把热 key 复制出多个带后缀的副本 key,分散读压力。
我对大 key 和热 key 有个统一的治理原则:不要让任何一个单独的 key,成为 Redis 节点的单点瓶颈。这个原则写进代码评审 checklist 里,比出了事再救火有用得多。
6. 常见问题速查与个人体检清单
6.1 线上典型问题与排查思路
把我这几年遇到的高频问题整理成速查表,方便你直接对照:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 内存飙升后不下降 | 大 key 过期,删除时阻塞,碎片率高 | INFO memory看碎片率,SLOWLOG get看 DEL 耗时长,碎片超过 1.5 考虑重启 |
| 写请求超时 | AOF 刷盘过慢或 fork 阻塞 | 看日志里有无Asynchronous AOF fsync,调appendfsync策略,避开持久化高峰 |
| 缓存击穿/雪崩 | 过期时间集中、热点 key | INFO stats看expired_keys变化,调整过期时间随机化 |
used_memory不高但机器 OOM | 内存碎片 + fork 子进程内存 | mem_fragmentation_ratio过高,降低 maxmemory 留出余量 |
| KEYS 命令导致卡顿 | O(N) 扫描全局 key | 排查慢日志,全部改用 SCAN 命令 |
| 集群节点内存不均匀 | 数据倾斜,大 key 存在单节点 | 用--memkeys找出大 key,拆分或迁移 |
上面每一条都是真实项目里踩过的坑。我最想强调的就是第一行:内存飙升后不下降,很多时候不是内存泄漏,而是used_memory下降但used_memory_rss还挂着不动。碎片率看着 1.3、1.4,你以为还能忍,结果再来一次大 key 过期,峰值内存就爆了。遇到这种情况,该重启果断重启,业务抖动 5 秒和内存爆掉宕机 10 分钟,你选一个。
6.2 个人体检清单:生产 Redis 上线前过一遍
如果你负责的 Redis 要上线,我建议至少守着这套清单走一遍:
maxmemory设置了没有?是否留了 20% 余量?maxmemory-policy选的是不是和业务匹配?缓存场景是不是 allkeys-lru?INFO memory的碎片率在 1 到 1.5 之间吗?- 有没有定期跑
redis-cli --bigkeys --memkeys排查大 key? - AOF 刷盘策略是不是 everysec?
aof-use-rdb-preamble yes开启了吗? - 过期的缓存 key 有没有加随机偏移?
- 有没有开启慢日志监控,告警阈值合不合理?
- 持久化和高并发写的时间窗口是否会互相叠加?
这套清单我每次上线新业务都会过一遍,省下来的排障时间远超写的时间。你如果能把第 5 天学的主从复制、哨兵和高可用架构,和第 6 天的内存调优组合起来,基本上就能撑起一个小型公司的 Redis 运维盘子了。
最后提醒一句,Day 7 我们通常会聊 Redis Cluster 集群的搭建和运维。但在进集群之前,先把单机内存这关过了。单机表现都不好,集群只会把单点问题放大成多点问题。今天的内容,我建议你直接在测试环境把OBJECT ENCODING和MEMORY USAGE挨个试一遍,看着真实数据变化,比背结论有效得多。