ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis单线程为何能跑11万QPS?深入解析高性能原理与持久化机制

Redis单线程为何能跑11万QPS?深入解析高性能原理与持久化机制

简介:这份PPT面向后端开发、运维及缓存技术学习者,系统梳理Redis核心知识,帮助读者快速建立从原理到配置的完整认知。内容涵盖Redis高性能读写特性、string/hash/set/sorted set/list等数据结构、原子性操作与主从复制机制,并客观分析在线扩容困难、宕机影响读写等局限。配置部分逐项讲解redis.conf中port、timeout、daemonize、databases等参数含义;持久化部分对比RDB快照与AOF日志两种策略的配置项、优缺点及适用场景;命令部分整理key与String常用操作,如EXISTS、DEL、EXPIRE、SET、INCR等。资源包为1个pptx文件,大小约1.59MB,结构紧凑,适合培训分享或自学参考。目前已有161人学习,可作为Redis入门与面试复习的轻量资料。

1. 单线程的 Redis 凭什么跑到 11 万 QPS:从一次压测翻车说起

很多人第一次看到 Redis 的性能数据都会愣一下:读 110000 次/s、写 81000 次/s,而它居然是单线程处理命令的。直觉上,多线程才是高性能的代名词,单线程应该慢得可怜才对。可现实恰恰相反——Redis 用一条主线程扛住了绝大多数互联网公司的缓存流量,这背后的机制值得每一个后端开发者搞懂。

这份《Redis 分享》资料把 Redis 的简介、配置、持久化、五大命令族、事务和发布订阅串成了一条完整的链路,正好适合拿来拆解「为什么这么设计」。它解决的核心问题是:在内存里用最简单的模型,把数据结构的读写做到极致,同时用 RDB 和 AOF 两套持久化机制兜住数据安全的底。搞懂这套机制,你才能明白什么时候该用 Redis、什么时候它会翻车、配置项到底在调什么。下面从「为什么单线程能快」这个反直觉结论切入,一层层往下挖。

2. 单线程模型与数据结构:为什么不用锁还能保证原子性

2.1 单线程 + 内存 + IO 多路复用,三件事凑出高性能

Redis 快的原因不是某一项黑科技,而是三个条件叠加。第一,数据全放内存,内存随机访问的延迟是纳秒级,磁盘是毫秒级,差了六个数量级,这是物理层面的优势。第二,命令处理走单线程,没有线程切换开销,没有锁竞争,没有死锁风险,每个命令从进入到返回是一条直线。第三,网络层用 IO 多路复用(常见的实现思路是 epoll)把大量连接的事件收拢到一个线程里轮询,避免了为每个连接开线程的资源消耗。

这三件事缺一不可。如果数据在磁盘上,单线程就是灾难;如果多线程处理命令,就得加锁,加锁就带来竞争和上下文切换,性能反而可能下降。Redis 的选择是:把复杂度挡在数据结构内部,把简单留给并发模型。所以「原子性」在 Redis 里不是靠锁实现的,而是靠「同一时刻只有一个命令在执行」这个事实天然保证的。资料里写「所有操作都是原子性的」,根子就在这里。

提示:单线程指的是命令执行线程。持久化、异步删除等操作会 fork 子进程或使用后台线程,不要理解成整个进程只有一个线程。

2.2 五种数据结构的底层编码与命令语义

资料里列了 string、hash、set、sortedset、list 五类结构,命令非常多,但理解它们的关键不是背命令,而是搞清每种结构「适合表达什么关系」。

string 是最基础的键值对,SET/GET/MSET/MGET是批量读写,INCR/DECR/INCRBY/DECRBY是原子计数器,SETEX带过期时间,APPEND追加内容,GETRANGE截取子串,STRLEN取长度。这里有个容易忽略的点:INCR在 key 不存在时会先初始化为 0 再执行,所以它天然适合做计数器,不需要先判断存在性。

hash 表达的是「一个对象有多个字段」,HSET/HGET/HMSET/HMGET是字段级读写,HGETALL取全部字段值,HKEYS/HVALS/HLEN取字段名、值、数量,HINCRBY对某个字段做原子加法。用 hash 存对象比用多个 string 键更省内存,也更容易整体管理。

list 是双向链表语义,LPUSH/RPUSH从两端插入,LPOP/RPOP从两端弹出,LRANGE按区间取元素,LTRIM裁剪区间,LREM按 count 的正负决定从头还是从尾删除。BLPOP/BRPOP是阻塞版本,列表为空时连接会挂起直到有元素或超时——这是实现简单消息队列的常用手法。

set 是无序不重复集合,SADD/SREM/SCARD/SISMEMBER是基础操作,SDIFF/SINTER/SUNION做差集、交集、并集,带STORE后缀的版本会把结果存到目标 key。SPOP随机弹出一个元素,SSCAN用于渐进式遍历大集合,避免SMEMBERS一次性拉爆内存。

sortedset 在资料里被提到但命令没展开,它的核心是每个成员带一个 score,按 score 排序,适合排行榜、延时队列这类场景。

