【轉】計數系統架構實踐一次搞定

1、需求緣起html

不少業務都有「計數」需求,以微博爲例:web

微博首頁的我的中心部分,有三個重要的計數:數據庫

  • 關注了多少人的計數json

  • 粉絲的計數後端

  • 發佈博文的計數緩存


微博首頁的博文消息主體部分,也有有不少計數,分別是一條博文的:架構

  • 轉發計數併發

  • 評論計數異步

  • 點贊計數性能

  • 甚至是瀏覽計數

 

業務複雜,計數擴展頻繁,數據量大,併發量大的狀況下,計數系統的架構演進與實踐,是本文將要討論的問題。

 

2、業務分析與計數初步實現

典型的互聯網架構,經常分爲這麼幾層:

  • 調用層:處於端上的browser或者APP

  • 站點層:拼裝html或者json返回的web-server

  • 服務層:提供RPC調用接口的service

  • 數據層:提供固化數據存儲的db,以及加速存儲的cache

 

針對「緣起」裏微博計數的例子,主要涉及「關注」業務,「粉絲」業務,「微博消息」業務,通常來講,會有相應的db存儲相關數據,相應的service提供相關業務的RPC接口:

  • 關注服務:提供關注數據的增刪查改RPC接口

  • 粉絲服務:提供粉絲數據的增刪查改RPC接口

  • 消息服務:提供微博消息數據的增刪查改RPC接口,消息業務相對比較複雜,涉及微博消息、轉發、評論、點贊等數據的存儲

 

對關注、粉絲、微博業務進行了初步解析,那首頁的計數需求應該如何知足呢?


很容易想到,關注服務+粉絲服務+消息服務均提供相應接口,就能拿到相關計數數據

例如,我的中心首頁,須要展示博文數量這個計數,web層訪問message-servicecount接口,這個接口執行:

select count(*) from t_msg where uid = XXX

同理,也很容易拿到關注,粉絲的這些計數。

 

這個方案叫作「count」計數法,在數據量併發量不大的狀況下,最容易想到且最常用的就是這種方法,但隨着數據量的上升,併發量的上升,這個方法的弊端將逐步展示。

 

例如,微博首頁有不少條微博消息,每條消息有若干計數,此時計數的拉取就成了一個龐大的工程:

整個拉取計數的僞代碼以下:

list<msg_id> = getHomePageMsg(uid);// 獲取首頁全部消息

for( msg_id in list<msg_id>){ // 對每一條消息

         getReadCount(msg_id);  // 閱讀計數

         getForwordCount(msg_id); // 轉發計數

         getCommentCount(msg_id); // 評論計數

         getPraiseCount(msg_id); // 贊計數

}

其中:

  • 每個微博消息的若干個計數,都對應4個後端服務訪問

  • 每個訪問,對應一條count的數據庫訪問(count要了老命了)

其效率之低,資源消耗之大,處理時間之長,可想而知。

 

「count」計數法方案,能夠總結爲:

  • 多條消息屢次查詢,for循環進行

  • 一條消息屢次查詢,多個計數的查詢

  • 一次查詢一個count,每一個計數都是一個count語句

 

那如何進行優化呢?

 

3、計數外置的架構設計

計數是一個通用的需求,有沒有可能,這個計數的需求實如今一個通用的系統裏,而不是由關注服務、粉絲服務、微博服務來分別來提供相應的功能呢(不然擴展性極差)?


這樣須要實現一個通用的計數服務。

 

經過分析,上述微博的業務能夠抽象成兩類:

  • 用戶(uid)維度的計數:用戶的關注計數,粉絲計數,發佈的微博計數

  • 微博消息(msg_id)維度的計數:消息轉發計數,評論計數,點贊計數

 

因而能夠抽象出兩個表,針對這兩個維度來進行計數的存儲

t_user_count (uid, gz_count, fs_count, wb_count);

t_msg_count (msg_id, forword_count, comment_count, praise_count);

 

甚至能夠更爲抽象,一個表搞定全部計數:

t_count(id, type, c1, c2, c3, …)

經過type來判斷,id到底是uid仍是msg_id,但並不建議這麼作。

 

存儲抽象完,再抽象出一個計數服務對這些數據進行管理,提供友善的RPC接口

這樣,在查詢一條微博消息的若干個計數的時候,不用進行屢次數據庫count操做,而會轉變爲一條數據的多個屬性的查詢

for(msg_id in list<msg_id>) {

select forword_count, comment_count, praise_count 

    from t_msg_count 

    where msg_id=$msg_id;

}

 

甚至,能夠將微博首頁全部消息的計數,轉變爲一條IN語句(不用屢次查詢了)的批量查詢:

select * from t_msg_count 

    where msg_id IN

    ($msg_id1, $msg_id2, $msg_id3, …);

IN查詢能夠命中msg_id彙集索引,效率很高。

 

方案很是帥氣,接下來,問題轉化爲:當有微博被轉發、評論、點讚的時候,計數服務如何同步的進行計數的變動呢?


若是讓業務服務來調用計數服務,勢必會致使業務系統與計數系統耦合。


以前的文章介紹過,對於不關心下游結果的業務,可使用MQ來解耦(具體請查閱《到底何時該使用MQ?》),在業務發生變化的時候,向MQ發送一條異步消息,通知計數系統計數發生了變化便可:

