问题

Redis 7 的线程模型是怎样的?为什么说Redis是单线程但又有多线程?

答案

1. 核心概念

Redis的线程模型经常被误解为”纯单线程”,实际上Redis采用的是“单线程命令执行 + 多线程IO处理” 的混合架构。

准确描述

  • 命令执行:单线程(保证原子性和简单性)
  • 网络IO:多线程(提升吞吐量)
  • 后台任务:多线程(避免阻塞)

2. 线程模型演进

2.1 版本演进历程

Redis 1.0-3.x (纯单线程)
    |
    +--> 主线程:网络IO + 命令执行 + 持久化
    |
    缺点:大key删除、AOF刷盘会阻塞

Redis 4.0 (引入BIO线程)
    |
    +--> 主线程:网络IO + 命令执行
    +--> BIO线程:文件关闭、AOF刷盘、懒惰删除
    |
    改进:耗时操作异步化

Redis 6.0 (多线程IO)
    |
    +--> 主线程:命令执行
    +--> IO线程:网络读写、命令解析
    +--> BIO线程:后台任务
    |
    改进:网络IO并行化

Redis 7.0 (增强多线程)
    |
    +--> 主线程:命令执行(单线程)
    +--> IO线程:网络读写(默认启用)
    +--> BIO线程:后台任务(优化调度)
    |
    改进:性能优化、默认启用多线程

3. 完整线程架构

3.1 线程分类

Redis 7 线程架构
├── 主线程 (Main Thread)
│   └── 命令执行、事件循环
│
├── IO线程池 (IO Threads)
│   ├── IO Thread 1: 网络读写
│   ├── IO Thread 2: 网络读写
│   ├── IO Thread 3: 网络读写
│   └── IO Thread N: 网络读写
│
└── BIO线程池 (Background IO Threads)
    ├── BIO_CLOSE_FILE: 关闭文件
    ├── BIO_AOF_FSYNC: AOF刷盘
    └── BIO_LAZY_FREE: 懒惰删除

3.2 线程协作流程

客户端请求完整流程:

[客户端] --1--> [IO线程] --2--> [主线程] --3--> [IO线程] --4--> [客户端]
                  ↓                ↓                ↓
              读取socket        执行命令        序列化响应
              解析协议          (单线程)        写入socket

后台任务流程:

[主线程] --5--> [BIO线程]
                  ↓
              异步执行
              (关闭文件/刷盘/释放内存)

4. 单线程命令执行

4.1 为什么命令执行是单线程?

优势

  1. 无锁设计:避免多线程竞争和锁开销
  2. 原子性保证:命令天然原子执行
  3. 简单可靠:无并发bug,易于维护
  4. 高效执行:纯内存操作,单线程足够快

4.2 事件循环模型

// Redis主线程事件循环(简化版)
void aeMain(aeEventLoop *eventLoop) {
    while (!eventLoop->stop) {
        // 1. 处理定时事件(过期key清理、统计等)
        processTimeEvents(eventLoop);

        // 2. 等待文件事件(客户端连接、命令请求)
        aeApiPoll(eventLoop, tvp);

        // 3. 处理文件事件
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

            // 读事件:接收客户端命令
            if (fe->mask & AE_READABLE) {
                fe->rfileProc(eventLoop, fd, fe->clientData, mask);
            }

            // 写事件:返回响应给客户端
            if (fe->mask & AE_WRITABLE) {
                fe->wfileProc(eventLoop, fd, fe->clientData, mask);
            }
        }
    }
}

4.3 单线程性能瓶颈

# 单线程性能极限测试
redis-benchmark -t set,get -n 1000000 -q

# 单核CPU极限:约10万QPS
# 瓶颈:
# 1. 网络IO(socket读写)
# 2. 协议解析(RESP协议)
# 3. 响应序列化

5. 多线程IO优化

5.1 IO线程工作原理

// IO线程处理流程(简化版)
void *IOThreadMain(void *myid) {
    long id = (long)myid;

    while(1) {
        // 等待主线程分配任务
        listIter li;
        listNode *ln;
        listRewind(io_threads_list[id], &li);

        // 处理读操作(解析命令)
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            readQueryFromClient(c);  // 读取并解析命令
        }

        // 处理写操作(序列化响应)
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            writeToClient(c);  // 序列化并写入socket
        }

        // 通知主线程完成
        io_threads_pending[id] = 0;
    }
}

5.2 主线程与IO线程同步

时间轴:主线程与IO线程协作

主线程                    IO线程1         IO线程2
  |                         |               |
  |-- 分配客户端 ---------->|               |
  |                         |               |
  |-- 分配客户端 ----------------------->   |
  |                         |               |
  |-- 等待完成              |               |
  |                         |               |
  |                    [读取+解析]     [读取+解析]
  |                         |               |
  |<-- 完成通知 ------------|               |
  |<-- 完成通知 -------------------------|   |
  |                         |               |
  |-- 执行命令(单线程)    |               |
  |                         |               |
  |-- 分配响应 ------------>|               |
  |-- 分配响应 ----------------------->     |
  |                         |               |
  |                    [序列化+写入]   [序列化+写入]
  |                         |               |
  |<-- 完成通知 ------------|               |
  |<-- 完成通知 -------------------------|   |

5.3 配置与性能

# redis.conf 配置

# IO线程数(默认4,建议值:CPU核心数的50%-75%)
io-threads 4

# 启用读多线程(默认yes)
io-threads-do-reads yes

# 性能提升对比:
# io-threads 1(单线程):10万QPS
# io-threads 4(多线程):25万QPS(提升150%)
# io-threads 8(多线程):35万QPS(提升250%)

