2020年,今日頭條Java後端面試覆盤 & Redis 6.0多線程IO模型

2020年,今日頭條Java後端面試覆盤 & Redis 6.0多線程IO模型



上週參加了字節跳動的面試,整場下來一共70分鐘,面試官很是Nice,無奈本身太過緊張,不少準備好的知識點都沒有可以準確傳達意思。面試


面試中由於在簡歷上有提到Redis相關的內容,那麼毫無疑問就會被問到了。先從經典的問題開始:Reids爲何這麼快?那天然會回答諸如單線程、IO多路複用等固定套路,而後這裏由於一直有關注Redis的相關新聞,知道Redis 6.0年底發佈了RC1版本,其中新特性包括多線程IO,那麼天然想在面試中說起一下。面試官應該對這點比較感興趣,因而就繼續探討了這個多線程IO的模型。算法


  • Q:Redis 6多線程是指什麼?數據庫

  • A:Redis這邊將部分處理流程改成多線程,具體來講是..後端

  • Q:是指查詢是多線程嗎?網絡

  • A:應該說是處理請求的最後部分改成了多線程,由於這些部分涉及到數據的IO,是整個(Redis)模型中最耗時的部分,因此改爲了多線程;這部分以前的好比用戶請求進來、將請求放入一個隊列中,仍是單線程的。(注意這部分回答是錯誤的,實際上Redis是將網絡IO的部分作成了多線程,後文繼續分析)數據結構

  • Q:若是我有一個SET操做的話,是單線程仍是多線程?多線程

  • A:多線程。(回答也是錯的)併發

  • Q:那若是是,由於Redis都是內存操做,若是多線程操做一個數據結構的話會有問題嗎?ide

  • A:Emm,目前我理解的模型上看確實會有問題,好比並發改同一個Key,那可能Redis有對應處理這些問題好比進行加鎖處理。(確實不瞭解,回答也天然是錯的)post

  • Q:好,下一個問題..


這裏先總結一下:


  • 由於Antirez在Redis Day介紹過,因此就瞭解到了有這麼個新Feature,可是具體的實現由於沒有看過源碼,因此實際上對這個多線程模型的理解是有誤差的。

  • 若是對這些點沒有十足的把握的話,面試中嘗試本身思考和解決這樣的問題實際上仍是會比較扣分,首先若是猜錯了的話確定不行,其次即便是猜對了也很難有足夠的知識儲備去複述出完整的模型出來,也會讓本身一邊思考一邊表達起來很費勁。




因而坑坑窪窪地堅持完了70分鐘的面試,再總結一下作得不足的地方,由於是1.5Year經驗,面試官主要考察:


  • 現有的業務的一些設計細節的問題:要提早準備好你想介紹給面試官的業務系統,我的認爲應該從業務中選出一兩個難度比較大的點會比較合適。此次面試沒有可以拿出對應的業務來介紹,是準備不到位。

  • 數據庫的基礎知識:這塊以爲回答得還能夠,不過有的時候由於準備的東西比較多,會常常想充分地展示和描述,有的時候可能會比較冗長,也是表達不夠精確的問題。

  • 計算機網絡的基礎知識:不是科班畢業,沒有可以答完美,實際上問題並不難。

  • 計算機系統的基礎知識:同上。

  • 一道算法題:字節跳動給的算法題仍是偏簡單和經典的,建議多刷題和看Discussion總結。


因此就這樣結束了第一次的社招面試,總體來講幾個方向的基礎知識須要回去再多寫多看就能夠了,而後表達上儘可能控制時間和範圍,深刻的內容若是面試官但願和你繼續探討,天然會發問,若是沒問,能夠說起可是不該該直接展開講。




Redis的Threaded IO


面試結束後立刻知道這塊的回答有問題,檢查果真如此。因此也就借這個機會將Threaded IO對應的源碼看了一遍,後續若是有機會的話,但願能跟下一位面試官再來探討這個模型。


