Redis为什么这么快?拆解内存、单线程与数据结构的三大支柱
说到Redis做过几年后端的基本都听过类似对比同样一条查询MySQL忙活半天返回几百毫秒Redis呢微秒级就搞定了。很多人把这个差距简单归因于“Redis把数据放内存所以快”我当年也这么以为直到认真翻了一遍源码和官方文档才发现这句话只说对了三分之一。Redis的极致性能背后是整个体系精心设计的结果不是单一因素能解释的。这篇文章我想把这三根真正的“支柱”拆开讲透全内存的数据访问设计、单线程事件循环与I/O多路复用、以及底层那套专门为性能打磨的数据结构。不管你是刚入门想看明白Redis为什么快还是准备面试想答出和别人不一样的深度又或者是要做缓存治理、分布式锁这些实战场景这篇文章都能给你一个完整的分析框架。1. 先搞清楚快到底快在哪里1.1 这个“快”不是玄学是能跑出数据的快先看一组数据。官方曾经公布过在给定的测试条件下Redis单实例可以跑到10万以上的QPS。我自己的服务器上用redis-benchmark压过SET和GET混合场景下单线程轻松跑到8万QPS左右生产环境里配合客户端连接池单实例抗几万并发毫无压力。这还没算上Redis集群和主从读扩展之后的能力。你对比一下普通的关系型数据库单机MySQL读写混合能做到几千到一万QPS已经很了不起大部分时候还要靠分库分表、读写分离才能撑住业务。为什么差距这么悬殊核心只有一个数据路径上的每个环节Redis都做了极致取舍。内存、线程模型、数据结构这三样东西单独拎出来任何一门技术都有但Redis把它们组合到了一个极其纯粹的执行模型里才造就了那种“快到不真实”的体验。1.2 三大支柱的整体逻辑先有个框架为了让后面的分析不那么散我先给个整体框架。第一根支柱是数据存储介质Redis把所有数据都放在内存里这是它快的地基第二根支柱是执行模型Redis用单线程加I/O多路复用处理命令避免了多线程环境里锁竞争和上下文切换的巨大开销第三根支柱是数据结构Redis为不同使用场景设计了SDS、跳表、压缩列表等底层结构把内存访问效率压榨到了极致。三根支柱不是孤立的它们互相咬合。内存访问快但还需要高效的数据结构来组织单线程避免并发问题但必须配合非阻塞I/O才能撑住高并发数据结构再高效如果执行模型拖后腿一样快不起来。所以说Redis的性能不是“某一环很强”而是整条链路都不弱没有明显的短板。下面一根一根说。2. 第一根支柱把数据放在内存里这是天然的加速器2.1 内存和磁盘的速度差距大到你想象不到很多人对“内存比磁盘快”这件事没有具体的体感我换个说法。内存随机访问延迟大概是100纳秒级别也就是0.0001毫秒而SSD随机读延迟一般在0.1到0.2毫秒机械硬盘更离谱是5到10毫秒。算下来内存比SSD快上千倍比机械硬盘快十万倍。这个数量级意味着什么MySQL这类存储引擎哪怕做了各种缓存优化最终数据还是要落到磁盘上事务提交、页落盘都可能触发磁盘I/O。而Redis从设计上就把“所有数据常驻内存”作为前提读写操作直接操作内存地址从源头砍掉了磁盘I/O这条最慢的路径。这就是它快的第一层逻辑选了一条更快的高速公路。但这里有个容易被忽略的点Redis并不是完全不碰磁盘。RDB快照、AOF日志、主从同步都会涉及磁盘I/O。只不过这些操作都被设计成了异步或后台进行不阻塞主命令链路。我们在5.2节再展开讲持久化对性能的影响这里只要记住一句话主路径上的数据访问Redis确保不碰磁盘。2.2 数据模型的精巧设计让内存优势真正落地单纯把数据放内存还不够Redis快还得益于它的数据模型。你想想MySQL里查一条数据要经过解析SQL、查询优化器、执行引擎、存储引擎、B树索引查找最终才能定位到数据页。Redis呢它根本不搞SQL解析那一套每个命令内部就是一个哈希表查找或者链表遍历典型操作的时间复杂度是O(1)或者O(log N)。比如我们要查一个字符串keyRedis直接通过哈希表定位到dictEntry取出对应的SDS字符串完事了。查询一个集合成员走的是哈希表或跳表也不会全表扫描。这种“结构简单、操作直接”的模型让内存访问的优势得到了最大程度的放大。换句话说Redis的快不仅因为内存更因为它没有用复杂的查询机制来抵消内存带来的优势。再补充一点Redis的key本身就是二进制安全的可以是任意字符串连空字符都能存。这意味着你可以设计极其高效的key格式比如把多级维度直接拼成一个key省去多表关联的麻烦。这一块在缓存治理项目里特别有用合理设计key能做到一次查询拿全数据而不是来回多次访问。2.3 内存快但数据不能丢持久化和性能的平衡内存访问虽然快但有一个致命弱点一断电全没了。所以Redis提供了RDB和AOF两种持久化机制这也让很多人产生了疑问既然要写磁盘性能为什么没崩关键在于设计上的取舍。RDB是生成某一时刻的全量快照主进程通过fork子进程来异步落盘子进程负责写文件主进程继续处理命令。AOF则是追加写命令日志但是支持配置成everysec也就是每秒刷一次盘极端情况下最多丢一秒数据但正常命令处理完全不受磁盘写入的影响。把持久化放到后台或者低频路径上Redis保住了主干道上的速度。我做了多年缓存治理之后感悟最深的一点是Redis从设计之初就确定了一个原则——性能优先可靠性用工程手段弥补。它不会为了持久化去牺牲每一次读取和写入的延迟而是把可靠性的成本都放到了异步路径上。理解了这一点就理解了Redis为什么能在内存型KV存储里一骑绝尘。3. 第二根支柱单线程模型 I/O多路复用越简单越快3.1 为什么单线程反而是最佳选择一听“单线程”很多做并发编程的人会下意识觉得是缺点多核CPU不是浪费了吗但Redis不仅活得很好还跑得飞快。秘密在于Redis的性能瓶颈从来不是CPU而是网络I/O和内存带宽。单线程模式少了很多多线程编程的麻烦没有锁竞争没有线程上下文切换的开销没有共享数据的缓存失效问题整个命令执行过程是确定的、可预测的。这里有一个很关键的点Redis是将“单线程”和“事件循环”绑在一起用的。它的主线程其实是一个不停循环的事件处理器从I/O多路复用器里拿到就绪的事件执行对应的命令回调然后回到循环。每一步都是串行执行命令之间不可能互相干扰天然就是线程安全的。这也是Redis之所以能提供原子操作的原因之一每条命令不需要额外加锁因为整个执行过程根本没有并发。我举个生活中的类比。一个顶级厨师一个人配菜、炒菜、装盘虽然只能一次做一道菜但每道菜之间不用互相等待、不用协调锅具出菜节奏非常稳定。如果换成一个团队反而要解决锅谁用、灶谁占、菜谁洗的问题协调成本比做菜本身还高。Redis就是这个“顶级厨师”它选择把协调成本归零。3.2 从select到epollI/O多路复用的进化单线程再快如果无法同时感知成千上万个客户端连接的状态一样是白搭。Redis采用的方案是I/O多路复用在Linux平台上用的是epoll在macOS上用的是kqueue在Windows上使用的是select模型这就导致官方不建议在Windows上直接跑生产环境的Redis。epoll的精髓在于“事件驱动”Redis只需要告诉内核“我在等这些连接上有没有数据可读、可写”然后阻塞在等待队列里。当某个客户端发送命令时内核把就绪的事件告诉RedisRedis才去处理它。在没有事件的时候Redis可以睡大觉不浪费CPU有事件的时候立刻响应。这样单线程就能扛住几十万个并发连接而不是为每个连接开一个线程。我自己用strace看过Redis处理请求时的系统调用正常情况下基本看不到accept、read这种阻塞调用只有epoll_wait在那里循环等待。这就是为什么Redis在高并发下CPU占用依然平稳因为它的CPU全部花在了真正干活上而不是空转轮询。3.3 单线程的边界为什么6.0又开始引入多线程如果单线程那么好为什么Redis 6.0还要引入多线程I/O这不是打脸而是针对瓶颈的精准修正。随着业务规模扩大单线程在超高并发下会出现一个经典瓶颈网络数据的读写accept、read、write这些系统调用本身是CPU密集操作。当请求量到达百万级时光是在内核态和用户态之间拷贝数据就占据了大量CPU时间导致命令执行时间被压缩。Redis 6.0的解决方式是网络I/O部分可以配置多线程也就是多个线程负责从客户端读取请求、解析完以后把命令丢给主线程去执行执行完的结果再由I/O线程们写回客户端。真正操作数据结构的还是单线程所以命令执行的原子性没有被破坏。这相当于给厨师配了几个端菜的人炒菜还是一个人炒但上菜速度明显提升了。这个演进过程特别值得学习不是无脑跟随“多线程一定更快”的潮流而是先分析瓶颈到底在哪里再针对瓶颈做最小的改造。Redis的性能哲学从来都是对症下药而不是过度设计。4. 第三根支柱底层数据结构每一纳秒都在抠4.1 SDS字符串不只是包了一层char数组Redis没有直接用C语言的字符串而是自己实现了SDSSimple Dynamic String简单动态字符串。从名字看好像只是封装实际上每个设计点都踩在C字符串的痛点上。C字符串用strlen获取长度要遍历到\0时间复杂度O(N)SDS在结构体里记录len字段O(1)就能拿到长度C字符串拼接容易覆盖内存越界SDS在写入前会检查空间不够就扩展C字符串要求内容里不能有\0否则会截断而SDS用len字段判断长度所以可以存任意二进制数据图片、序列化对象都行。改两个小细节带来的收益在Redis的字符串操作场景里被无限放大。字符串是最常用的数据类型缓存、计数器、分布式锁用的都是它字符串的效率上去了整个系统的性能也就稳了。4.2 跳表、压缩列表与快速列表不同规模下用不同武器有序集合类型ZSet在成员数量少、元素体积小的时候Redis用的是压缩列表ziplist把所有元素紧凑地排列在一起省内存、CPU缓存命中率高一旦数据量变大或者元素长度变长Redis会自动转换为跳表加哈希表的组合结构。跳表是一种多层链表插入、删除、查找都能做到O(log N)而且比平衡树实现简单得多不需要复杂的旋转操作对Redis这种追求极致简单和稳定的项目来说非常合适。哈希和列表类型也有类似的优化策略。List列表在数据量小的时候是压缩列表大了以后会转成快速列表quicklist本质上是很多个压缩列表用双向指针串起来既能快速在头部尾部操作又不会因为内存过于分散而降低缓存命中率。这些转换都是Redis自动完成的不需要开发者干预但它们直接影响实际运行的性能表现面试官特别喜欢问这个细节。4.3 哈希表的渐进式rehash扩容不卡顿的秘诀哈希表是Redis存储所有key的核心结构。随着数据量增长哈希表需要扩容但一个大哈希表如果一次性rehash所有数据重新映射可能阻塞主线程几十毫秒甚至更久。Redis采用的是渐进式rehash扩容时新旧两个哈希表同时存在每次客户端请求命令时Redis顺手迁移一小部分数据通常是100个桶几轮请求之后数据就全部迁移完了整个过程对客户端无感知也不会出现“扩容卡住”的时间窗。这个设计说白了就是把一次性的工作量摊到多次请求中用时间换平滑度。Redis之所以在扩容期间性能依然稳定正是靠这种“细水长流”的策略。它也是分布式系统里“增量迁移”的经典范例我在做数据迁移方案时经常引用这个思路。4.4 数据结构和五大类型如何对应Redis对外提供String、List、Hash、Set、ZSet五种基础数据类型但对内这些类型在不同条件下会使用不同的底层编码。比如小的Hash和ZSet用ziplist大的用 hashtable小的Set用整数集合intset大的用hashtable。用object encoding命令可以查看某个key当前的编码类型。我记得在压测一个缓存服务时发现某个Hash对象里有几万个字段实际读取一个字段竟然卡了十几毫秒。后来一看编码从ziplist转成了hashtable之后单字段读取很快问题其实是网络往返。但如果反过来一个很小的Hash对象如果一直用hashtable编码内存浪费就会很严重。理解底层结构会让你在使用Redis时知道什么样的数据形态是最适合它的这也是学会数据类型之后的进阶能力。5. 支柱之外的提速细节协议、命令与持久化取舍5.1 协议简单到极致RESP的底层好处Redis的客户端与服务端通信使用RESP协议。这个协议是纯文本的每一行都带一个类型标识符比如OK、-Error、$5\r\nhello\r\n。服务器解析起来几乎没有状态机复杂度不需要解析JSON、XML这种带括号、带引号的复杂格式效率极高。这里也有一个实际意义协议越简单客户端实现就越容易生态就越繁荣。你看到网上有各种各样的redis客户端可视化工具比如Redis Desktop Manager、Another Redis Desktop Manager连接起来都很方便这背后都是因为RESP协议极为简洁。我在团队里也经常用可视化工具排查大key配合SCAN命令效率非常高。5.2 持久化配置怎么选影响QPS和安全性持久化设计对性能的影响是双面的。如果开启AOF且刷盘策略设置为always每条写命令都会同步刷磁盘性能会掉得非常厉害设置为everysecRedis延迟执行磁盘写入性能几乎无损设置为no刷盘时机交给操作系统性能最好但数据丢失风险最高。实际生产我建议用everysec再配合RDB快照做兜底既能保住大多数性能又能把数据丢失控制在可接受范围。别去看网上那些极端的“关掉持久化换性能”的教程除非你的业务对数据丢失完全无所谓否则一次意外宕机就够你后悔很久。5.3 缓存治理里那些“拖慢Redis”的坑性能再好的组件用错了照样崩。最常见的三个坑大key一个Hash里有几十万字段或者一个String存了好几MB删除、传输都会阻塞主线程。需要拆分key或者用unlink异步删除。热key某个key的访问量异常高把Redis单实例的CPU打满。解决办法是本地缓存兜底、或者用读写分离把读流量分散。缓存穿透查了不存在的key每次都直达数据库。用布隆过滤器挡一层比Redis加锁加固更有效。还有一个高频场景是Redis分布式锁。很多人直接用SETNX加锁但忘了设置过期时间结果业务异常时锁不释放更规范的做法是用SET key value NX EX timeout一条命令完成加锁和过期时间设置释放锁时用Lua脚本比对值再删除保证原子性。这些实践里Redis本身的性能不是瓶颈但设计不当反而会把Redis拖慢值得留意。6. 实测与面试怎么验证Redis真的快6.1 一条命令跑出基准数据说再多不如自己压一次。Redis官方自带redis-benchmark一条命令就能看到当前服务器的性能数据redis-server --port 6379 --daemonize yes redis-benchmark -t set,get -n 100000 -c 50这条命令会模拟50个并发客户端总共发10万次SET和GET请求。我自己在普通云服务器上跑的结果是SET约9万QPSGET约10万QPS平均延迟在1毫秒以内。你可以对照这个数据感受一下Redis的内存和单线程模型到底有多能打。如果想测试Redis集群或主从架构可以在Docker里快速搭建一套主从环境把部分读流量切到从节点观察整体吞吐的变化。搭建完成后用另一台机器作为客户端做压测效果会更真实。6.2 面试高频题Redis为什么快的完整回答模板面试题里出现频率极高的是“Redis为什么这么快”很多人就回一句“内存快”。面试官其实期待听到的是一套有层次的分析纯内存访问数据路径短。单线程模型避免了锁竞争和上下文切换。I/O多路复用用极少的线程管理海量连接。高效底层数据结构SDS、跳表、压缩列表让内存访问效率最大化。持久化异步化主路径不刷盘。如果你能顺着这几点往下讲最好再结合一次实际压测的数据那就不是一个背题的人而是一个真的在排查过性能问题的人。这也是Redis资料里比“下载、安装、配置”那些基础内容更值得反复琢磨的方向。我自己的感受是Redis这三种设计思想放到任何技术栈里都不过时。内存优先、少就是多、把耗时的活挪到后台这三点不只是Redis能快的原因也是我们做性能优化时可以反复套用的思维模型。最后多说一句。很多人装了Redis、跑通了主从、看了可视化工具里的数据就觉得自己理解了Redis。但性能问题往往是在你没想到的地方冒出来的比如一个大key、一次REPL的阻塞、一次错误的配置都能让“快到飞起”的Redis突然变慢。所以这篇文章讲的三大支柱本质上是帮你建立对Redis的“心智模型”——当你对它的快有了正确的认识也就知道该从哪里下手排查慢的问题了。