2.3 key 的通用操作与过期机制

所有数据结构共享一套 key 级命令。EXISTS判断存在返回 1 或 0,DEL删除一个或多个 key(不存在的会被忽略),EXPIRE设置生存时间(单位秒),TTL返回剩余生存时间,PERSIST移除过期时间让 key 永久保留,RANDOMKEY随机返回一个 key 但不删除,RENAME改名(key 不存在或新旧同名会报错),TYPE返回值类型,KEYS pattern按模式查找。

KEYS的匹配规则值得单独说:KEYS *匹配全部,KEYS h?llo匹配 hello、hallo、hxllo(? 匹配一个字符),KEYS h*llo匹配 hllo、heeeeello(* 匹配任意长度),KEYS h[ae]llo匹配 hello 和 hallo 但不匹配 hillo([] 匹配指定字符之一)。

注意:KEYS是 O(N) 操作,会阻塞单线程,生产环境用SCAN系列命令做渐进式遍历,别在线上直接KEYS *。

3. 持久化机制:RDB 和 AOF 到底在赌什么

3.1 RDB 快照:fork 子进程换性能,代价是丢数据

RDB 的思路很直接:把某一时刻数据库里的全部数据写成一个二进制副本,数据丢了就用这个副本恢复。核心配置在redis.conf里:save 60 1000表示 60 秒内有 1000 次更改就触发一次保存;stop-writes-on-bgsave-error no控制保存出错时是否停止接收数据;rdbcompress yes决定是否压缩;dbfilename dump.rdb定义文件名。

RDB 的性能优势来自 fork。保存时父进程 fork 出一个子进程,子进程负责把数据写盘,父进程继续处理客户端请求,两者通过操作系统的写时复制(COW)机制共享内存页。这样保存工作不占用主线程,Redis 能维持高吞吐。

但 fork 本身是有成本的。数据量越大,fork 需要复制的页表越大,耗时越长,这段时间主线程会被卡住,无法在毫秒级响应客户端。资料里明确写了这个缺点:「经常 fork 子进程备份数据,当数据量大时,fork 过程非常耗时,导致 redis 无法在毫秒级别内响应客户端请求」。另一个缺点是丢数据:如果 Redis 在两次快照之间宕机,这段时间的写入全部丢失。

3.2 AOF 追加日志:用文件体积换数据安全

AOF 换了个思路:不存数据快照,而是把每一条写命令追加到文件末尾。恢复时重新执行这个文件里的命令,就能重建数据。核心配置包括appendonly yes开启追加,appendfsync控制同步策略,no-appendfsync-on-rewrite no决定重写期间是否暂停同步,auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64m控制重写触发条件。

appendfsync有三个取值,这是 AOF 最关键的取舍点。everysec每秒写入并完成磁盘同步,最多丢 1 秒数据,是默认值也是大多数场景的平衡点;always每次写入都同步,几乎不丢数据但性能最差;no写入但不主动同步,交给操作系统决定,性能最好但丢数据风险最大。

AOF 的代价是文件膨胀。同样的数据集,AOF 文件通常比 RDB 大得多,恢复时要逐条重放命令,耗时长。所以需要 rewrite:当文件大小达到上次重写时的一倍(auto-aof-rewrite-percentage 100)且超过 64MB(auto-aof-rewrite-min-size 64m)时,触发重写把冗余命令合并。

3.3 两种策略的取舍与组合

把两种持久化放在一起对比,取舍就很清楚了:

维度RDBAOF
数据安全两次快照间可能丢大量数据everysec 最多丢 1 秒
文件体积小,二进制压缩大,命令文本
恢复速度快,直接加载慢,逐条重放
性能影响fork 耗时随数据量增长同步策略影响吞吐
适用场景定期备份、容忍丢数据数据安全要求高

常见的实现思路是两者同时开启:用 RDB 做冷备和快速恢复,用 AOF 保证数据安全。重启时优先用 AOF 恢复(因为它更完整),AOF 不可用时回退到 RDB。资料里没有明确写组合策略,但从两种机制的优缺点可以推出这个结论。

4. 事务与发布订阅:被误解的两个特性

4.1 MULTI/EXEC 不是数据库事务

Redis 事务的命令只有五个:MULTI标记事务开始,EXEC执行队列里的所有命令,DISCARD放弃事务,WATCH监视 key,UNWATCH取消监视。

它的工作方式是:MULTI之后的所有命令不会立即执行,而是被放进一个队列,直到EXEC才按顺序原子性地执行。这里的「原子性」指的是「要么全部执行,要么全部不执行」——但注意,它不保证回滚。如果队列里某条命令执行时报错(比如对 string 用了 list 命令),其他命令照样执行,Redis 不会撤销已执行的部分。

WATCH是乐观锁的实现。执行WATCH key后,如果在EXEC之前这个 key 被其他客户端改动过,整个事务会被打断(abort),EXEC返回失败。这适合「读-改-写」场景:先 WATCH,再读值,再 MULTI 写入,如果中途被别人改了,事务失败,客户端重试。