綜述


本次新增的代碼位於networking.c中,很顯然多線程生效的位置就能猜出來是在網絡請求上。做者但願改進讀寫緩衝區的性能,而不是命令執行的性能主要緣由是:


  • 讀寫緩衝區的在命令執行的生命週期中是佔了比較大的比重

  • Redis更傾向於保持簡單的設計,若是在命令執行部分改用多線程會不得不處理各類問題,例如併發寫入、加鎖等


那麼將讀寫緩衝區改成多線程後整個模型大體以下:


2020年,今日頭條Java後端面試覆盤 & Redis 6.0多線程IO模型



具體模型


線程初始化(initThreadedIO)


首先,若是用戶沒有開啓多線程IO,也就是io_threads_num == 1時直接按照單線程模型處理;若是超過線程數IO_THREADS_MAX_NUM上限則異常退出。


緊接着Redis使用listCreate()建立io_threads_num個線程,而且對主線程(id=0)之外的線程進行處理:


  • 初始化線程的等待任務數爲0

  • 獲取鎖,使得線程不能進行操做

  • 將線程tid與Redis中的線程id(for循環生成)進行映射


/* Initialize the data structures needed for threaded I/O. */
void initThreadedIO(void) {
   io_threads_active = 0; /* We start with threads not active. */

   /* Don't spawn any thread if the user selected a single thread:
    * we'll handle I/O directly from the main thread. */
   // 若是用戶沒有開啓多線程IO直接返回 使用主線程處理
   if (server.io_threads_num == 1) return;
   // 線程數設置超過上限
   if (server.io_threads_num > IO_THREADS_MAX_NUM) {
       serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
                            "The maximum number is %d.", IO_THREADS_MAX_NUM);
       exit(1);
   }

   /* Spawn and initialize the I/O threads. */
   // 初始化io_threads_num個對應線程
   for (int i = 0; i < server.io_threads_num; i++) {
       /* Things we do for all the threads including the main thread. */
       io_threads_list[i] = listCreate();
       if (i == 0) continue; // Index 0爲主線程

       /* Things we do only for the additional threads. */
       // 非主線程則須要如下處理
       pthread_t tid;
       // 爲線程初始化對應的鎖
       pthread_mutex_init(&io_threads_mutex[i],NULL);
       // 線程等待狀態初始化爲0
       io_threads_pending[i] = 0;
       // 初始化後將線程暫時鎖住
       pthread_mutex_lock(&io_threads_mutex[i]);
       if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
           serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
           exit(1);
       }
       // 將index和對應線程ID加以映射
       io_threads[i] = tid;
   }
}


讀事件到來(readQueryFromClient)


Redis須要判斷是否知足Threaded IO條件,執行if (postponeClientRead(c)) return;,執行後會將Client放到等待讀取的隊列中,並將Client的等待讀取Flag置位:


