從上圖中能夠看出只有如下3個地方用的是多線程,其餘地方都是單線程:
1:接收請求參數
2:解析請求參數
3:請求響應,即將結果返回給client
很明顯以上3點各個請求都是互相獨立互不影響的,很適合用多線程,特別是請求體/響應體很大的時候,更能體現多線程的威力。而操做數據庫是請求之間共享的,若是使用多線程的話適合讀寫鎖。而操做數據庫自己是很快的(就是對map的增刪改查),單線程不必定就比多線程慢,固然也有多是做者偷懶,懶得實現罷了,但此次的多線程模型仍是值得咱們學習一下的。
2:redis多線程是怎麼實現的?
先大體說一下多線程的流程:
1:服務器啓動時啓動必定數量線程,服務啓動的時候能夠指定線程數,每一個線程對應一個隊列(list *io_threads_list[128]),最多128個線程。
2:服務器收到的每一個請求都會放入全局讀隊列clients_pending_read,同時將隊列中的元素分發到每一個線程對應的隊列io_threads_list中,這些工做都是在主線程中執行的。
3:每一個線程(包括主線程和子線程)接收請求參數並作解析,完過後在client中設置一個標記CLIENT_PENDING_READ,標識參數解析完成,能夠操做數據庫了。(主線程和子線程都會執行這個步驟)
4:主線程遍歷隊列clients_pending_read,發現設有CLIENT_PENDING_READ標記的,就操做數據庫
5:操做完數據庫就是響應client了,響應是一組函數addReplyXXX,在client中設置標記CLIENT_PENDING_WRITE,同時將client加入全局寫隊列clients_pending_write
6:主線程將全局隊列clients_pending_write以輪訓的方式將任務分發到每一個線程對應的隊列io_threads_list
7:全部線程將遍歷本身的隊列io_threads_list,將結果發送給client
3:redis多線程是怎麼作到無鎖的?
上面說了多線程的地方都是互相獨立互不影響的。可是每一個線程的隊列就存在兩個兩個線程訪問的狀況:主線程向隊列中寫數據,子線程消費,redis的實現有點反直覺。按正常思路來講,主線程在往隊列中寫數據的時候加鎖;子線程複製隊列&並將隊列清空,這個兩個動做是加鎖的,子線程消費複製後的隊列,這個過程是不須要加鎖的,按理來講主線程和子線程的加鎖動做都是很是快的。可是redis並無這麼實現,那麼他是怎麼實現的呢?
redis多線程的模型是主線程負責蒐集任務,放入全局讀隊列clients_pending_read和全局寫隊列clients_pending_write,主線程在將隊列中的任務以輪訓的方式分發到每一個線程對應的隊列(list *io_threads_list[128])
1:一開始子線程的隊列都是空,主線程將全對隊列中的任務分發到每一個線程的隊列,並設置一個隊列有數據的標記(_Atomic unsigned long io_threads_pending[128]),io_threads_pending[1]=5表示第一個線程的隊列中有5個元素
2:子線程死循環輪訓檢查io_threads_pending[index] > 0,有數據就開始處理,處理完成以後將io_threads_pending[index] = 0,沒數據繼續檢查
3:主線程將任務分發到子線程的隊列中,本身處理本身隊列中的任務,處理完成後,
等待全部子線程處理完全部任務,繼續收集任務到全局隊列,在將任務分發給子線程,這樣就避免了主線程和子線程同時訪問隊列的狀況,主線程向隊列寫的時候子線程還沒開始消費,子線程在消費的時候主線程在等待子線程消費完,子線程消費完後主線程纔會往隊列中繼續寫,就必須加鎖了。由於任務是平均分配到每一個隊列的,因此每一個隊列的處理時間是接近的,等待的時間會很短。
4:源碼執行流程
爲了方便你看源碼,這裏加上一些代碼的執行流程
啓動socket監聽,註冊鏈接處理函數,鏈接成功後建立鏈接對象connection,建立client對象,經過aeCreateFileEvent註冊client的讀事件
main -> initServer -> acceptTcpHandler -> anetTcpAccept -> anetGenericAccept -> accept(獲取到socket鏈接句柄)
connCreateAcceptedSocket -> connCreateSocket -> 建立一個connection對象
acceptCommonHandler -> createClient建立client鏈接對象 -> connSetReadHandler -> aeCreateFileEvent -> readQueryFromClient
main -> aeMain -> aeProcessEvents -> aeApiPoll(獲取可讀寫的socket) -> readQueryFromClient(若是可讀) -> processInputBuffer -> processCommandAndResetClient(多線程下這個方法在當前流程下不會執行,而由主線程執行)
在多線程模式下,readQueryFromClient會將client信息加入server.clients_pending_read隊列,listAddNodeHead(server.clients_pending_read,c);
主線程會將server.clients_pending_read中的數據分發到子線程的隊列(io_threads_list)中,子線程會調用readQueryFromClient就行參數解析,主線程分發完任務後,會執行具體的操做數據庫的命令,這塊是單線程
若是參數解析完成會在client->flags中加一個標記CLIENT_PENDING_COMMAND,在主線程中先判斷client->flags & CLIENT_PENDING_COMMAND > 0,說明參數解析完成,纔會調用processCommandAndResetClient,以前還擔憂若是子線程還在作參數解析,主線程就開始執行命令難道不會有問題嗎?如今一切都清楚了
main -> aeMain -> aeProcessEvents -> beforeSleep -> handleClientsWithPendingReadsUsingThreads -> processCommandAndResetClient -> processCommand -> call
讀是屢次讀:socket讀緩衝區有數據,epoll就會一直觸發讀事件,因此讀多是屢次的
寫是一次寫:往socket寫數據是在子線程中執行的,直接循環直到數據寫完位置,就算某個線程阻塞了,也不會像單線程那樣致使全部任務都阻塞
執行完相關命令後,就是將結果返回給client,回覆client是一組函數,咱們以addReply爲例,說一下執行流程,執行addReply仍是單線程的,將client信息插入全局隊列server.clients_pending_write。
addReply -> prepareClientToWrite -> clientInstallWriteHandler -> listAddNodeHead(server.clients_pending_write,c)
在主線程中將server.clients_pending_write中的數據以輪訓的方式分發到多個子線程中
beforeSleep -> handleClientsWithPendingWritesUsingThreads -> 將server.clients_pending_write中的數據以輪訓的方式分發到多個線程的隊列中io_threads_list
list *io_threads_list[IO_THREADS_MAX_NUM];是數組雙向鏈表,一個線程對應其中一個隊列
子線程將client中的數據發給客戶端,因此是多線程
server.c -> main -> initThreadedIO(啓動必定數量的線程) -> IOThreadMain(線程執行的方法) -> writeToClient -> connWrite -> connSocketWrite
網絡操做對應的一些方法,全部connection對象的type字段都是指向CT_Socketredis
ConnectionType CT_Socket = {
.ae_handler = connSocketEventHandler,
.close = connSocketClose,
.write = connSocketWrite,
.read = connSocketRead,
.accept = connSocketAccept,
.connect = connSocketConnect,
.set_write_handler = connSocketSetWriteHandler,
.set_read_handler = connSocketSetReadHandler,
.get_last_error = connSocketGetLastError,
.blocking_connect = connSocketBlockingConnect,
.sync_write = connSocketSyncWrite,
.sync_read = connSocketSyncRead,
.sync_readline = connSocketSyncReadLine
};