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频繁更新数据和删除key,jemalloc分配器在回收内存时不一定能归还给操作系统,而是留在进程内复用。碎片率高了以后,即使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来缩小丢失窗口。注意,即使设置了AOF,RDB仍然有存在价值——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设的是0,Redis不主动断任何一个空闲连接,运维重启了服务端才恢复。后来把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快照留存下来。这看起来麻烦,但等你在深夜排查一个诡异问题时,会发现这些记录是救命稻草。