6. BIO后台线程

6.1 三类BIO线程

// BIO线程类型定义
#define BIO_CLOSE_FILE    0  // 关闭文件
#define BIO_AOF_FSYNC     1  // AOF刷盘
#define BIO_LAZY_FREE     2  // 懒惰删除

// 任务队列
list *bio_jobs[BIO_NUM_OPS];
pthread_mutex_t bio_mutex[BIO_NUM_OPS];
pthread_cond_t bio_newjob_cond[BIO_NUM_OPS];

6.2 任务调度

// 主线程提交BIO任务
void bioCreateLazyFreeJob(lazy_free_fn free_fn, void *arg) {
    struct bio_job *job = zmalloc(sizeof(*job));
    job->free_fn = free_fn;
    job->arg = arg;

    pthread_mutex_lock(&bio_mutex[BIO_LAZY_FREE]);
    listAddNodeTail(bio_jobs[BIO_LAZY_FREE], job);
    pthread_cond_signal(&bio_newjob_cond[BIO_LAZY_FREE]);
    pthread_mutex_unlock(&bio_mutex[BIO_LAZY_FREE]);
}

// BIO线程执行任务
void *bioProcessBackgroundJobs(void *arg) {
    while(1) {
        pthread_mutex_lock(&bio_mutex[type]);

        // 等待任务
        while(listLength(bio_jobs[type]) == 0) {
            pthread_cond_wait(&bio_newjob_cond[type], &bio_mutex[type]);
        }

        // 取出任务
        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        pthread_mutex_unlock(&bio_mutex[type]);

        // 执行任务
        job->free_fn(job->arg);
    }
}

7. 并发安全保障

7.1 数据结构无锁设计

// Redis核心数据结构(单线程访问,无需加锁)
typedef struct redisDb {
    dict *dict;           // 键空间(仅主线程访问)
    dict *expires;        // 过期字典(仅主线程访问)
    dict *blocking_keys;  // 阻塞键(仅主线程访问)
} redisDb;

7.2 IO线程同步机制

// IO线程计数器(原子操作)
_Atomic unsigned long io_threads_pending[IO_THREADS_MAX_NUM];

// 主线程等待IO线程完成
void waitIOThreadsFinish(void) {
    for (int j = 1; j < server.io_threads_num; j++) {
        while(io_threads_pending[j] != 0) {
            // 自旋等待
        }
    }
}

8. 性能对比测试

8.1 不同线程配置性能

# 测试命令
redis-benchmark -t set,get -n 1000000 -c 100 -d 256 -q

# 结果对比:
# io-threads 1:  SET: 95000 req/s,  GET: 100000 req/s
# io-threads 2:  SET: 160000 req/s, GET: 180000 req/s
# io-threads 4:  SET: 250000 req/s, GET: 280000 req/s
# io-threads 8:  SET: 320000 req/s, GET: 360000 req/s

8.2 监控指标

# 查看线程统计
INFO stats
# io_threaded_reads_processed: IO线程处理的读操作
# io_threaded_writes_processed: IO线程处理的写操作

# 查看CPU使用
INFO cpu
# used_cpu_sys: 系统CPU时间
# used_cpu_user: 用户CPU时间

9. 最佳实践

9.1 线程数配置建议

# 根据CPU核心数配置
# 4核CPU:io-threads 2-3
# 8核CPU:io-threads 4-6
# 16核CPU:io-threads 6-8

# 示例配置(8核CPU)
io-threads 4
io-threads-do-reads yes

9.2 Java客户端连接池配置

// Lettuce客户端配置(适配Redis多线程)
@Configuration
public class RedisConfig {

    @Bean
    public LettuceConnectionFactory connectionFactory() {
        // 连接池配置
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxTotal(200);        // 最大连接数
        poolConfig.setMaxIdle(50);          // 最大空闲连接
        poolConfig.setMinIdle(20);          // 最小空闲连接
        poolConfig.setMaxWaitMillis(3000);  // 最大等待时间

        // 客户端配置
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(2))
            .build();

        RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration();
        serverConfig.setHostName("localhost");
        serverConfig.setPort(6379);

        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}

9.3 性能调优检查清单

# 1. 检查Redis版本(建议7.0+)
redis-server --version

# 2. 检查多线程配置
CONFIG GET io-threads*

# 3. 检查CPU使用率(建议 < 80%)
INFO cpu

# 4. 检查慢查询(避免阻塞主线程)
SLOWLOG GET 10

# 5. 检查大key(避免阻塞)
redis-cli --bigkeys

10. 常见误区

10.1 误区1:Redis是纯单线程

错误:Redis只有一个线程 正确:命令执行是单线程,但有多个IO线程和BIO线程

10.2 误区2:多线程会导致并发问题

错误:多线程会有数据竞争 正确:命令执行仍是单线程,数据结构无并发访问

10.3 误区3:线程越多越好

错误:io-threads设为16或更多 正确:过多线程导致上下文切换开销,建议不超过CPU核心数

答题总结

Redis 7的线程模型是“单线程执行 + 多线程IO” 的混合架构:

  1. 主线程:单线程执行命令,保证原子性和简单性
  2. IO线程:多线程处理网络读写,提升吞吐量2-3倍
  3. BIO线程:后台处理耗时任务,避免阻塞主线程
  4. 并发安全:命令执行单线程,数据结构无锁设计
  5. 配置建议:io-threads设为CPU核心数的50%-75%

面试时可补充:Redis选择单线程执行命令是因为纯内存操作足够快,瓶颈在网络IO,因此通过多线程优化IO而非命令执行,这是一种精准的性能优化策略。