资讯动态

Redis内存与性能调优实战:从内存排查到慢查询治理

发布时间:2026/10/6 16:39:19 来源:尧图企业网站定制
我接手过不少 Redis 实例最让人头疼的从来不是功能开发而是内存曲线像心电图一样往上爬、性能瓶颈怎么查都查不到根因。明明maxmemory也设置了淘汰策略也配了内存却还是在涨明明只是存了些小字符串几十个 G 说没就没。后来一次次排查下来才发现问题往往不是某一条命令而是一串被忽略的默认配置、数据结构编码方式和操作系统层面的联动。这篇 Day 6就把内存和性能调优的完整排查链路梳理出来顺便把我踩过的坑也一并交代清楚。这篇内容适合三类人一是 Redis 刚入门、想把内存占用这件事彻底搞明白的二是已经上了生产、但遇到内存只涨不跌或者偶发超时问题的三是纯粹想系统做一次 Redis 缓存治理、把性能压榨到位的运维和开发。1. 先把“内存去哪了”这个问题查清楚内存调优的第一原则不是急着改配置而是搞清楚内存到底花在哪儿了。很多人一上来就把maxmemory调大、把淘汰策略改成 allkeys-lru结果内存还是爆原因就是没定位到真正的消耗点。1.1 用 info memory 快速定位内存水位连接上 Redis 之后第一条命令我建议永远是info memory。它能一次性把所有内存维度的关键指标列出来。127.0.0.1:6379 info memory # Memory used_memory:30549424 used_memory_human:29.14M used_memory_rss:51249152 used_memory_rss_human:48.88M used_memory_peak:36549424 used_memory_peak_human:34.86M used_memory_lua:30720 used_memory_scripts:0 number_of_cached_scripts:0 maxmemory:0 maxmemory_policy:noeviction mem_fragmentation_ratio:1.68重点看这几个字段used_memory_humanRedis 分配器实际分配给数据结构的字节数也就是数据集本身的真实大小。如果你的 value 很小这个数字还很大说明 key 数量非常多固定开销已经把空间吃掉了。used_memory_peak_human历史峰值。如果当前值和峰值相差很大说明曾经有过一次大规模写入或者大 key 产生现在虽然降下来了但碎片可能还在。used_memory_rss_human操作系统视角下 Redis 进程实际占用的物理内存。这个值一般比used_memory大差值主要来自内存碎片和加载的动态库、共享内存。mem_fragmentation_ratio等于used_memory_rss / used_memory。比值在 1 到 1.5 之间是健康的超过 1.5 说明碎片过高小于 1 则要警惕很可能发生了 swap性能会瞬间崩掉。maxmemory和maxmemory_policy如果maxmemory:0Redis 默认不限制内存使用这在生产环境里就是一个定时炸弹。我的排查习惯是先看used_memory_human判断数据集规模再对比mem_fragmentation_ratio看碎片最后翻used_memory_peak_human看历史上有没有大起大落。这套动作做完心里基本就能画出一个内存画像了。1.2 逐层拆解数据对象、过期键、碎片与持久化缓冲单看used_memory还不够它只是一个总量。实际内存消耗可以拆成下面几层数据本身每个 key-value 除了数据占用的空间Redis 还要为它维护一个dictEntry、SDS 字符串头、redisObject对象头。一个小字符串的实际内存开销往往是本身长度的好几倍。过期键如果开启了EXPIRERedis 会额外维护过期字典大量过期键在等待被动清理时内存会一直占着不放。持久化缓冲AOF 重写、RDB 快照、主从同步时的复制积压缓冲区repl_backlog都会占用额外内存。很多人忽略了这个一开 AOF 后内存涨几十 MB 就慌了其实这是正常的。内存碎片随着数据频繁增删分配器会产生大量碎块。碎片率过高时used_memory看着正常但 RSS 已经快把机器撑爆了。Lua 脚本与模块虽然一般占用不大但极端情况下也会成为变量。我曾经遇到过一档子事一个实例只有 500 万个小 key每个 value 不超过 40 字节但used_memory_human显示已经接近 800 MB。原因就是 key 太长平均一个 key 60 多个字符加上固定开销一个键值对轻松上 300 字节。后来把 key 缩短、把部分零散 key 收拢进 hash内存直接降了 40%。想对单个 key 精确分析可以用memory usage127.0.0.1:6379 memory usage mykey 1040它返回的是这个 key 在当前编码下的完整内存开销包括对象头、SDS、分配器等所有部分。对于排查大 key 和验证优化效果非常有用。2. 数据结构层面的内存优化编码方式与存储模型如果说内存排查是“看病”结构选型就是“开药”。Redis 每个数据类型都有不止一种底层编码而编码方式决定了同样的数据能省多少内存。这个知识点特别关键也特别容易被人忽视。2.1 紧凑编码与常规编码的边界Redis 为了在数据量小时省内存引入了几种紧凑编码intset当 set 里全是整数且数量小于set-max-intset-entries默认 512时set 用整数数组存储每个元素只是固定长度的整数没有额外指针和对象头。listpack / ziplistlist、hash、zset 在小数据量时采用紧凑编码。以 hash 为例当 field 数小于hash-max-listpack-entries默认 128且每个 field 和 value 长度小于hash-max-listpack-value默认 64时整体是一段连续内存一旦超过阈值就会转换为哈希表内存开销陡增。quicklistRedis 3.2 之后 list 使用 quicklist本质是多个小 ziplist 串起来兼顾内存和读写效率。相关参数是list-max-listpack-size。embstr 与 rawstring 类型在 44 字节以内使用 embstr 编码对象头和字符串内容在同一个内存块里超过阈值后转为 raw需要两次分配。我见过不少初学者在一个 hash 里塞几千个 field每个 field 都是一长串 JSON结果内存爆炸。原因很简单listpack 转成了 hashtable每个 field 都需要独立的 redisObject、SDS 和 dictEntry额外开销一下就上来了。如果你想验证某个 key 当前用了什么编码用object encoding127.0.0.1:6379 object encoding myhash listpack2.2 实战案例hash 聚合与 bitmap 的极限省内存内存优化的核心思路就两条减少 key 数量和减少重复结构开销。先看一个典型场景记录用户行为日志如果每个用户每一条操作都生成一个独立的 string那 key 数量直接爆炸。比如一个月 1000 万条行为就是 1000 万个 key光固定开销就吃掉几百兆。换成 hash 聚合按用户维度分桶一个用户一个 keyfield 是日期或行为编号value 是计数。同样的数据量key 数量从 1000 万降到可能 10 万内存立省 80% 以上。但注意hash 聚合必须控制每个 hash 的 field 数量在hash-max-listpack-entries以内否则转换编码后反而不划算。另一个省内存利器是 bitmap。存储用户某天是否上线最粗暴的方案是给每个用户建一个 string 存online一亿用户就是上亿个 key。用 bitmap 的话一天一个 key每个 key 的 bit 位对应一个用户 ID10 亿用户一天只占 10 亿 bit也就是大约 125 MB一个月也就 3.75 GB但换来的却是零散 key 的彻底消灭。做在线状态、签到统计、活跃用户去重这些场景bitmap 都是最优解。用 Redis 实现位图操作也非常直接# 用户 10001 在 2024-06-01 上线 SETBIT online:20240601 10001 1 # 查询该用户当天是否上线 GETBIT online:20240601 10001 # 统计当天上线用户数 BITCOUNT online:20240601这套方案在内存上的优势几乎是碾压级的。我在做某个日活过千万的统计项目时就是靠 bitmap 把缓存集群的内存占用从每天 GB 级别降到了几十 MB。2.3 从 Redis 类型到序列化方式的连锁优化value 本身的序列化方式也对内存和性能有直接影响。常见的问题是用 Java 的 JDK 序列化或者直接用 JSON 字符串存对象。JSON 的好处是直观坏处是引号、冒号、字段名全是冗余字符。一个简单的用户对象JSON 可能三四百字节用 protobuf 或 MessagePack 能压到一两百字节。在高并发场景下值减少一半网络 IO 和内存都能成倍释放。我在生产里一般建议内部缓存优先用 MessagePack 或 protobuf线上排查用 RedisDesktopManager 或 AnotherRedisDesktopManager 这类可视化工具时配合redis-cli --raw也能看个大概。值越小network瓶颈出现的时间就越晚这比单纯调maxmemory更有用。序列化这块还有一个容易被忽略的点一致性 Hash 和缓存 key 设计。key 如果包含大量相同前缀建议用短前缀加短 ID比如用u:1001替代user:profile:1001一条 key 能省十几个字节几亿条下来就是几个 G。3. 淘汰策略与过期键别让 Redis 顶着内存裸奔结构选型优化好之后接下来要面对的是“内存满了怎么办”的问题。这里不是简单的选一个maxmemory-policy就完事而是要理解淘汰策略和过期清理机制之间的联动。3.1 maxmemory-policy 五种策略怎么选maxmemory决定了 Redis 能使用的最大内存上限。当写入导致内存超过这个上限时Redis 会根据maxmemory-policy执行淘汰。常见策略如下策略行为适用场景noeviction不淘汰写入直接报错 OOM数据库型使用几乎不推荐在缓存场景用allkeys-lru对所有 key 按近似 LRU 淘汰最通用的缓存场景allkeys-lfu对所有 key 按访问频率淘汰访问频率差异非常明显的场景volatile-lru只在设置了过期时间的 key 里淘汰需要保证部分常驻 key 不丢volatile-ttl按剩余 TTL 短的先淘汰有明确时效性的数据这个表不看没关系关键在选型逻辑。大多数缓存场景直接选allkeys-lru就够了因为它不区分 key 是否带过期时间使用最简单。如果有一批 key 必须常驻比如配置类数据那就选volatile-lru给所有缓存 key 都设置过期时间常驻 key 不设过期就不会被淘汰。allkeys-lfu适合那种“少数 key 扛住绝大多数访问”的业务比如资讯热榜LFU 能避免某个偶发热门 key 把长期高频 key 挤掉。注意Redis 的 LRU 是近似实现不是精确 LRU靠maxmemory-samples采样来近似。默认采样数是 5如果业务对淘汰精度要求高可以调到 10换来的是额外的 CPU 消耗。我一般在 QPS 特别高、key 规模大的场景保留默认值在低 QPS 的缓存实例上调到 10 也没问题。3.2 分布式锁场景下的内存治理顺着热点词里提到的 Redis 分布式锁多说一句。很多团队用SET key value NX EX seconds实现分布式锁思路是对的但锁 key 的内存管理经常被忽略。最常见的问题有两个第一个是锁 key 忘了设置过期时间。拿到锁的线程挂了锁就永久留在 Redis 里。如果业务里每次加锁都会生成一个带随机后缀的 key时间一长就是几万个死锁 key。我见过生产环境里分布式锁 key 累积到几百万个内存白白浪费几个 G。治理办法是加锁时必须带过期时间且线程结束时要主动释放并配合 Lua 脚本保证判断和删除的原子性。第二个是锁的 value 太大。有人习惯把锁 value 塞一整个业务对象 JSON这是大忌。锁的 value 只需要一个唯一标识比如 UUID 字符串几十字节就够了。value 越大加锁、解锁和网络传输的开销都越大。对于分布式锁这种高频小 key 场景还要留意过期键的清理压力。如果设置了同样的 TTL大量锁在同一秒过期Redis 周期删除会被触发一次大扫除主线程可能出现短暂的卡顿。解决办法是 TTL 加一个小的随机抖动比如基础 5 秒再随机加 0 到 1 秒。3.3 过期键的惰性删除与主动删除Redis 的过期键删除不是实时的它采用“惰性删除 定期删除”的混合策略惰性删除访问 key 时才检查是否过期过期则删除。优点是省 CPU缺点是过期键可能长期占内存。主动删除每 100ms 随机抽一批设置了过期时间的 key检查并清除过期键。这两种机制配合起来平时没问题但存在一个隐患如果大量 key 在同一时间过期主动删除扫不过来内存会在短时间内“虚高”直到访问触发惰性删除才逐渐释放。针对这个隐患我给所有需要设置过期时间的 key 都养成了一个习惯过期时间不要用整点、整分而是加一个随机偏移。比如缓存 30 分钟实际 TTL 设成1800 random(0, 60)秒。这样能避免同时过期的“雷峰塔效应”对内存和 CPU 都是一个很大的保护。当内存压力已经顶到maxmemory时Redis 的淘汰顺序和过期清理会产生联动。如果大部分 key 都设了过期时间volatile-lru会先把最冷且带过期的 key 清掉如果选择allkeys-lru则所有 key 一视同仁。这个联动关系一定要心里有数否则会出现“明明设置了淘汰策略关键 key 还是被清掉”的诡异现象。4. 性能瓶颈排查与调优实践内存问题聊完了接下来是性能调优。Redis 是单线程模型6.0 之后网络 IO 可以多线程但命令执行仍然单线程这意味着任何一条慢命令都可能阻塞整个实例。所以性能优化的核心就是消灭慢命令降低单命令耗时。4.1 先从慢查询日志下手Redis 天生提供了slowlog功能不需要额外安装任何东西。开启方式是在配置文件里设置阈值slowlog-log-slower-than 10000 # 单位是微秒默认 10 毫秒 slowlog-max-len 128 # 最多保存多少条慢日志然后通过命令查看127.0.0.1:6379 SLOWLOG GET 10返回的每条记录会包含执行时间戳、耗时微秒、命令参数。我遇到线上超时问题时第一反应就是SLOWLOG GET。这比猜网络、查代码要高效得多。常见的慢命令黑名单KEYS *全量遍历 key数据量大时就是灾难必须用SCAN替代。SMEMBERS对超大 set 返回全部成员同理用SSCAN。HGETALL对超大 hash 返回全部 field用HSCAN。ZRANGE大范围查询一次拉取几千条 zset 数据非常耗时。SORT、LREM、MGET大量 key本质都是 O(N) 复杂度操作。排查时如果发现某个命令频繁进慢日志有两条路一是用SCAN系列分批处理二是把大 key 拆分。比如一个 hash 存了几十万个 field读的时候可以按 field 分段读取而不是一次性HGETALL。4.2 操作系统层与 Redis 配置的联动调优性能问题不只在 Redis 本身OS 层的几个设置很容易被忽略而且影响巨大。关掉透明大页THP。Linux 的透明大页会把内存页从 4KB 合并成 2MB虽然对普通应用有好处但对 Redis 是灾难。因为 Redis 做 RDB 快照或 AOF 重写时需要 fork 子进程如果开启了 THP写时复制的粒度从 4KB 变成 2MB一旦父进程在此期间修改数据内存瞬时翻倍的概率极高。官方也建议关闭echo never /sys/kernel/mm/transparent_hugepage/enabled注意这个操作重启后会失效所以要在系统服务里加一条配置或者用 tuned 之类的工具持久化。调整内存分配策略。vm.overcommit_memory设置为 1可以让 fork 在内存紧张时也能成功避免子进程启动失败导致持久化中断sysctl vm.overcommit_memory1设置合理的tcp-backlog和maxclients。高并发场景下连接数不够客户端请求会被拒绝或排队表现为超时。配置里把maxclients设为实际预期的连接上限同时检查系统的文件描述符限制。还有一个常见坑是 Redis 所在机器的cpu被其他进程抢占。如果部署在容器里最好给 Redis 容器设置 CPU 配额否则一个高频bgrewriteaof可能把整个节点 CPU 打满。4.3 客户端超时异常的排查链路很多 Java 后端都用 Lettuce 连接 Redis线上的经典报错是RedisCommandTimeoutException。出现这个异常的时候别急着调大超时时间先走一遍排查SLOWLOG GET查是不是有慢命令卡住了主线程。看info clients的connected_clients是否接近上限。看实例 CPU 使用率和info stats里的instantaneous_ops_per_sec。看网络延迟用redis-cli --latency测试本机到 Redis 的往返延迟。检查客户端连接池是否被打满Lettuce 默认是单连接复用高并发时可能出现信号量排队。我曾经遇到过一例服务端 Redis 本身毫无压力但客户端大面积超时。查了一圈发现是 Redis 部署在容器里宿主机网卡被打满导致网络包延迟和丢包。调优不只看 Redis 进程还要看宿主机整体负载。超时时间不是越大越好。设太大故障时请求会长时间挂起拖垮线程池设太小正常抖动也会被误报。一般建议 500ms 到 2s 之间根据业务容忍度调整。4.4 多线程 IO 与惰性删除的合理使用Redis 6.0 引入了 IO 多线程机制把网络读写分散到多个线程但命令执行依然是单线程。如果你的实例网络吞吐是瓶颈可以在配置里开启io-threads 4 io-threads-do-reads yes注意io-threads-do-reads的开启要谨慎因为读多线程和延迟要求高的场景会有微妙影响。我一般只有在单实例 QPS 超过 10 万且大量小 value 时才开普通场景开不开区别不大。另外删除大 key 是单线程模型下最容易造成阻塞的操作。4.0 之后引入了UNLINK可以异步释放内存127.0.0.1:6379 UNLINK bigkey日常运维扫到大 key 时千万不要直接DEL尤其是几百 MB 的 hash 或 zset直接DEL会卡住实例好几秒。用UNLINK或者分批删能把阻塞降到最小。FLUSHDB/FLUSHALL也可以加ASYNC后缀异步清空。5. 日常监控与调优效果的量化验证调优动了哪些配置、省了多少内存、提升多少性能不能靠感觉要靠数据。我每次调优前后都会做两件事压测和监控记录。5.1 redis-benchmark 压测的读法Redis 自带的redis-benchmark能快速压出大致的 QPS。常用命令redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -r 1000000 -P 16参数解释-c 5050 个并发连接。-n 100000总共发 10 万条请求。-r 1000000随机生成 100 万个可能的 key。-P 16流水线批量一次发 16 条命令。压测结果会输出每个命令的 RPS每秒请求数和平均延迟。我主要看两个值一是SET/GET的 QPS 是否达到预期二是 p99 延迟是否在个位数毫秒。如果 QPS 上不去但 CPU 没打满大概率瓶颈在网络或结构设计上如果 CPU 打满但 QPS 很低就需要检查是否存在慢命令或持久化频率太高。5.2 监控指标与阈值建议生产环境里我至少每天看一眼这些指标超过阈值就报警指标获取方式建议阈值内存碎片率info memory的mem_fragmentation_ratio大于 1.5 报警淘汰 key 数info stats的evicted_keys增量持续增长需关注过期 key 数info stats的expired_keys增量短时间内暴涨需关注命中率keyspace_hits / (keyspace_hits keyspace_misses)低于 80% 需排查阻塞客户端数info clients的blocked_clients大于 0 需关注瞬时 QPSinfo stats的instantaneous_ops_per_sec根据机器能力自定义有了这些数据你就能回答“为什么最近慢了”这类问题。我曾经处理过一个案例某个实例的evicted_keys每十分钟涨几万个但命中率只有 60%。原因是业务把所有数据都塞进 Redis没有做缓存与数据库的分层每次缓存 miss 都会打到数据库数据库扛不住又反向放大了缓存压力。后来把热数据单独梳理出名单只缓存 top 20% 的数据命中率直接回到 95% 以上内存占用也降了 60%。5.3 定期巡检的三个固定动作最后分享一个我固定的巡检动作每次排查调优前都先做这三步扫描大 keyredis-cli --bigkeys它会统计每种类型里最大的 key并给出建议。检查持久化配置确认save策略和appendfsync是否符合业务需求。缓存场景建议appendfsync everysec既保证安全又不拖垮主线程。回顾最近一次配置变更和发布记录很多性能问题不是慢慢变差的而是某次配置调整或业务上线引起的。这三步做完90% 的内存和性能问题都能暴露出来。Redis 的内存与性能调优从来不是一蹴而就的工作而是一个需要持续根据业务特征调整的过程。把诊断方法、结构选型、淘汰策略、运维监控这四件事跑通你手里的 Redis 才真正算是“被管理起来了”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