數據結構精講:從原理到實戰–學習筆記03

數據結構精講:從原理到實戰–學習筆記03

本筆記是記錄學習 《數據結構精講:從原理到實戰》,作者是:蔡元楠,Google Brain資深工程師。

如有侵權,聯繫刪除!

鏈表

每一個元素就保存了兩部分的內容,一部分是元素本身的值,另一部分是下一個元素的地址,而最後一個元素的下一個地址我們可以保存一個 0x0 的值來表示這個元素是最後一個了。這時候這些數據的內存就如下圖所示:
在這裏插入圖片描述
這種不保存在連續存儲空間中,而每一個元素裏都保存了到下一個元素的地址的數據結構,我們稱之爲鏈表(Linked List)。鏈表上的每一個元素又可以稱它爲節點(Node),而鏈表中第一個元素,稱它爲頭節點(Head Node),最後一個元素稱它爲尾節點(Tail Node)

此類線性鏈表的特點是:只有順序訪問才能遍歷元素,而無法通過計算地址直接獲取某個具體的值。所以時間複雜度爲O(n)。但是它的優點是空間利用率很高,不會有閒置的內存空間,並且可以利用很小的碎片內存。

對於我們來說,只有節點裏的值是可以利用上的,而保存節點地址的內存其實對於我們來說是無法應用的。所以鏈表的空間利用率上相當於值的大小除以值的大小和節點地址大小的和。但是在現實應用中,鏈表中保存的值遠遠不是一個基本類型就這麼簡單,當我們所保存的值的大小越大的時候,空間利用率也會越高。

對於數組來說插入的時間複雜度是O(n),而對於鏈表來說,如果保存一個尾地址,尾插和頭插的時間複雜度都是O(1),中間插入的時間複雜度是O(n)

鏈表的各種形式

單鏈表(singly linked list)

在這裏插入圖片描述

雙鏈表(doubly linked list)

在這裏插入圖片描述

環鏈表(circular linked list)

在這裏插入圖片描述

鏈表在 Apache Kafka 中的應用

如何重新設計定時器算法

一般我們可以把定時器的概念抽象成 4 個部分,它們分別是:

  1. 初始化定時器,規定定時器經過了多少單位時間之後超時,並且在超時之後執行特定的程序;

  2. 刪除定時器,終止一個特定的定時器;

  3. 定時器超時進程,定時器在超時之後所執行的特定程序;

  4. 定時器檢測進程,假設定時器裏的時間最小顆粒度爲 T 時間,則每經過 T 時間之後都會執行這個進程來查看是否定時器超時,並將其移除。

維護無序定時器列表

最簡單粗暴的方法,當然就是直接用數組或者鏈表來維護所有的定時器了。從前面的學習中我們可以知道,在數組中插入一個新的元素所需要的時間複雜度是 O(N),而在鏈表的結尾插入一個新的節點所需要的時間複雜度是 O(1),所以在這裏可以選擇用鏈表來維護定時器列表。假設我們要維護的定時器列表如下圖所示:

在這裏插入圖片描述

它表示現在系統維護了 3 個定時器,分別會在 3T、T 和 2T 時間之後超時。如果現在用戶又插入了一個新定時器,將會在 T 時間後超時,我們會將新的定時器數據結構插入到鏈表結尾,如下圖所示:
在這裏插入圖片描述每次經過 T 時間之後,定時器檢測進程都會從頭到尾掃描一遍這個鏈表,每掃描到一個節點的時候都會將裏面的時間減去 T,然後判斷這個節點的值是否等於 0 了,如果等於 0 了,則表示這個定時器超時,執行定時器超時進程並刪除定時器,如果不等於,則繼續掃描下一個節點。

這種方法的好處是定時器的插入和刪除操作都只需要 O(1) 的時間。但是每次執行定時器檢測進程的時間複雜度爲 O(N)。如果定時器的數量還很小時還好,如果當定時器有成百上千個的時候,定時器檢測進程就會成爲一個瓶頸了。

維護有序定時器列表

這種方法是上述方法的改良版本。我們可以還是繼續維護一個定時器列表,與第一種方法不一樣的是,每次插入一個新的定時器時,並不是將它插入到鏈表的結尾,而是從頭遍歷一遍鏈表,將定時器的超時時間按從小到大的順序插入到定時器列表中。還有一點不同的是,每次插入新定時器時,並不是保存超時時間,而是根據當前系統時間和超時時間算出一個絕對時間出來。例如,當前的系統時間爲 NowTime,超時時間爲 2T,那這個絕對時間就爲 NowTime + 2T。

假設原來的有序定時器列表如下圖所示:

在這裏插入圖片描述
當我們要插入一個新的定時器,超時的絕對時間算出爲 25 Dec 2019 9:23:34,這時候我們會按照超時時間從小到大的順序,將定時器插入到定時器列表的開頭,如下圖所示:
在這裏插入圖片描述
維護一個有序的定時器列表的好處是,每次執行定時器檢測進程的時間複雜度爲 O(1),因爲每次定時器檢測進程只需要判斷當前系統時間是否是在鏈表第一個節點時間之後了,如果是則執行定時器超時進程並刪除定時器,如果不是則結束定時器檢測進程。

