Article
从单机到2000万QPS:Redis缓存架构演进实战拆解
mhpn.cn 原创整理 · 三门峡/驻马店本地视角 · 阅读约 3 分钟 · 看完可电话咨询
TEMPLATE PREVIEW · 文章页模板示意 · 正文由后台文章数据自动填充 · 配图自动生成

我把一套知识问答类社区的后端缓存体系从单机 Redis 一路折腾到千万级 QPS 的过程完整拆开讲一遍。这个演进过程并不神秘核心是解决三件事单实例能力不够怎么办数据量装不下怎么办热点流量打崩节点怎么办。搞清楚这三个问题你也能设计出一套扛住 2000 万 QPS 的 Redis 架构。先说结论2000 万 QPS 绝对不是靠某一种黑科技堆出来的而是多级缓存、分片路由、连接层优化、热点治理共同作用的结果。真正打到 Redis 主进程的请求可能只有总 QPS 的两到三成剩下的都被客户端本地缓存和接入层挡掉了。这一点在后面的演进过程里会反复出现。1. 演进的整体脉络与设计思路1.1 为什么所有系统都是从单机起步几乎每个业务系统刚上线时的 Redis 都是单机的这不是偷懒而是最合理的决策。早期用户量小请求量低一个 8 核 16G 的实例跑上几万 QPS 绰绰有余。方案越简单越好单机没有网络分区问题没有数据迁移问题也没有一致性争议谁都能维护。我见过不少团队一上来就规划三机房五集群最后发现维护成本比业务成本还高。Redis 的演进应该跟随业务曲线走而不是跟着技术潮流走。单机 Redis 最大的价值就是让业务以最低复杂度跑起来把精力先留给产品逻辑。1.2 从单机到千万级的关键跳板我们的演进路线大致经历了四个阶段单机主备、读写分离、分片集群、多级缓存加接入层优化。每一步都是被逼的。单机主备是为了解决宕机问题主库挂了备库顶上业务不中断。读写分离是为了解决读多写少场景下 CPU 和网卡被打满的问题把读流量分散到从节点。分片集群是为了解决单机内存装不下的问题把数据按 key 打散到多个节点。多级缓存和接入层优化才是冲击千万 QPS 的核心因为不管单机还是集群直接打到 Redis 的请求总量总是有限制的。在规划演进路线时我建议你画一张简单的流量分层图。最外层是客户端本地缓存接着是代理层和接入层再往里是 Redis 集群最后是数据库。每一层都负责过滤掉一部分请求层层递减才不至于让 Redis 裸奔。1.3 2000 万 QPS 的目标拆解2000 万 QPS 听起来吓人拆开算一下就没那么夸张。假设业务整体 2000 万 QPS其中 60% 到 70% 是重复查询同一批热点数据客户端本地缓存能挡掉其中的 95%。剩下的约 600 万到 800 万请求进入接入层接入层再做一次短时间缓存又能挡掉一半。最终到达 Redis 集群的请求量大概在 300 万到 500 万 QPS 左右。如果把 500 万 QPS 分摊到 50 个分片实例上每个实例只需要承担 10 万 QPS。这正是单实例 Redis 完全能够承受的范围。所以目标拆解很重要你不需要让单个 Redis 去扛 2000 万而是要让每一层都只做自己该做的事。注意做容量规划时永远别拿纯理论值去算必须把缓存命中率、重复请求比例、热点集中度这些因素全部加进去否则你会发现架构搭得再大也扛不住真实流量。2. 单机时代的正确用法与瓶颈界定2.1 单实例 Redis 的真实能力上限很多人对 Redis 单实例 QPS 有误解觉得官方 benchmark 能跑 10 万到 12 万 QPS业务上就一定没问题。实测下来生产环境中单实例能稳定跑 6 万到 8 万 QPS 已经很不错了原因在于业务操作不会像 benchmark 一样全是简单的 SET 和 GET。你会有 MGET、PIPELINE、LUA、事务、复杂查询也会有超大 value 的读写。一旦命令的平均耗时从 0.05 毫秒涨到 0.3 毫秒QPS 就会明显下滑。Redis 是单线程模型所有命令串行执行一个耗时 1 秒的慢命令就能让整个实例卡住。我在单机阶段做过的优化按收益排序是这样的第一合并请求把多次 RTT 合并成一次 PIPELINEQPS 能翻一倍。第二去掉可有可无的持久化AOF 的 everysec 改成关闭或加大刷盘间隔CPU 和 IO 压力立刻降下来。第三减少大 Key把超过 100KB 的 value 拆分或压缩吞吐提升非常明显。2.2 单机没崩之前要做的三件事别等到 Redis 被压垮再治理单机阶段就应该做三件基本功。第一是慢查询日志分析定期捞取超过 100 毫秒的命令逐个优化。慢命令往往是 SORT、KEYS、SMEMBERS 这类全量操作必须处理掉。第二是大 Key 巡检用 redis-cli --bigkeys 跑一遍也能通过 DEBUG OBJECT 或 memory usage 命令统计把大 Key 列入改造清单。第三是热点 Key 分析提前识别流量集中在哪些 key 上为后续改造做准备。这三件事决定了你什么时候该主动升级架构。它们发生频率越高说明单机越接近瓶颈。等 Redis 的机器 load average 持续超过 4网卡跑满 70% 以上CPU 出现单核 100% 而其他核空闲的情况就可以启动下一阶段了。2.3 单机架构下最容易忽视的隐患单机部署还有一个经常被忽视的问题机器故障恢复时间。没有主备的情况下数据全在内存里机器硬件故障意味着缓存全部丢失。虽然 Redis 数据可以从数据库回源但瞬时回源会把数据库打垮造成雪崩。主备加上哨兵是单机阶段最必要的投资。主备之间用异步复制主库正常写入从库实时同步全量数据。主库宕机后哨兵在几秒内完成主从切换客户端重新感知新主节点业务几乎无感。很多人觉得主备浪费一台机器实际上它花的是最少的钱买的是最关键的容灾能力这账必须算清楚。3. 主从与读写分离水平扩展的第一步3.1 主从复制的核心原理和注意点主从复制的核心是 RDB 快照加命令传播。从库启动或断线重连时主库生成一份 RDB 全量发送给从库之后持续推送写命令流。这个机制有两个坑全量同步期间主库要做 fork 生成快照内存和 CPU 都会出现尖刺从库数量太多时主库要同时给多个从库发送不同阶段的增量数据网络负担很重。所以从库数量不能无节制增加。我在实践中通常控制在一主两从最多一主三从。如果还需要更多只读副本就把从库之间再挂一层级联复制避免主库连接数爆炸。注意主从复制是异步的严格来说存在数据丢失窗口。主库写入成功但尚未同步到从库时主库宕机这部分数据就丢了。对缓存系统来说通常可以接受但你要有清醒认知不要在主从架构上面做需要强一致性的业务逻辑。3.2 读写分离的真正收益和实操姿势读写分离的逻辑很简单写请求打主库读请求打从库。问题在于客户端怎么知道请求该往哪发。最粗放的方式是在代码里配置两个连接池写操作走主池读操作走从池。稍微讲究一点的做法是在代理层做读写分离。我实测下来的收益是从库分担了大约 65% 的读流量后主库的 CPU 使用率直接从 82% 降到 35%慢查询数量下降了 80%。但读写分离也引入了新问题最主要的是复制延迟。从库的数据通常是几百毫秒到秒级的延迟刚写完立刻去读很可能读到旧数据。解决办法是代价极高的要么业务上接受最终一致性要么对敏感操作强制读主库。3.3 哨兵高可用切换的实战体会哨兵本身也是一个集群至少三个节点通过投票机制决定主库是否下线。这套机制能工作但有一个特点必须知道网络抖动时可能触发误切换。我曾经遇到一场事故两个机房之间的网络抖动持续 30 秒哨兵集群判断主库不可达完成切换后网络又恢复导致旧主库和新主库同时短暂存活产生脑裂。规避脑裂的办法是调大 quorum 和切换超时时间让哨兵多确认几次再宣布主库死亡。慢一点没关系缓存系统短暂不可写几千毫秒远比脑裂造成的数据错乱损失小。哨兵模式下还要注意客户端的版本新版客户端大多支持服务发现能感知到主节点变化自动切换老版本客户端就必须自己实现断线重连逻辑。4. 分片集群数据切分与容量扩展4.1 为什么必须分片内存和 CPU 的双重天花板主从和读写分离解决的是能力瓶颈但没有解决容量问题。单台机器内存再怎么加总归有上限而且 Redis 单线程模型对 CPU 的利用率也到了一个极限。当内存使用超过 30GB 时RDB 快照的 fork 耗时和内存拷贝压力已经让线上操作出现明显抖动。分片集群的本质是把数据按照某个规则打散到多台机器。分片后每个实例只负责全量数据的一部分内存、CPU、网络全部得到了水平扩展。2000 万 QPS 的目标里如果没有分片集群不可能承载 500 万 QPS 的直接访问因为单机的网卡和 socket 连接数已经先到极限了。4.2 Redis Cluster 与代理分片两种路线的取舍当时我们对比了 Redis Cluster 和代理分片方案。Redis Cluster 的优势是去中心化客户端直连节点没有代理层转发损耗劣势是客户端要处理重定向逻辑而且多 key 操作跨 slot 时不支持。代理分片的优势是客户端完全无感知不管底层怎么变客户端只连代理。劣势是代理本身可能成为瓶颈和单点。我们的选择是分场景混用核心缓存走集群直连追求最低延迟边缘业务走代理降低客户端改造成本。Redis Cluster 使用 16384 个哈希槽每个 key 经过 CRC16 计算得到一个槽位槽位分配到不同节点上。客户端拿到 key 的槽位后如果和目标节点不一致服务端会返回 ASK 或 MOVED 重定向客户端需要重新发起请求。在压测环境下这种重定向会带来一次额外 RTT所以客户端要做好缓存路由表尽量直接命中正确节点。4.3 扩缩容实战从扩容开始讲细节扩容是分片集群最考验操作的部分。扩大 Redis Cluster 时会先把新节点加入集群然后从其他节点迁移部分哈希槽到新节点。迁移过程中每个槽的数据会以 slot 为单位生成增量迁移任务发送到目标节点并持续同步新产生的写入。这里最容易踩的坑是迁移速度太快导致源节点 CPU 飙升。Redis 迁移对源节点是扫描 key 加序列化加网络发送过程中源节点内部还有内存拷贝。我在线上控制迁移速率每秒钟迁移 80 到 120 个槽单个大 key 迁移前先单独处理大 key迁移期间监控源节点的 load average。整个集群扩到 40 个节点时平滑程度仍然可控。数据倾斜是分片之后必然会遇到的事。哈希槽是均匀的但业务 key 的访问频率不是均匀的。一个超级热点 key 落到某个节点后该节点流量会远超其他节点。这种场景要么做热点 key 的本地缓存要么在 key 后面拼接随机后缀打散到多个节点由客户端负责聚合查询。注意设计 key 时要提前想好分片规则。如果某个业务场景必须频繁执行跨多 key 的操作尽量让这些 key 落在同一个槽或同一个节点的同号段中否则 Cluster 模式下就需要引入额外的中间层做合并复杂度会指数级上升。5. 从 Redis 本身到 Redis 生态多级缓存与接入层优化5.1 客户端本地缓存才是千万 QPS 的第一道防线到了这个阶段如果还想着让所有请求都打到 Redis 上架构一定扛不住 2000 万 QPS。真正撑住流量的第一层是客户端本地缓存也就是进程内缓存。一个进程内可以放一个带过期时间的本地缓存比如全量 100 万条基础配置设置成分钟级过期。热点数据的客户端缓存命中率在业务峰值时期可以稳定在 90% 以上。假设总请求量 200 万 QPS本地缓存命中 180 万打到 Redis 的就只剩 20 万。这就是多级缓存最核心的威力。实现本地缓存时要注意内存上限和淘汰策略。我用的是固定容量加 LRU 淘汰每条记录预估容量并设置最大条目数超过后淘汰最久未使用的。每次请求先查本地未命中再查 Redis查到后写入本地并设置过期时间。这里最关键的参数是过期时间太短命中率上不来太长容易读到旧数据一般是秒级或分钟级根据业务容忍度调整。5.2 连接层的优化连接池、协议多路复用和管道Redis 的瓶颈之一在于连接数量。每个实例能承载的连接数受 fd 限制和内存限制影响几万个连接会把网卡打满。优化方向是缩小连接总量。具体做法是客户端使用连接池控制每个实例的连接数在 50 到 200 之间而不是无限制创建连接。第二个优化点是管道模式。业务中有大量小的读请求每次请求来回一个 RTT管道可以把多个命令一次性打包发出。实测使用管道后同样场景下的 QPS 从 8 万提升到 16 万左右。第三个优化点是协议本身Redis 2.6 之后的 RESP3 协议在纯文本解析上有优化客户端升级到支持新协议的版本也能获得 10% 到 20% 的吞吐提升。5.3 缓存一致性多级缓存最容易翻车的地方引入客户端本地缓存后最大的问题是一致性。Redis 里的数据更新了本地缓存怎么同步常见做法有三种版本号轮询、过期时间容忍、主动失效推送。版本号轮询是最稳妥的每次请求带上版本号低于最新版本的本地缓存直接作废。主动失效推送是让 Redis 作为 pub/sub 消息源数据变更时发布 key 失效事件各客户端订阅事件后移出本地缓存。前者增加一次判断后者引入消息依赖。我采用的折中方案是热点数据使用主动失效推送加秒级过期兜底非热点数据只用过期时间。延迟双删也是个常见技巧。先更新数据库再删 Redis key然后等几百毫秒再删一次 Redis key。第一次删除是为了清掉旧数据第二次删除是为了清掉在第一次删除和数据库更新之间可能被写入的脏数据。这个技巧能解决大多数并发场景下的缓存不一致但不能根治只能结合业务容忍度使用。5.4 热点 Key 探测与击穿、雪崩的防护千万级 QPS 系统最怕的三件事是缓存穿透、缓存击穿、缓存雪崩。穿透是查询根本不存在的数据每次都会打到数据库。解决办法是为空结果建立短失效时间的缓存同时使用布隆过滤器快速过滤大概率不存在的 key。击穿是某个热点 key 缓存失效的瞬间大量请求同时回源。解决办法是分布式锁或互斥锁让只有一个请求回源其他请求短暂等待。雪崩是大规模 key 同一时间失效。解决办法是给过期时间加上随机扰动让失效点分散同时设置多级缓存兜底。热点 key 探测可以用客户端统计加 Redis 端的 monitor 命令配合。生产环境里我用本地计数器和 Redis 的 sorted set 做滑动窗口统计识别一个时间窗口内访问频次超过阈值的 key自动触发本地缓存预热和打散策略。这套机制上线后热点 key 导致的单节点 CPU 抖动问题基本消失。6. 高可用、监控与故障复盘6.1 监控指标只盯 QPS 远远不够几千个 Redis 实例的监控不能只看总 QPS必须拆到实例。我常用的监控指标体系是每个实例的 CPU 使用率、内存使用率、连接数、慢命令数量、网络入包出包速率、复制延迟、key 过期数量。每个指标都有对应的告警阈值。慢命令数量是最容易被忽视的指标。Redis 单线程下一个慢命令会阻塞整个实例所以慢命令监控必须做成分钟级实时只要出现一次耗时 500 毫秒以上的命令立即告警。大 key 也要定期扫描我把巡检脚本接入了定时任务每天凌晨对全量实例做一次 bigkey 分析结果推送到值班群从源头降低故障概率。6.2 典型故障主从切换丢数据、热点打爆分片我遇到的故障里影响范围最大的一次是热点活动页面的 key 与其他大量数据落在同一个分片。活动爆发时该分片的 CPU 先被打满接着连接数超限整个分片不可用。由于其他分片健康部分路由请求会被重定向到故障分片客户端不断重试最终导致大面积超时。处理过程分三步第一步通过监控定位到故障分片与热点 key。第二步在客户端配置紧急降温开关直接让热点 key 短时间变成动态 key并开启本地缓存。第三步等流量平稳后把该分片的一部分哈希槽迁移出去重新均衡数据分布。这次事件让我养成了两个习惯大促前必须做热点 key 预分析分片集群永远预留 20% 到 30% 的冗余容量。主从切换丢数据的故障也遇到过。主库在写入后、从库完成同步前宕机哨兵切换后最近一小段时间的缓存数据丢失。由于我们底层有数据库兜底最终的一致性问题不大但业务确实出现了短时间缓存回源。这个故障的结论是对一致性敏感的数据不要放在 Redis 异步复制链路里要么走数据库要么做特殊标记允许回源。6.3 容量规划与压测方法架构演进不能靠感觉必须用压测数据说话。我做的压测是分层的先压单实例找出单个 Redis 在当前硬件上的最大稳定 QPS再压集群找出分片后整体吞吐最后压全链路加入客户端本地缓存和接入层看整体能扛多少。压测工具用 redis-benchmark 打底再用自研压测脚本模拟真实业务流量分布。这里的关键是要模拟热点倾斜不能假设每个 key 访问次数都一样。真实环境往往是 20% 的 key 承担 80% 的流量压测里如果不加热点 key结果会和线上差距很大。用真实流量回放方式压测是最准确的压测时同步采集 Redis 端的慢命令、CPU、延迟三项数据综合判断容量水位。7. 常见问题与排查技巧实录7.1 缓存穿透问题的排查套路线上突然出现数据库负载飙升第一反应是查缓存穿透。我一般会先看 Redis 的命中率监控是否突然下降再看数据库的慢日志是不是出现了大量同一个空 key 的查询或者多个不存在的 key 轮询查询。如果确认穿透立刻加布隆过滤器加载全部合法 key并为空查询设置短缓存比如 30 秒到 60 秒。布隆过滤器实现时要注意误判率的参数设置。通常在数据量 1000 万时使用 10 个 bit 位加 7 个哈希函数误判率在 1% 左右足够满足业务。过滤器冷启动时可以分批加载 key配合 Redis 里的 bitmap 或独立的布隆模块整体影响可控。7.2 热点 Key 导致 CPU 抖动的定位方法CPU 抖动时先在 Redis 实例上用 monitor 命令抓取当前正在执行的命令观察是否有某个 key 或某条命令高频出现。再配合 INFO commandstats 查看命令调用次数分布。如果确认热点 key立即在客户端对该 key 做短时间本地缓存或将 key 打散成带随机后缀的多个子 key。打散后的 key 在查询时必须经过客户端聚合比如 set 了 64 个分片 key一次查询需要并行访问全部 64 个分片再聚合结果。这里要评估聚合带来的额外开销通常热点 key 的 value 不大并行查询反而会提升整体性能。压测数据表明打散 16 份时聚合消耗可以忽略但打散 256 份时聚合耗时反而上升。7.3 分片集群里的数据倾斜与事务边界排查数据倾斜先用 CLUSTER SLOTS 查看各节点槽位分布再用 INFO memory 对比各节点内存占用。如果有节点内存明显高于其他节点就用 RANDOMKEY 加 DEBUG OBJECT 找出大体积 key然后针对性地迁移到空闲节点或拆分。集群模式下事务边界是一个必须注意的问题。Redis Cluster 默认不支持跨 slot 事务某些业务逻辑依赖多个 key 原子操作时就必须改用 LUA 脚本并将所有 key 声明到同一个哈希标签中。哈希标签是 key 中包含花括号的部分例如 {user:123}:profile 和 {user:123}:orders 会路由到同一个槽位。我强烈建议在设计 key 阶段就规划好跨 key 操作。7.4 一次真实失败案例的完整复盘有一次我们上线新的缓存预热逻辑批量往 Redis 写入大量 key。由于没有控制写入速率预热脚本把某个分片的网络带宽打满了导致该分片的正常读写全部超时。这次故障让我明白预热和批量写入操作必须有自我保护机制必须做速率限制和分片间串联执行不能一窝蜂并发写。从那之后所有批量操作都增加了严格的控制层包括每秒写入量限制、单条数据大小上限检查、失败重试和退避机制。Redis 的系统能力没有上限问题但当业务系统没有自控能力时系统越强伤害越大。这个教训比任何技术方案都深刻。经历了这一切之后我最大的体会是架构演进不是做加法而是做减法。你要逐渐减少 Redis 核心节点需要处理的请求量而不是想方设法提高单节点的处理能力。缓存命中率、连接复用率、热点打散率这三个指标才是我日常最关心的。2000 万 QPS 不是某个架构的终点只要业务还在增长就会有新的瓶颈出现。但只要你把每一层的边界划分清楚把监控和治理做在前面再大的流量都有路可走。
看完文章还有疑问?直接问顾问
三门峡、驻马店特种作业考证问题:报名条件、考试批次、材料整理、证书复审,电话或邮箱都能找到我们,当天回复,企业团报另对接 HR 专人。
预约咨询 18236992212