面對海量數據的計數器要如何做?
在地鐵上,你可能經常使用微博瀏覽、點贊熱門話題,甚至參與抽獎活動并轉發相關內容。這些行為涉及到微博數據統計中的各種指標,主要包括:
- 微博的互動數據:評論數、點贊數、轉發數、瀏覽數、表態數等;
- 用戶的社交數據:粉絲數、關注數、發布微博數、私信數等。
微博維度的計數代表了一條微博在平臺上的受歡迎程度,而用戶維度的數據,特別是粉絲數,則反映了用戶在微博社交網絡中的影響力和受關注程度。這些計數信息對于用戶和平臺都具有重要意義
但在設計計數系統時,不少人會出現性能不高、存儲成本很大的問題,比如,把計數與微博數據存儲在一起,這樣每次更新計數的時候都需要鎖住這一行記錄,降低了寫入的并發。在我看來,之所以出現這些問題,還是因為你對計數系統的設計和優化不甚了解,所以要想解決痛點,你有必要形成完備的設計方案。
計數在業務上的特點
微博系統中微博條目的數量已經超過了千億級別。僅僅計算微博的轉發、評論、點贊、瀏覽等核心計數,其數據量級已經達到了幾千億的級別。而微博條目的數量還在不斷高速增長,隨著微博業務的不斷發展,微博維度的計數種類也可能會持續擴展(比如增加了表態數)。因此,僅僅是微博維度上的計數量級就已經過了萬億級別。
此外,微博的用戶量級已經超過了 10 億,用戶維度的計數量級相比微博維度來說雖然相差很大,但也達到了百億級別。面對如此龐大的數據量,如何存儲這些過萬億級別的數字,對我們來說確實是一大挑戰。
考慮到訪問量大和性能要求高的情況,對于微博這樣擁有數億活躍用戶的社交平臺來說,計數系統需要能夠應對每秒數百萬次的訪問量,同時要求在毫秒級別內返回結果。為了達到這樣的性能要求,我們可以采取一些簡單而有效的方法,比如選擇高性能的存儲和緩存技術,優化數據庫設計和查詢,采用分布式架構,以及設置負載均衡和故障恢復機制。這樣可以保證系統在高并發情況下仍然能夠快速、穩定地處理大量請求,滿足用戶的需求
支撐高并發的計數系統要如何設計
在最初設計計數系統時,微博的流量還沒有現在這么龐大。我們遵循了KISS(Keep It Simple and Stupid)原則,選擇了使用MySQL來存儲計數數據。這是因為MySQL是我們團隊最熟悉的數據庫,我們在運維方面也有豐富的經驗。舉個具體的例子來說,我們將微博的計數數據存儲在MySQL數據庫中的單個表中,每個微博對應一行記錄,包括評論數、點贊數、轉發數等計數數據列。這樣的設計簡單易于實現和維護,符合我們當時的需求和團隊的技術水平。
以微博 ID 為主鍵,然后將轉發數、評論數、點贊數和瀏覽數等微博維度的計數數據分別存儲在單獨的列中,這樣可以方便地通過一條SQL語句來獲取特定微博的計數數據。例如:
select repost_count, comment_count, praise_count, view_count from t_weibo_count where weibo_id = ?在數據量級和訪問量級都不大的情況下,采用以微博ID為主鍵,將轉發數、評論數、點贊數和瀏覽數等計數數據存儲在單個MySQL表中的方式是最簡單的。但隨著微博的不斷壯大,之前的計數系統面臨了諸多問題和挑戰。
隨著微博用戶數量和發布的微博數量迅速增加,計數數據量級也隨之飛速增長。當MySQL數據庫單表的存儲量級達到幾千萬時,性能會受到損耗。因此,為了解決這些問題,我們考慮采用分庫分表的方式,將數據量分散存儲,以提升讀取計數數據的性能。
我們用“weibo_id”作為分區鍵,在選擇分庫分表的方式時,考慮了下面兩種:
對于分庫分表的方式,有兩種常見的策略可以考慮。一種是根據微博ID進行哈希分庫分表,另一種是根據微博ID生成的時間來進行分庫分表。
首先,根據哈希算法對weibo_id計算哈希值,然后根據這個哈希值確定需要存儲到哪一個數據庫的哪一張表中。這種方法可以將數據均勻地分散到多個數據庫和表中,以實現負載均衡和提升讀取性能。
另一種方式是按照weibo_id生成的時間來進行分庫分表。可以利用發號器生成的ID中的時間戳信息,將微博數據按照時間戳進行分庫分表,比如每天一張表或者每月一張表等。這樣可以根據微博的發布時間快速定位到對應的數據庫和表,便于數據的管理和查詢。
因為越是最近發布的微博,計數數據的訪問量就越大,所以雖然我考慮了兩種方案,但是按照時間來分庫分表會造成數據訪問的不均勻,最后用了哈希的方式來做分庫分表。
圖片
在微博最初的版本中,首頁信息流并不展示計數數據,因此使用MySQL可以承受當時的計數數據讀取訪問量。但隨著微博的發展,首頁信息流也開始展示轉發、評論和點贊等計數數據,導致信息流的訪問量急劇增加。僅僅依靠數據庫已無法滿足如此高的并發讀取需求。
為了應對這一挑戰,我們考慮使用Redis來加速讀請求。通過部署多個Redis從節點來提升可用性和性能,并通過Hash的方式對數據進行分片,以保證計數的讀取性能。然而,采用數據庫+緩存的方式存在一個嚴重的弊端:無法保證數據的一致性。例如,如果數據庫寫入成功而緩存更新失敗,就會導致數據不一致,從而影響計數的準確性。
因此,為了解決數據一致性的問題,我們最終決定完全拋棄MySQL,全面采用Redis作為計數的存儲組件。Redis的高性能和內存存儲特性使其能夠輕松應對高并發的讀取請求,并且通過持久化機制和主從復制,可以保證數據的持久性和可用性,同時也降低了數據不一致的風險。
圖片
針對熱門微博高頻寫入的情況,可以考慮以下簡單的方法來降低寫入壓力:
- 異步處理: 將計數寫入操作異步化,先將操作記錄在消息隊列中,再由后臺任務異步處理寫入計數數據,減輕數據庫的寫入壓力。
- 計數緩存: 使用緩存暫時存儲計數數據,減少對數據庫的直接寫入請求,提高寫入性能。
- 合并寫入: 將相同微博的計數操作合并,減少數據庫的寫入次數,如多個用戶同時轉發同一條微博時,將轉發操作合并為一次寫入計數數據的操作。
- 分片存儲: 根據微博ID進行分片存儲,將數據分散到不同存儲節點上,分散寫入壓力。
- 寫入限流: 實行寫入限流策略,限制每個用戶或微博的寫入頻率,防止寫入請求過載數據庫。
圖片
如何降低計數系統的存儲成本
在微博這樣的場景下,我們面臨著處理萬億級別計數數據的挑戰。對于這種規模的數據存儲,我們需要在有限的成本下實現全量計數數據的存取。Redis作為內存存儲系統,相較于使用磁盤存儲的MySQL,存儲成本差異巨大。舉例來說,一臺服務器可以掛載2TB的磁盤,但內存可能只有128GB,這意味著磁盤存儲空間是內存的16倍。
Redis因其通用性而對內存的使用較為粗放,存在大量指針和額外數據結構開銷。比如,若要存儲一個KV類型的計數信息,鍵(Key)是8字節的長整型weibo_id,值(Value)是4字節整型的轉發數,在Redis中將會占用超過70個字節的空間,這造成了空間的巨大浪費。
在面對這一問題時,如何優化存儲空間呢?
我建議對原生Redis進行改造,采用新的數據結構和數據類型來存儲計數數據。我的改造主要涉及兩點:
首先,原生Redis在存儲Key時是按照字符串類型來存儲的。比如,一個8字節的Long類型的數據,需要28個字節的存儲空間(8字節的字符串頭部信息 + 19字節的數字長度 + 1字節的字符串結尾標志)。如果我們直接使用Long類型來存儲,只需要8個字節,節省了20個字節的空間。
其次,我去除了原生Redis中多余的指針。現在,如果要存儲一個鍵值對(KV)信息,只需要12個字節(8字節的weibo_id + 4字節的轉發數),相比之前有很大的改進。
同時,我們也會使用一個大的數組來存儲計數信息,存儲的位置是基于 weibo_id 的哈希值來計算出來的,具體的算法像下面展示的這樣:
同時,我們也會使用一個大的數組來存儲計數信息,存儲的位置是基于 weibo_id 的哈希值來計算出來的,具體的算法像下面展示的這樣:在對原生Redis進行改造后,我們還需要進一步考慮如何節省內存的使用。舉例來說,微博的計數數據包括轉發數、評論數、瀏覽數、點贊數等等。如果每個計數都需要存儲weibo_id,那么總共需要的存儲空間是48字節(8字節的weibo_id * 4個微博ID + 每個計數4字節)。
然而,我們可以將相同微博ID的計數數據存儲在一起,這樣就只需要記錄一個微博ID,省去了多余的三個微博ID的存儲開銷。這樣一來,存儲空間就進一步減少了。
不過,即使經過上面的優化,由于計數的量級實在是太過巨大,并且還在以極快的速度增長,所以如果我們以全內存的方式來存儲計數信息,就需要使用非常多的機器來支撐。
針對微博計數數據具有明顯的熱點屬性的情況,我們考慮優化計數服務,增加SSD磁盤,將時間上較久遠的數據存儲在磁盤上,內存中只保留最近的數據,以盡量減少服務器的使用。
具體做法是,將較久遠的計數數據dump到SSD磁盤上,而內存中僅保留最近的數據。當需要讀取冷數據時,使用單獨的I/O線程異步地從SSD磁盤加載冷數據到一個單獨的Cold Cache中。

