系統設計之分布式計數器

應用場景
說起計數器,大多數人都不陌生,畢竟計數器的應該實在是太多太多了。小到一個博客系統的文章數目,大到抖音視頻點贊數、評論數,淘寶中商品庫存數量等等。
可以說計數的目的就是為一個對象打上一個數字,這個數字用于表征某種業務含義。通常情況下,我們不一定需要顯示地去創建一個計數器,比如我們要統計店鋪的寶貝數量,只要寫一個 SQL 語句把剩余的商品數量實時統計出來,這樣實現的精確度最高,但是缺陷就是如果流水數量很大,會出現明顯的性能瓶頸。比如說,我們以抖音的點贊數為例,對于一個火熱的視頻,有百萬級的點贊流水,顯然每次都有count如此大的數量是不可能的。
所以,這個時候計數器的需求就橫空出世了,計數器,簡單理解就是幫助我們快速獲取 count 結果的機制。

計數器使用案例:高性能獲取計數值
分布式計數器的實現
單機計數器的實現沒什么好說的,每種編程語言都提供了對應的數據結構,這里我們來分析下分布式計數器的實現方法。通常,我們有兩種選擇:MySQL 計數器、Redis 計數器。
① 基于 MySQL 實現計數器
使用 MySQL 來實現計數器,我們可以單獨創建一張表,這個表主要有一個業務主鍵列,用于表示業務id(比如視頻id),同時需要有個計數列,用于記錄當前的計數值。

一張簡單的 MySQL 表
當有數據增加時,可以使用樂觀鎖保證冪等性,如果執行失敗自旋重試即可。
// 先 select 出當前 current_count
select count as current_count from xxx where id = 'xxx'
// 更新計數值
update xxx set count = current_count + 1 where id = 'xxx' and count = current_count
用 MySQL 實現計數器很簡單,而且如果業務數據也在 MySQL 中,那么可以很方便地做跨表事務,保證整體數據的一致性。但是缺陷也很明顯,因為 `update` 語句存在行鎖(甚至如果id不是主鍵,可能是間隙鎖),那么在競爭激烈的情況下,可能存在嚴重的性能退化。
這個時候,可以考慮做一下性能優化:減小鎖粒度。
實現也很簡單,就是相同業務 ID 可以用 X 條數據,每次更新的時候隨機更新一條,這樣鎖沖突的概率就降低到 1 / X 了,查詢計數值的時候需要修改為對相同業務 ID 求 Sum(count)。
② 基于 Redis 實現計數器
使用 Redis 來作為分布式計數器也是一種常見的手段,相比于 MySQL,Redis 幾乎不存在性能問題(單機可支持10w qps+),并且 Redis 內置了 `IncrBy` 操作,可以原子的實現計數的累加。
但是,使用 Redis 作為計數器有個困擾之一就是操作是非冪等的,比如你調用了 `IncrBy` 命令后,收到網絡錯誤,你無法確定服務端到底是執行成功了,還是執行失敗了。這導致你無法確定是否應該重試,最終導致計數結果的偏差,典型的兩軍問題。
為了解決這個問題,最常見的方法是使用 LUA 腳本,在每次要執行 INCR 的時候,同時使用 `SETNX` 設置一個值,LUA 腳本保證 SETNX 和 INCR 操作同時成功或者同時失敗(原子性),這樣當你收到錯誤的返回信息時,是否要重試僅是判斷對應的 KEY 是否已經設置成功了。
舉個栗子:某個視頻收到一個點贊,假設點贊的業務id=1000,那么 LUA 腳本的執行邏輯是 `SETNX 1000 true` + `IncrBy countKey` 同時成功。
最后,使用 Redis 計數器,要防止熱 KEY。雖然 Redis 能承受的請求量很大, 但是畢竟是單點存儲(讀寫分離),所有寫請求還是都打在同個節點上,需要評估對單個節點的寫入 QPS,務必防止超熱的 KEY 出現。
權衡:一致性與可用性
通常情況下,計數器和流水單獨計算,由于是異構存儲,可能存在一定的不一致性。
這個時候,我們需要權衡業務對不一致性的容忍情況,一般需要權衡的是可用性以及一致性的沖突。
如果一致性很重要,可以考慮使用 MySQL 模式,將業務數據與計數器做在同個事務中,保證強一致,或者引入分布式事務,來保證異構存儲的一致性,或者是使用 Redis 計數器 + LUA 腳本模式等。
但是,需要注意的是無論是何種模式,一致性高的,必然性能、可用性會有所折損。如果業務沒有強訴求,無需搞得這么復雜,可以引入一個定時回掃腳本,定時更正下即可。
記住,不考慮業務的架構,都是耍流氓。
結束語
在我們的業務開發工作中,經常會遇到計數器的訴求。剛開始,我覺得很簡單,不就是 Redis Incr 一下嗎?實際上,當業務變得復雜,當數據量變得龐大,當對計數器的一致性要求變高,這一切在演進中都變得復雜而難以處理。
上面的是我在日常工作中總結的兩種比較常用且有效的分布式計數器實現方案,如果你的工作中也有用到,也可以嘗試嘗試。如果暫時沒有用到,適當的了解,在面試、日常的工作交流中相信也都會受用。































