Redis性能调优指南:内存淘汰、持久化与延迟排查实践 1. 参数调优前先搞清楚Redis到底卡在哪很多人一提到Redis调优上手就是改maxmemory、改save策略改完发现效果不大甚至更糟。我见过不少同事折腾一整天最后发现瓶颈根本不在Redis参数上而是应用层把Redis当成了万能存储塞进去一大堆不该放的数据。先说一个基本判断Redis的参数调优不是玄学也不是抄一份最佳实践就能搞定的事。它本质上是在回答三个问题——内存够不够、吞吐够不够、数据安全级别要多少。这三个问题的答案直接决定了你该动哪几个参数以及参数之间怎么搭配。所以动参数之前我建议先做一轮现状摸底主要看三块通过INFO STATS看命令处理量、命中率、拒绝连接数判断是CPU打满还是内存吃紧。通过INFO MEMORY看内存分配、碎片率、key总量和平均大小判断内存水位。通过SLOWLOG GET 20看慢查询这一步非常关键很多隐藏的阻塞问题都是在这里暴露的。我习惯把这轮数据存个快照改完参数之后再拉一次对比。没有基线数据就调优基本属于凭感觉开车出了问题连回滚的依据都没有。另外要提一句现在很多云厂商提供的托管Redis都能在控制台直接看监控面板但我仍然建议自己拉一次INFO层面的原始数据。原因很简单监控面板的指标是聚合过的很多细节——比如某个特定命令的耗时分布、某个key的访问频次——只有直接看原始统计才能发现问题。2. maxmemory与内存淘汰策略保命参数的正确设置方式2.1 maxmemory不是设得越大越好maxmemory是Redis内存使用的天花板这个参数本身不难理解但很多人设的时候容易走极端。一种是不设让Redis随便用另一种是设得过于保守导致大量淘汰。先说为什么不设不行。Redis虽然是内存数据库但它跑在操作系统之上内存用完之前操作系统会先触发OOM Killer直接把Redis进程杀掉。这个后果比淘汰一些key严重得多。所以生产环境必须设置maxmemory而且必须低于物理内存总量要给操作系统、其他进程、以及Redis自身的复制积压缓冲区和客户端输出缓冲区留出空间。具体留多少没有标准答案我自己在裸金属部署时常见的做法是机器总内存的60%-70%给Redis。比如32GB的机器maxmemory设20GB左右。如果是容器化部署需要看Pod的内存限额一般设限额的80%左右避免Redis内存增长把容器打爆。为什么不用满因为Redis的内存除了存储数据本身还要承担一些瞬时开销主从复制的积压缓冲区、客户端输出缓冲区、AOF重写期间的临时内存翻倍、以及内存碎片。这些都可能让实际RSS常驻内存超过maxmemory的设定值。如果设得太满任何一个瞬时高峰都可能让进程被系统杀掉。2.2 淘汰策略选不对命中率会雪崩设置maxmemory之后第二个必须面对的问题是内存满了新写入怎么办这就要配maxmemory-policy。Redis 4.0之后主流的淘汰策略有这么几类我直接说结论noeviction不淘汰写不进去报错。适合数据绝对不能丢的场景但Redis当存储用才会有这种需求当缓存用基本不会选它。allkeys-lru对所有key按LRU近似算法淘汰最久没用的。这是最常见的缓存场景配置。allkeys-lfu按访问频率淘汰适合访问热点非常集中的场景。volatile-lru只对设了过期时间的key做LRU淘汰没设过期时间的key永不被淘汰。如果你混用缓存和持久化存储这个策略可以保护那些“不能丢”的数据。我的经验是绝大多数纯缓存场景allkeys-lru是最省心的选择。它的实现是近似LRU不是精确的Redis用采样方式在候选key里挑最久没访问的淘汰默认采样数maxmemory-samples是5。如果对淘汰精度有要求可以提高采样数到10但会略增CPU开销。这里有一个容易被忽略的点volatile-lru听起来很美好能保住没有过期时间的key但实际运行中如果大部分key都设了过期时间它和allkeys-lru区别不大如果只有一小部分key设了过期时间那淘汰压力会全部集中在这部分key上可能导致有TTL的数据大量提前消失缓存命中率瞬间雪崩。所以选策略之前先想清楚你的key里有没有TTL、比例是多少。2.3 内存碎片应该主动处理INFO MEMORY里的mem_fragmentation_ratio是个值得长期关注的指标。简单说这个比值是RSS除以分配的内存总量正常在1到1.5之间。如果长期在1.5以上说明内存碎片严重白白浪费空间。碎片是怎么来的Redis频繁更新数据和删除keyjemalloc分配器在回收内存时不一定能归还给操作系统而是留在进程内复用。碎片率高了以后即使maxmemory还没打满实际占用的物理内存也可能已经很高。处理方式有两种一种是用MEMORY PURGE命令这个命令需要谨慎使用实测中它偶尔会阻塞另一种更推荐在低峰期执行CONFIG SET activedefrag yes让Redis后台自动整理碎片。激活之后可以配合active-defrag-threshold-lower和active-defrag-ignore-bytes控制启动整理的条件避免整理本身消耗过多CPU。我在一个模拟项目里遇到过碎片率飙升到1.8的情况开启自动整理一周后回落到了1.2左右效果还是明显的。但要注意碎片整理期间主线程CPU会有所上升如果本身实例已经到了高负载建议在低峰期开。3. 持久化参数RDB和AOF之间不是二选一3.1 save参数RDB触发频率的时机选择RDB的本质是定期把内存快照写入磁盘。触发方式有两种一种是SAVE命令同步阻塞另一种是BGSAVE命令fork子进程写盘主进程不阻塞。生产环境默认执行的BGSAVE就是第二种。sava配置项Redis配置里是save seconds changes格式定义了自动触发BGSAVE的条件。比如默认配置里有save 900 1 save 300 10 save 60 10000意思是900秒内至少有1次写操作就做快照300秒内至少10次写操作就做快照60秒内至少10000次写操作就做快照。很多人的误区是把save时间调大减少快照频率以为能降低磁盘压力。这个方向没错但代价是数据丢失窗口变长。如果Redis在两次快照之间宕机这期间写入的数据全部丢失。对缓存场景来说无所谓重启后从上游数据库回源就行但如果你把Redis当作准持久化存储就得认真权衡。我个人的实践是对缓存场景要么不开启RDB注释掉save配置要么用save 900 1这种宽松节奏兜底对需要恢复数据的场景快照间隔不要超过5分钟同时必须叠加AOF来缩小丢失窗口。注意即使设置了AOFRDB仍然有存在价值——AOF重写期间Redis需要RDB作为基础快照两者不是互斥关系。3.2 appendfsync每秒刷盘是多数场景的甜点值AOF的刷盘策略有always、everysec、no三档这个参数直接关系到数据安全性和性能之间的取舍。always每条写命令都同步刷盘。数据最安全但性能损失大实测在高并发下吞吐能下降一半以上一般不建议用在写入频繁的场景。everysec每秒刷一次盘宕机最多丢1秒的数据。这是绝大多数生产环境的选择性能和可靠性平衡得最好。no刷盘时机交给操作系统理论上性能最好但宕机时丢失数据量不可控。我在压测环境里对比过always和everysec写入吞吐从12万QPS掉到5万QPS左右差距非常惊人。所以除非你的业务对数据安全有极端要求比如金融级场景否则不要轻易碰always。还有一个相关参数是appendfsync的兄弟no-appendfsync-on-rewrite默认是no。这个参数控制AOF重写期间要不要继续刷盘。如果开了yes重写期间不刷盘性能更平滑但一旦宕机重写窗口期的数据会丢。我建议保持默认的no别为了这点性能去牺牲数据安全AOF重写本身并不频繁。3.3 AOF重写参数的连锁反应AOF文件会随着写入不断变大Redis提供了自动重写机制由两个参数控制auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb意思是AOF文件比上次重写后的文件增长了100%时且文件大小超过64MB触发重写。重写时Redis会fork子进程把当前数据集转换成最小化命令序列替换掉旧的AOF文件。这里要提醒的是AOF重写瞬间的内存和使用高峰容易被低估。虽然fork用了COW写时复制机制但如果重写期间写入量大父子进程共享的内存页大量被复制RSS会飙高。我把这个现象叫做“内存双峰”在maxmemory设置太过激进的情况下这个双峰可能导致内存超过物理上限。应对办法一是给maxmemory留足冗余二是关注aof-rewrite-incremental-fsync这个参数默认yes即重写过程中每32MB增量刷一次盘避免一次性刷大文件造成IO尖刺。另一个实用做法是手动错峰比如在业务低峰用BGREWRITEAOF触发一次重写避免它自动撞上高峰期。4. 网络与客户端交互参数高峰期卡顿的隐形元凶4.1 timeout连接空闲多久会被断开timeout默认是0表示连接永不超时。很多人在内网环境觉得没必要设但实际运行中这个参数很有意义。客户端因为网络抖动或代码bug建了连接但不再使用这些半开连接会占着Redis的文件描述符量大了之后新连接进不来表现就是“Redis连不上了”。我遇到过一个真实案例某服务端的连接池配置不合理大量空闲连接不回收Redis的connected_clients从正常的几百涨到几千文件描述符被打满。当时timeout设的是0Redis不主动断任何一个空闲连接运维重启了服务端才恢复。后来把timeout设为30秒再配合连接池的空闲回收策略问题就很少出现了。不过要注意timeout只对普通客户端连接生效不会误杀正在执行阻塞命令如BLPOP、SUBSCRIBE的连接这个设计比较合理。4.2 tcp-backlog与somaxconn并发连接排队的两道闸tcp-backlog默认是511它表示Redis内核socket监听的排队连接数上限。如果短时间内有大量新连接涌入超过这个值内核会丢弃多余的连接请求客户端那边就是connection refused。光调Redis这一层还不够。Linux内核的somaxconn参数默认是128它限制了所有socket的accept队列长度。即使tcp-backlog设为511只要somaxconn没调大实际有效队列长度还是以128为准。所以这是一组配合参数改Redis配置的同时要检查系统层。排查方法很简单看客户端日志里有没有“Cannot assign requested address”或者“Connection reset by peer”这类报错同时拉一下Redis的INFO STATS看rejected_connections字段。如果这个值持续增长基本就是backlog不够用了。4.3 输出缓冲区限制大key和慢消费的连锁崩溃client-output-buffer-limit控制三类连接的输出缓冲区上限normal普通客户端、replica从库节点、pubsub发布订阅客户端。这里我重点提醒普通客户端的配置client-output-buffer-limit normal 0 0 0默认是0表示不限制。为什么不限制会出问题如果你执行了一条KEYS *或者SMEMBERS返回了超大集合而客户端消费速度跟不上Redis会先把结果缓存在输出缓冲区里。不限制的话这个缓冲区可能无限增长直到吃光内存而且它不受maxmemory淘汰机制管——淘汰只是针对普通key输出缓冲区不在其中。我对生产实例一般会主动设一条限制比如client-output-buffer-limit normal 256mb 128mb 60含义是硬限制256MB软限制128MB且持续60秒超过就断开连接。宁可断开一个连接让客户端感知并重试也不能让Redis内存被输出缓冲拖垮。另外防止KEYS *这种命令在线上执行是基本功。真要遍历key老老实实用SCAN。我在团队里立过一条规矩代码评审阶段见到KEYS直接打回没有例外。4.4 慢查询阈值与延迟监控参数效果怎么验证调优不是改完就算完得知道改动有没有实际效果。我每次调完都会重点看两个指标一个是SLOWLOG GET的长度变化另一个是延迟分布。slowlog-log-slower-than默认10000微秒10毫秒如果你的业务对延迟敏感建议下调到5000甚至2000微秒这样能捕捉到更多潜在慢操作。slowlog-max-len默认128条可以调大到512免得排查问题时日志被冲掉。延迟监控除了用redis-cli --latency观察实例整体延迟更重要的是结合慢查询日志反推具体是哪些命令在拖后腿。常见的大头无非几种大key的集合操作、HGETALL取全量大hash、排序和交集并集运算、以及大量key过期导致的同步清理。5. 数据过期与内存清理定期删除的“坑”比想象中大Redis删除过期key用的是惰性删除加定期删除的组合策略这个机制本身很成熟但在key量极大且过期时间集中的场景下会有一个容易被忽视的尖峰问题。惰性删除是指访问key时才检查是否过期过期了就删。但Redis不能只靠这一个机制否则大量的过期key会一直占着内存。所以后台有一个定期删除的循环任务Redis 2.6之后由hz参数控制频率默认10即每秒执行10次后台任务。每次任务会随机抽取一批key检查过期情况删除其中的过期key但为了防止CPU被完全占用每次执行时间有限制。这种机制在正常情况下运行良好但假设你导入了一批数据全部设置了同一个过期时间比如凌晨3点统一过期后台删除加上惰性删除的叠加在那个时间点会形成CPU尖峰甚至引起请求延迟抖动。处理这个问题的思路有三种过期时间做抖动不要把过期时间设成固定值加一个随机偏移量比如TTL 基础值 random(0, 300)。这个做法成本最低效果最明显。控制单次导入的key总量如果一次性导入上千万key尽量分批不要集中在同一批。关注hz和dynamic-hz的调整hz越大后台任务执行越频繁过期key被及时清理但CPU开销也变大hz越小CPU更省但过期key滞留内存的时间更长。默认10对大多数场景够用我只有在key量超过千万级时才会考虑调到16-20。这里再说一个内存上的隐性消耗key明明过期了但过期key仍占着内存没被清理INFO MEMORY里的expired_keys和实际内存趋势能看出来。如果发现内存居高不下但明明有很多key过期多半是过期key清理不及时这时除了调整hz还要检查是不是大量长TTL的key堆积导致的。6. 一次实战调优的完整排查链路最后分享一个我实际经历过的问题用完整的排查路径来说明前面这些参数是如何被一个接一个挖出来的。某个模拟项目上线后Redis实例在每天下午3点到4点之间出现周期性的延迟抖动最严重的时候P99延迟从2毫秒飙到300毫秒以上。表面上看是典型的“高峰期不够用”但加机器这种事不能乱来我决定先从参数和运行机制入手。第一步拉慢查询。SLOWLOG GET 50的结果显示慢命令集中在DEL和SETEX单条耗时在200毫秒到1秒不等。这里有个点值得注意DEL一个key本身很快但如果这个key的value是一个包含几百万字段的hash删除时同步释放内存的过程会阻塞主线程。第二步查key大小。用redis-cli --bigkeys跑了一轮发现确实存在几个超大key最大的一个hash有超过300万字段。这就是根源——大key的删除和重建造成的阻塞。第三步追溯这些大key是怎么产生的。看业务代码发现是把用户的全量行为数据都往一个hash里塞没有做拆分。这个问题靠参数调优解决不了必须改应用层逻辑。改法是把hash按时间维度拆成多个key每个key的字段数控制在1万以内。第四步处理其他连带问题。既然发现了大key顺手检查内存碎片率发现是1.4属于偏高但可以接受的范围检查连接数空闲连接很多把timeout从0改成60秒检查淘汰策略确认是allkeys-lru没毛病。第五步改完上线后持续观察一周。P99延迟回到3毫秒以内慢查询基本消失内存碎片率在运行几天后自动降到1.2左右。整个过程没有动maxmemory和appendfsync因为基线数据显示内存水位和持久化策略都不是瓶颈。这个案例我想说明的是参数调优不是孤立活动它只是整个性能排查链路中的一个环节。很多时候问题藏在应用层的数据结构设计里参数只是把问题暴露得更清楚。我自己调了这么多年Redis最深的一个体会是——每个参数背后都对应一个取舍。你要快就得多吃内存你要安全就得付出写盘的性能你要大key自由就得承担阻塞的风险。参数之间还会互相影响不是单一维度的最优解。所以别迷信任何一份“最佳参数配置”回到你自己的监控数据里找答案才是唯一靠谱的路。最后补一个小建议每次改动不管多小都用CONFIG SET做在线调整并搭配CONFIG REWRITE持久化到配置文件。同时把改动前后的INFO快照留存下来。这看起来麻烦但等你在深夜排查一个诡异问题时会发现这些记录是救命稻草。