首页 / 资讯中心 / 文章详情

Redis单线程为何能跑11万QPS?深度解析核心机制与配置

Redis单线程为何能跑11万QPS?深度解析核心机制与配置 ★ FEATURED ARTICLE
简介这份PPT面向希望系统掌握Redis的开发者与运维人员以技术分享的形式梳理内存数据结构存储的核心知识帮助读者建立从原理认知到配置实操的完整脉络。压缩包内仅含1个pptx文件体积约1.59MB以图文幻灯片承载讲解内容便于直接演示或按章节自学。目前已有162人学习说明内容具备一定的参考价值。幻灯片围绕简介与使用两大板块展开先说明Redis的高读写性能、原子性、string/hash/set/sorted set/list等丰富数据结构及主从复制能力并客观指出在线扩容复杂、宕机影响部分读写等局限随后深入redis.conf常用配置项如端口、超时、守护进程与数据库数量再对比RDB快照与AOF日志两种持久化策略的配置命令、优缺点与适用场景最后整理key与String等常用命令的用法。整体结构清晰适合用作团队内部分享底稿或入门复习提纲。1. 单线程的 Redis 凭什么跑到 11 万 QPS很多人第一次看到 Redis 的性能数字会犯嘀咕读 110000 次/s、写 81000 次/s而它偏偏是单线程处理命令的。多核时代一个单线程程序把一堆多线程服务按在地上摩擦这事本身就值得刨根问底。答案不在“C 语言写得快”这种糊弄人的说法里而在于它把“慢”的环节全部绕开了没有磁盘 IO 挡在命令路径上、没有锁竞争、没有线程上下文切换命令执行退化成一次内存里的数据结构操作加一次事件回调。搞懂这条主线后面所有的配置项、持久化策略、主从复制才有落脚点——它们本质上都是在回答同一个问题当数据只存在内存里怎么在“快”和“不丢”之间做交易。这份分享材料把 Redis 的配置、持久化、五大数据结构命令和事务、发布订阅串成了一条线正好顺着这条线把机制拆开看。2. 事件循环与单线程模型为什么快快在哪2.1 一条命令从 socket 到内存的完整路径要理解 Redis 的性能得先看它怎么处理一个连接。常见的实现思路是 Reactor 模式主线程用一个事件循环event loop同时监听大量 socket哪个连接可读就处理哪个读完解析成命令直接在内存里的字典上执行再把结果写回输出缓冲区。整个过程没有为每个连接开线程也没有把请求丢进线程池排队。这条路径上有几个关键设计。第一网络 IO 用的是非阻塞 socket 加多路复用select/epoll/kqueue 这类机制单线程就能盯住成千上万个连接不会因为某个连接没数据就把整个进程卡住。第二命令执行阶段是纯内存操作一次 GET 就是哈希表查找一次 LPUSH 就是链表头插耗时在纳秒到微秒级根本不需要并行。第三协议解析和回复序列化都是流式的边读边解析不攒完整包再处理。// 事件循环的骨架简化示意非源码逐行 while (!stop) { // 1. 等待事件就绪超时时间由最近的时间事件决定 numevents aeApiPoll(eventLoop, tvp); for (j 0; j numevents; j) { aeFileEvent *fe eventLoop-events[eventLoop-fired[j].fd]; // 2. 可读事件读 socket - 解析协议 - 执行命令 - 写回复 if (fe-mask AE_READABLE) fe-rfileProc(eventLoop, fd, fe-clientData, mask); // 3. 可写事件把输出缓冲区里的回复刷给客户端 if (fe-mask AE_WRITABLE) fe-wfileProc(eventLoop, fd, fe-clientData, mask); } // 4. 处理时间事件比如 serverCron 里的过期键清理、持久化触发 processTimeEvents(eventLoop); }这段骨架说明了单线程模型的核心所有客户端共享同一个循环命令一个接一个串行执行。串行带来的直接后果是“天然原子”——没有两个命令会同时改同一个 key所以单条命令不需要加锁。这也是材料里“所有操作都是原子性”这句话的真正来源它不是靠锁实现的而是靠“根本不存在并发执行命令这回事”。2.2 单线程的边界哪些活被挪出了主线程单线程能扛住高并发前提是主线程上不能有慢操作。所以 Redis 把几类耗时活儿挪了出去。持久化的 RDB 快照靠 fork 子进程完成主线程只负责 fork 那一瞬间AOF 的 fsync 可以配置成后台线程或由主线程按策略触发后来的版本里删除大 key 的释放内存操作也可以放到后台线程。主线程始终只做“读事件、执行命令、写回复”这三件事。这里有个容易被忽略的细节fork 本身在主线程上是阻塞的。数据量越大页表越大fork 耗时越长这段时间主线程完全停摆。材料里 RDB 的缺点写着“当数据量大时fork 过程非常耗时导致 redis 无法在毫秒级别内响应客户端请求”说的就是这件事。所以单线程模型的性能上限很大程度上取决于你有没有在主线程上塞进不该塞的东西——一个 KEYS * 扫全库、一次大 key 的 DEL、一次超大集合的 SMEMBERS都能让整个实例卡住。提示判断一个操作会不会拖垮实例标准很简单——它的时间复杂度是不是跟数据量成正比且在主线程执行。O(N) 且 N 很大的命令在生产环境基本等于定时炸弹。2.3 为什么不用多线程执行命令一个自然的疑问是既然多核闲着为什么不让多个线程并行执行命令原因在于一旦并行共享数据结构的读写就必须加锁锁的粒度、死锁、缓存行伪共享这些问题会立刻把实现复杂度拉满而收益并不明显——瓶颈往往在网络 IO 和内存带宽不在 CPU 计算。用单线程换来的是实现简单、行为可预测、调试容易。这也是一个典型的设计取舍牺牲多核并行度换取确定性和低延迟。理解这一点后面看主从复制、事务、发布订阅时很多“为什么这么设计”就顺了。3. 五大数据结构的底层编码命令背后的数据结构选择3.1 string、hash、list 的编码切换材料里列了一大串命令SET/GET、HSET/HGET、LPUSH/LRANGE看起来只是 API 差异实际上每种类型背后都有不止一种底层编码Redis 会根据数据规模自动切换。这是它“数据结构丰富”之外更值得关注的一层同一类型小数据量和大数据量走的是完全不同的实现。string 类型底层是 SDS简单动态字符串相比 C 字符串多了长度字段和预分配空间取长度 O(1)追加时不必每次重新分配。数字型的值还会被编码成整数直接存省掉字符串开销。hash 类型在小数据量时用压缩列表ziplist后来是 listpack元素少且每个元素短的时候一整块连续内存存完省指针、省内存、缓存友好超过阈值就转成哈希表。list 同理小的时候用压缩列表大了转成双向链表后来的 quicklist 是两者的折中把链表节点做成压缩列表块。# 观察编码切换先看小 hash 的编码 redis-cli object encoding user:1 # 元素少且短 - ziplist / listpack redis-cli hset user:1 name tom age 20 redis-cli object encoding user:1 # 塞入一个超长 value触发编码转换 redis-cli hset user:1 bio $(python3 -c print(x*200)) redis-cli object encoding user:1 # - hashtableobject encoding返回的就是当前 key 实际使用的底层编码。这个命令在生产排查里非常有用如果你发现一个本该很小的 hash 变成了 hashtable说明它曾经被塞过大 value内存占用会比预期高不少。编码转换是单向的转过去就不会自动转回来这也是内存“莫名其妙涨上去”的常见原因之一。3.2 set 与 sorted set集合运算和排序的代价set 类型在元素全是整数且数量不多时用整数集合intset有序存储、二分查找内存极省一旦加入非整数元素或超过阈值转成哈希表。材料里 SDIFF、SINTER、SUNION 这些集合运算命令复杂度跟参与运算的集合大小相关两个十万级的集合做交集主线程要实打实算一遍这就是前面说的“O(N) 命令要小心”的具体例子。sorted set 更特殊它要同时支持“按成员查分数”和“按分数范围取成员”所以底层是哈希表加跳表skiplist的组合。哈希表负责 O(1) 查分数跳表负责 O(log N) 的范围查询和排名。跳表相比平衡树的好处是实现简单、范围遍历天然有序、插入删除不用旋转。ZRANGEBYSCORE、ZRANK 这类命令能高效工作全靠这层结构。理解了这个组合你就明白为什么 sorted set 既能当排行榜又能当延时队列的索引——前者靠分数排序后者靠分数存时间戳做范围扫描。3.3 过期键与内存回收TTL 背后的惰性删除材料里 EXPIRE、TTL、PERSIST 这组命令看着简单背后是两套删除策略的配合。Redis 不会为每个设置了过期时间的 key 起一个定时器那样开销太大。实际做法是惰性删除加定期删除访问一个 key 时先检查是否过期过期就删并返回空同时后台周期性随机抽样一批带过期时间的 key删掉其中过期的。抽样而不是全扫是为了控制单次耗时。这个设计带来一个必须知道的后果过期 key 不一定在到期那一刻就被释放内存可能要等到被访问或者被抽样到。所以内存监控里看到“明明过期了内存还没降”不一定是 bug。另外主从架构下过期删除由主节点驱动从节点收到主节点同步的 DEL 才删自己不会主动删过期 key这是为了保证主从数据一致。这些细节材料没展开但它们是理解“为什么 TTL 行为和直觉有偏差”的关键。4. 持久化RDB 与 AOF 的取舍与实现4.1 RDB 快照fork 与写时复制RDB 的思路是把某一时刻的全量数据写成一个二进制文件。材料里给了核心配置save 60 1000表示 60 秒内 1000 次更改就触发dbfilename dump.rdb指定文件名rdbcompress yes控制压缩stop-writes-on-bgsave-error no决定保存失败时是否停止写入。真正值得讲的是它怎么在不阻塞主线程的情况下拿到一致快照。答案是 fork 加写时复制copy-on-write。fork 出子进程后父子进程共享同一份物理内存页子进程遍历内存写文件如果主线程此时修改了某个页操作系统会把这个页复制一份给主线程改子进程看到的仍是 fork 那一刻的旧页。这样既不用停主线程又能保证快照是 fork 瞬间的一致视图。# 手动触发一次后台快照观察 fork 耗时和内存变化 redis-cli bgsave redis-cli info stats | grep latest_fork_usec # 最近一次 fork 耗时微秒 redis-cli info memory | grep used_memory # 快照期间内存可能上涨COW 复制页latest_fork_usec是排查卡顿的重要指标。如果这个值到了几十毫秒甚至上百毫秒说明实例数据量大、页表大每次 bgsave 都会造成可感知的延迟毛刺。stop-writes-on-bgsave-error这个配置的含义也要说清设成 yes 时如果上次 bgsave 失败主节点会拒绝所有写命令逼你正视持久化故障设成 no 则继续接受写入但你要自己承担数据丢失风险。生产上这个值的选择本质是在“可用性”和“数据安全”之间表态。4.2 AOF追加日志、fsync 策略与重写AOF 走的是另一条路把每条写命令追加到文件末尾重启时重放。材料里的核心配置是appendonly yes、appendfsync everysec/always/no、auto-aof-rewrite-percentage 100、auto-aof-rewrite-min-size 64m。appendfsync三档的差别必须掰开说。always每条命令都 fsync 到磁盘最安全但最慢磁盘 IO 直接成为瓶颈everysec每秒 fsync 一次最多丢一秒数据是绝大多数场景的默认选择no交给操作系统决定何时刷盘性能最好但丢失窗口不可控。材料里写“redis 默认 1 秒写入一次 aof那么最多丢失 1 秒的数据”对应的就是everysec。AOF 文件会越来越大因为同一个 key 被改了一百次就记了一百条命令。重写rewrite机制解决这个问题fork 子进程根据当前内存里的数据生成一份“最小命令集”的新 AOF比如一百次 INCR 最终只写一条 SET。重写期间主线程的新写入会同时追加到旧 AOF 和一个重写缓冲区子进程完成后把缓冲区补进去保证不丢命令。auto-aof-rewrite-percentage 100的意思是当前文件比上次重写后大了一倍就触发auto-aof-rewrite-min-size 64m是兜底下限文件太小就不折腾。# 查看 AOF 状态与重写进度 redis-cli info persistence | grep -E aof_enabled|aof_rewrite_in_progress|aof_last_bgrewrite_status|aof_current_size|aof_base_size # 手动触发重写 redis-cli bgrewriteaofaof_current_size和aof_base_size的比值就是判断要不要重写的依据。如果这个比值长期偏高但没触发重写先检查auto-aof-rewrite-min-size是不是把门槛设太高了。4.3 两种策略怎么选一张取舍表维度RDBAOF数据丢失窗口两次快照之间可能几分钟everysec 下约 1 秒文件体积小二进制压缩大命令文本恢复速度快直接加载慢逐条重放对主线程影响fork 耗时COW 内存上涨fsync 策略影响写延迟重写时 fork适合场景定期备份、灾难恢复对数据安全要求高的主存储常见的落地做法是两者同时开RDB 用于备份和快速恢复AOF 用于把丢失窗口压到秒级。材料里把两者的优缺点并列摆出来其实已经暗示了这个结论——它们不是二选一而是互补。真正要决策的是appendfsync选哪档、save规则设多密这两个参数直接决定了你在延迟和数据安全上的位置。5. 主从复制、事务与发布订阅一致性的边界在哪5.1 主从复制异步同步带来的读写分离代价材料提到“主机会自动将数据同步到从机可以进行读写分离”也提到“主机从机的宕机都会导致前端部分读写请求失败”。这两句话合起来点出了主从复制的核心事实它是异步的。主节点执行完写命令后把命令传播给从节点从节点重放。主节点不会等从节点确认再回复客户端所以从节点上的数据天然落后于主节点这个延迟就是“复制延迟”。读写分离把读请求打到从节点就得接受“刚写进去的数据可能读不到”也就是常说的读写不一致。对一致性要求高的读比如刚下单查订单要么强制走主节点要么在业务层做补偿。复制的过程大致分三步从节点连上主节点后先做全量同步主节点 fork 生成 RDB 发给从节点期间的写命令缓存在缓冲区发完 RDB 再补发缓冲区之后进入命令传播阶段做增量同步。网络断开重连时如果主节点的复制积压缓冲区还留着断点之后的数据就做部分重同步否则又得全量来一遍。全量同步的 fork 和传输开销是主从架构下另一个需要盯的点。5.2 事务MULTI/EXEC 到底保证了什么材料里的事务命令是 MULTI、EXEC、DISCARD、WATCH、UNWATCH。这里最容易踩的坑是把 Redis 事务和关系库事务划等号。Redis 事务的语义是MULTI 之后的命令进队列EXEC 时按顺序一次性执行执行期间不会被其他客户端的命令插进来。它保证的是“这一批命令连续执行不被打断”但不保证“要么全成功要么全回滚”。如果队列里某条命令在运行时出错比如对字符串执行 LPUSH其他命令照样执行Redis 不会回滚。只有命令入队阶段就语法错误的情况EXEC 才会整体拒绝。这个差异源于它的实现命令是逐条执行的没有 undo log回滚的成本太高。所以材料里“原子性”这个词要理解成“不被穿插”而不是“可回滚”。WATCH 提供的是乐观锁监视某个 keyEXEC 时如果发现它被别的客户端改过就放弃执行。这是实现 CAS 类逻辑的手段比如“读值、计算、写回”这种需要防止并发覆盖的场景。但要注意 WATCH 的粒度是 key监视太多 key 会放大冲突概率重试逻辑得自己写。# 乐观锁示例只有 balance 没被别人改过才扣款 redis-cli watch balance redis-cli get balance # 假设读到 100 redis-cli multi redis-cli decrby balance 30 redis-cli exec # 若期间 balance 被改返回 nil需重试5.3 发布订阅没有持久化的消息通道SUBSCRIBE/PUBLISH 这组命令实现了一个轻量的消息广播。发布者往频道发消息所有订阅了该频道的客户端都能收到。材料里还列了 PUBSUB CHANNELS 查活跃频道、PUBSUB NUMSUB 查订阅者数量。它的关键限制在于“发完就忘”消息不落盘、不重放订阅者断线期间的消息全部丢失也没有确认机制发布者不知道谁收到了。所以它适合做实时通知、配置变更广播这类“丢了也无所谓”的场景不适合当可靠消息队列用。另外订阅模式的客户端会进入一个特殊状态只能收订阅消息不能执行普通命令后来的版本有所放宽这也是使用时要留意的行为差异。理解了这个边界就不会拿它去扛订单、支付这类不能丢消息的链路。6. 配置与运维的避坑清单那些文档不会主动告诉你的6.1 常见问题与排查现象实例周期性卡顿监控上看到延迟毛刺。原因大概率是 bgsave 或 AOF 重写触发的 fork 阻塞了主线程数据量越大越明显。解决用info stats看latest_fork_usec用info persistence看重写频率把save规则调稀、避开业务高峰做备份或者把实例拆小别让单实例数据量无限膨胀。现象内存持续上涨删了 key 也不降。原因可能是过期 key 还没被抽样删除也可能是编码转换后内存回不去比如 hash 从 ziplist 转成 hashtable还可能是客户端输出缓冲区堆积。解决用info memory看used_memory和mem_fragmentation_ratio用object encoding抽查大 key 的编码用client list看输出缓冲区大小。现象主从数据对不上从节点读到旧值。原因异步复制固有的延迟或者从节点曾经断连做过全量同步。解决关键读走主节点监控复制偏移量info replication里的master_repl_offset和从节点的slave_repl_offset差值持续偏大就要查网络和主节点负载。现象执行 KEYS * 之后整个服务像挂了。原因KEYS 是 O(N) 全库扫描在主线程执行数据量大时直接堵死事件循环。解决生产禁用 KEYS改用 SCAN 游标分批遍历在配置里用rename-command把危险命令改名或禁用。现象AOF 文件暴涨磁盘告警。原因写操作频繁但重写没触发或者auto-aof-rewrite-min-size设得太大。解决检查aof_current_size与aof_base_size的比值适当调低重写门槛必要时手动bgrewriteaof。6.2 配置项背后的默认值逻辑材料里那张配置表每一项的默认值都不是随手定的。port 6379是约定俗成的端口timeout 0表示不主动断开空闲连接因为服务端很难判断客户端是真空闲还是只是暂时没请求daemonize yes让进程后台化配合logfile把日志落到文件databases 16给了 16 个逻辑库但集群模式下只有 0 号库可用这个限制在单机切集群时经常翻车。tcp-backlog 511是 listen 队列长度高并发短连接场景下如果出现连接被拒可以适当调大。这些参数单独看是配置项连起来看就是一套“默认保守、按需放开”的运维哲学。7. 设计取舍的思考快与不丢之间的那条线把这份材料从头到尾捋一遍会发现 Redis 的每一个设计决策都落在同一条轴上数据在内存里所以快内存会丢所以要在持久化和复制上做补偿补偿有代价所以要让你自己选补偿到什么程度。RDB 和 AOF 的选择、appendfsync的三档、主从的异步复制、事务的不回滚全都是这条轴上的刻度没有绝对的对错只有你愿意为“不丢”付多少延迟和复杂度。我一般会这么理解Redis 不是数据库是一个“带持久化选项的缓存和数据结构服务器”。把它当数据库用就得接受它在事务回滚、强一致、在线扩容上的先天不足把它当缓存用就别在它身上压不能丢的数据。材料里提到的“较难支持在线扩容”也是同一个逻辑——单机内存有上限横向扩展要靠集群分片而分片会带来跨槽操作、事务范围、批量命令的限制这些成本在选型时就得算进去。验证一个 Redis 部署是否健康我习惯看三个数latest_fork_usec看持久化对延迟的冲击master_repl_offset与从节点偏移的差值看复制健康度used_memory与maxmemory的比值看是否逼近淘汰阈值。这三个数稳配置基本就没跑偏。至于具体参数怎么调没有放之四海皆准的答案得拿你自己的读写比例、能容忍的丢失窗口、磁盘 IO 能力去试。血泪经验是别等到线上卡顿了才去翻info命令的输出。从那以后我每次上线新实例都强制先把info all的基线打一份存着出问题时才有对照。这份分享材料把命令和配置列得很全正好可以当自查清单用配合object encoding、info persistence这些命令边看边验证比死记参数有用得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站