☰
Redis性能成倍提升:请求链路、命令成本、连接模型与部署调优
2026/10/27 4:22:05 网站建设 项目流程

线上缓存一抖,业务侧就开始报警,这种场面做过高并发系统的人都不陌生。前阵子我把一套分布式缓存的核心链路重新梳理了一遍,按26个主题拆开逐个推敲,重点就盯着一件事:怎么把Redis处理性能成倍拉上去。这里说的“成倍”不是换个机器、加个配置就完事,而是从请求链路、命令成本、连接模型、部署形态四个层面同时下手,把原本被浪费掉的吞吐量抠出来。Redis处理性能的提升,本质上是一笔账:一次命令从客户端发出到结果返回,时间花在哪、哪些开销可以摊销、哪些压力可以分摊。搞不清楚这笔账,盲目加机器往往只是把同样的瓶颈复制了一遍。这篇文章适合已经用过Redis、但一到压测和调优就凭感觉的开发者,也适合正在设计缓存层、需要判断什么时候该上集群、什么时候该改客户端的同学。下面我按自己实际落地的顺序,把方案选型、参数计算、代码改造和踩坑记录完整拆开讲。

1. 成倍提升之前:先把Redis的性能账本算清楚

1.1 单线程事件循环不是瓶颈的全部

很多人一提到Redis性能,第一反应是“单线程”,仿佛单线程就是天花板。这个理解只对了一半。Redis的命令执行确实是单线程的,但它的单线程模型恰恰是为了避免锁竞争和上下文切换,内存操作本身是纳秒级的,单条GET/SET的执行时间通常在微秒量级。真正把吞吐卡住的,往往不是命令执行本身,而是命令到达之前和返回之后的那些环节:客户端序列化、网络往返、内核协议栈拷贝、响应反序列化,以及偶尔冒出来的大key和慢命令。

我做过一次对比,同样的机器、同样的数据量,单连接逐条GET的QPS只有几千,换成批量Pipeline之后直接冲到几万,命令执行逻辑一行没改。原因很简单,单连接串行请求时,每次都要等一个完整的网络往返,Redis再快也得等包回来。单线程事件循环基于epoll,能同时处理大量连接,但它处理的是“已经到达的命令”,命令在路上花的时间它管不了。所以判断瓶颈要先看CPU:如果Redis进程单核跑满,说明命令处理确实到顶了,这时候要考虑分片;如果CPU不高但QPS上不去,大概率是网络往返或客户端在拖后腿。

还有一个容易被忽略的点:单线程不代表所有操作都便宜。KEYS *、对大hash执行HGETALL、对大zset执行全范围ZRANGE,这些命令的时间复杂度是O(N),N是整个集合的大小,执行期间会阻塞其他所有请求。我见过一个线上案例,某个运营后台误触发了一次全量key扫描,Redis延迟瞬间从1毫秒飙到800毫秒,业务侧超时一片。后来把扫描改成SCAN分批,并限制在从节点执行,问题才消失。所以优化Redis处理性能的第一步,不是调参数,而是先确认有没有这类“一颗老鼠屎坏一锅汤”的命令。

1.2 一次命令从发出到返回,时间都花在哪

把一次Redis请求拆开,大致经过这些环节:业务代码组装参数、客户端序列化成RESP协议、写入TCP发送缓冲区、经过内核协议栈、到达服务端网卡、Redis事件循环读取并解析、执行命令、把响应写回缓冲区、网络返回客户端、客户端反序列化、业务代码拿到结果。每一段都有成本,只是占比不同。同机房内网RTT通常在0.1到0.5毫秒,跨机房可能到1到5毫秒;一次简单的GET命令执行可能只要几十微秒,但加上往返,单连接串行QPS就被限制在几千。

这里有个很实用的估算公式:单连接串行请求的吞吐上限约等于1除以RTT。假设RTT是0.2毫秒,理论上一秒最多5000次往返;如果每次往返只带一条命令,那单连接QPS就是5000左右。用了Pipeline之后,一次往返带N条命令,吞吐上限就变成N除以RTT,但N不能无限大,因为客户端要缓存请求、服务端要缓存响应,响应包太大会撑爆缓冲区,也会拉长单次延迟。我的经验是把单批响应控制在1MB以内,超过就拆批。

