工程实践 Redis

Redis 线程与 IO 模型:单线程高效的原因

详解 Redis 4.x 的单线程模型、6.x 的多线程 IO、I/O Multiplexing(epoll/select/kqueue)、以及 Redis 速度快的真正原因。

发布于 2026/03/21 更新于 2026/03/21 1 分钟

“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 引入了多线程 IOio-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 过期策略)