經過以上優化措施,我們的計數服務現在已經能夠支撐高并發大數據量的考驗,無論是在性能、成本還是可用性方面都能夠滿足業務需求。通過微博設計計數系統的例子,我想強調的是,在系統設計過程中需要了解當前系統面臨的痛點,并針對這些痛點進行細致的優化。
舉例來說,微博計數系統的痛點是存儲成本。因此,我們在后期的優化中主要圍繞如何使用有限的服務器存儲全量的計數數據展開。即使對開源組件(如Redis)進行深度定制可能會增加運維成本,但這些優化都被視為實現計數系統的必要權衡。通過深入了解系統痛點并針對性地進行優化,我們能夠更好地提高系統的性能、降低成本,并確保系統的可用性。
總結
數據庫 + 緩存的方案是計數系統的初級階段,完全可以支撐中小訪問量和存儲量的存儲服務。如果你的項目還處在初級階段,量級還不是很大,那么你一開始可以考慮使用這種方案。
通過對原生 Redis 組件的改造,我們可以極大地減小存儲數據的內存開銷。
使用 SSD+ 內存的方案可以最終解決存儲計數數據的成本問題。這個方式適用于冷熱數據明顯的場景,你在使用時需要考慮如何將內存中的數據做換入換出。
隨著互聯網技術的發展,越來越多的業務場景需要大量的內存資源來存儲業務數據,但對性能或延遲要求不高。全內存存儲會帶來極大的成本浪費,因此一些開源組件開始支持使用SSD替代內存存儲冷數據,比如Pika和SSDB。我建議您了解它們的實現原理,以便在需要時在項目中使用。
在微博的計數服務中也采用了類似的思路,將熱點數據存儲在內存中,而將冷數據存儲在SSD上,這樣既保證了性能,又降低了成本。如果您的業務需要大量內存存儲熱點數據,不妨考慮采用類似的思路來優化您的系統。


