除了RTT,序列化成本也经常被低估。Java原生序列化把一个对象转成字节数组,动辄几百字节,还带类元信息;换成Protobuf或者MessagePack,体积能降一半以上。别小看这点体积,在QPS几万的场景下,网络带宽和客户端CPU都会被它吃掉。我曾经把一个缓存value从Java序列化换成JSON再换成MessagePack,同样的压测客户端,QPS提升了将近40%,Redis服务端CPU反而降了。原因就是客户端序列化耗时下降,请求发得更快,服务端处理同样数量的命令但等待更少。

1.3 三条优化主线:摊销RTT、降低单命令成本、分散单实例压力

把上面这些账算完,优化路径其实就很清楚了,我习惯归成三条主线。第一条是摊销网络往返,核心手段是Pipeline、批量命令和Lua脚本,让一次网络交互携带更多有效工作。第二条是降低单命令成本,包括选对数据结构、拆大key、减少value体积、避免慢命令,让每条命令执行得更快、占用更少。第三条是分散单实例压力,通过读写分离、集群分片、多级缓存把请求导到更多实例上,突破单机CPU和网卡上限。

这三条主线不是互斥的,实际落地往往要组合使用。我通常先做第一条和第二条,因为改动集中在客户端和键设计,风险可控、见效快;等到单实例确实扛不住,再考虑第三条。顺序反了会怎样?先上集群,结果每个分片里还是有大key和慢命令,问题只是被分散了,没有消失,运维复杂度还上去了。我见过一个团队在QPS刚过两万时就急着扩成六主六从,结果热key依旧打满某一个分片,其他分片却很闲,最后还得回头做热key拆分。所以先榨干单实例,再谈横向扩展,这个顺序更稳。

2. 方案选型:不同量级该选哪条路

2.1 单机、主从、哨兵、集群的边界

单机适合数据量不大、QPS在几万以内、对可用性要求不极端的场景,部署简单,排查方便。主从在单机基础上做数据副本,主写从读,适合读多写少;哨兵在主从之上解决故障自动切换,但不解决分片,数据量还是受单机内存限制。集群把数据按16384个槽分散到多个主节点,每个主节点再挂从节点,理论上可以线性扩展,但带来了跨槽操作的限制。

选型的判断依据其实就两个:数据量和写QPS。数据量超过单机内存的70%,或者写QPS接近单实例上限,就该考虑集群。读QPS高但写不多,可以先做主从读写分离,成本比集群低得多。这里有个细节,集群模式下多key操作必须落在同一个槽,MGET跨槽会直接报CROSSSLOT,Lua脚本和事务也一样。所以上集群之前要先梳理业务里的多key操作,用hash tag把相关key绑到同一个槽,比如user:{1001}:info和user:{1001}:order,大括号里的内容相同就会落到同一个槽。这个改造要提前做,别等上了集群再回头改代码。

哨兵和集群的高可用机制也不一样。哨兵需要客户端感知主从切换,集群自带槽位重定向和故障转移。如果团队运维力量有限、数据量又没到必须分片的程度,我倾向于先用主从加哨兵,把客户端连接池和超时做好,别一上来就追求集群。集群的槽迁移、节点扩缩容、跨槽问题,每一样都需要额外的监控和预案,小团队很容易被这些复杂度反噬。

2.2 Pipeline、批量命令与Lua脚本的取舍

这三样东西经常被混着用,但语义和适用场景完全不同。Pipeline只是把多条命令打包发送,不保证原子性,中间可能插入其他客户端的命令;它的价值纯粹是减少网络往返。批量命令如MGET、MSET是Redis原生支持的多key操作,原子性取决于命令本身,但同样受跨槽限制。Lua脚本把一段逻辑放到服务端执行,能保证原子性,适合“读-判断-写”这种需要一致性的场景,但脚本执行期间会阻塞其他命令,所以脚本必须短小。

我的选择顺序通常是:能用单命令批量解决的,优先用MGET/MSET;不能批量但允许非原子的,用Pipeline;必须原子且逻辑简单的,用Lua。Pipeline的批次大小建议从50到200开始压测,观察P99和客户端内存,太小摊销效果不明显,太大响应包膨胀、失败重试成本高。Lua脚本的执行时间要控制在10毫秒以内,绝对不要在脚本里循环遍历大集合,也不要执行KEYS。曾经有个团队用Lua做库存扣减,脚本里带了一个全量扫描,结果大促时整个分片卡死,教训很直接。

