計(jì)數(shù)系統(tǒng)架構(gòu)實(shí)踐一次搞定
一、需求緣起
很多業(yè)務(wù)都有“計(jì)數(shù)”需求,以微博為例:
微博首頁(yè)的個(gè)人中心部分,有三個(gè)重要的計(jì)數(shù):
- 關(guān)注了多少人的計(jì)數(shù)
- 粉絲的計(jì)數(shù)
- 發(fā)布博文的計(jì)數(shù)
微博首頁(yè)的博文消息主體部分,也有有很多計(jì)數(shù),分別是一條博文的:
- 轉(zhuǎn)發(fā)計(jì)數(shù)
- 評(píng)論計(jì)數(shù)
- 點(diǎn)贊計(jì)數(shù)
- 甚至是瀏覽計(jì)數(shù)
在業(yè)務(wù)復(fù)雜,計(jì)數(shù)擴(kuò)展頻繁,數(shù)據(jù)量大,并發(fā)量大的情況下,計(jì)數(shù)系統(tǒng)的架構(gòu)演進(jìn)與實(shí)踐,是本文將要討論的問(wèn)題。
二、業(yè)務(wù)分析與計(jì)數(shù)初步實(shí)現(xiàn)
典型的互聯(lián)網(wǎng)架構(gòu),常常分為這么幾層:
- 調(diào)用層:處于端上的browser或者APP
- 站點(diǎn)層:拼裝html或者json返回的web-server層
- 服務(wù)層:提供RPC調(diào)用接口的service層
- 數(shù)據(jù)層:提供固化數(shù)據(jù)存儲(chǔ)的db,以及加速存儲(chǔ)的cache
針對(duì)“緣起”里微博計(jì)數(shù)的例子,主要涉及“關(guān)注”業(yè)務(wù),“粉絲”業(yè)務(wù),“微博消息”業(yè)務(wù),一般來(lái)說(shuō),會(huì)有相應(yīng)的db存儲(chǔ)相關(guān)數(shù)據(jù),相應(yīng)的service提供相關(guān)業(yè)務(wù)的RPC接口:
- 關(guān)注服務(wù):提供關(guān)注數(shù)據(jù)的增刪查改RPC接口
- 粉絲服務(wù):提供粉絲數(shù)據(jù)的增刪查改RPC接口
- 消息服務(wù):提供微博消息數(shù)據(jù)的增刪查改RPC接口,消息業(yè)務(wù)相對(duì)比較復(fù)雜,涉及微博消息、轉(zhuǎn)發(fā)、評(píng)論、點(diǎn)贊等數(shù)據(jù)的存儲(chǔ)
對(duì)關(guān)注、粉絲、微博業(yè)務(wù)進(jìn)行了初步解析,那首頁(yè)的計(jì)數(shù)需求應(yīng)該如何滿足呢?
很容易想到,關(guān)注服務(wù)+粉絲服務(wù)+消息服務(wù)均提供相應(yīng)接口,就能拿到相關(guān)計(jì)數(shù)數(shù)據(jù)。
例如,個(gè)人中心首頁(yè),需要展現(xiàn)博文數(shù)量這個(gè)計(jì)數(shù),web層訪問(wèn)message-service的count接口,這個(gè)接口執(zhí)行:
- select count(*) from t_msg where uid = XXX
同理,也很容易拿到關(guān)注,粉絲的這些計(jì)數(shù)。
這個(gè)方案叫做“count”計(jì)數(shù)法,在數(shù)據(jù)量并發(fā)量不大的情況下,最容易想到且最經(jīng)常使用的就是這種方法,但隨著數(shù)據(jù)量的上升,并發(fā)量的上升,這個(gè)方法的弊端將逐步展現(xiàn)。
例如,微博首頁(yè)有很多條微博消息,每條消息有若干計(jì)數(shù),此時(shí)計(jì)數(shù)的拉取就成了一個(gè)龐大的工程:
整個(gè)拉取計(jì)數(shù)的偽代碼如下:
- list<msg_id> = getHomePageMsg(uid);// 獲取首頁(yè)所有消息
- for( msg_id in list<msg_id>){ // 對(duì)每一條消息
- getReadCount(msg_id); // 閱讀計(jì)數(shù)
- getForwordCount(msg_id); // 轉(zhuǎn)發(fā)計(jì)數(shù)
- getCommentCount(msg_id); // 評(píng)論計(jì)數(shù)
- getPraiseCount(msg_id); // 贊計(jì)數(shù)
- }
其中:
- 每一個(gè)微博消息的若干個(gè)計(jì)數(shù),都對(duì)應(yīng)4個(gè)后端服務(wù)訪問(wèn)
- 每一個(gè)訪問(wèn),對(duì)應(yīng)一條count的數(shù)據(jù)庫(kù)訪問(wèn)(count要了老命了)
其效率之低,資源消耗之大,處理時(shí)間之長(zhǎng),可想而知。
“count”計(jì)數(shù)法方案,可以總結(jié)為:
- 多條消息多次查詢,for循環(huán)進(jìn)行
- 一條消息多次查詢,多個(gè)計(jì)數(shù)的查詢
- 一次查詢一個(gè)count,每個(gè)計(jì)數(shù)都是一個(gè)count語(yǔ)句
那如何進(jìn)行優(yōu)化呢?
三、計(jì)數(shù)外置的架構(gòu)設(shè)計(jì)
計(jì)數(shù)是一個(gè)通用的需求,有沒(méi)有可能,這個(gè)計(jì)數(shù)的需求實(shí)現(xiàn)在一個(gè)通用的系統(tǒng)里,而不是由關(guān)注服務(wù)、粉絲服務(wù)、微博服務(wù)來(lái)分別來(lái)提供相應(yīng)的功能呢(否則擴(kuò)展性極差)?
這樣需要實(shí)現(xiàn)一個(gè)通用的計(jì)數(shù)服務(wù)。
通過(guò)分析,上述微博的業(yè)務(wù)可以抽象成兩類:
- 用戶(uid)維度的計(jì)數(shù):用戶的關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),發(fā)布的微博計(jì)數(shù)
- 微博消息(msg_id)維度的計(jì)數(shù):消息轉(zhuǎn)發(fā)計(jì)數(shù),評(píng)論計(jì)數(shù),點(diǎn)贊計(jì)數(shù)
于是可以抽象出兩個(gè)表,針對(duì)這兩個(gè)維度來(lái)進(jìn)行計(jì)數(shù)的存儲(chǔ):
- t_user_count (uid, gz_count, fs_count, wb_count);
- t_msg_count (msg_id, forword_count, comment_count, praise_count);
甚至可以更為抽象,一個(gè)表搞定所有計(jì)數(shù):
- t_count(id, type, c1, c2, c3, …)
通過(guò)type來(lái)判斷,id究竟是uid還是msg_id,但并不建議這么做。
存儲(chǔ)抽象完,再抽象出一個(gè)計(jì)數(shù)服務(wù)對(duì)這些數(shù)據(jù)進(jìn)行管理,提供友善的RPC接口:
這樣,在查詢一條微博消息的若干個(gè)計(jì)數(shù)的時(shí)候,不用進(jìn)行多次數(shù)據(jù)庫(kù)count操作,而會(huì)轉(zhuǎn)變?yōu)橐粭l數(shù)據(jù)的多個(gè)屬性的查詢:
- for(msg_id in list<msg_id>) {
- select forword_count, comment_count, praise_count
- from t_msg_count
- where msg_id=$msg_id;
- }
甚至,可以將微博首頁(yè)所有消息的計(jì)數(shù),轉(zhuǎn)變?yōu)橐粭lIN語(yǔ)句(不用多次查詢了)的批量查詢:
- select * from t_msg_count
- where msg_id IN
- ($msg_id1, $msg_id2, $msg_id3, …);
IN查詢可以***msg_id聚集索引,效率很高。
方案非常帥氣,接下來(lái),問(wèn)題轉(zhuǎn)化為:當(dāng)有微博被轉(zhuǎn)發(fā)、評(píng)論、點(diǎn)贊的時(shí)候,計(jì)數(shù)服務(wù)如何同步的進(jìn)行計(jì)數(shù)的變更呢?
如果讓業(yè)務(wù)服務(wù)來(lái)調(diào)用計(jì)數(shù)服務(wù),勢(shì)必會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)與計(jì)數(shù)系統(tǒng)耦合。
之前的文章介紹過(guò),對(duì)于不關(guān)心下游結(jié)果的業(yè)務(wù),可以使用MQ來(lái)解耦(具體請(qǐng)查閱《到底什么時(shí)候該使用MQ?》),在業(yè)務(wù)發(fā)生變化的時(shí)候,向MQ發(fā)送一條異步消息,通知計(jì)數(shù)系統(tǒng)計(jì)數(shù)發(fā)生了變化即可:
如上圖:
- 用戶新發(fā)布了一條微博
- msg-service向MQ發(fā)送一條消息
- counting-service從MQ接收消息
- counting-service變更這個(gè)uid發(fā)布微博消息計(jì)數(shù)
這個(gè)方案稱為“計(jì)數(shù)外置”,可以總結(jié)為:
- 通過(guò)counting-service單獨(dú)保存計(jì)數(shù)
- MQ同步計(jì)數(shù)的變更
- 多條消息的多個(gè)計(jì)數(shù),一個(gè)批量IN查詢完成
計(jì)數(shù)外置,本質(zhì)是數(shù)據(jù)的冗余,架構(gòu)設(shè)計(jì)上,數(shù)據(jù)冗余必將引發(fā)數(shù)據(jù)的一致性問(wèn)題,需要有機(jī)制來(lái)保證計(jì)數(shù)系統(tǒng)里的數(shù)據(jù)與業(yè)務(wù)系統(tǒng)里的數(shù)據(jù)一致,常見(jiàn)的方法有:
- 對(duì)于一致性要求比較高的業(yè)務(wù),要有定期check并fix的機(jī)制,例如關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博消息計(jì)數(shù)等
- 對(duì)于一致性要求比較低的業(yè)務(wù),即使有數(shù)據(jù)不一致,業(yè)務(wù)可以接受,例如微博瀏覽數(shù),微博轉(zhuǎn)發(fā)數(shù)等
四、計(jì)數(shù)外置緩存優(yōu)化
計(jì)數(shù)外置很大程度上解決了計(jì)數(shù)存取的性能問(wèn)題,但是否還有優(yōu)化空間呢?
像關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博消息計(jì)數(shù),變化的頻率很低,查詢的頻率很高,這類讀多些少的業(yè)務(wù)場(chǎng)景,非常適合使用緩存來(lái)進(jìn)行查詢優(yōu)化,減少數(shù)據(jù)庫(kù)的查詢次數(shù),降低數(shù)據(jù)庫(kù)的壓力。
但是,緩存是kv結(jié)構(gòu)的,無(wú)法像數(shù)據(jù)庫(kù)一樣,設(shè)置成t_uid_count(uid, c1, c2, c3)這樣的schema,如何來(lái)對(duì)kv進(jìn)行設(shè)計(jì)呢?
緩存kv結(jié)構(gòu)的value是計(jì)數(shù),看來(lái)只能在key上做設(shè)計(jì),很容易想到,可以使用uid:type來(lái)做key,存儲(chǔ)對(duì)應(yīng)type的計(jì)數(shù)。
對(duì)于uid=123的用戶,其關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博消息計(jì)數(shù)的緩存就可以設(shè)計(jì)為:
此時(shí)對(duì)應(yīng)的counting-service架構(gòu)變?yōu)椋?/p>
如此這般,多個(gè)uid的多個(gè)計(jì)數(shù),又可能會(huì)變?yōu)槎啻尉彺娴脑L問(wèn):
- for(uid in list<uid>) {
- memcache::get($uid:c1, $uid:c2, $uid:c3);
- }
這個(gè)“計(jì)數(shù)外置緩存優(yōu)化”方案,可以總結(jié)為:
- 使用緩存來(lái)保存讀多寫(xiě)少的計(jì)數(shù)(其實(shí)寫(xiě)多讀少,一致性要求不高的計(jì)數(shù),也可以先用緩存保存,然后定期刷到數(shù)據(jù)庫(kù)中,以降低數(shù)據(jù)庫(kù)的讀寫(xiě)壓力)
- 使用id:type的方式作為緩存的key,使用count來(lái)作為緩存的value
- 多次讀取緩存來(lái)查詢多個(gè)uid的計(jì)數(shù)
五、緩存批量讀取優(yōu)化
緩存的使用能夠極大降低數(shù)據(jù)庫(kù)的壓力,但多次緩存交互依舊存在優(yōu)化空間,有沒(méi)有辦法進(jìn)一步優(yōu)化呢?
當(dāng)當(dāng)當(dāng)當(dāng)!
不要陷入思維定式,誰(shuí)說(shuō)value一定只能是一個(gè)計(jì)數(shù),難道不能多個(gè)計(jì)數(shù)存儲(chǔ)在一個(gè)value中么?
緩存kv結(jié)構(gòu)的key是uid,value可以是多個(gè)計(jì)數(shù)同時(shí)存儲(chǔ)。
對(duì)于uid=123的用戶,其關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博消息計(jì)數(shù)的緩存就可以設(shè)計(jì)為:
這樣多個(gè)用戶,多個(gè)計(jì)數(shù)的查詢就可以一次搞定:
- memcache::get($uid1, $uid2, $uid3, …);
然后對(duì)獲取的value進(jìn)行分析,得到關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博計(jì)數(shù)。
如果計(jì)數(shù)value能夠事先預(yù)估一個(gè)范圍,甚至可以用一個(gè)整數(shù)的不同bit來(lái)存儲(chǔ)多個(gè)計(jì)數(shù),用整數(shù)的與或非計(jì)算提高效率。
這個(gè)“計(jì)數(shù)外置緩存批量?jī)?yōu)化”方案,可以總結(jié)為:
- 使用id作為key,使用同一個(gè)id的多個(gè)計(jì)數(shù)的拼接作為value
- 多個(gè)id的多個(gè)計(jì)數(shù)查詢,一次搞定
六、計(jì)數(shù)擴(kuò)展性優(yōu)化
考慮完效率,架構(gòu)設(shè)計(jì)上還需要考慮擴(kuò)展性,如果uid除了關(guān)注計(jì)數(shù),粉絲計(jì)數(shù),微博計(jì)數(shù),還要增加一個(gè)計(jì)數(shù),這時(shí)系統(tǒng)需要做什么變更呢?
之前的數(shù)據(jù)庫(kù)結(jié)構(gòu)是:
- t_user_count(uid, gz_count, fs_count, wb_count)
這種設(shè)計(jì),通過(guò)列來(lái)進(jìn)行計(jì)數(shù)的存儲(chǔ),如果增加一個(gè)XX計(jì)數(shù),數(shù)據(jù)庫(kù)的表結(jié)構(gòu)要變更為:
- t_user_count(uid, gz_count, fs_count, wb_count, XX_count)
在數(shù)據(jù)量很大的情況下,頻繁的變更數(shù)據(jù)庫(kù)schema的結(jié)構(gòu)顯然是不可取的,有沒(méi)有擴(kuò)展性更好的方式呢?
當(dāng)當(dāng)當(dāng)當(dāng)!
不要陷入思維定式,誰(shuí)說(shuō)只能通過(guò)擴(kuò)展列來(lái)擴(kuò)展屬性,通過(guò)擴(kuò)展行來(lái)擴(kuò)展屬性,在“架構(gòu)師之路”的系列文章里也不是***次出現(xiàn)了(具體請(qǐng)查閱《啥,又要為表增加一列屬性?》《這才是真正的表擴(kuò)展方案》《100億數(shù)據(jù)1萬(wàn)屬性數(shù)據(jù)架構(gòu)設(shè)計(jì)》),完全可以這樣設(shè)計(jì)表結(jié)構(gòu):
- t_user_count(uid, count_key, count_value)
如果需要新增一個(gè)計(jì)數(shù)XX_count,只需要增加一行即可,而不需要變更表結(jié)構(gòu):
七、總結(jié)
小小的計(jì)數(shù),在數(shù)據(jù)量大,并發(fā)量大的時(shí)候,其架構(gòu)實(shí)踐思路為:
- 計(jì)數(shù)外置:由“count計(jì)數(shù)法”升級(jí)為“計(jì)數(shù)外置法”
- 讀多寫(xiě)少,甚至寫(xiě)多但一致性要求不高的計(jì)數(shù),需要進(jìn)行緩存優(yōu)化,降低數(shù)據(jù)庫(kù)壓力
- 緩存kv設(shè)計(jì)優(yōu)化,可以由[key:type]->[count],優(yōu)化為[key]->[c1:c2:c3]
即:
優(yōu)化為:
數(shù)據(jù)庫(kù)擴(kuò)展性優(yōu)化,可以由列擴(kuò)展優(yōu)化為行擴(kuò)展
即:
優(yōu)化為:
計(jì)數(shù)系統(tǒng)架構(gòu)先聊到這里,希望大家有收獲。
【本文為51CTO專欄作者“58沈劍”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】