欢迎光临
我们一直在努力

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

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


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

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

Redis到底有多快?

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

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

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

作为对比:

数据库/缓存典型QPS数据存储方式
Redis 10万~100万 内存
Memcached 10万~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 SLEEP 0 # 不sleep
127.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)**的数据结构:

struct sdshdr {
int len; // 已使用长度(O(1)获取字符串长度,不需要遍历)
int free; // 未使用长度
char buf[]; // 字符数组(二进制安全,不会有\\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引入了异步删除(用后台线程执行UNLINK、FLUSHALL 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
单机QPS 10万~100万 10万~50万
数据结构 丰富(String/Hash/List/Set/ZSet) 简单(Key-Value)
持久化 RDB + AOF
内存管理 自定义(多种编码) Slab Allocation
高可用 Sentinel/Cluster 需要客户端分片(如Twemproxy)
线程模型 单线程(I/O多线程可选) 多线程
最大Value 512MB 1MB
功能 事务/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 -c 50 -n 100000

# 指定测试命令
$ redis-benchmark -c 50 -n 100000 -t set,get,incr

# 测试Pipeline效果
$ redis-benchmark -c 50 -n 100000 -P 16
# -P 16 表示每次Pipeline发送16条命令

# 测试大数据Value
$ redis-benchmark -c 50 -n 10000 -d 1024
# -d 1024 表示Value大小为1024字节

# 只测试SET命令的延迟分布
$ redis-benchmark -c 50 -n 100000 -t set –csv
"SET","112359.55","0.233","0.101","0.455"
# 分别是:命令名、QPS、平均延迟ms、最小延迟ms、最大延迟ms

一个简单的Pipeline对比测试:

# 无Pipeline
$ redis-benchmark -c 50 -n 100000 -t set -P 1
SET: 110000.00 requests per second, p50=0.231 msec

# Pipeline 16
$ redis-benchmark -c 50 -n 100000 -t set -P 16
SET: 850000.00 requests per second, p50=0.031 msec

# Pipeline 64
$ redis-benchmark -c 50 -n 100000 -t set -P 64
SET: 1350000.00 requests per second, p50=0.019 msec

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

总结

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

  • 完全基于内存——纳秒级访问速度,比磁盘快1000~100000倍
  • 高效的数据结构——SDS、ziplist/listpack、skiplist、intset等针对场景精细化优化
  • 单线程无锁——避免了线程切换和锁竞争的开销,CPU缓存命中率极高
  • 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为什么这么快——单线程也能称王的秘密


    赞(0)
    未经允许不得转载:171主机测评 » 【Redis从入门到精通】第35篇:Redis为什么这么快——单线程也能称王的秘密
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址