还有一个常见误区是把Pipeline当成事务用。Pipeline中途某条命令失败,其他命令照样执行,客户端要自己检查每条结果。如果业务要求全部成功或全部失败,要么用MULTI/EXEC事务,要么用Lua,但事务同样受集群同槽限制。我在实际项目里更倾向用Lua处理需要原子的短逻辑,用Pipeline处理日志上报、批量查询这类允许部分失败的场景,边界划清楚,后面排查问题会轻松很多。

2.3 客户端连接池与线程模型的选型

客户端这一层对性能的影响,很多时候比Redis服务端配置还大。传统Jedis是每个操作借还连接,池化能复用连接,但池大小设置不合理就会出问题。Lettuce基于Netty,多个线程共享同一个连接,不需要大池,适合高并发场景。选哪个要看团队技术栈和运维习惯,但不管选哪个,连接池的核心参数都要算,不能拍脑袋。

池大小的估算公式是:所需并发数等于峰值QPS乘以平均响应时间(秒),再乘一个1.2到1.5的冗余系数。举个例子,峰值QPS是3万,平均响应时间0.8毫秒,并发数就是30000乘以0.0008等于24,乘1.5后约36,池大小设40左右就够了。设太大反而有害,连接过多会让Redis频繁做上下文切换,内存开销也上去了。同时池大小不要超过Redis的maxclients的70%,给其他客户端和运维连接留余量。

超时设置也要按分位数来。连接超时可以设1秒,读写超时按P99的1.5到2倍设,比如P99是5毫秒,读写超时设10到20毫秒,不要设成几百毫秒甚至不设。超时太长会把故障放大,一个慢请求占着连接不放,后面请求排队,连接池很快耗尽。重试只对幂等命令做,最多两次,并且要指数退避,别搞成固定间隔重试风暴。我踩过一次坑,客户端把超时设成3秒,又没有重试上限,Redis抖动时连接池瞬间打满,业务线程全卡在等连接上,最后是靠限流和缩短超时才恢复的。

2.4 多级缓存与本地缓存的引入时机

当Redis本身已经优化到瓶颈,但业务对延迟仍然敏感,就该考虑多级缓存了。最前面放一层本地缓存,比如Caffeine或者Guava Cache,存那些访问频率极高、更新不频繁的热key,TTL设短一点,比如几百毫秒到几秒。本地缓存命中后直接返回,连网络往返都省了,对热key的QPS削减非常明显。但本地缓存带来一致性问题,多实例之间数据可能不同步,所以只适合对最终一致可接受的场景,比如商品详情、配置项、榜单快照。

引入本地缓存之前,先把热key找出来。可以用redis-cli --hotkeys,前提是淘汰策略设成LFU;也可以用MONITOR短时间采样,但MONITOR本身影响性能,采样时间要短。找到热key后评估更新频率,如果每秒都在变,本地缓存意义不大,反而增加不一致窗口;如果几分钟才变一次,就很适合。我一般会把本地缓存的失效做两层:短TTL自然过期,加上业务侧的消息通知主动失效,尽量把不一致窗口压到秒级以内。

多级缓存不是越靠前越好,本地缓存会占用业务进程的堆内存,缓存对象太大或者key太多,容易引发GC问题。我的做法是限制本地缓存的条目数和单条大小,比如最多一万条、单条不超过10KB,超过就走Redis。这样既吃到热key的红利,又不至于把业务进程拖垮。

3. 把单次请求的成本压到最低:核心参数与数据结构

3.1 键设计与数据结构选型

键设计看起来是小问题,但在高QPS下会累积成可观的成本。key太长会占用更多内存,网络传输和哈希计算也多一点;key太短又不利于排查。我通常用“业务前缀:对象ID:属性”的格式,控制在几十字节以内,避免在key里放时间戳、随机数这类高基数内容,否则内存碎片和过期管理都会变麻烦。还有一个原则是不要把不同生命周期的数据塞进同一个key,否则过期时间没法分别设置。