這種方法的好處是執行定時器檢測進程和刪除定時器的時間複雜度爲 O(1),但因爲要按照時間從小到大排列定時器,每次插入的時候都需要遍歷一次定時器列表,所以插入定時器的時間複雜度爲 O(N)。

維護定時器“時間輪”

“時間輪”(Timing-wheel ) 在概念上是一個用數組並且數組元素爲鏈表的數據結構來維護的定時器列表,常常伴隨着溢出列表(Overflow List) 來維護那些無法在數組範圍內表達的定時器。“時間輪”有非常多的變種,今天我來解釋一下最基本的“時間輪”實現方式。

首先基本的“時間輪”會將定時器的超時時間劃分到不同的週期(Cycle)中去,數組的大小決定了一個週期的大小。例如,一個“時間輪”數組的大小爲 8,那這個“時間輪”週期的大小就爲 8T。同時,我們維護一個最基本的“時間輪”還需要維護以下幾個變量:

  1. “時間輪”的週期數,用 S 來表示;

  2. “時間輪”的週期大小,用 N 來表示;

  3. “時間輪”數組現在所指向的索引,用 i 來表示。

現在的時間我們可以用 S×N + i 來表示,每次我們執行完一次定時器檢測進程之後,都會將 i 加 1。當 i 等於 N 的時候,我們將 S 加 1,並且將 i 歸零。因爲“時間輪”裏面的數組索引會一直在 0 到 N-1 中循環,所以我們可以將數組想象成是一個環,例如一個“時間輪”的週期大小爲 8 的數組,可以想象成如下圖所示的環:
在這裏插入圖片描述

那麼我們假設現在的時間是 S×N + 2,表示這個“時間輪”的當前週期爲 S,數組索引爲 2,同時假設這個“時間輪”已經維護了一部分定時器鏈表,如下圖所示:
在這裏插入圖片描述
如果我們想新插入一個超時時間爲 T 的新定時器進這個時間輪,因爲 T 小於這個“時間輪”週期的大小 8T,所以表示這個定時器可以被插入到當前的“時間輪”中,插入的位置爲當前索引爲 1 + 2 % 8 = 3 ,插入新定時器後的“時間輪”如下圖所示:
在這裏插入圖片描述
如果我們現在又想新插入一個超時時間爲 9T 的新定時器進這個“時間輪”,因爲 9T 大於或等於這個“時間輪”週期的大小 8T,所以表示這個定時器暫時無法被插入到當前的週期中,我們必須將這個新的定時器放進溢出列表裏。溢出列表存放着新定時器還需要等待多少週期才能進入到當前“時間輪”中,我們按照下面公式來計算還需等待的週期和插入的位置:

還需等待的週期:9T / 8T = 1
新定時器插入時的索引位置:(9T + 2T) % 8T = 3

我們算出了等待週期和新插入數組的索引位置之後,就可以更新溢出列表,如下圖所示:在這裏插入圖片描述

在“時間輪”的算法中,定時器檢測進程只需要判斷“時間輪”數組現在所指向的索引裏的鏈表爲不爲空,如果爲空則不執行任何操作,如果不爲空則對於這個數組元素鏈表裏的所有定時器執行定時器超時進程。而每當“時間輪”的週期數加 1 的時候,系統都會遍歷一遍溢出列表裏的定時器是否滿足當前週期數,如果滿足的話,則將這個位置的溢出列表全部移到“時間輪”相對應的索引位置中。

在這種基本“時間輪”的算法裏,定時器檢測進程的時間複雜度爲 O(1),而插入新定時器的時間複雜度取決於超時時間,因爲插入的新定時器有可能會被放入溢出列表中從而需要遍歷一遍溢出列表以便將新定時器放入到相對應週期的位置。

Apache Kafka 的 Purgatory 組件

Apache Kafka 是一個開源的消息系統項目,主要用於提供一個實時處理消息事件的服務。與計算機網絡裏面的 TCP 協議需要用到大量定時器來判斷是否需要重新發送丟失的網絡包一樣,在 Kafka 裏面,因爲它所提供的服務需要判斷所發送出去的消息事件是否被訂閱消息的用戶接收到,Kafka 也需要用到大量的定時器來判斷髮出的消息是否超時然後重發消息。

而這個任務就落在了 Purgatory 組件上。在舊版本的 Purgatory 組件裏,維護定時器的任務採用的是 Java 的 DelayQueue 類來實現的。DelayQueue 本質上是一個堆(Heap)數據結構,這個概念將會在第 09 講中詳細介紹。現在我們可以把這種實現方式看作是維護有序定時器列表的一種變種。這種操作的一個缺點是當有大量頻繁的插入操作時,系統的性能將會降低。

因爲 Kafka 中所有的最大消息超時時間都已經被寫在了配置文件裏,也就是說我們可以提前知道一個定時器的 MaxInterval,所以新版本的 Purgatory 組件則採用的了我們上面所提到的變種“時間輪”算法,將插入定時器的操作性能大大提升。根據 Kafka 所提供的檢測結果,採用 DelayQueue 時所能處理的最大吞吐率爲 25000 RPS,採用了變種“時間輪”算法之後,最大吞吐率則達到了 105000 RPS。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章