如上圖:

  • 用戶新發布了一條微博

  • msg-serviceMQ發送一條消息

  • counting-serviceMQ接收消息

  • counting-service變動這個uid發佈微博消息計數

 

這個方案稱爲計數外置,能夠總結爲:

  • 經過counting-service單獨保存計數

  • MQ同步計數的變動

  • 多條消息的多個計數,一個批量IN查詢完成

 

計數外置本質是數據的冗餘,架構設計上,數據冗餘必將引起數據的一致性問題,須要有機制來保證計數系統裏的數據與業務系統裏的數據一致,常見的方法有:

  • 對於一致性要求比較高的業務,要有按期checkfix的機制,例如關注計數,粉絲計數,微博消息計數等

  • 對於一致性要求比較低的業務,即便有數據不一致,業務能夠接受,例如微博瀏覽數,微博轉發數等

 

4、計數外置緩存優化

計數外置很大程度上解決了計數存取的性能問題,可是否還有優化空間呢?


像關注計數,粉絲計數,微博消息計數,變化的頻率很低,查詢的頻率很高,這類讀多些少的業務場景很是適合使用緩存來進行查詢優化,減小數據庫的查詢次數,下降數據庫的壓力。

 

可是,緩存是kv結構的,沒法像數據庫同樣,設置成t_uid_count(uid, c1, c2, c3)這樣的schema如何來對kv進行設計呢?


緩存kv結構的value是計數,看來只能在key上作設計,很容易想到,可使用uid:type來作key,存儲對應type的計數。


對於uid=123的用戶,其關注計數,粉絲計數,微博消息計數的緩存就能夠設計爲:

 

此時對應的counting-service架構變爲:

 

如此這般,多個uid的多個計數,又可能會變爲屢次緩存的訪問

for(uid in list<uid>) {

 memcache::get($uid:c1, $uid:c2, $uid:c3);

}

 

這個計數外置緩存優化方案,能夠總結爲:

  • 使用緩存來保存讀多寫少的計數(其實寫多讀少,一致性要求不高的計數,也能夠先用緩存保存,而後按期刷到數據庫中,以下降數據庫的讀寫壓力)

  • 使用id:type的方式做爲緩存的key,使用count來做爲緩存的value

  • 屢次讀取緩存來查詢多個uid的計數

 

5、緩存批量讀取優化

緩存的使用可以極大下降數據庫的壓力,但屢次緩存交互依舊存在優化空間,有沒有辦法進一步優化呢?

 

噹噹噹當!

不要陷入思惟定式,誰說value必定只能是一個計數,難道不能多個計數存儲在一個value中麼?


緩存kv結構的keyuidvalue能夠是多個計數同時存儲


對於uid=123的用戶,其關注計數,粉絲計數,微博消息計數的緩存就能夠設計爲:

這樣多個用戶,多個計數的查詢就能夠一次搞定:

memcache::get($uid1, $uid2, $uid3, …);

而後對獲取的value進行分析,獲得關注計數,粉絲計數,微博計數。


若是計數value可以事先預估一個範圍,甚至能夠用一個整數的不一樣bit來存儲多個計數,用整數的與或非計算提升效率。

 

這個「計數外置緩存批量優化」方案,能夠總結爲:

  • 使用id做爲key,使用同一個id的多個計數的拼接做爲value

  • 多個id的多個計數查詢,一次搞定

 

6、計數擴展性優化

考慮完效率,架構設計上還須要考慮擴展性,若是uid除了關注計數,粉絲計數,微博計數,還要增長一個計數,這時系統須要作什麼變動呢?


以前的數據庫結構是:

t_user_count(uid, gz_count, fs_count, wb_count)

這種設計,經過列來進行計數的存儲,若是增長一個XX計數,數據庫的表結構要變動爲:

t_user_count(uid, gz_count, fs_count, wb_count, XX_count)

在數據量很大的狀況下,頻繁的變動數據庫schema的結構顯然是不可取的,有沒有擴展性更好的方式呢?

 

噹噹噹當!

不要陷入思惟定式,誰說只能經過擴展列來擴展屬性,經過擴展行來擴展屬性,在「架構師之路」的系列文章裏也不是第一次出現了(具體請查閱《啥,又要爲表增長一列屬性?》《這纔是真正的表擴展方案》《100億數據1萬屬性數據架構設計》),徹底能夠這樣設計表結構:

t_user_count(uid, count_key, count_value)

若是須要新增一個計數XX_count,只須要增長一行便可,而不須要變動表結構:

 

7、總結

小小的計數,在數據量大,併發量大的時候,其架構實踐思路爲:

  • 計數外置:由「count計數法」升級爲「計數外置法」

  • 讀多寫少,甚至寫多但一致性要求不高的計數,須要進行緩存優化,下降數據庫壓力

  • 緩存kv設計優化,能夠[key:type]->[count],優化爲[key]->[c1:c2:c3]

即:

優化爲:

  • 數據庫擴展性優化,能夠由列擴展優化爲行擴展

即:

優化爲:

 

計數系統架構先聊到這裏,但願你們有收穫。

相關文章
相關標籤/搜索