数据结构选型要按访问模式来。存对象字段,用Hash比String加JSON更省内存,也能单独更新某个字段,但field数量不要太多,超过一千个就考虑拆。存列表用List,但只从两端操作;需要按范围查询用ZSet;去重计数用Set或者HyperLogLog,后者有误差但内存极低;布尔状态用Bitmap,比如签到场景,一年365天只需要几十字节。很多性能问题不是Redis慢,而是用错了结构,比如用String存一个大JSON,每次更新都要整体读写,QPS和带宽都被放大。

这里补一个常被忽略的点:Hash的field数量多时,HGETALL会阻塞,应该用HSCAN分批;Set很大时用SSCAN;ZSet范围查询要带LIMIT。删除大集合不要用DEL,用UNLINK异步删除,避免阻塞主线程。这些操作在低峰期做,别在业务高峰期执行。

3.2 大key拆分与热key治理

大key是Redis性能的隐形杀手。判断标准不绝对,我的经验是String超过10KB、Hash/Set/ZSet元素超过5000、List超过5000,就该警惕。大key的危害有三层:网络传输时间长、删除时阻塞、迁移时卡槽。排查用redis-cli --bigkeys,它会扫描并给出每种类型的最大key,但要注意这个命令本身有开销,放在从节点或者低峰期执行。

拆分大key的思路是按维度打散。比如一个用户的全部订单存在一个Hash里,可以按订单ID取模拆成多个Hash,order:{userId}:{0..9},每次只查对应分片。或者按时间分片,按月存不同key。拆完之后要注意聚合查询的逻辑,客户端要能合并结果。删除大key用UNLINK,如果版本不支持,就先改名为一个临时key,再慢慢删。

热key的治理思路不一样,热key本身可能不大,但QPS集中在一个key上,会把某个分片的CPU打满。治理手段有三个:本地缓存挡住大部分读;给key加随机后缀做读分片,比如hotkey:{0..9},写入时写十份,读取时随机选一个;读写分离,把读流量导到从节点。加后缀的方案会增加写入成本和一致性复杂度,只适合读远多于写、且能容忍短时间不一致的场景。我一般优先用本地缓存,实在不行再考虑后缀分片。

3.3 序列化、压缩与网络参数

序列化方案的选择前面提过,核心就是体积小、速度快。JSON可读性好但体积大,Protobuf和MessagePack体积小但需要定义schema。如果value超过1KB,可以考虑压缩,Snappy或者LZ4的压缩比不错,CPU开销也能接受,但小于1KB的数据压缩反而得不偿失,压缩头和解压时间会抵消收益。我的经验阈值是1KB起步,压缩后如果体积没降30%以上,就放弃压缩。

网络参数方面,tcp-nodelay yes建议开启,禁用Nagle算法,减少小包延迟;tcp-keepalive设300秒左右,及时发现死连接;timeout不要设太小,设0或者300秒,避免空闲连接被误杀导致客户端反复重连。tcp-backlog默认511,在高并发下可以调到2048,同时把系统somaxconn调大。Redis 6之后支持io-threads,把网络读写放到多线程,但命令执行还是单线程。这个参数不是越大越好,4到8线程通常够了,小包场景提升有限,反而增加线程切换开销。开启时记得同时设io-threads-do-reads yes。

3.4 持久化与内存配置的隐性影响

持久化对性能的影响经常被低估。RDB做fork时,虽然子进程写时复制,但fork本身要复制页表,内存越大耗时越长,期间主线程会短暂阻塞。AOF的everysec策略每秒刷盘一次,性能不错,但最多丢一秒数据;always每条都刷,性能下降明显;no交给操作系统,不推荐。混合持久化aof-use-rdb-preamble yes兼顾恢复速度和文件体积,我一般会开。

AOF重写期间CPU和磁盘IO都会上升,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size要设合理,别让重写太频繁。内存方面,maxmemory设成物理内存的70%到80%,留出fork和碎片的余量;淘汰策略按业务选,纯缓存用allkeys-lru,有持久数据用volatile-lru。activedefrag yes可以在运行时整理碎片,但会消耗CPU,碎片率不高时没必要开。系统层面记得关闭透明大页,设vm.overcommit_memory=1,这两个是老生常谈,但每次换机器都有人忘。

