4.Redis线程模型
4.Redis线程模型
Redis 是单线程还是多线程?
Redis 的线程模型演进体现了 稳中求进 的设计理念。核心命令处理保持单线程,避免了复杂的线程同步问题,同时通过多线程优化非核心路径,实现了性能与稳定性的平衡。
从 4.0 版本开始,Redis 引入了异步删除机制,将耗时的删除操作交给后台线程处理,避免阻塞主线程。6.0 版本则更进一步,将网络 IO 操作(读写 socket)交给多线程处理,显著提升了高并发场景下的性能。
这种设计思路非常巧妙:核心路径保持简单可靠,非核心路径则充分利用多线程提升性能。就像高速公路,主干道保持畅通,辅路则可以根据需要灵活调度。
让我们看看 Redis 6.0 中多线程 IO 的关键源码:
// server.h
struct redisServer {
// ... 其他字段 ...
int io_threads_num; // IO 线程数
int io_threads_active; // IO 线程是否激活
list *io_threads_list; // IO 线程列表
};
// networking.c
void initThreadedIO(void) {
server.io_threads_active = 0;
if (server.io_threads_num == 1) return;
// 创建 IO 线程
for (int i = 0; i < server.io_threads_num; i++) {
pthread_t tid;
pthread_create(&tid, NULL, IOThreadMain, (void*)(long)i);
server.io_threads_list[i] = tid;
}
}
Redis 为什么选择单线程做核心处理?
很多人会质疑:多线程不是更快吗?为什么 Redis 反其道而行?这要从 Redis 的本质说起。
Redis 是一个内存数据库,所有数据都在内存中操作,处理速度极快。真正的瓶颈不在 CPU,而在网络 IO。多线程虽然能提升 CPU 利用率,但会带来线程切换、锁竞争等开销,反而可能降低整体性能。
Redis 的单线程性能确实让人惊叹,这要归功于其精心设计的三大加速器。
首先是内存存储。所有数据都在内存中操作,没有磁盘 IO 的拖累,访问延迟低至微秒级。这就像把数据放在 CPU 缓存里,访问速度自然快。
其次是高效的数据结构。Redis 的每个数据结构都经过极致优化,比如 ZSet 用跳表实现范围查询,Hash 用 ziplist 存储小对象。这些优化让数据操作变得轻量高效。
最重要的是 IO 多路复用。Redis 使用 epoll/kqueue 机制,让单线程能够同时处理多个连接。这就像餐厅的服务员,虽然只有一个人,但能同时服务多桌客人。
让我们看看 Redis 中 epoll 的实现:
// ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
state->epfd = epoll_create(1024); // 创建 epoll 实例
eventLoop->apidata = state;
return 0;
}
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
struct epoll_event ee;
ee.events = 0;
mask |= eventLoop->events[fd].mask;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.u64 = 0;
ee.data.fd = fd;
// 添加事件到 epoll
if (epoll_ctl(state->epfd, EPOLL_CTL_ADD, fd, &ee) == -1) return -1;
return 0;
}
这段代码展示了 Redis 如何使用 epoll 来高效处理并发连接。epoll 的 ET 模式(边缘触发)让 Redis 能够以最小的系统调用处理最多的连接。
Redis 单线程性能如何?
很多人对单线程有误解,认为它一定性能差。但 Redis 用实力证明:单线程也能做到极致性能。
以我的实测数据为例,在 Mac 上运行 redis-benchmark:
redis-benchmark -t set,get -n 100000
结果显示 SET 操作超过 10 万 QPS,GET 操作更是达到 15 万 QPS。这个性能已经能满足绝大多数业务需求。
Redis 的高性能来自三个方面:
- 内存存储:没有磁盘 IO,访问延迟微秒级
- 高效数据结构:每个数据结构都经过极致优化
- IO 多路复用:基于 epoll/kqueue,高效处理并发连接
Redis 6.0 之后引入了多线程,你知道为什么吗?
Redis 6.0 的多线程改造是一个深思熟虑的决定。在之前的版本中,所有操作都在单线程中处理,包括网络 IO。这在并发量不大时没有问题,但随着用户量增长,网络 IO 处理成了瓶颈。
具体来说,当并发连接数很高时,单线程需要花大量时间在 socket 读写上,导致命令执行被延迟。这就像餐厅只有一个服务员,既要接待客人,又要传菜,忙不过来。
Redis 6.0 的解决方案很巧妙:将网络 IO 交给多线程处理,主线程专注于命令执行。这样既保持了命令执行的原子性,又提升了网络处理能力。
Redis 6.0 的多线程是默认开启的吗?
Redis 没有默认开启多线程,虽然多线程能提升性能,但 Redis 团队认为应该让用户根据实际需求决定是否启用。
要开启多线程 IO,需要两个配置项:
io-threads 4
io-threads-do-reads yes
其中 io-threads
设置线程数,建议设置为 CPU 核心数。io-threads-do-reads
控制是否让线程处理读操作,写操作默认就是多线程处理。
这种设计很合理:多线程虽然能提升性能,但也会增加系统复杂度。对于大多数应用来说,单线程已经足够,没必要引入多线程的复杂性。
Redis 6.0 的多线程主要负责命令执行的哪一块?
Redis 6.0 的线程分工非常清晰:多线程负责网络 IO,主线程负责命令执行。这种设计既利用了多核性能,又保持了命令执行的原子性。
具体来说,多线程处理网络 IO 相关的操作,包括读取客户端请求和写回响应数据。而主线程则专注于命令的解析、执行和数据修改。这种分工就像餐厅的服务员和厨师:服务员(多线程)负责接待客人、传菜,厨师(主线程)负责烹饪。这样既能提高服务效率,又保证了菜品质量。
让我们看看 Redis 6.0 中多线程处理网络 IO 的关键源码:
// networking.c
void *IOThreadMain(void *myid) {
long id = (unsigned long)myid;
while(1) {
// 等待主线程分配任务
for (int j = 0; j < server.io_threads_num; j++) {
if (io_threads_pending[id] != 0) break;
}
// 处理读事件
if (io_threads_op == IO_THREADS_OP_READ) {
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 读取客户端请求
readQueryFromClient(c->conn);
}
}
// 处理写事件
else if (io_threads_op == IO_THREADS_OP_WRITE) {
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 写回响应数据
writeToClient(c,0);
}
}
}
}
这段代码展示了 Redis 6.0 中多线程如何处理网络 IO。主线程通过 io_threads_op
控制多线程的工作模式,多线程则根据模式执行相应的读写操作。这种设计让网络 IO 处理变得高效,同时又不影响命令执行的原子性。