int postponeClientRead(client *c) {
   if (io_threads_active && // 線程是否在不斷(spining)等待IO
       server.io_threads_do_reads && // 是否多線程IO讀取
       !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
   {//client不能是主從,且未處於等待讀取的狀態
       c->flags |= CLIENT_PENDING_READ; // 將Client設置爲等待讀取的狀態Flag
       listAddNodeHead(server.clients_pending_read,c); // 將這個Client加入到等待讀取隊列
       return 1;
   } else {
       return 0;
   }
}


這時server維護了一個clients_pending_read,包含全部處於讀事件pending的客戶端列表。


如何分配client給thread(handleClientsWithPendingReadsUsingThreads)


首先,Redis檢查有多少等待讀的client:


listLength(server.clients_pending_read)


若是長度不爲0,進行While循環,將每一個等待的client分配給線程,當等待長度超過線程數時,每一個線程分配到的client可能會超過1個:


int item_id = 0;
while((ln = listNext(&li))) {
   client *c = listNodeValue(ln);
   int target_id = item_id % server.io_threads_num;
   listAddNodeTail(io_threads_list[target_id],c);
   item_id++;
}


而且修改每一個線程須要完成的數量(初始化時爲0):


for (int j = 1; j < server.io_threads_num; j++) {
   int count = listLength(io_threads_list[j]);
   io_threads_pending[j] = count;
}


等待處理直到沒有剩餘任務:


while(1) {
   unsigned long pending = 0;
   for (int j = 1; j < server.io_threads_num; j++)
       pending += io_threads_pending[j];
   if (pending == 0) break;
}


最後清空client_pending_read:


listRewind(server.clients_pending_read,&li);
while((ln = listNext(&li))) {
   client *c = listNodeValue(ln);
   c->flags &= ~CLIENT_PENDING_READ;
   if (c->flags & CLIENT_PENDING_COMMAND) {
       c->flags &= ~ CLIENT_PENDING_COMMAND;
       processCommandAndResetClient(c);
   }
   processInputBufferAndReplicate(c);
}
listEmpty(server.clients_pending_read);


如何處理讀請求


在上面的過程當中,當任務分發完畢後,每一個線程按照正常流程將本身負責的Client的讀取緩衝區的內容進行處理,和原來的單線程沒有太大差別。


每輪處理中,須要將各個線程的鎖開啓,而且將相關標誌置位:


void startThreadedIO(void) {
   if (tio_debug) { printf("S"); fflush(stdout); }
   if (tio_debug) printf("--- STARTING THREADED IO ---\n");
   serverAssert(io_threads_active == 0);
   for (int j = 1; j < server.io_threads_num; j++)
       // 解開線程的鎖定狀態
       pthread_mutex_unlock(&io_threads_mutex[j]);
   // 如今能夠開始多線程IO執行對應讀/寫任務
   io_threads_active = 1;
}


一樣結束時,首先須要檢查是否有剩餘待讀的IO,若是沒有,將線程鎖定,標誌關閉:


void stopThreadedIO(void) {
   // 須要中止的時候可能還有等待讀的Client 在中止前進行處理
   handleClientsWithPendingReadsUsingThreads();
   if (tio_debug) { printf("E"); fflush(stdout); }
   if (tio_debug) printf("--- STOPPING THREADED IO [R%d] [W%d] ---\n",
       (int) listLength(server.clients_pending_read),
       (int) listLength(server.clients_pending_write));
   serverAssert(io_threads_active == 1);
   for (int j = 1; j < server.io_threads_num; j++)
       // 本輪IO結束 將全部線程上鎖
       pthread_mutex_lock(&io_threads_mutex[j]);
   // IO狀態設置爲關閉
   io_threads_active = 0;
}


其餘補充


Redis的Threaded IO模型中,每次全部的線程都只能進行讀或者寫操做,經過io_threads_op控制,同時每一個線程中負責的client依次執行:


// 每一個thread有可能須要負責多個client
listRewind(io_threads_list[id],&li);
while((ln = listNext(&li))) {
   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);
   } else {
       serverPanic("io_threads_op value is unknown");
   }
}


每一個線程執行readQueryFromClient,將對應的請求放入一個隊列中,單線程執行,最後相似地由多線程將結果寫入客戶端的buffer中。


總結


Threaded IO將服務讀Client的輸入緩衝區和將執行結果寫入輸出緩衝區的過程改成了多線程的模型,同時保持同一時間所有線程均處於讀或者寫的狀態。可是命令的具體執行還是以單線程(隊列)的形式,由於Redis但願保持簡單的結構避免處理鎖和競爭的問題,而且讀寫緩衝區的時間佔命令執行生命週期的比重較大,處理這部分的IO模型會給性能帶來顯著的提高。

特別聲明:本文素材來源於網絡,僅做爲分享學習之用,若有侵權,請聯繫刪除!



推薦閱讀

金三銀四季,阿里工做10多年Java大牛的「心得」,獻給迷茫中的你

相關文章
相關標籤/搜索