4. 实操全过程:从压测基线到翻倍提升

4.1 压测环境搭建与基线数据采集

动手改之前先建基线,否则后面说“提升了一倍”没有依据。压测环境尽量贴近生产:同样的机器规格、同样的网络拓扑、同样的value大小。工具可以用redis-benchmark做快速验证,命令是redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -P 16 -t set,get -q,其中-c是并发连接数,-n是总请求数,-P是Pipeline批大小。更贴近真实业务的是memtier_benchmark,能模拟读写比例和key分布。

基线要记录的指标不只QPS,还包括P50、P95、P99延迟、Redis进程CPU、网络带宽、连接数、内存使用和碎片率。压测时固定value大小,比如1KB,固定客户端数量,跑三轮取稳定值。同时用redis-cli --latency -h host -p port持续观察延迟,用INFO stats看instantaneous_ops_per_sec,用INFO commandstats看每类命令的调用次数和耗时。基线数据存下来,后面每改一项就对比一次,避免多项改动混在一起说不清是谁的功劳。

我还会在压测期间盯一下客户端的CPU和GC。很多时候Redis服务端还没到瓶颈,压测客户端先撑不住了,结果误判成Redis性能不行。压测客户端最好独立机器,不要和Redis混部。

4.2 Pipeline改造:从逐条发到批量发

Pipeline改造是最容易见效的一步。以Java Jedis为例,逐条GET的写法是循环调用jedis.get(key),改成Pipeline:

Pipeline pipeline = jedis.pipelined(); for (String key : keys) { pipeline.get(key); } List<Object> results = pipeline.syncAndReturnAll();

注意批次不要一次把所有key都塞进去,按200到500一批循环处理,每批结束后检查结果。Python的redis-py写法类似:

pipe = client.pipeline(transaction=False) for key in keys: pipe.get(key) results = pipe.execute()

transaction=False表示不开启事务,纯粹批量发送,性能更好。如果业务需要原子性,再考虑transaction=True或者Lua。改造后要观察响应包大小,如果单批返回超过1MB,就把批次调小。还要处理部分失败的情况,Pipeline不会因为某条命令失败而中断,客户端要逐条检查结果,记录失败key后续补偿。

Lua脚本适合“读不到就写”的原子逻辑,比如:

local value = redis.call('GET', KEYS[1]) if value then return value end redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) return ARGV[1]

脚本里只做简单判断和写入,不要循环大集合。集群模式下要确保KEYS[1]和脚本里用到的其他key在同一个槽,否则直接报错。

4.3 连接池参数计算与客户端调优

连接池参数按前面的公式算。假设压测目标是峰值3万QPS,平均响应时间0.8毫秒,并发数24,冗余系数1.5,池大小36到40。Jedis配置里maxTotal=40、maxIdle=40、minIdle=10、maxWaitMillis=200。maxWaitMillis不要设太大,等200毫秒还拿不到连接,说明池不够或者有泄漏,快速失败比长时间排队更好。

Lettuce不需要这么大的池,因为它共享连接,重点设命令超时和连接超时。不管用哪个客户端,都要监控连接池的活跃数、空闲数和等待数。等待数持续大于0,说明池太小或者下游变慢;活跃数长期等于最大值,也要警惕。连接泄漏的排查方法是看CLIENT LIST里的连接年龄和空闲时间,如果大量连接空闲时间很短但又不释放,多半是借出没归还。

超时和重试要配套。连接超时1秒,读写超时按P99的1.5倍设,重试只对GET这类幂等命令,最多两次,间隔带随机抖动。非幂等命令比如INCR不要自动重试,否则可能重复扣减。限流和熔断也要有,Redis整体不可用时,快速失败并降级到本地缓存或者数据库,别让请求全堆在连接池上。

4.4 读写分离与集群分片落地

读多写少的场景,先做主从读写分离。从节点设readonly yes,客户端把读请求路由到从节点,写请求走主节点。要注意复制延迟,用INFO replication对比主从的master_repl_offset,延迟超过业务容忍度时,关键读还是走主。从节点可以承担压测、大key扫描、备份这些重活,减轻主节点压力。

