做过完整链路压测的人大概率都遇到过一种“玄学”业务应用和数据库的指标看起来都正常但压测一上并发接口P99直接翘头。追到最后问题总是指向一个常常被忽略的地方——Redis内存。Redis之所以能扛住高并发靠的是把所有数据都放在内存里但这恰恰是一把双刃剑内存用完再快的Redis也会变成业务链路上的拖油瓶。这篇文章想围绕性能测试场景把Redis内存管理这件事讲透从底层开销到观测方法从压测调优到排障思路尽量少讲虚的多放硬货适合正在做性能测试、又经常被缓存层背锅的同学参考。先说明一点这里聊的“管理Redis内存”不是让你去当DBA而是作为性能测试人员你得知道Redis的内存在压测中扮演什么角色、怎么观测、怎么判断是不是因为内存问题导致结果失真以及用什么手段提前预防。这样才能在压测报告的“瓶颈分析”里给出让人服气的结论而不是简单写一句“Redis内存不够建议扩容”。1. 性能测试为什么要死磕Redis内存1.1 链路中间件的蝴蝶效应缓存命中率左右压测曲线在绝大多数业务压测场景里Redis不是核心存储而是挡在数据库前面的缓冲层。它把热点数据放在内存里让请求尽可能不穿透到数据库。可一旦内存不足Redis会按配置的淘汰策略开始“裁员”先把一些Key踢出去腾出空间给新写入的数据。这种淘汰一发生缓存命中率就往下掉。别小看命中率掉几个百分点。假设你压测一个查询订单的接口Redis缓存命中率从95%掉到70%意味着原来只有5%流量打到数据库现在变成30%的流量穿透过去。数据库连接池、磁盘IO、主从延迟全都被放大整个链路的表现就是响应时间陡增、TPS上不去。你在JMeter里看监控曲线第一眼看到的是数据库CPU飘红很容易误判成“数据库性能问题”。如果Redis用的是maxmemory-policy noeviction情况更麻烦。内存耗尽之后所有写操作直接报错主线程大批量处理写命令时会同步阻塞压测现场的报错率会突然飙升。这种故障表现和中间件超时很像但根源完全是另一个层面的问题。1.2 性能测试人员最常踩的Redis内存误判我见过不少压测团队在Redis内存上栽跟头误判方式基本是下面这几种。只看服务端CPU和数据库完全忽略Redis内存指标。很多压测监控大盘把Redis只当作“缓存中间件”觉得它就是个读多写少的快设备内存不足只会导致数据淘汰不至于挂服务。但实际压测中内存不足引发的连锁反应远比CPU占用高可怕。默认配置就是合理配置。Redis刚启动时如果没有显式设置maxmemory64位系统默认情况下内存是不受限的。这不代表安全反而意味着真到物理内存吃紧时操作系统会直接触发OOM把进程杀掉或者大量换页导致Redis被拖到濒死。默认淘汰策略是noeviction一旦达到内存上限写操作直接拒绝对压测结果的影响是灾难性的。只用used_memory一个指标判断内存状况。used_memory只能反映分配器分配给Redis对象的那部分内存不包含碎片和进程自身开销。你看到used_memory只有2GB但机器的RSSResident Set Size可能已经4GB了因为内存碎片、主从复制缓冲区、AOF重写缓冲区都算在进程的物理内存里。单看一个指标压测中出现内存RSS飙升你会完全没感觉。误把“压测中内存增长”当成“内存泄漏”。Redis内存在压测中升高是正常的触达峰值后不回落原因可能是碎片、过期键未及时回收、大流量下的复制缓冲区膨胀等不一定就是业务代码泄漏。这需要一套完整的分诊方法稍后会展开说。认清了这些误区你就会明白性能测试里管好Redis内存本质上是保证压测数据可信度。内存一旦出问题你测出来的“系统瓶颈”很可能是Redis淘汰策略惹的祸而不是系统真正的容量上限。2. Redis内存到底花在哪从底层结构到真实开销2.1 一份数据背后藏着的三笔隐形账单很多人对Redis内存的理解是“存了多少数据就占多大内存”。实际完全不是这样。Redis内存的花销至少包含三部分。第一笔是键空间的字典开销。Redis本身是一个大的哈希表dict每个键值对都会生成一个dictEntry结构这个结构里保存了键指针、值指针以及指向下一个冲突项的哈希表链表指针。在64位系统上光是这个dictEntry基本就要占约24字节。键值对里的Key本身又是SDS字符串对象还有字符串头部的元数据。比较极端的场景下你存100万个很短的Key每个Key的综合字典开销能有50~100字节。也就是说哪怕Value只有几个字节实际占用的内存也可能翻好几倍。第二笔是数据结构的内部开销。String类型相对直接它就是一个SDS但Hash、List、Set、ZSet这类复杂结构除了存数据本身还要维护元数据、指针、编码结构内存开销比同等数据量的纯字符串高得多。Redis官方一直推荐用Hash存对象、少用大String就是因为Hash能把字段组织得更紧凑还能利用小编码来省内存。第三笔是内存分配器的“四舍五入”。Redis默认使用jemalloc作为内存分配器它会把内存按大小分类管理而不是按字节精确分配。比如你申请一个137字节的对象分配器可能直接给你一块192字节的区块。单个对象看着浪费不大量一多碎片率和实际占用差距就会非常可观。这也是为什么你删除了一大批Key之后used_memory下降了但RSS可能没怎么降——分配器握在手里的大块内存不会立刻还给操作系统。2.2 不同数据类型的内存性价比实测对比先看最基础的String类型。它适合存储简单的值、状态标记、序列化后的完整缓存对象但如果你把一个大JSON整体塞进去这个JSON字符串会有多大Redis就真实占多大内存没有任何优化空间。压测时最怕的就是这种“大JSON直接塞String”的用法因为每次读写的单位都很大内存带宽、网络带宽、反序列化开销全部被放大。用Hash就聪明一些。比如一个用户对象有姓名、年龄、等级、手机号你可以把它拆成多个field存在同一个Hash Key里。子数据量小的时候Redis会用紧凑的listpack编码来存储内存占用比一个个小String键低很多而且还天然解决了“同一条缓存的多个字段一起失效”的问题。缺点是Hash的单个Key访问要传入field名客户端代码稍微复杂一点但对于压测数据量大、字段重复率高的场景省下来的内存相当可观。ZSet的场景也很典型。排名榜、时间线这类数据一个ZSet成员既要保存member又要保存score底层再套一层跳表或listpack。如果你用ZSet存用户积分排行榜明明只有几万用户内存却轻松超过几十MB原因就在这份额外开销上。常规建议是数据量可控就用ZSet的紧凑编码数据量大起来之后要及时评估是否值得为排序功能付出这么多内存或者考虑换用专门的排序存储组件。列表List则要小心大List。一个List里塞几十万个元素读取尾部和头部固定范围的操作看似很快但对象本身在内存里是一大块连续数据块Redis主线程处理删除或trim时如果一次性操作了大量元素会造成明显的阻塞。压测中如果你观察到某个Redis节点在固定时间点出现毛刺经常就是这种大List在作怪。3. 压测全程的内存观测法指标、命令与可视化工具3.1 压测前中后分别盯哪些内存指标压测前先记录一组基准数据。用redis-cli info memory能看到一堆以used_memory开头的指标其中有几个必须盯紧。used_memoryRedis分配器实际分配的内存总量代表Redis真正占用的逻辑内存。used_memory_rss向操作系统申请到的物理内存包括碎片和进程自身开销。used_memory_peak历史峰值用于判断当前是否接近以前踩过的警戒线。mem_fragmentation_ratioRSS与used_memory的比值。正常在1.0~1.5之间超过1.5说明碎片很严重。maxmemory当前配置的内存上限。evicted_keys因内存淘汰被逐出的Key数量。expired_keys因过期被动删除的Key数量。压测前要做三件事确认maxmemory已经按机器内存预算设置好检查keyspace里现在有多少Key、预计压测会增加多少扫描一下是否有大Key。压测中建议每5秒采集一次INFO memory和INFO stats把used_memory、used_memory_rss、evicted_keys、expired_keys差值记下来。压测结束后再补采一遍MEMORY DOCTOR的输出看碎片率和分配器状态。不推荐压测时总在可视化面板上刷新因为刷新本身会漏掉瞬时变化。更好的方式是把采集脚本跑起来把数据落成CSV结束后画趋势图这样才能对应上TPS、P99的波动点。3.2 命令工具箱INFO、MEMORY USAGE、MEMORY DOCTOR在你的压测跳板机上下面几条命令是主力的信息源。INFO memory是最基础的能看到内存整体视图但看不到具体是哪个Key占了内存。要定位具体对象用MEMORY USAGE key它可以告诉你指定键到底占用了多少字节。对于Hash这类复合结构还可以带SAMPLES参数做采样估算比如MEMORY USAGE user:1001 SAMPLES 10。MEMORY STATS能看更细的分配器数据包括peak.allocated、total.allocated、fragmentation等。Redis 4.0以上还有MEMORY DOCTOR命令它会自动给你一段健康检查报告提示你当前碎片率是否过高、是否有大量键同时过期、是否需要MEMORY PURGE等。我习惯在压测结束后跑一下这条命令当作一次免费的内存体检。大Key扫描用redis-cli --bigkeys。它会遍历整个键空间按数据类型统计出最大的若干个Key还会给一份摘要。注意它是一把全量扫描别在生产高峰期跑压测环境是无所谓的。# 压测前做一次大键体检 redis-cli -h 192.168.1.10 -p 6379 --bigkeys # 压测中每5秒采集一次关键内存指标 while true; do redis-cli -h 192.168.1.10 -p 6379 INFO memory \ | awk -F: /used_memory|used_memory_rss|used_memory_peak|mem_fragmentation_ratio|maxmemory/{print $1$2} \ redis_mem_$(date %m%d).csv sleep 5 done3.3 可视化客户端Redis Desktop Manager不是万能药不少测试同事都喜欢装Redis Desktop Manager或者Another Redis Desktop Manager看键列表、清Key确实方便。但这类工具偏重日常操作压测期间的持续监控和数据回放它并不擅长只适合你手动逛逛看某个具体Key的编码和TTL。真正的监控基线还是靠命令行脚本加时序监控平台。压测环境如果没有接入监控系统就用上一节那个while true采集脚本把数据保存下来结束后用Excel或者Python画趋势图。实测下来半天的压测采集几百条记录处理起来并不费劲比盯着面板拍脑袋靠谱得多。4. 把Redis内存压下来的七种手段与选型逻辑4.1 maxmemory与淘汰策略先定预算再谈压测压测前我强烈建议先给Redis设置内存预算。怎么定一般是把物理内存的一半或者三分之二留给Redis。如果机器是32GB内存Redis的maxmemory就设20GB左右留出空间给系统本身和旁路缓冲。设置方法很简单。# 运行时设置 redis-cli CONFIG SET maxmemory 20gb redis-cli CONFIG SET maxmemory-policy allkeys-lru淘汰策略的选择是重点。压测环境的Redis如果承担的都是可重建的缓存数据allkeys-lru是比较稳妥的选择它允许所有键参与LRU淘汰热点数据不容易被误杀冷数据会被优先清掉。如果业务里有一部分Key是持久化数据、不能被淘汰那就得用volatile-lru只淘汰那些设置了TTL的Key没设过期时间的Key永远不参与淘汰。allkeys-random和volatile-random只适合对缓存无差别对待的场景比如纯配置缓存、随机会丢失也无所谓的场景。volatile-ttl会优先淘汰即将过期的Key适合大量短期有效缓存。LFU系列策略适合热点分布极其集中的场景用访问频率替代最近访问时间来判断淘汰目标但需要额外开启maxmemory-policy后观察行为压测时容易因为策略切换带来缓存命中率波动所以别在压测进行到一半时改策略。有个极端场景必须提前预防如果Redis同时存了业务核心数据比如分布式锁、登录态和一些临时缓存而临时缓存又特别多压测时allkeys-lru很可能把核心的锁Key给淘汰掉导致应用层频繁报错。这种情况我会开两套Redis实例或者把核心Key的TTL设得很长让volatile-lru只淘汰短生命周期的Key。4.2 序列化、压缩与字段裁剪让单个Value瘦下来压测团队经常忽略序列化方式对内存的影响。你用Java的JDK序列化存一个用户对象和用Protobuf序列化同一个对象体积可能相差好几倍。同样的缓存内容在Redis里的物理内存占用也会相差好几倍。很多压测场景里的内存暴涨根本原因是应用层序列化太胖而不是Redis本身设置不合理。优先选二进制序列化方案能不用JSON字符串就不用。JSON虽然可读性好但字段名、结构化的花括号和引号都是实实在在的内存。在可读性要求高的场景至少也要用压缩算法把Value压缩后写入比如Gzip压缩后再存。CPU换内存这笔账大多数时候是划算的。对象型缓存用Hash替代整个JSON字符串单个大对象拆成多个小字段Redis的内存编码更紧凑更新字段时还不用整个重写。尽量别缓存大数据集合。一个Value里有几十万条用户记录读一次Redis再往内存塞一次应用节点也受不了这种场景该考虑换存储方案而不是硬抗Redis。按我的实践把一个普通Java对象从JSON存储改成Protobuf内存能省30%~50%压测中同样键数量下内存回落速度也更快。前提是应用层的序列化改造本身不引入新的CPU热点压测时要一并观察。4.3 压缩编码与配置项小对象都有的隐藏福利Redis对Hash、List、Set、ZSet这类结构在小数据量时有一套紧凑编码。早期是ziplist新的版本用listpack和quicklist。打开一个Hash你可以用OBJECT ENCODING key看看它当前用的编码格式。如果是listpack说明内存挺紧凑如果变成hashtable说明元素数量或单个元素大小超过了配置阈值编码升级了内存占用会明显上台阶。这些阈值由配置项控制比如hash-max-listpack-entries、hash-max-listpack-value、set-max-intset-entries等。压测时如果发现某个Key因为编码升级导致内存陡增可以考虑调高这些阈值把更多小对象留在紧凑编码里。但要记住阈值不是越高越好超过合理范围反而会让单个对象查询变慢形成CPU热点。取值一般参考官方默认值上下浮动不建议无脑拉到极大。另一个容易被忽略的配置是activedefrag也就是自动碎片整理。Redis 4.0以上支持开启主动碎片整理在碎片率高的时候自动搬运内存来降低碎片率但对CPU有一定消耗。压测前如果确认内存碎片率高可以开启它跑一段时间压测时再评估是否临时关闭以免整理过程干扰响应时间。我倾向于压测前先整理压测中保持稳定不动态开关。5. 高发内存问题的排查链路与避坑手册5.1 内存涨而不回收的常见病因压测中经常出现一个现象内存涨上去了压测结束后却不掉下来或者掉得很慢。排障时按顺序看四件事。先看是不是碎片。mem_fragmentation_ratio高于1.5说明分配器里的碎片占了大量物理内存哪怕对象逻辑内存已经释放RSS也居高不下。此时可以手动执行MEMORY PURGE把空闲的连续大块内存归还给操作系统。如果MEMORY PURGE之后还是很高多半是分配器的内存块被少量残余大对象卡住了只能等业务错峰重启实例。再看是不是过期键堆积。INFO stats里的expired_keys计数器如果急剧上升说明大量Key同时过期Redis会在访问时惰性删除或定期采样删除这些删除动作本身会消耗CPU内存回收需要时间。压测结束后如果看到曲线是阶梯式下降的就是过期清理在分批进行不是异常。然后看大Key造成的分配器僵局。一个大List占了几百MB删除它时Redis主线程会一次性释放一大块内存释放完之后的空闲块分配器不一定能还回系统。这种情形即使MEMORY PURGE也不一定有效只能把大Key改造成多个小Key避免单块内存太大。最后看复制和持久化缓冲。主从复制场景里主节点要维护每个从节点的复制积压缓冲区AOF重写时会额外开一块内存保存重写缓冲区。大流量下这些临时内存很容易占到几个GB压测中表现为RSS比used_memory高不少压测一停就自动回落这属于正常现象。5.2 一次压测内存飙高的完整排查思路分享一个我实际遇到的案例帮助你把上面的知识点串起来。当时压测一个用户列表查询接口JMeter并发从500升到1000整个接口TPS反而下降错误率升高。看监控Redis的used_memory_rss直接顶到物理内存上限mem_fragmentation_ratio掉到不到1.0说明RSS几乎全被实际占用填满了。我先跑redis-cli INFO stats发现evicted_keys在压测期间暴涨了几十万立刻判断是淘汰策略已经把缓存大量清理掉了。再看maxmemory发现这个测试环境Redis的maxmemory没有设置它其实是靠物理内存硬撑的。再顺手扫了一轮--bigkeys发现一个List里有50万条结构化数据单个Key就占了接近2GB内存而且这个Key之前根本不在我预期的大数据统计里。最终结论压测中这个接口的查询全落到数据库是因为一个大List缓存被淘汰策略清理后缓存命中率骤降而大List本身因为体量太大也占掉了大量内存预算。处理方式是改造成多条小Key分片缓存顺便给Redis配好maxmemory和allkeys-lru第二次压测时TPS曲线就恢复正常。这个案例的关键一步是没有直接被“Redis内存不够”带偏而是先分诊是淘汰问题、碎片问题还是大Key问题再决定改配置还是改业务代码。5.3 大Key与热Key压测中最容易忽视的干扰源大Key和热Key是Redis里最影响压测稳定性的两个变量它们看起来像是“容量问题”其实是“单分片性能问题”。大Key会导致网络传输慢、删除操作阻塞主线程、内存分配器出现碎片黑洞。判断方式就是redis-cli --bigkeys重点看是否有单个Key超过几十MB。一旦发现就需要把大Key拆成多个小Key比如Hash按ID范围分片List按时间窗口分桶ZSet按月份分Key。热Key则会造成单线程的Redis CPU飙到极限而其他分片或实例却很闲。排查热Key可以用redis-cli --hotkeys但它要求maxmemory-policy开启了LFU策略才统计准确。性能测试人员更常用的做法是看压测过程中Redis节点的instantaneous_ops_per_sec是否集中在某个时间点爆表再通过客户端侧的请求统计定位具体Key。压测场景如果包含热门直播、秒杀这类高集中请求必须提前做热Key预案本地缓存兜底或者在应用层做随机前缀分流。否则压测进行到高潮阶段Redis主线程单点CPU100%TPS上不去你可能会误以为服务线程池满了实际上卡点就压在Redis的单线程上。6. 个人踩坑后整理的内存管理清单每次压测前我都会把下面这份清单过一遍省了不少破事直接照抄也行。确认Redis版本4.0以下很多内存排查命令不可用先升级。压测前设置明确的maxmemory不要裸奔再根据业务数据性质选好淘汰策略。记录压测前的used_memory、used_memory_rss、Key数量、命中率作为基线。执行一次--bigkeys提前发现超大的Key能拆就拆。确认应用层序列化方式避免JSON字符串直接大量入缓存。压测中至少每5秒采集一次内存指标落盘而不是只看面板。重点盯evicted_keys增量只要它快速增长说明缓存命中率和压测结论已经失真。压测后跑一次MEMORY DOCTOR看碎片率是否超1.5考虑MEMORY PURGE。检查业务代码里是否同时存在短TTL和长TTL的Key避免过期风暴影响内存回收曲线。对多实例压测关注不同节点之间的内存分布差异单个节点内存飙升往往是热Key或Key分布不均造成的。最后再分享一个小习惯我每次压测前都会打一条INFO memory把基准信息随手记到压测报告附页里。这个习惯帮我避开了不少“压测结果明明有波动却找不到原因”的尴尬。Redis内存管理说到底就是一件需要提前做、全程盯、事后复盘的事远不是什么高深莫测的黑魔法但它确实直接决定你的性能测试报告靠不靠谱。