Redis 6多线程IO模型
Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。
一、Redis单线程模型回顾
#
1.1 经典单线程模型
┌─────────────────────────────────────────┐ │ Redis 单线程模型 │ │ │ │ 客户端1 ──┐ │ │ 客户端2 ──┼──> 连接队列 ──> 事件循环 │ │ 客户端3 ──┘ │ (单线程) │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ 读取请求 │ │ │ │ 解析命令 │ │ │ │ 执行命令 │ │ │ │ 发送响应 │ │ │ └──────────┘ │ └─────────────────────────────────────────┘
优点: - 无锁操作,简单高效 - 无竞态条件 - 顺序执行保证一致性
局限: - 网络IO和命令执行串行 - 多核CPU无法充分利用
|
#
1.2 单线程的性能瓶颈
Redis单线程的耗时构成:
处理一个请求: 1. 读取请求(网络IO) 2. 解析协议 3. 执行命令(内存操作,通常很快) 4. 发送响应(网络IO)
在10Gbps网络下: - 读取1KB请求:约1us - 执行命令:约1us - 发送1KB响应:约1us - 总计:约3us
QPS理论上限:1,000,000 / 3 ≈ 333,000
实际瓶颈往往在: - 网络IO读写 - 协议解析 - 大量小请求的网络往返
|
二、Redis 6多线程IO
#
2.1 设计思想
Redis 6多线程IO模型:
┌─────────────────────────────────────────┐ │ 主线程(事件循环) │ │ │ │ ┌──────────────┐ │ │ │ 监听连接 │ │ │ │ 分配任务 │ │ │ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 命令执行 │ <-- 保持单线程 │ │ │ (关键) │ │ │ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 分配响应 │ │ │ └──────────────┘ │ └─────────────────────────────────────────┘ │ ▼ 分配IO任务 ┌─────────────────────────────────────────┐ │ IO线程1 IO线程2 IO线程3 │ │ │ │ 读取请求 读取请求 读取请求 │ │ 协议解析 协议解析 协议解析 │ │ 发送响应 发送响应 发送响应 │ └─────────────────────────────────────────┘
关键设计: - 命令执行仍在主线程单线程执行 - IO线程只处理:读取请求、协议解析、发送响应 - 通过锁-free的设计,主线程和IO线程通过队列协作
|
#
2.2 为什么命令执行不改为多线程
1. 复杂性:需要大量锁,增加复杂度 2. 竞态条件:多线程访问共享数据结构 3. 性能下降:锁竞争可能抵消多线程优势 4. 破坏原子性:单条命令的原子性保证
Redis的设计哲学: - 命令执行保持单线程(简单高效) - IO密集型操作使用多线程(提升吞吐)
|
三、配置和使用
#
3.1 开启多线程IO
# redis.conf (Redis 6.0+)
# 开启多线程IO io-threads-do-reads yes io-threads 4
# 参数说明: # io-threads: IO线程数量(包含主线程) # - 建议设置为CPU核心数 # - 默认1(单线程,即关闭多线程IO) # - 最大128 # # io-threads-do-reads: 是否用IO线程处理读取 # - 默认no(只用于发送响应) # - 设为yes后,读取和发送都用多线程
|
#
3.2 配置建议
# 4核服务器 io-threads 4
# 8核服务器 io-threads 8
# 16核服务器 io-threads 16
# 超过16核的服务器 # 建议io-threads设为8(收益递减) io-threads 8
|
#
3.3 验证配置
redis-cli CONFIG GET io-threads*
redis-cli CLIENT LIST | grep flags=M
redis-cli INFO server | grep io_threads
|
四、性能测试
#
4.1 测试环境
硬件: - CPU: Intel Xeon E5-2680 v4 @ 2.4GHz (14核28线程) - 内存: 64GB - 网络: 10Gbps
软件: - Redis 6.2 - 客户端: redis-benchmark
|
#
4.2 测试命令
redis-benchmark -h localhost -p 6379 -t get -n 1000000 -c 50 -P 100 --threads 4
redis-benchmark -h localhost -p 6379 -t get -n 1000000 -c 50 -P 100 --threads 4
|
#
4.3 性能对比
| 场景 |
单线程 |
多线程(4) |
提升 |
| GET (1KB value) |
250K QPS |
450K QPS |
80% |
| SET (1KB value) |
220K QPS |
400K QPS |
82% |
| GET (10KB value) |
180K QPS |
350K QPS |
94% |
| SET (10KB value) |
160K QPS |
310K QPS |
94% |
| Pipeline GET |
1.5M QPS |
2.2M QPS |
47% |
| Lua脚本 |
200K QPS |
200K QPS |
0% |
结论:
- 大value场景提升更明显(IO瓶颈更明显)
- Pipeline场景提升有限(已减少IO开销)
- Lua脚本无提升(命令执行仍是单线程)
五、源码分析
#
5.1 IO线程初始化
void initThreadedIO(void) { server.io_threads_active = 0; for (int i = 0; i < server.io_threads_num; i++) { io_threads_list[i] = listCreate(); if (i == 0) continue; pthread_t tid; pthread_create(&tid, NULL, IOThreadMainFunction, (void*)(unsigned long)i); io_threads[i] = tid; } }
void *IOThreadMainFunction(void *myid) { long id = (unsigned long)myid; while(1) { for (int j = 0; j < 1000000; j++) { if (io_threads_pending[id] != 0) break; } listIter li; listNode *ln; listRewind(io_threads_list[id], &li); while((ln = listNext(&li)) != NULL) { client *c = listNodeValue(ln); if (io_threads_op == IO_THREADS_OP_WRITE) { writeToClient(c, 0); } else if (io_threads_op == IO_THREADS_OP_READ) { readQueryFromClient(c->conn); } listDelNode(io_threads_list[id], ln); } io_threads_pending[id] = 0; } }
|
#
5.2 任务分配
int handleClientsWithPendingWritesUsingThreads(void) { int processed = listLength(server.clients_pending_write); if (processed == 0) return 0; if (server.io_threads_num == 1 || processed < IO_THREADS_MIN_OPS) { return handleClientsWithPendingWrites(); } listIter li; listNode *ln; listRewind(server.clients_pending_write, &li); int item_id = 0; while((ln = listNext(&li)) != NULL) { client *c = listNodeValue(ln); int target_id = item_id % server.io_threads_num; listAddNodeTail(io_threads_list[target_id], c); item_id++; } io_threads_op = IO_THREADS_OP_WRITE; for (int j = 1; j < server.io_threads_num; j++) { io_threads_pending[j] = 1; } handleClientsWithPendingWrites(); while(1) { int pending = 0; for (int j = 1; j < server.io_threads_num; j++) { pending += io_threads_pending[j]; } if (pending == 0) break; } return processed; }
|
六、生产环境建议
#
6.1 什么时候开启多线程IO
| 场景 |
建议 |
| 大量小value读写 |
开启,效果显著 |
| 大value读写 |
开启,效果最显著 |
| 主要是Lua脚本 |
不开启,无提升 |
| 主要是Pipeline |
效果有限,可不开 |
| CPU核心数 < 4 |
不开启,主线程已够用 |
| 低延迟要求 |
不开启,多线程有微小延迟 |
#
6.2 配置模板
# redis.conf
# 基础配置 port 6379 daemonize yes
# 内存配置 maxmemory 32gb maxmemory-policy allkeys-lru
# 持久化配置 appendonly yes appendfsync everysec
# IO线程配置(根据CPU核心数调整) io-threads 8 io-threads-do-reads yes
# 其他优化 tcp-keepalive 300 timeout 0
|
#
6.3 监控多线程性能
redis-cli INFO server | grep io_threads
redis-cli INFO stats | grep instantaneous_ops_per_sec
redis-cli --latency-history -i 1
|
七、常见问题
#
7.1 多线程IO不生效
redis-cli INFO server | grep redis_version
redis-cli CONFIG GET io-threads
redis-cli INFO server | grep multithread
|
#
7.2 多线程IO导致延迟增加
原因: - 线程切换开销 - 任务分配和同步开销
解决: - 减少io-threads数量 - 只开启写多线程(io-threads-do-reads no) - 确保有足够的并发客户端
|
#
7.3 与CPU亲和性
taskset -c 0-7 redis-server /etc/redis/redis.conf
|
八、Redis 6+ 其他新特性
#
8.1 ACL(访问控制列表)
ACL SETUSER alice on >password ~* +@all
ACL SETUSER bob on >password ~* +get +set -@dangerous
ACL LIST
|
#
8.2 SSL/TLS支持
# redis.conf port 0 tls-port 6379 tls-cert-file /path/to/redis.crt tls-key-file /path/to/redis.key tls-ca-cert-file /path/to/ca.crt
|
#
8.3 客户端缓存(Tracking)
九、总结
| 特性 |
Redis 5 |
Redis 6 |
Redis 7 |
| IO模型 |
单线程 |
多线程IO |
多线程IO+Function |
| ACL |
无 |
有 |
有 |
| SSL |
无 |
有 |
有 |
| 客户端缓存 |
无 |
有 |
有 |
| 多线程命令 |
无 |
无 |
部分支持 |
Redis 6多线程IO的核心价值:
- 不破坏单线程执行模型:命令执行仍单线程
- 显著提升吞吐量:大value场景提升80-90%
- 简单配置:只需两个参数
- 向后兼容:默认关闭,不影响现有部署
使用建议:
- 4核以上服务器建议开启
- io-threads设为CPU核心数或略少
- 大value场景效果最明显
- 配合redis-benchmark验证效果
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结
选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。