单实例写QPS到顶,再上集群。集群至少三主三从,槽位均匀分布。客户端用JedisCluster或者Lettuce的集群模式,它们会自动处理MOVED和ASK重定向。上集群前把多key操作改成同槽,用hash tag绑定。扩容时用redis-cli --cluster reshard迁移槽,迁移期间会有ASK重定向,客户端要支持,迁移尽量在低峰期做,大key提前拆掉,否则迁移会卡住。

集群的监控要关注槽位分布、节点间网络、故障转移次数。cluster-node-timeout默认15秒,根据业务容忍度调整,太小容易误判,太大故障恢复慢。集群模式下不要用KEYS、FLUSHALL这类全量命令,SELECT也不支持,数据库只有0号库,这些限制要在设计阶段就考虑进去。

4.5 系统参数与Redis配置调优

系统层面,把文件描述符限制调大,ulimit -n 65535,somaxconn调到65535,tcp_max_syn_backlog相应调大。关闭透明大页,设vm.overcommit_memory=1,vm.swappiness=1,避免内存被换出。这些参数改完要重启或者用sysctl -w生效,记得写进开机配置。

Redis配置里,maxclients按实际连接数设,比如10000,留出余量;tcp-backlog 2048;timeout 300;慢查询阈值slowlog-log-slower-than 10000,也就是10毫秒,慢查询日志保留128条;latency-monitor-threshold 100,超过100毫秒的操作记录下来。io-threads按CPU核数设4到8,开启读多线程。AOF用everysec,开启混合持久化,重写阈值设合理。

改配置要一项一项改,每改一项压测一轮,记录变化。一次性改十几个参数,出了问题根本不知道是哪个引起的。我一般的顺序是:先客户端Pipeline和连接池,再键设计和序列化,再系统参数,最后持久化和集群。每一步都有回滚方案,压测通过再上生产。

4.6 验证结果与回归观察

改造完成后用同一套压测脚本复测,对比基线。我最近一次落地的结果是QPS从4万左右提升到9万,P99从8毫秒降到2.3毫秒,Redis进程CPU从单核打满变成多核均衡,错误率保持在万分之一以下。这个提升不是靠单一手段,而是Pipeline摊销往返、value体积压缩、连接池调优、从节点分担读流量几项叠加出来的。

上线后要观察至少24小时,重点看延迟分位数、慢查询数量、连接数、内存碎片率和主从延迟。压测环境和生产环境有差异,压测通过不代表生产一定稳。回归期间保留回滚开关,比如Pipeline批次大小、是否走从节点读,都可以通过配置中心动态调整。还有一点,提升之后客户端CPU可能反而上升,因为请求发得更密了,要同步观察业务机器的负载,别把瓶颈从Redis转移到业务进程。

5. 常见问题与排查技巧实录

5.1 延迟毛刺与慢查询定位

延迟毛刺是最难缠的问题,平均延迟看着正常,P99突然飙到几百毫秒。排查顺序我一般是:先看毛刺发生的时间点,再看SLOWLOG GET 10有没有慢命令,再看LATENCY HISTORY和LATENCY GRAPH,最后看系统监控里的磁盘IO、网络和内存。周期性毛刺比如每小时一次,多半和RDB保存、AOF重写或者定时任务有关;偶发毛刺要看有没有大key删除、KEYS扫描、集群槽迁移。

redis-cli --latency适合长时间观察,能看出延迟分布。INFO latencystats可以看每类命令的延迟分位数。如果慢查询里出现HGETALL、ZRANGE、SMEMBERS这类命令,基本就是大key问题,改成HSCAN、带LIMIT的范围查询或者分批SSCAN。如果慢查询是DEL,换成UNLINK。还有一种情况是客户端GC导致的毛刺,Redis侧看起来很健康,但客户端停顿几百毫秒,请求堆积后集中打过来,这种要看客户端的GC日志。

5.2 内存抖动、碎片与淘汰

内存问题通常表现为两种:一种是used_memory持续上涨直到触发淘汰,另一种是used_memory不高但RSS很高,碎片率mem_fragmentation_ratio超过1.5。前者要查是不是有key没设过期时间,或者业务写入量突增;后者可以考虑开启activedefrag,或者在低峰期重启实例。注意activedefrag会消耗CPU,碎片不严重时别开。

