天天看點

計數系統架構實踐一次搞定 | 架構師之路

提醒,本文較長,可提前收藏/轉發。

一、需求緣起

很多業務都有“計數”需求,以微網誌為例:

計數系統架構實踐一次搞定 | 架構師之路

微網誌首頁的個人中心部分,有三個重要的計數:

關注了多少人的計數

粉絲的計數

釋出博文的計數

計數系統架構實踐一次搞定 | 架構師之路

微網誌首頁的博文消息主體部分,也有有很多計數,分别是一條博文的:

轉發計數

評論計數

點贊計數

甚至是浏覽計數

在業務複雜,計數擴充頻繁,資料量大,并發量大的情況下,計數系統的架構演進與實踐,是本文将要讨論的問題。

二、業務分析與計數初步實作

計數系統架構實踐一次搞定 | 架構師之路

典型的網際網路架構,常常分為這麼幾層:

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

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

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

資料層:提供固化資料存儲的db,以及加速存儲的cache

針對“緣起”裡微網誌計數的例子,主要涉及“關注”業務,“粉絲”業務,“微網誌消息”業務,一般來說,會有相應的db存儲相關資料,相應的service提供相關業務的RPC接口:

計數系統架構實踐一次搞定 | 架構師之路

關注服務:提供關注資料的增删查改RPC接口

粉絲服務:提供粉絲資料的增删查改RPC接口

消息服務:提供微網誌消息資料的增删查改RPC接口,消息業務相對比較複雜,涉及微網誌消息、轉發、評論、點贊等資料的存儲

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

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

計數系統架構實踐一次搞定 | 架構師之路

例如,個人中心首頁,需要展現博文數量這個計數,web層通路message-service的count接口,這個接口執行:

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語句

那如何進行優化呢?

三、計數外置的架構設計

計數是一個通用的需求,有沒有可能,這個計數的需求實作在一個通用的系統裡,而不是由關注服務、粉絲服務、微網誌服務來分别來提供相應的功能呢(否則擴充性極差)?

這樣需要實作一個通用的計數服務。

通過分析,上述微網誌的業務可以抽象成兩類:

使用者(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-service向MQ發送一條消息

counting-service從MQ接收消息

counting-service變更這個uid釋出微網誌消息計數

這個方案稱為“計數外置”,可以總結為:

通過counting-service單獨儲存計數

MQ同步計數的變更

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

計數外置,本質是資料的備援,架構設計上,資料備援必将引發資料的一緻性問題,需要有機制來保證計數系統裡的資料與業務系統裡的資料一緻,常見的方法有:

對于一緻性要求比較高的業務,要有定期check并fix的機制,例如關注計數,粉絲計數,微網誌消息計數等

對于一緻性要求比較低的業務,即使有資料不一緻,業務可以接受,例如微網誌浏覽數,微網誌轉發數等

四、計數外置緩存優化

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

像關注計數,粉絲計數,微網誌消息計數,變化的頻率很低,查詢的頻率很高,這類讀多些少的業務場景,非常适合使用緩存來進行查詢優化,減少資料庫的查詢次數,降低資料庫的壓力。

但是,緩存是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的計數

五、緩存批量讀取優化

緩存的使用能夠極大降低資料庫的壓力,但多次緩存互動依舊存在優化空間,有沒有辦法進一步優化呢?

當當當當!

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

緩存kv結構的key是uid,value可以是多個計數同時存儲。

計數系統架構實踐一次搞定 | 架構師之路

這樣多個使用者,多個計數的查詢就可以一次搞定:

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

然後對擷取的value進行分析,得到關注計數,粉絲計數,微網誌計數。

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

這個“計數外置緩存批量優化”方案,可以總結為:

使用id作為key,使用同一個id的多個計數的拼接作為value

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

六、計數擴充性優化

考慮完效率,架構設計上還需要考慮擴充性,如果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,隻需要增加一行即可,而不需要變更表結構:

計數系統架構實踐一次搞定 | 架構師之路

七、總結

小小的計數,在資料量大,并發量大的時候,其架構實踐思路為:

計數外置:由“count計數法”更新為“計數外置法”

讀多寫少,甚至寫多但一緻性要求不高的計數,需要進行緩存優化,降低資料庫壓力

緩存kv設計優化,可以由[key:type]->[count],優化為[key]->[c1:c2:c3]

即:

計數系統架構實踐一次搞定 | 架構師之路

優化為:

計數系統架構實踐一次搞定 | 架構師之路

資料庫擴充性優化,可以由列擴充優化為行擴充

計數系統架構實踐一次搞定 | 架構師之路
計數系統架構實踐一次搞定 | 架構師之路

計數系統架構先聊到這裡,希望大家有收獲。

===【完】===