【Redis从入门到精通】第24篇:Redis内存优化完全指南——从对象系统到encoding调优

【Redis从入门到精通】第24篇:Redis内存优化完全指南——从对象系统到encoding调优 上一篇【第23篇】ZSet对象——ziplist和skiplist的完美组合下一篇【第25篇】Redis数据库那些事——16个数据库的选择与键空间管理前面几篇文章我们把Redis五大对象类型的底层编码都扒了个遍。但你有没有想过一个问题知道了ziplist、intset、skiplist这些编码之后到底能帮我们省多少内存怎么在实际项目里真正把内存优化落地这篇文章就是来回答这个问题的。我们从内存分析工具出发逐层深入各类型的内存优化策略最后给出一个可直接对照执行的内存优化清单。准备好了吗让我们开始榨干Redis内存的每一字节。一、先做个体检INFO memory解读想知道Redis内存用得怎么样了第一步就是看INFO memory——这是Redis的内存体检报告。127.0.0.1:6379INFO memory# Memoryused_memory:841624# Redis分配器分配的内存总量字节used_memory_human:821.90K# 人类可读格式used_memory_rss:5132288# 操作系统角度看Redis占用的物理内存used_memory_rss_human:4.89M# 人类可读格式used_memory_peak:872864# 内存使用峰值used_memory_peak_human:852.41K used_memory_peak_perc:96.42%# 当前内存占峰值的百分比used_memory_overhead:829472# 管理数据开销内部数据结构used_memory_startup:810128# Redis启动时的基础内存消耗used_memory_dataset:12152# 纯数据占用的内存used_memory_dataset_perc:10.72%# 数据内存占比allocator_allocated:843912# 分配器实际分配的内存allocator_active:892928# 分配器活跃的内存页allocator_resident:5079040# 分配器驻留在物理内存中的大小total_system_memory:17115602944# 系统总内存total_system_memory_human:15.94G used_memory_lua:37888# Lua脚本占用的内存used_memory_lua_human:37.00K used_memory_scripts:0# 脚本缓存占用的内存used_memory_scripts_human:0B number_of_cached_scripts:0 maxmemory:0# 最大内存限制0表示无限制maxmemory_human:0B maxmemory_policy:noeviction# 淘汰策略allocator_allocator:jemalloc-5.2.1# 内存分配器类型active_defrag_running:0# 是否正在执行内存碎片整理lazyfree_pending_objects:0# 待异步释放的对象数lazyfreed_objects:0# 已异步释放的对象数这里面的关键指标整理如下指标含义关注点used_memoryRedis分配的内存总量核心指标反映数据开销used_memory_rssOS视角的物理内存占用如果比used_memory大很多说明碎片严重mem_fragmentation_ratio内存碎片率 used_memory_rss / used_memory1.5建议关注2需要处理used_memory_dataset纯数据内存占used_memory比例越高越好used_memory_overhead管理开销包括了复制缓冲区、客户端缓冲区等maxmemory最大内存限制生产环境一定要设maxmemory_policy超限时的淘汰策略见第七节详解内存碎片率看不见的内存黑洞内存碎片率是使用Redis时最容易被忽视却又最要命的指标之一。它的计算公式很简单mem_fragmentation_ratio used_memory_rss / used_memory这个比值代表操作系统实际分配的内存和你认为Redis正在使用的内存之间的水分。来看一个直观的解释内存碎片率示意 情况1碎片率 1.0 1.2健康 used_memory: |████████████████████| (100MB) used_memory_rss:|████████████████████| (110MB) 碎片率110MB / 100MB 1.1 ✅ 情况2碎片率 2.0严重 used_memory: |████████████████████| (100MB) used_memory_rss:|████████████████████████████████████████| (250MB) 碎片率250MB / 100MB 2.5 ❌ 这多出来的150MB去哪了 → 被内存碎片吃掉了为什么会有碎片jemalloc分配器为了提高分配效率会按固定大小如8B、16B、32B…将内存分成不同规格的内存块。当你频繁删除大小不一的数据时释放回去的内存块无法拼接成大块就形成了碎片。解决碎片问题有三板斧重启简单粗暴但有效——Redis重启后碎片率归零主动碎片整理通过配置activedefrag自动清理调优jemalloc调整jemalloc-bg-thread等参数# 开启主动碎片整理CONFIG SET activedefragyes# 设置触发条件CONFIG SET active-defrag-ignore-bytes 100mb# 碎片超过100MB才开始整理CONFIG SET active-defrag-threshold-lower10# 碎片率超过110%开始整理# 查看整理状态INFO memory|grepdefrag二、内存构成你的内存都去哪儿了Redis的内存消耗由多个部分组成用一张图说清楚Redis内存消耗全景图 总内存 (used_memory_rss) │ ├── 自身内存 (used_memory) │ │ │ ├── 数据内存 (used_memory_dataset) │ │ ├── 对象值String的value、Hash的field/value等 │ │ └── 对象头redisObject结构体约16字节/对象 │ │ │ └── 管理开销 (used_memory_overhead) │ ├── 内部数据结构dict的哈希表、skiplist节点等 │ ├── 客户端缓冲区输出缓冲区 │ ├── 复制积压缓冲区replication backlog │ ├── AOF缓冲区 │ ├── Lua脚本缓存 │ └── 集群信息 │ └── 内存碎片used_memory_rss - used_memory 的差值部分 └── jemalloc未归还给OS的内存碎片冷知识Redis中每一个key-value对不论值多小至少都要消耗约50-70字节的额外开销redisObjectGDBdictEntrySDS头等。这就是为什么几万个很小的key比几个大的key要耗费更多内存。三、OBJECT ENCODING看清你的数据长什么样OBJECT ENCODING命令是我们整个内存优化工具箱里最重要的一把手术刀——它能告诉你任何key当前使用的是什么底层编码。# 查看编码127.0.0.1:6379OBJECT ENCODING mykeyembstr# 常用的其他OBJECT子命令127.0.0.1:6379OBJECT REFCOUNT mykey# 引用计数(integer)1127.0.0.1:6379OBJECT IDLETIME mykey# 空闲时间秒(integer)120127.0.0.1:6379OBJECT FREQ mykey# LFU频率Redis 4.0(integer)3各种数据类型的编码速查表打印出来贴在显示器旁边数据类型可能的编码最优编码触发条件Stringintint值是整数字符串Stringembstrembstr短字符串≤44字节Stringraw—长字符串44字节Hashziplist/listpackziplist/listpackfield≤512且value≤64字节Hashhashtable—超过ziplist阈值Listquicklistquicklist所有情况Redis 3.2Setintsetintset全是整数且≤512个Sethashtable—非整数或超过阈值ZSetziplist/listpackziplist/listpack元素≤128且member≤64字节ZSetskiplist—超过ziplist阈值四、各类型的内存优化策略String类型最小的优化最大的收益String看似简单但内存优化空间很大# 策略1用int编码存整数# 整数比字符串省很多内存127.0.0.1:6379SET count100OK127.0.0.1:6379OBJECT ENCODING countint# 存为int编码省内存127.0.0.1:6379SET counthello100OK127.0.0.1:6379OBJECT ENCODING countembstr# 不再是整数编码变了# 策略2避免大String# ❌ 1MB的String——读写都慢SET big:data...1MB的JSON...# ✅ 拆分成多个小StringSET data:part1...第一段...SET data:part2...第二段...# 策略3善用GETRANGE# 不需要GET整个大String再截取127.0.0.1:6379SET long:text...5000字符的文本...127.0.0.1:6379GETRANGE long:text100200# 只取需要的部分Hash类型控制编码阈值Hash内存优化的核心就是保持ziplist编码配置项建议值理由hash-max-ziplist-entries默认512如果单个Hash总超512根据实际调大hash-max-ziplist-value默认64value超过64字节就该用hashtable了拆分策略每个Hash 100-500个field既保持ziplist又不至于太多key# 示例100万用户信息存储# ❌ 方案A100万个独立Hash keyziplist但不优雅fori1,1000000: HSET user:$iname...age...# ✅ 方案B按用户ID % 2000分桶每个桶约500个fieldHSET user:01:name张三1:age282:name李四2:age35... HSET user:1501:name王五501:age22...# 每个桶都是ziplist编码节省大量内存List类型quicklist的压缩List底层是quicklist可以通过压缩中间节点来省内存# 查看当前List编码127.0.0.1:6379OBJECT ENCODING mylistquicklist# 配置压缩深度compression depth# list-compress-depth 0: 不压缩默认# list-compress-depth 1: 压缩两端各1个节点以外的所有节点# list-compress-depth 2: 压缩两端各2个节点以外的所有节点# ...127.0.0.1:6379CONFIG SET list-compress-depth2OKquicklist中的数据压缩策略用ASCII图说明quicklist压缩策略 (list-compress-depth 1) quicklist链表结构 ---------- ---------- ---------- ---------- ---------- | 节点0 | - | 节点1 | - | 节点2 | - | 节点3 | - | 节点4 | | (未压缩) | | (压缩) | | (压缩) | | (压缩) | | (未压缩) | ---------- ---------- ---------- ---------- ---------- ^ ^ | | 首尾节点不压缩 list-compress-depth1 (频繁读写热点) 仅压缩中间节点踩坑提示压缩虽然省内存但有CPU开销。如果你的List是频繁读写的比如消息队列list-compress-depth 0不压缩可能更合适。但如果列表主要是堆积存储比如历史记录深度的压缩能省不少内存。Set类型让intset多活一会儿Set优化很简单尽量让元素是整数。# ❌ 字符串ID——hashtable编码127.0.0.1:6379SADD user:tags100110021003127.0.0.1:6379OBJECT ENCODING user:tagshashtable# 1001是字符串# ✅ 整数ID——intset编码127.0.0.1:6379SADD user:tags100110021003127.0.0.1:6379OBJECT ENCODING user:tagsintset# 省内存如果确实需要存字符串调大set-max-intset-entries让大一点的Set也能保持intset# 如果你的整数Set常规在1000个左右127.0.0.1:6379CONFIG SET set-max-intset-entries1024ZSet类型阈值调优ZSet的优化思路和Hash一样——保持在ziplist编码范围内# 调大ziplist阈值让更多ZSet保持紧凑编码127.0.0.1:6379CONFIG SET zset-max-ziplist-entries256127.0.0.1:6379CONFIG SET zset-max-ziplist-value128但要注意ziplist编码下ZSET的查询是O(N)的。如果你在ziplist编码的ZSet里频繁做ZRANK和ZSCORE数据量大了之后是有性能代价的。这里需要在内存和性能之间找到平衡。五、大Key的排查与治理大Key是Redis性能的头号杀手。一个大Key可能让整个Redis实例陷入卡顿。大Key的危害大Key对Redis的影响 正常操作: [请求1] [请求2] [请求3] [请求4] 时间线: |--| |--| |--| |--| 遇到大Key: 时间线: [请求1] [--请求2(HGETALL 10万元素)--] [请求3超时] | | -- 阻塞了300ms -- -- 客户端断开 因为Redis是单线程处理命令的一个大Key操作会阻塞后续所有请求排查方法# 方法1redis-cli --bigkeys快速扫描# 扫描整个实例找出每种类型的最大Key$ redis-cli--bigkeys# 输出示例# Biggest string found: big:json has 1024000 bytes# Biggest hash found: user:data has 12580 fields# Biggest list found: message:queue has 50000 items# Biggest set found: uv:today has 340000 members# Biggest zset found: leaderboard has 128000 members# 方法2SCAN DEBUG OBJECT更精确但不推荐频繁使用127.0.0.1:6379SCAN0COUNT10001)10242)1)user:10012)user:1002...127.0.0.1:6379DEBUG OBJECT user:1001 Value at:0x7fd2c0a0c3c0 refcount:1 encoding:hashtable serializedlength:8524 lru:1284567 lru_seconds_idle:300# 方法3MEMORY USAGERedis 4.0127.0.0.1:6379MEMORY USAGE user:1001(integer)8536# 这个key占了8536字节大Key处理策略大Key类型拆分策略示例大String分片存储big:data:0big:data:1…大Hash分桶HSETuser:0user:1… 每个桶500个field大List按时间拆分msg:2026-05msg:2026-06…大Set按分类标签拆分tag:techtag:gametag:sports…大ZSet按时间段/分段拆分rank:dailyrank:weeklyrank:monthly六、内存分析工具推荐除了Redis自带的命令还有几款第三方工具能帮你更直观地看到内存使用情况工具类型功能适用场景redis-rdb-toolsPython库解析RDB文件生成内存报告离线分析不影响线上实例rdbtoolsPython CLI分析RDB中每类key的内存占比快速排查哪个类型的key吃内存最多redis-memory-analyzerRuby gem分析MEMORY USAGE采样数据实时分析慎用RedisInsightGUI桌面应用可视化内存分布、Key大小排行日常运维监控redis-cli --bigkeys内置命令扫描实例找大Key应急排查# rdb-tools安装和使用示例$ pipinstallrdbtools python-lzf# 分析RDB文件$ rdb-cmemory dump.rdbmemory.csv# 按类型汇总内存$ rdb-cmemory dump.rdb--bytes128-fmemory.csv $awk-F,{sum[$2]$3} END {for(k in sum) print k, sum[k]}memory.csv七、内存淘汰策略满了之后怎么办当Redis内存达到maxmemory上限时需要根据淘汰策略决定如何处理新写入请求。Redis提供了8种策略策略淘汰对象特点适用场景noeviction不淘汰直接报错最安全不会丢数据不允许丢数据的场景为测试/默认allkeys-lru所有key中最近最少用的近似LRU算法传统选择通用缓存场景volatile-lru有过期时间的key中LRU只淘汰带TTL的key缓存持久混合场景allkeys-lfu所有key中最不频繁用的Redis 4.0防偶发热点有热Key、冷Key区分明显的场景volatile-lfu有过期时间的key中LFULFU的volatile版本缓存中冷热分化明显allkeys-random所有key中随机淘汰最简单CPU开销最小对缓存质量要求不高的场景volatile-random有过期时间的key中随机随机淘汰的volatile版本同上只要过期key可淘汰volatile-ttl有过期时间的key中TTL最短的优先淘汰快过期的有过期时间的临时数据场景LRU vs LFU 的选择很多人在allkeys-lru和allkeys-lfu之间纠结。简单来说LRU vs LFU 的典型场景 场景电商首页缓存 LRU最近最少使用 - 首页商品缓存每5分钟刷新 - 用户疯狂刷新首页LRU会保留首页数据 ✅ - 今天大促首页被频繁访问 → 不会被淘汰 LFU最不频繁使用 - 商品详情页缓存 - 爆款商品每天被看10万次长尾商品每天被看5次 - LFU会保留爆款商品的缓存淘汰冷门商品 ✅ - 不会因为一次偶尔的访问就提升冷门商品的优先级 一句话总结 LRU关心最近有没有人用它→适合有时间局部性的场景 LFU关心历史上有多少人用它→适合有冷热分化的场景踩坑提示生产环境一定不要用默认的noeviction一旦内存满了所有写操作都会返回错误你的应用会瞬间大面积报错。哪怕用最简单的allkeys-lru也比noeviction强。八、内存优化清单Checklist下面这张清单可以直接作为内存优化的行动指南逐项排查序号检查项操作预期效果1是否设置了maxmemoryCONFIG SET maxmemory 值避免OOM被系统杀掉2淘汰策略是什么CONFIG GET maxmemory-policy不是noeviction就OK3碎片率是否1.5INFO memory看 fragmentation_ratio开启activedefrag4String对象是否大量用embstrSCANOBJECT ENCODING合理值用int小于44字节用embstr5Hash是否都在用ziplistOBJECT ENCODING抽查调大ziplist阈值或分桶6Set是否都是intset编码OBJECT ENCODING抽查尽量用整数ID7ZSet的member长度是否超64字节OBJECT ENCODING抽查缩短member名8是否有大Keyredis-cli --bigkeys拆分大Key9是否有大量过期Key堆积INFO stats看expired_keys调整淘汰策略10客户端缓冲区是否膨胀CLIENT LIST看omem限制输出缓冲区11是否启用了Lazy FreeCONFIG GET lazyfree*Redis 4.0推荐开启12AOF/RDB是否过于频繁检查BGSAVE/BGREWRITEAOF日志调整save参数内存预算估算公式最后给一个快速估算内存的方法方便你做容量规划总内存需求 ≈ 数据内存keyvalue的总大小 开销redisObject dictEntry等≈ 每个key额外 50-70 字节 碎片按1.2倍计算 缓冲区复制积压客户端输出≈ 按100MB预留 安全余量20% 示例100万个平均1KB的Hash key 数据内存: 1,000,000 × 1KB 1,000 MB key开销: 1,000,000 × 60字节 60 MB Hash内部开销: 每个Hash的dict ziplist头 ≈ 20 MB 碎片(1.2): (10006020) × 0.2 216 MB 缓冲区: 100 MB 安全余量(20%): (10006020216100) × 0.2 ≈ 280 MB --------------------------------------------------- 总计: 约 1,676 MB ≈ 1.6 GB小结Redis内存优化是个系统工程不是改一两个参数就能搞定的。回顾整篇文章的核心思路先做体检INFO memory和OBJECT ENCODING是你的听诊器和X光机先看清楚现状分类型优化每种类型都有最适合的编码方式关键在于让数据保持在紧凑编码状态治碎片碎片率1.5就该关注了activedefrag是你的帮手剁大Key大Key是万恶之源redis-cli --bigkeys定期扫描选对淘汰策略生产环境至少用allkeys-lru别裸奔noeviction持续监控内存优化不是一次性工作把它纳入日常运维下一篇我们将深入Redis的数据库概念——那16个编号从0到15的数据库到底是怎么回事SELECT命令背后发生了什么键空间管理和SCAN游标又是怎么工作的敬请期待上一篇【第23篇】ZSet对象——ziplist和skiplist的完美组合下一篇【第25篇】Redis数据库那些事——16个数据库的选择与键空间管理