大量key同时过期也会引起抖动,因为过期删除是惰性加定期采样,集中过期时主线程要处理大量删除。解决办法是给过期时间加随机偏移,比如基础TTL加0到300秒的随机值,把过期分散开。淘汰策略要和业务匹配,纯缓存用allkeys-lru,有不能丢的数据用volatile-lru并且确保这类key都设了过期时间,否则淘汰不到它们。maxmemory别设成物理内存的100%,留出fork和碎片的余量,70%到80%比较稳妥。

5.3 连接数暴涨与超时风暴

连接数突然暴涨,先用CLIENT LIST看连接的cmd、age、idle字段。如果大量连接处于空闲但又不释放,多半是连接池泄漏或者客户端没有正确关闭连接;如果大量连接卡在某个命令上,说明下游变慢导致连接堆积。CLIENT LIST TYPE normal可以过滤掉订阅类连接,看着更清楚。maxclients打满后新连接会被拒绝,报错“max number of clients reached”,这时候要先限流保命,再排查根因。

超时风暴往往和重试策略有关。下游一慢,客户端超时,然后重试,重试又加重下游负担,形成正反馈。解决办法是超时时间按分位数设、重试次数限制、加指数退避和随机抖动,同时在入口做限流和熔断。Redis前面最好有一层本地缓存兜底,Redis不可用时返回旧数据或者默认值,别让请求直接打到数据库。我踩过一次坑,客户端重试间隔固定100毫秒且不限次数,Redis抖动两秒,重试请求翻了几倍,最后是靠紧急限流才压下去的。

5.4 集群槽位、迁移与跨槽问题

集群最常见的报错是CROSSSLOT,多key操作不在同一个槽。解决就是用hash tag,比如把user:1001:info和user:1001:order改成user:{1001}:info和user:{1001}:order。注意hash tag只取大括号里的内容做CRC16,大括号里不要放太多变化的内容,否则分片不均匀。MOVED和ASK重定向是正常现象,客户端要能自动处理并刷新槽缓存,如果客户端版本太老不支持,就会频繁报错。

槽迁移时大key会卡住,因为迁移是同步的,一个大key可能几十MB,迁移期间该槽的请求会阻塞。迁移前用--bigkeys扫一遍,把大key拆掉或者提前迁移。迁移用redis-cli --cluster reshard,指定源节点、目标节点和槽数量,过程中观察cluster nodes的槽状态。故障转移时间由cluster-node-timeout控制,设太小容易误判,设太大恢复慢,15秒左右是常见起点,再按业务容忍度调。

5.5 常见问题速查表

现象可能原因排查命令处理建议
P99延迟周期性飙高RDB保存或AOF重写INFO persistence、LATENCY HISTORY调整重写阈值,低峰期执行,考虑混合持久化
慢查询里出现HGETALL/ZRANGE大key或大范围查询SLOWLOG GET 10、--bigkeys改HSCAN、加LIMIT、拆分大key
连接数暴涨连接池泄漏或下游变慢CLIENT LIST、池监控修连接归还、缩短超时、加限流
内存碎片率高频繁增删、value大小不均INFO memory看碎片率开启activedefrag或低峰重启
CROSSSLOT报错多key不在同一槽客户端错误日志用hash tag绑定相关key
集群迁移卡住大key同步迁移CLUSTER NODES、--bigkeys迁移前拆大key,低峰期迁移
大量key同时过期过期时间集中INFO keyspace、业务写入逻辑TTL加随机偏移,分散过期
客户端超时但Redis正常客户端GC或网络抖动客户端GC日志、--latency优化序列化、调大堆或换客户端

这张表我一般贴在排查手册第一页,遇到问题先对照一遍,能省不少时间。表里的命令都不重,生产环境可以直接跑,但--bigkeys和MONITOR这类要控制频率,别在高峰期反复执行。

我现在的习惯是每次上线前先跑一遍基线压测,把QPS、P99、CPU、连接数记下来,改一项测一项,不把多个变量混在一起。踩过的坑告诉我,Redis性能提升最忌讳一上来就加机器、堆配置,先把网络往返摊销掉、把大key和慢命令清掉、把连接池算清楚,往往一台机器就能干原来三台的活。后面如果业务继续涨,再按数据量和写QPS的边界去扩主从或者上集群,心里也有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询