当前位置: 首页 > news >正文

【Redis从入门到精通】第35篇:Redis为什么这么快——单线程也能称王的秘密

上一篇【第34篇】Redis事件驱动模型——epoll/select/kqueue的多路复用
下一篇【第35篇】Redis为什么这么快——单线程也能称王的秘密


网上有个经典的面试题:"Redis是单线程的,为什么还这么快?"能答好这个问题的人不多,大多数人只能说个"因为基于内存"就词穷了。

这篇文章,我们把Redis快的所有原因掰开揉碎了讲清楚。不仅告诉你它为什么快,还要告诉你它什么时候会慢,以及怎么让它更快

Redis到底有多快?

先来看一组数据。用redis-benchmark对一台普通服务器进行压测:

# 基本性能测试(50个并发客户端,10万次请求)$ redis-benchmark-c50-n100000-qSET:112359.55requests per second GET:125000.00requests per second INCR:121951.22requests per second LPUSH:113636.36requests per second LPOP:117647.06requests per second SADD:114942.53requests per second HSET:107526.88requests per second

单机10万+QPS,这是一台普通服务器的表现。在更好的硬件和优化配置下,最新版Redis可以达到100万QPS

作为对比:

数据库/缓存典型QPS数据存储方式
Redis10万~100万内存
Memcached10万~50万内存
MySQL几千~几万磁盘
PostgreSQL几千~几万磁盘
MongoDB几千~几万磁盘+内存缓存

Redis比传统数据库快了1~2个数量级,即使和同为内存数据库的Memcached相比也毫不逊色,而且功能丰富得多。

快的原因一:完全基于内存

这是最根本、最好理解的原因——内存操作的速度远远快于磁盘操作

访问延迟对比: CPU L1 Cache: 0.5 ns(纳秒) CPU L2 Cache: 7 ns 内存(RAM): 100 ns SSD固态硬盘: 100,000 ns(100微秒) = 0.1 ms 机械硬盘(随机): 10,000,000 ns(10毫秒) 网络请求(同城): 500,000 ns(0.5毫秒) 内存 vs SSD 差距:1000倍 内存 vs 机械硬盘差距:100,000倍

Redis的所有数据都存储在内存中,读写操作直接访问RAM,没有任何磁盘寻道、旋转、磁头移动的物理开销。

用一个比喻:MySQL读数据就像去图书馆找书——你先要查目录(索引),然后走到对应的书架,找到对应的书架层,再抽出那本书。每一步都有物理移动的时间成本。而Redis读数据就像书就在你手边,伸手就能拿到。

# 直观感受:SET/GET操作的延迟通常在微秒级127.0.0.1:6379>DEBUG SLEEP0# 不sleep127.0.0.1:6379>SET test_key test_value# 延迟:< 0.1ms(100微秒以内)# 对比MySQL的一次查询# 即使有索引,也通常需要 0.5~5ms# 如果没有索引,可能需要 10ms~数秒

快的原因二:高效的数据结构

光有内存还不够——如果你的内存数据结构设计得不好,照样会慢。Redis在数据结构上做了大量的精细化优化,针对不同使用场景使用了最合适的编码方式。

Redis数据结构编码优化全景: String(字符串) ├── int编码:整数直接存储(省内存,比较快) ├── embstr编码:≤44字节的小字符串(一次内存分配) └── raw编码:大于44字节的字符串(SDS,预分配+惰性释放) Hash(哈希) ├── ziplist/listpack编码:少量字段时使用(紧凑存储,省内存) └── hashtable编码:字段多时使用(O(1)查找) List(列表) ├── quicklist编码:ziplist+双向链表(分段压缩,平衡内存和操作效率) └── (Redis 3.2之前用ziplist或linkedlist) Set(集合) ├── intset编码:全部是整数时使用(有序数组,二分查找) └── hashtable编码:有非整数或元素多时使用 Sorted Set(有序集合) ├── ziplist/listpack编码:少量元素时使用 └── skiplist + hashtable编码:元素多时使用 ├── skiplist:范围查询O(log n) └── hashtable:单点查询O(1)

SDS——Redis的字符串黑科技

Redis没有直接使用C语言的字符串(char*+\0结尾),而是自己实现了一个叫**SDS(Simple Dynamic String)**的数据结构:

structsdshdr{intlen;// 已使用长度(O(1)获取字符串长度,不需要遍历)intfree;// 未使用长度charbuf[];// 字符数组(二进制安全,不会有\0截断问题)};

SDS相比C字符串的优势:

特性C字符串(char*)SDS
获取长度O(n)遍历O(1)直接读len字段
缓冲区溢出手动检查,容易出错自动扩容
内存修改每次修改重新分配预分配+惰性释放
二进制安全不能存储含\0的数据可以存储任意二进制数据

跳表(Skip List)——Redis的"高速公路"

有序集合(Sorted Set)使用跳表实现范围查询,这是Redis最精妙的数据结构之一:

跳表结构示意(查找元素6): Level 3: [1] ────────────────────────── [9] │ │ Level 2: [1] ────── [4] ────────── [9] │ │ │ Level 1: [1] ── [3] ─ [4] ── [6] ── [9] │ │ │ │ Level 0: [1]─[2]─[3]─[4]─[5]─[6]─[7]─[8]─[9] 查找路径:1 → 4 → 6(只检查了3个节点!) 等效于二分查找的O(log n)复杂度

跳表的查询效率接近平衡树(O(log n)),但实现更简单,范围查询更方便,而且内存开销更可控。

快的原因三:单线程无锁

这是很多人感到困惑的地方——单线程怎么反而更快了?

答案是:在Redis的工作负载下,单线程避免了多线程的额外开销,总体性能反而更好

多线程的隐藏成本

多线程的开销清单: 1. 上下文切换开销 每次线程切换需要保存/恢复寄存器状态 耗时:~1~10微秒 高并发时可能每秒切换数千次 2. 锁竞争开销 多个线程访问共享数据需要加锁 加锁/解锁本身需要原子操作(CPU指令级别) 锁等待会导致线程阻塞 可能出现死锁、活锁、优先级反转等问题 3. CPU缓存失效 每个CPU核心有自己的L1/L2缓存 多线程修改共享数据会导致缓存行失效(Cache Line bouncing) 严重时性能可能下降10倍以上 4. 代码复杂度增加 多线程程序容易出并发bug 调试困难,测试不充分

单线程为什么在这里更优?

Redis的命令执行极快(微秒级),而多线程的锁竞争和上下文切换通常也是微秒级。如果命令执行本身够快,加锁的额外开销就可能比命令执行本身还多。

单线程处理10000个命令: 总耗时 = 10000 × 1微秒 = 10毫秒 多线程(4线程)处理10000个命令: 命令执行 = 10000 × 1微秒 / 4 = 2.5毫秒 锁竞争 ≈ 10000 × 0.5微秒 = 5毫秒 上下文切换 ≈ 2000 × 2微秒 = 4毫秒 总耗时 ≈ 11.5毫秒 反而更慢了!

此外,单线程还有两个重要好处:

  • CPU缓存友好:只有一个线程访问数据,CPU缓存命中率极高
  • 代码简单可靠:没有并发问题,不容易出bug

踩坑提示:Redis并不是"绝对单线程"。Redis 4.0引入了异步删除(用后台线程执行UNLINKFLUSHALL ASYNC等操作),Redis 6.0引入了I/O多线程(网络读写多线程),Redis 7.0引入了更多后台任务线程。但命令执行始终是单线程的

快的原因四:I/O多路复用

上一篇文章我们详细讲了Redis的I/O多路复用机制。这里简要回顾它对性能的贡献:

I/O多路复用对Redis性能的贡献: 1. 单线程处理数万连接 不需要为每个连接创建线程 节省了大量的内存(每个线程栈至少1MB) 避免了线程调度的开销 2. 非阻塞IO 读写操作不会阻塞等待 命令执行完立刻可以处理下一个客户端 3. 事件驱动 没有请求时epoll_wait阻塞等待,不消耗CPU 有请求时立即响应,延迟极低

如果没有I/O多路复用,Redis要处理10000个并发连接,就需要10000个线程(BIO模型),每个线程1MB栈空间就是10GB内存——这还没算线程切换的开销。

一次Redis命令请求的完整生命周期

让我们把所有因素串联起来,追踪一条命令从发送到返回的完整旅程:

客户端 网络层 Redis服务器 │ │ │ │ SET key value\r\n │ │ │ ──────────────────► │ TCP传输 │ │ │ ──────────────────► │ │ │ │ ① epoll_wait检测到 │ │ │ FD可读 │ │ │ ② readQueryFromClient │ │ │ 读取数据到querybuf │ │ │ ③ 解析RESP协议 │ │ │ SET key value │ │ │ ④ 查找key │ │ │ hashtable O(1) │ │ │ ⑤ 执行SET命令 │ │ │ 写入SDS,< 1微秒 │ │ │ ⑥ 生成响应 │ │ │ +OK\r\n │ │ │ ⑦ 追加到输出缓冲区 │ │ │ ⑧ 注册写事件 │ │ TCP传输 │ │ │ ◄────────────────── │ ⑨ sendReplyToClient │ +OK\r\n │ 发送响应 │ FD可写时发送 │ ◄────────────────── │ │ │ │ │ │ 总延迟:< 1ms │ │

整个过程中:

  • 网络:两次TCP传输(通常<1ms,局域网)
  • 读解析:epoll通知+读取+解析(~几微秒)
  • 命令执行:内存操作(~<1微秒)
  • 写响应:epoll通知+发送(~几微秒)

瓶颈通常在网络,不在Redis本身。这就是为什么Pipeline(批量发送命令减少网络往返)能大幅提升性能。

哪些操作会阻塞Redis?

虽然Redis很快,但有些操作确实会导致阻塞,影响其他客户端的请求:

操作 时间复杂度 阻塞风险 ───────────────────────────────────────── KEYS * O(N) 高!全库扫描 FLUSHALL O(N) 高!清空所有数据 SORT mylist O(N log N) 中,N是列表大小 LRANGE mylist 0 -1 O(N) 中,获取全部元素 SMEMBERS bigset O(N) 中,返回全部成员 HGETALL bighash O(N) 中,返回全部字段 SUNION set1 set2 O(N) 中,N是较大集合的大小 删除含过期key的db O(N) 低~中,取决于过期key数量 大Key操作 O(大) 高,如1MB的String

踩坑提示

  • 永远不要在生产环境用KEYS *,用SCAN代替
  • 大Key(Value超过10KB)是Redis慢查询的头号元凶
  • 如果必须执行耗时操作,考虑使用UNLINK(异步删除)或FLUSHALL ASYNC
  • SLOWLOG GET 10查看慢查询日志

突破Redis单机性能上限

虽然Redis单机性能很强,但总有极限。突破的方式:

1. Pipeline——减少网络往返

# 不用Pipeline:每次请求都要一次网络往返# 1000次SET = 1000次网络往返 ≈ 1000ms# 使用Pipeline:一次发送多个命令echo-e"SET k1 v1\nSET k2 v2\nSET k3 v3\n..."|redis-cli--pipe# 1000次SET = 1次网络往返 ≈ 几毫秒# 性能提升100~1000倍!

2. Cluster分片——水平扩展

Redis Cluster 分片示意: 客户端 │ ├── Node A (Master) ◄── Node A' (Slave) │ ├── key1, key5, key9 (slot 0~5460) │ ├── Node B (Master) ◄── Node B' (Slave) │ ├── key2, key6, key10 (slot 5461~10922) │ └── Node C (Master) ◄── Node C' (Slave) ├── key3, key7, key11 (slot 10923~16383) 总QPS = 单机QPS × 节点数(理论上线性扩展) 3个节点:30万~90万QPS

3. 合理使用数据结构

选择合适的数据结构能显著提升性能:

# 反模式:用List存储需要去重的数据LPUSH mylist user1 user2 user3 user1 user2# 正确做法:用Set自动去重SADD myset user1 user2 user3 user1 user2# 反模式:用String存储JSON,每次取全部GET user:1001# 返回整个JSON字符串# 正确做法:用Hash按字段存取HGET user:1001 name HGET user:1001 age

Redis vs Memcached

同为内存数据库,Redis和Memcached的性能对比:

特性RedisMemcached
单机QPS10万~100万10万~50万
数据结构丰富(String/Hash/List/Set/ZSet)简单(Key-Value)
持久化RDB + AOF
内存管理自定义(多种编码)Slab Allocation
高可用Sentinel/Cluster需要客户端分片(如Twemproxy)
线程模型单线程(I/O多线程可选)多线程
最大Value512MB1MB
功能事务/Lua脚本/发布订阅/流纯KV缓存

性能上两者相近,但Redis在功能上全面碾压。这也是Redis逐渐成为主流选择的原因——既然性能差不多,当然是功能多的赢了

Redis 7.x的性能优化

Redis 7.x在性能方面做了不少改进:

Redis 7.x 性能优化点: 1. Multi-part AOF 减少AOF重写的内存压力,降低fork延迟 2. listpack替代ziplist 更紧凑的内存编码,更少的内存碎片 3. 函数(Function) 替代Lua脚本,更高效的脚本管理 4. acllog改进 更细粒度的权限控制,减少安全检查开销 5. 改进的client-eviction 在内存满时更智能地驱逐客户端连接

用redis-benchmark压测Redis

最后,让我们实际操作一下redis-benchmark

# 基础压测:50个并发,10万次请求$ redis-benchmark-c50-n100000# 指定测试命令$ redis-benchmark-c50-n100000-tset,get,incr# 测试Pipeline效果$ redis-benchmark-c50-n100000-P16# -P 16 表示每次Pipeline发送16条命令# 测试大数据Value$ redis-benchmark-c50-n10000-d1024# -d 1024 表示Value大小为1024字节# 只测试SET命令的延迟分布$ redis-benchmark-c50-n100000-tset--csv"SET","112359.55","0.233","0.101","0.455"# 分别是:命令名、QPS、平均延迟ms、最小延迟ms、最大延迟ms

一个简单的Pipeline对比测试:

# 无Pipeline$ redis-benchmark-c50-n100000-tset-P1SET:110000.00requests per second,p50=0.231msec# Pipeline 16$ redis-benchmark-c50-n100000-tset-P16SET:850000.00requests per second,p50=0.031msec# Pipeline 64$ redis-benchmark-c50-n100000-tset-P64SET:1350000.00requests per second,p50=0.019msec

可以看到,Pipeline将性能从11万QPS提升到了135万QPS,提升超过12倍。这就是减少网络往返的威力。

总结

Redis为什么这么快?四个核心原因:

  1. 完全基于内存——纳秒级访问速度,比磁盘快1000~100000倍
  2. 高效的数据结构——SDS、ziplist/listpack、skiplist、intset等针对场景精细化优化
  3. 单线程无锁——避免了线程切换和锁竞争的开销,CPU缓存命中率极高
  4. I/O多路复用——epoll/kqueue让单线程高效处理海量并发连接

另外记住:

  • Redis并不是"绝对单线程"——后台线程处理持久化、异步删除、I/O读写等
  • 瓶颈通常在网络而非Redis本身——用Pipeline和Cluster突破瓶颈
  • 大Key和慢命令是Redis性能的头号杀手
  • redis-benchmark是你评估Redis性能的好帮手

理解了Redis为什么快,你不仅能更好地使用它,还能在面对性能问题时快速定位原因。接下来我们将深入Redis的客户端世界,看看Redis是怎么管理每一个连接的。下一篇《Redis客户端属性大揭秘》为你揭开面纱。


上一篇【第34篇】Redis事件驱动模型——epoll/select/kqueue的多路复用
下一篇【第35篇】Redis为什么这么快——单线程也能称王的秘密


http://www.jsqmd.com/news/938957/

相关文章:

  • 浏览器音乐解锁工具:3分钟解决你的加密音乐播放难题
  • 2026年GEO源码服务商选型深度评测与避坑指南 - 品牌报告
  • 焦作母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • 【Claude博弈论实战指南】:20年AI架构师亲授3大经典场景建模方法与避坑清单
  • 2026年想找有社区交流功能的手机阅读器?这些选择别错过!
  • 2026年薪酬设计五步法:从零搭建公平激励体系
  • 【Redis从入门到精通】第36篇:Redis客户端属性大揭秘——一个连接背后有多少状态
  • 葫芦岛母婴除甲醛CMA甲醛检测治理公司深度测评:清醛卫士稳居榜首 - 五金回收
  • Windows Defender完全移除终极指南:专业级系统性能优化与安全组件深度清理
  • 3个简单步骤彻底解决魔兽争霸III现代化难题:WarcraftHelper完全指南
  • 【顶刊】基于ESO+MFPCC+ADRC,二阶三阶ESO扩展状态观测器的PMSM驱动器无模型预测电流电机控制算法
  • 深度实战:构建79万条中文医疗对话数据集的完整指南
  • 【Redis从入门到精通】第37篇:Redis服务器启动全流程——从redis-server到ready to accept
  • 深度解析:2026年现阶段山东不错的电线杆制造厂选哪家 - 2026年企业资讯
  • 标注软件WPF-LabelImg的使用教程
  • 基于树莓派与电子墨水屏的慢速电影播放器DIY全攻略
  • WarcraftHelper完整使用教程:魔兽争霸3性能优化终极指南
  • 打破音乐枷锁:3分钟掌握开源音频解密核心技术
  • 给物理模拟新手的Geant4保姆级入门:从第一个例子到看懂运行日志
  • Linux 组调度的 cfs_bandwidth 结构体:带宽控制的核心配置
  • 【Redis从入门到精通】第38篇:serverCron——Redis的“心跳“定时任务干了哪些活
  • 湘潭CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • AI | ollama - [入门]
  • 旧项目安装QtFusion找不到IMcore:补wheel依赖还是迁移VibeFlux
  • 告别模板化论文困局:okbiye 分层式毕设创作体系,从资料上传到终稿排版全链路落地
  • 基于LM324运放的土壤湿度监测电路设计与实践
  • BetterGI AI自动化游戏辅助工具完整教程:从新手到专家的原神自动化指南
  • STM32F103硬件SPI直驱GC9A01芯片1.28寸240×240 TFT屏,含GUI与测试例程
  • 基于Arduino与HC-SR04的超声波表情显示系统设计与实现
  • 如何轻松地将 iTunes 备份传输到三星