一、基礎概念

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;
}
  1. 事件分發循環
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 IO 多路複用模型_多路複用

配置示例(redis.conf):

# 開啓多線程I/O
io-threads 4          # 啓用4個I/O線程(通常設為CPU核心數)
io-threads-do-reads yes  # 啓用讀多線程(寫默認開啓)

六、與其他模型的對比

Redis IO 多路複用模型_Redis_02

七、實際監控與調優

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 多路複用模型是其高性能的基石:

  1. 單線程事件循環避免了鎖競爭和上下文切換
  2. 多路複用技術高效管理大量連接
  3. 純內存操作保證極快的響應速度
  4. 漸進式演進在保持核心簡單的同時引入多線程優化I/O