注意:Redis 事务不支持回滚,这是和关系型数据库事务最大的区别。别把它当成通用事务用,它更适合「批量命令打包执行」和「乐观锁」两个场景。

4.2 发布订阅:轻量消息通道,但不持久

发布订阅的命令有SUBSCRIBE订阅频道、PUBLISH发消息到频道、UNSUBSCRIBE退订、PUBSUB CHANNELS列出活跃频道、PUBSUB NUMSUB返回指定频道的订阅者数量。

它的模型是:订阅者订阅频道,发布者往频道发消息,所有订阅者都能收到。但有两个关键限制。第一,消息不持久化,发布时如果没有订阅者,消息直接丢弃。第二,订阅者断线重连期间的消息全部丢失,没有补偿机制。所以它适合做实时通知、配置推送这类「丢了也无所谓」的场景,不适合做可靠消息队列。

资料里提到「可以用于简单的消息发送与订阅」,这个「简单」二字很关键。真正需要可靠投递的场景,得用 list 的BLPOP或者专业消息中间件。

5. 避坑与排查:那些配置项背后的血泪经验

5.1 现象:线上突然大量请求超时,日志显示 fork 耗时飙升

原因:RDB 的save触发太频繁,或者数据量太大导致 fork 子进程时复制页表耗时过长,主线程被阻塞。资料里明确写了这个风险。

解决:调大save的间隔和阈值,比如从save 60 1000改成save 900 1;或者关闭自动 RDB,改用从节点做持久化,主节点只负责读写。数据量特别大时,考虑拆分实例。

5.2 现象:AOF 文件涨到几十 GB,重启恢复要几个小时

原因:auto-aof-rewrite-percentage和auto-aof-rewrite-min-size设置不合理,或者 rewrite 一直没触发。AOF 文件体积通常远大于 RDB,恢复时逐条重放命令,文件越大越慢。

解决:确认 rewrite 配置生效,auto-aof-rewrite-min-size不要设得过大;定期手动触发BGREWRITEAOF;如果数据安全要求不高,可以只保留 RDB。

5.3 现象:appendfsync always配置后写入性能断崖式下跌

原因:always要求每次写入都完成磁盘同步,磁盘 IO 成为瓶颈,单线程被同步操作卡住。

解决:改回everysec,接受最多丢 1 秒数据的代价。如果业务真的不能丢数据,应该在上层做补偿,而不是靠always硬扛。

5.4 现象:用KEYS *查 key 导致整个实例卡死

原因:KEYS是 O(N) 操作,在单线程模型下会阻塞所有其他命令,key 数量多时直接卡死。

解决:用SCAN系列命令做游标式渐进遍历,每次返回一小批,不阻塞主线程。生产环境禁用KEYS。

5.5 现象:主从切换后部分写入丢失

原因:主从复制是异步的,主节点写入后立即返回,从节点可能还没同步。主节点宕机时,未同步的写入就丢了。资料里提到「主机从机的宕机都会导致前端部分读写请求失败」。

解决:对数据一致性要求高的场景,用WAIT命令等待指定数量的从节点确认;或者接受异步复制的最终一致性,在上层做幂等和补偿。

6. 从配置项反推设计哲学:Redis 的边界在哪里

把这份资料从头到尾看一遍,会发现 Redis 的每一个设计选择都在做同一件事:用简单换性能,把复杂度推给使用者。单线程换掉了锁和上下文切换,RDB 的 fork 换掉了主线程的阻塞,AOF 的 everysec 换掉了绝对的数据安全,发布订阅换掉了消息持久化。它从来不试图做一个「什么都能干」的系统,而是把边界画得很清楚。

这个边界在哪里?在线扩容难,资料里直接写了「较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂」。主从异步复制会丢数据。事务不支持回滚。发布订阅不持久。这些都是设计取舍的结果,不是 bug。

我一般会这么理解:Redis 的定位是「内存里的高速数据结构服务器」,它的所有能力都围绕这个定位展开。持久化是为了重启后能恢复,不是为了做数据库;主从是为了读写分离和高可用,不是为了强一致;事务是为了批量执行和乐观锁,不是为了 ACID。一旦你把它当数据库用,要求强一致、要求不丢数据、要求在线扩容,就会处处碰壁。

验证一个 Redis 配置是否合理,我习惯问三个问题:数据丢了能接受吗?能接受丢多少?恢复速度要求多快?这三个问题的答案直接决定 RDB 和 AOF 怎么配、appendfsync选哪个、要不要开从节点做持久化。资料里把两种持久化的优缺点列得很清楚,但最终选哪个,取决于你的业务能容忍什么。

从那以后我每次配 Redis 都强制走一遍这三个问题,先把「能丢多少数据」定下来,再倒推持久化策略,而不是上来就抄一份配置文件。希望这份拆解帮到你,资料里的命令和配置项值得对着redis.conf逐条过一遍,比背命令有用得多。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进