一、基礎概念
1. 什麼是 I/O 多路複用?
- 核心思想:使用一個進程/線程同時監聽多個文件描述符(Socket),當某些描述符就緒(可讀/可寫)時,通知程序進行相應操作。
- 解決的問題:避免為每個連接創建線程/進程帶來的資源消耗,實現高併發連接處理。
2. Redis 的架構選擇
# 傳統多線程模型 vs Redis單線程+多路複用
傳統模型:1個連接 → 1個線程 → 高內存消耗、上下文切換開銷大
Redis模型:N個連接 → 1個線程 + I/O多路複用 → 低內存、無鎖、高效
二、Redis 中多路複用的實現
1. 支持的底層機制
Redis 在不同操作系統下使用不同的多路複用實現:
Linux: epoll(最優選擇)
macOS/BSD: kqueue
Solaris: evport
其他 Unix: select(性能較差,備選)
Redis 通過 ae(Async Event)抽象層統一封裝這些接口。
2. 核心工作流程
1. 初始化服務器,監聽端口
2. 將監聽套接字註冊到多路複用器
3. 進入事件循環:
通過多路複用器等待事件(阻塞調用)
事件就緒後返回:
新連接到達 → 接受連接,註冊讀事件
數據可讀 → 讀取命令,解析,放入命令隊列
可寫事件 → 將響應數據發送給客户端
c) 處理時間事件(定時任務)
4. 循環執行步驟 3
三、源碼級實現解析
1. 事件循環結構
typedef struct aeEventLoop {
int maxfd; // 當前最大文件描述符
int setsize; // 監聽的文件描述符數量上限
long long timeEventNextId; // 下一個時間事件ID
aeFileEvent *events; // 文件事件數組
aeFiredEvent *fired; // 就緒事件數組
aeTimeEvent *timeEventHead; // 時間事件鏈表頭
void *apidata; // 多路複用器的特定數據(epoll/kqueue等)
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
2. 事件註冊過程
// 以 epoll 為例的簡化邏輯
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData) {
// 1. 在 events 數組中記錄事件處理器
aeFileEvent *fe = &eventLoop->events[fd];
// 2. 調用底層 API 註冊事件
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return -1;
// 3. 設置回調函數
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
return 0;
}
- 事件分發循環
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 處理事件前執行的操作(如處理異步任務)
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 核心:多路複用等待事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_AFTER_SLEEP);
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
// 1. 計算最近的時間事件,確定多路複用的超時時間
// 2. 調用多路複用API(epoll_wait/kevent/select等)
numevents = aeApiPoll(eventLoop, tvp);
// 3. 遍歷就緒事件,調用相應的回調函數
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
if (fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
if (fe->mask & mask & AE_WRITABLE) {
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
}
// 4. 處理時間事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed;
}
四、性能優化細節
1. 為什麼 Redis 能單線程處理高併發?
- 純內存操作:數據操作在內存中完成,速度極快
- 非阻塞I/O:所有Socket設置為非阻塞模式
- 批量命令處理:支持管道(pipeline),減少網絡往返
- 高效數據結構:精心優化的數據結構實現
2. epoll 的優勢(Linux環境下)
# select/poll 的侷限性
1. 每次調用都需要傳遞所有監聽的fd(用户空間→內核空間複製)
2. 內核需要遍歷所有fd檢查就緒狀態 O(n)
3. 支持的文件描述符數量有限(select默認1024)
# epoll 的優化
1. epoll_create: 創建epoll實例
2. epoll_ctl: 添加/修改/刪除fd(僅增量更新)
3. epoll_wait: 獲取就緒事件(僅返回就緒的fd)
4. 使用紅黑樹管理fd,哈希表存儲就緒列表 O(1)複雜度
五、多線程擴展(Redis 6.0+)
Redis 6.0 引入了多線程I/O,但注意:
配置示例(redis.conf):
# 開啓多線程I/O
io-threads 4 # 啓用4個I/O線程(通常設為CPU核心數)
io-threads-do-reads yes # 啓用讀多線程(寫默認開啓)
六、與其他模型的對比
七、實際監控與調優
1. 監控指標
# 查看Redis事件循環狀態
redis-cli info stats | grep -E "(total_connections_received|instantaneous_ops_per_sec|total_commands_processed)"
# 查看網絡I/O
redis-cli info stats | grep -E "(total_net_input_bytes|total_net_output_bytes|rejected_connections)"
2. 性能瓶頸識別
- CPU瓶頸:單核跑滿,考慮分片或升級CPU
- 網絡瓶頸:網絡吞吐達到上限
- 內存瓶頸:OOM或頻繁交換
- 阻塞操作:慢查詢、大key、持久化阻塞
3. 配置建議
# 調整最大連接數(根據實際情況)
maxclients 10000
# 調整TCP backlog
tcp-backlog 511
# 調整客户端超時
timeout 0 # 永不斷開,適合內網
# 合理設置內存淘汰策略
maxmemory-policy allkeys-lru
八、總結
Redis 的 I/O 多路複用模型是其高性能的基石:
- 單線程事件循環避免了鎖競爭和上下文切換
- 多路複用技術高效管理大量連接
- 純內存操作保證極快的響應速度
- 漸進式演進在保持核心簡單的同時引入多線程優化I/O