“Redis 是单线程的,但它的性能比很多多线程系统还高。这不是矛盾,是 IO 模型设计的胜利。“
前言
Redis 早期确实是纯单线程模型(4.x 以前),6.0+ 引入了多线程 IO用于网络数据读写,但命令执行仍是单线程。理解 Redis 的线程模型,是理解它为什么这么快、以及什么场景会让它变慢的关键。
1. Redis 4.x 之前的单线程模型
客户端请求
↓
epoll/kqueue/IOCP(IO 多路复用)← 同时监听多个 socket
↓
命令解析 → 命令执行 → 响应写回
↓(都在单线程内完成)
客户端响应
核心设计:用 epoll(Linux)/kqueue(macOS)实现 I/O 多路复用,一个线程同时处理 thousands of 连接。
1.1 什么是 I/O 多路复用?
传统 BIO 模式:每个连接一个线程 → 1 万连接 = 1 万线程 → 线程切换成本极高。
# 伪代码:单线程轮询(性能差)
while True:
for conn in all_connections:
if conn.has_data():
data = conn.read()
process(data)
conn.write(response)
# 问题:如果 9999 个连接空闲,仍需逐个检查
# 伪代码:epoll 事件驱动(Redis 用的方式)
while True:
ready_list = epoll.wait() # 只返回有数据的连接
for conn in ready_list:
data = conn.read()
process(data)
conn.write(response)
# 优点:连接多时性能稳定,不随连接数线性增长
2. Redis 6.x 多线程 IO 模型
Redis 6.0 引入了多线程 IO(io-threads do read|write),但不是全局多线程:
┌─────────────────────┐
客户端请求 ─────────────→ │ Main Thread │
│ (命令解析+执行+响应) │
└──────────┬──────────┘
│ 读取请求队列
┌──────────▼──────────┐
│ IO Threads (N个) │
│ - 读取客户端数据 │
│ - 写回响应数据 │
│ - 实际不执行命令 │
└─────────────────────┘
关键点:
- 命令解析和执行仍在主线程
- IO Threads 只负责网络数据读写
io-threads 1= 禁用多线程(和 4.x 一样)io-threads 4= 1 个主线程 + 3 个 IO 线程
# 查看/配置 IO 线程数
CONFIG GET io-threads-do-reads
CONFIG SET io-threads-do-reads yes
CONFIG SET io-threads 4
3. Redis 为什么这么快?
Redis 快的原因(按重要性排序):
1. 内存存储(O(1) 访问)
- 数据全在内存,不涉及磁盘 IO
- GET/SET 操作是纯内存访问,纳秒级
2. 单线程(无锁、无切换)
- 没有线程切换开销
- 没有锁竞争(所有命令串行执行)
- 天然避免竞态条件
3. I/O 多路复用(epoll)
- 单线程同时处理万级并发
- 非阻塞 IO,不浪费 CPU 等待
4. 精心设计的数据结构
- 简单字符串(SDS):O(1) 长度获取
- Hash:渐进式 rehash,避免阻塞
- SkipList:有序集合,O(log N) 范围查询
- QuickList:压缩列表 + 双端链表,内存高效
5. C 语言实现
- 接近硬件,执行效率高
- 没有 GC 停顿(手动内存管理)
3.1 速度对比
操作耗时(近似值):
L1 cache reference: 0.5 ns
L2 cache reference: 7 ns
Redis GET (内存): 100-200 ns ← 比 L2 还慢一点
内存访问: 100 ns
SSD 读: 100,000 ns ← 比 Redis 慢 1000 倍!
网络往返 (同机房): 500,000 ns
网络往返 (跨洲): 150,000,000 ns
Redis 一次 GET ≈ 100 ns,1 秒能处理 ~1000 万次
4. 单线程的局限性
单线程是 Redis 的优势,也是它的瓶颈:
# 场景 1:慢命令导致所有请求阻塞
SLOWLEN 1 # 记录慢查询(>1ms)
# 执行以下命令会阻塞主线程:
LRANGE mylist 0 -1 # 返回整个列表(百万级数据)
KEYS * # 全表扫描(禁止在生产环境)
SORT myset # 排序操作(非 O(N) 复杂度)
# 场景 2:Big Key 导致阻塞
# GET 一个 10MB 的字符串 → 传输耗时 + 主线程阻塞
# SMEMBERS 一个百万成员的集合 → 全量遍历
# 场景 3:持久化操作
BGSAVE # fork 子进程写 RDB(COW 机制)
BGREWRITEAOF # AOF 重写(子进程)
# fork 期间会阻塞主线程(通常几毫秒,内存大时更长)
Redis 慢命令的 O(N) 家族:
# 以下命令慎用,尤其在数据量大时:
KEYS pattern # O(N) 全表 → 禁止生产使用
SCAN cursor # O(1) 渐进式 → 用它替代 KEYS
LRANGE list 0 -1 # O(N) 返回全部 → 加 COUNT 限制
SMEMBERS set # O(N) 返回全部
GETSET key newValue # O(N) → 已被 SET 替代
APPEND key value # O(1) ✓
LLEN key # O(1) ✓
SCARD key # O(1) ✓
5. 银行场景:识别和避免慢命令
@Service
@Slf4j
public class RedisHealthMonitor {
private final RedisTemplate<String, Object> redis;
// 监控慢查询
public List<String> getSlowQueries() {
List<Object> result = redis.execute(
(RedisCallback<List<Object>>) conn ->
conn.commands().slowLogGet(10) // 最近 10 条
);
return result.stream()
.map(Object::toString)
.toList();
}
// 检查大 Key(通过 SCAN + TYPE + DBSIZE 估算)
public void checkBigKeys() {
Map<String, Long> stats = new HashMap<>();
ScanOptions options = ScanOptions.scanOptions()
.match("*")
.count(1000)
.build();
try (Cursor<String> cursor = redis.scan(options)) {
cursor.forEachRemaining(key -> {
String type = redis.type(key).toString();
stats.merge(type, 1L, Long::sum);
});
}
log.info("Key 类型分布: {}", stats);
// String: 50000, Hash: 20000, List: 5000, Set: 10000
}
// 安全获取列表(限制长度)
public List<String> safeLrange(String key, int offset, int limit) {
// 不要用 LRANGE key 0 -1(返回全部)
// 用以下方式限制:
return redis.opsForList().range(key, offset, offset + limit - 1);
}
}
6. Redis 7.x 的并发模型演进
# Redis 7.x 新增:CLIENT LIST TYPE = master replica 等
CLIENT LIST | grep -E "libname=|libver="
# Redis 7.x 多线程改进:
# - io-threads 默认启用
# - 命令管道化(Command Pipelining)减少往返
# 客户端管道示例(减少网络往返)
Jedis jedis = new Jedis("localhost", 6379);
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("key:" + i, "value:" + i);
pipeline.get("key:" + i);
}
List<Object> results = pipeline.syncAndReturnAll();
// 1000 次操作只产生 1 次网络往返
7. 总结
Redis 线程模型:
Redis 4.x: 纯单线程(IO 多路复用 + 命令执行)
↓
Redis 6.x: 主线程 + IO Threads(读写多线程,执行仍单线程)
↓
Redis 7.x: 继续优化 io-threads,支持更多并发
速度快的根本原因:
1. 内存访问(纳秒级)
2. 单线程(无锁、无竞争、无切换)
3. epoll(万级并发)
4. 精心选择的数据结构
单线程的代价:
- CPU 不是瓶颈,但 CPU 密集型任务会卡死 Redis
- 一次只能执行一条命令(但 Pipeline 可以打包)
- 慢命令会影响所有客户端
银行系统优化建议:
- 避免 O(N) 慢命令,用 SCAN 替代 KEYS
- 控制 Big Key 大小(单个 value < 10MB)
- 使用 Pipeline 批量操作减少 RTT
- 监控 SLOWLOG,及时发现慢查询
相关阅读:[Redis 五种基本数据类型](/coding/Redis/Redis 五种基本数据类型) · [Redis Scan 命令用法](/coding/Redis/Redis Scan 命令用法) · [Redis 过期策略](/coding/Redis/Redis 过期策略)