1. 程式人生 > >程式猿修仙之路--資料結構之設計高效能訪客記錄系統

程式猿修仙之路--資料結構之設計高效能訪客記錄系統

菜菜呀,最近我有個想法呀!

(心想:又尼瑪有折磨人的想法了。) X總,您說~


我想給咱們的使用者做個個人空間,目前先有訪客記錄就可以,最近訪問的人顯示在最上邊,由於使用者量有十幾億,可能對效能要求比較高,三天後上線,你做一下吧!

(心想:一萬頭羊駝飄過!!)  但是X總,個人空間訪問量比較大,需要設計,測試等環節,三天不夠呀!~

這個關係到公司的生死存亡,你加加班就行了```

(心想:一億頭羊駝!!) 好吧,X總,我盡最大努力! 苦笑中。。。。~


需求要點


        每個使用者都有自己的個人空間,當有其他使用者來訪問的時候,需要新增訪客記錄,並且更新為最新的訪客,這裡設計到一個坑,如果存在這個使用者的訪問記錄需要更新使用者的最後訪問時間。那這個需求在技術維度來說,有什麼特點嗎?

先想10秒鐘,在接著往下看!!!

有什麼設計要點呢?

使用者的訪客記錄一定要快取,要不然怎麼抗住大併發呢?

由於最新的訪客記錄變化非常快,要有一種能快速新增新資料,刪除老資料的資料結構。


快取的篇章今日暫且不說,說一下以上的第二點,也就引出了今日資料結構主角:連結串列


連結串列


連結串列百科:連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列屬於線性結構


連結串列分類


1. 單鏈表連結串列中的元素的指向只能指向連結串列中的下一個元素或者為空,元素之間不能相互指向。也就是一種線性連結串列。

public class Node<T>

    {

        //當前節點的資料元素

        public T Data { get; set; }

        //當前節點的下一個元素

        public Node<T> NextNode { get; set; }

    }


2. 雙向連結串列:每個連結串列元素既有指向下一個元素的指標,又有指向前一個元素的指標,其中每個結點都有兩種指標

public class Node<T>

    {

        //當前節點的前一個節點

        public Node<T> PreNode { get; set; }

        //當前節點的資料元素

        public T Data { get; set; }

        //當前節點的下一個元素

        public Node<T> NextNode { get; set; }

    }



3. 迴圈連結串列:指的是在單向連結串列和雙向連結串列的基礎上,將兩種連結串列的最後一個結點指向第一個結點從而實現迴圈。


特性


1. 元素的數量可以隨時擴充。由於連結串列在物理的儲存單元上是非連續的,這就早就了它天生的優勢,我的節點可以在任意符合要求的地方分配記憶體。

2. 新增元素:

單鏈表:

        當在一個位置N之後插入新元素的時候,單鏈表首先把當前位置N的元素的Next指標指向新的元素,然後新的元素的Next指標指向N+1位置的元素。當然如果是在首位置插入新元素,只需要把新元素的Next指標指向連結串列的首元素即可,同理,如果要在單鏈表尾部插入新元素,只需要把單鏈表的尾部元素的Next指標指向新元素。至於迴圈單鏈表,無所謂首元素和尾元素之分。

雙向連結串列:

        在位置N之後新增新元素和單鏈表原理類似,原理也是修改元素的指標指向。但是這裡有一個不同,雙向連結串列要修改前後元素(N位置和N+1位置)和新元素三個Node的指標,所以略微麻煩一點。

3. 刪除元素:

單鏈表:

        當要刪除位置N的元素的時候,只需要把N-1位置元素的Next指標指向N+1即可。

雙向連結串列:

        當要刪除位置N的元素的時候,需要修改N-1位置元素的Next指標指向N+1元素,同時還要修改N+1位置元素的Pre指標指向N-1元素。

4. 查詢元素:

由於連結串列的元素在記憶體中並非連續,所以不能像陣列那樣擁有O(1)的查詢時間複雜度,只能是通過首元素去遍歷連結串列,所以時間複雜度為O(n)


程式設計


        

        給你10秒回到X總的需求中來。通過對連結串列的介紹,我們該選擇哪種連結串列呢?這裡我先說一下我的思路,如有錯誤請指正:


1. 當一個訪客進入個人空間的首頁時,大多數情況下,訪客記錄只需要快取前100條或者200條即可,也就是說這個場景是存在熱點資料的,80%(甚至更高)的請求命中在最近100條訪客資料上,很少人會去檢視很久以前的記錄。所以基於佔用記憶體空間上的考慮,我決定快取最近的100條訪客資料。


2. 假設我用連結串列快取了前100條資料,其中在非首位置有一條訪客A的記錄,此時A又訪問的這個使用者空間,我需要把A的記錄移到首位置,這個過程經歷了刪除A資料,在首位置新增A資料。假如A開始的位置是N,我在刪除N位置資料的時候,需要查詢N-1的位置元素修改其指標指向,如果是單鏈表由於當前位置N的元素中沒有N-1位置元素的資訊,所有需要重新遍歷連結串列。如果是雙向連結串列呢,位置N的元素中儲存了位置N-1的元素,所以沒有必要在重新遍歷連結串列了,這也是雙向連結串列對比單鏈表的優勢,雖然記憶體佔用上多了一個指標的記憶體大小,但是在實際的應用場景中更為常用。所以我選擇雙向連結串列。刪除操作和新增操作時間複雜度都是O(1).


3.     對同一個空間的訪問,必然存在鎖和多執行緒的問題。所以我在選擇框架的時候優先選擇了基於Actor模型的框架。避免了在同一個使用者空間上加鎖的操作。


4. 由於基於Actor模型的框架,所以我沒有采用類似Redis這樣的程序外快取,而是採用了程序內快取,畢竟網路傳輸的速度再快也比記憶體操作要慢的多。應用層的Actor服務天然支援分散式。如果對actor 不太瞭解的同學可以度娘一下。


優化


1. 閱讀到這裡你是否感覺哪裡有問題呢?是的,就是連結串列元素的查詢,由於只能是遍歷,所有連結串列查詢元素的時間複雜度為O(n),那有沒有辦法優化呢?那就是我們以後要講的另外一種資料結構了。


2. 空間的訪客記錄是以時間為維度的倒序排列,所以業務以及DB時間列的設計型別推薦為UTC時間戳long型別,畢竟long型別在多數語言中比datetime型別佔用記憶體要小很多。


3. 無論是否使用快取,使用者的訪問記錄都是需要DB來持久化的,當有大量的請求的時候,我們可以利用某種機制來批量持久化到DB,而不是一個請求就訪問資料庫一次。


4. 當對空間的訪客記錄實時性要求不是很高的時候,我們可以每10秒或者5秒更新快取,也就是批量更新快取,這比單條加鎖更新快取效果更好。



X總的個人空間需求並沒有結束,菜菜仍然在持續優化中,歡迎大佬指正


菜菜出品
一個不止於技術的公眾號