好友關(guān)系,百億關(guān)系鏈,架構(gòu)要如何設(shè)計(jì)?

繼續(xù)答星球粉絲提問,大數(shù)據(jù)量,高并發(fā)量,好友關(guān)系鏈、粉絲關(guān)系鏈要如何設(shè)計(jì)?
什么是關(guān)系鏈業(yè)務(wù)?
關(guān)系鏈主要分為兩類,弱好友關(guān)系與強(qiáng)好友關(guān)系,兩類都有典型的互聯(lián)網(wǎng)產(chǎn)品應(yīng)用。
(1) 弱好友關(guān)系的建立,不需要雙方彼此同意:
- 用戶A關(guān)注用戶B,不需要用戶B同意,此時(shí)用戶A與用戶B為弱好友關(guān)系,對(duì)A而言,暫且理解為“關(guān)注”;
- 用戶B關(guān)注用戶A,也不需要用戶A同意,此時(shí)用戶A與用戶B也為弱好友關(guān)系,對(duì)A而言,暫且理解為“粉絲”;
idol與fans這類微博粉絲關(guān)系鏈,是一個(gè)典型的弱好友關(guān)系應(yīng)用。
(2) 強(qiáng)好友關(guān)系的建立,需要好友關(guān)系雙方彼此同意:
用戶A請(qǐng)求添加用戶B為好友,用戶B同意,此時(shí)用戶A與用戶B則互為強(qiáng)好友關(guān)系,即A是B的好友,B也是A的好友。
QQ好友關(guān)系鏈,是一個(gè)典型的強(qiáng)好友關(guān)系應(yīng)用。

好友中心是一個(gè)典型的多對(duì)多業(yè)務(wù):
- 一個(gè)用戶可以添加多個(gè)好友;
- 也可以被多個(gè)好友添加;
其典型架構(gòu)為:

- friend-service:好友中心服務(wù),對(duì)調(diào)用者提供友好的RPC接口;
- db:對(duì)好友數(shù)據(jù)進(jìn)行存儲(chǔ);
弱好友關(guān)系,存儲(chǔ)層應(yīng)該如何實(shí)現(xiàn)?
通過弱好友關(guān)系業(yè)務(wù)分析,很容易了解到,其核心元數(shù)據(jù)為:
- guanzhu(uid, guanzhu_uid);
- fensi(uid, fensi_uid);
其中:
- guanzhu表,用戶記錄uid所有關(guān)注用戶guanzhu_uid;
- fensi表,用來記錄uid所有粉絲用戶fensi_uid;
需要強(qiáng)調(diào)的是,一條弱關(guān)系的產(chǎn)生,會(huì)產(chǎn)生兩條記錄,一條關(guān)注記錄,一條粉絲記錄。
例如:用戶A(uid=1)關(guān)注了用戶B(uid=2),A多關(guān)注了一個(gè)用戶,B多了一個(gè)粉絲,于是:
- guanzhu表要插入{1, 2}這一條記錄,1關(guān)注了2;
- fensi表要插入{2, 1}這一條記錄,2粉了1;
如何查詢一個(gè)用戶關(guān)注了誰?
回答:在guanzhu的uid上建立索引:
select * from guanzhu where uid=1;即可得到結(jié)果,1關(guān)注了2。
如何查詢一個(gè)用戶粉了誰?
回答:在fensi的uid上建立索引:
select * from fensi where uid=2;即可得到結(jié)果,2粉了1。
強(qiáng)好友關(guān)系,存儲(chǔ)層應(yīng)該如何實(shí)現(xiàn)?
(1) 方案一
通過強(qiáng)好友關(guān)系業(yè)務(wù)分析,很容易了解到,其核心元數(shù)據(jù)為:
friend(uid1, uid2);其中:
- uid1,強(qiáng)好友關(guān)系中一方的uid;
- uid2,強(qiáng)好友關(guān)系中另一方的uid;
uid=1的用戶添加了uid=2的用戶,雙方都同意加彼此為好友,這個(gè)強(qiáng)好友關(guān)系,在數(shù)據(jù)庫中應(yīng)該插入記錄{1, 2}還是記錄{2,1}呢?
回答:都可以。為了避免歧義,可以人為約定,插入記錄時(shí)uid1的值必須小于uid2。
例如:有uid=1,2,3三個(gè)用戶,他們互為強(qiáng)好友關(guān)系,那邊數(shù)據(jù)庫中可能是這樣的三條記錄
{1, 2}
{2, 3}
{1, 3}如何查詢一個(gè)用戶的好友呢?
回答:假設(shè)要查詢uid=2的所有好友,只需在uid1和uid2上建立索引,然后:
select * from friend where uid1=2
union
select * from friend where uid2=2即可得到結(jié)果。
(2) 方案二
強(qiáng)好友關(guān)系是弱好友關(guān)系的一個(gè)特例,A和B必須互為關(guān)注關(guān)系(也可以說,同時(shí)互為粉絲關(guān)系),即也可以使用關(guān)注表和粉絲表來實(shí)現(xiàn):
- guanzhu(uid, guanzhu_uid);
- fensi(uid, fensi_uid);
例如:用戶A(uid=1)和用戶B(uid=2)為強(qiáng)好友關(guān)系,即相互關(guān)注:
用戶A(uid=1)關(guān)注了用戶B(uid=2),A多關(guān)注了一個(gè)用戶,B多了一個(gè)粉絲,于是:
- guanzhu表要插入{1, 2}這一條記錄;
- fensi表要插入{2, 1}這一條記錄;
同時(shí),用戶B(uid=2)也關(guān)注了用戶A(uid=1),B多關(guān)注了一個(gè)用戶,A多了一個(gè)粉絲,于是:
- guanzhu表要插入{2, 1}這一條記錄;
- fensi表要插入{1, 2}這一條記錄;
兩種實(shí)現(xiàn),各有什么優(yōu)缺點(diǎn)?
對(duì)于強(qiáng)好友關(guān)系的兩類實(shí)現(xiàn):
- friend(uid1, uid2)表;
- 數(shù)據(jù)冗余guanzhu表與fensi表(后文稱正表T1與反表T2);
在數(shù)據(jù)量小時(shí),看似無差異,但數(shù)據(jù)量大時(shí),數(shù)據(jù)冗余的優(yōu)勢(shì)就體現(xiàn)出來了:
- friend表,數(shù)據(jù)量大時(shí),如果使用uid1來分庫,那么uid2上的查詢就需要遍歷多庫;
- 正表T1與反表T2通過數(shù)據(jù)冗余來實(shí)現(xiàn)好友關(guān)系,{1,2}{2,1}分別存在于兩表中,故兩個(gè)表都使用uid來分庫,均只需要進(jìn)行一次查詢,就能找到對(duì)應(yīng)的關(guān)注與粉絲,而不需要多個(gè)庫掃描;
畫外音:假如有10億關(guān)系鏈,必須水平切分。
數(shù)據(jù)冗余,是多對(duì)多關(guān)系,在數(shù)據(jù)量大時(shí),數(shù)據(jù)水平切分的常用實(shí)踐。
如何進(jìn)行數(shù)據(jù)冗余?
接下來的問題轉(zhuǎn)化為,好友中心服務(wù)如何來進(jìn)行數(shù)據(jù)冗余,常見有三種方法。
(1) 方法一:服務(wù)同步冗余。

顧名思義,由好友中心服務(wù)同步寫冗余數(shù)據(jù),如上圖1-4流程:
- 業(yè)務(wù)方調(diào)用服務(wù),新增數(shù)據(jù);
- 服務(wù)先插入T1數(shù)據(jù);
- 服務(wù)再插入T2數(shù)據(jù);
- 服務(wù)返回業(yè)務(wù)方新增數(shù)據(jù)成功;
優(yōu)點(diǎn):
- 不復(fù)雜,服務(wù)層由單次寫,變兩次寫;
- 數(shù)據(jù)一致性相對(duì)較高(因?yàn)殡p寫成功才返回);
缺點(diǎn):
- 請(qǐng)求的處理時(shí)間增加(要插入次,時(shí)間加倍);
- 數(shù)據(jù)仍可能不一致,例如第二步寫入T1完成后服務(wù)重啟,則數(shù)據(jù)不會(huì)寫入T2;
如果系統(tǒng)對(duì)處理時(shí)間比較敏感,引出常用的第二種方案。
(2) 方法二:服務(wù)異步冗余。

數(shù)據(jù)的雙寫并不再由好友中心服務(wù)來完成,服務(wù)層異步發(fā)出一個(gè)消息,通過消息總線發(fā)送給一個(gè)專門的數(shù)據(jù)復(fù)制服務(wù)來寫入冗余數(shù)據(jù),如上圖1-6流程:
- 業(yè)務(wù)方調(diào)用服務(wù),新增數(shù)據(jù);
- 服務(wù)先插入T1數(shù)據(jù);
- 服務(wù)向消息總線發(fā)送一個(gè)異步消息(發(fā)出即可,不用等返回,通常很快就能完成);
- 服務(wù)返回業(yè)務(wù)方新增數(shù)據(jù)成功;
- 消息總線將消息投遞給數(shù)據(jù)同步中心;
- 數(shù)據(jù)同步中心插入T2數(shù)據(jù);
優(yōu)點(diǎn):請(qǐng)求處理時(shí)間短(只插入1次)。
缺點(diǎn):
- 系統(tǒng)的復(fù)雜性增加了,多引入了一個(gè)組件(消息總線)和一個(gè)服務(wù)(專用的數(shù)據(jù)復(fù)制服務(wù));
- 因?yàn)榉祷貥I(yè)務(wù)線數(shù)據(jù)插入成功時(shí),數(shù)據(jù)還不一定插入到T2中,因此數(shù)據(jù)有一個(gè)不一致時(shí)間窗口(這個(gè)窗口很短,最終是一致的);
- 在消息總線丟失消息時(shí),冗余表數(shù)據(jù)會(huì)不一致;
如果想解除“數(shù)據(jù)冗余”對(duì)系統(tǒng)的耦合,引出常用的第三種方案。
(3) 方法三:線下異步冗余。

數(shù)據(jù)的雙寫不再由好友中心服務(wù)來完成,而是由線下的一個(gè)服務(wù)或者任務(wù)來完成,如上圖1-6流程:
- 業(yè)務(wù)方調(diào)用服務(wù),新增數(shù)據(jù);
- 服務(wù)先插入T1數(shù)據(jù);
- 服務(wù)返回業(yè)務(wù)方新增數(shù)據(jù)成功;
- 數(shù)據(jù)會(huì)被寫入到數(shù)據(jù)庫的log中;
- 線下服務(wù)或者任務(wù)讀取數(shù)據(jù)庫的log;
- 線下服務(wù)或者任務(wù)插入T2數(shù)據(jù);
優(yōu)點(diǎn):
- 數(shù)據(jù)雙寫與業(yè)務(wù)完全解耦;
- 請(qǐng)求處理時(shí)間短(只插入1次);
缺點(diǎn):
- 返回業(yè)務(wù)線數(shù)據(jù)插入成功時(shí),數(shù)據(jù)還不一定插入到T2中,因此數(shù)據(jù)有一個(gè)不一致時(shí)間窗口(這個(gè)窗口很短,最終是一致的);
- 數(shù)據(jù)的一致性依賴于線下服務(wù)或者任務(wù)的可靠性;
上述三種方案各有優(yōu)缺點(diǎn),可以結(jié)合實(shí)際情況選取。
數(shù)據(jù)冗余固然能夠解決多對(duì)多關(guān)系的數(shù)據(jù)庫水平切分問題,但又帶來了新的問題,如何保證正表T1與反表T2的數(shù)據(jù)一致性呢?
從上面的討論可以看到,不管哪種方案,因?yàn)閮刹讲僮鞑荒鼙WC原子性,總有出現(xiàn)數(shù)據(jù)不一致的可能,高吞吐分布式事務(wù)是業(yè)內(nèi)尚未解決的難題,此時(shí)的架構(gòu)優(yōu)化方向:最終一致性。并不是完全保證數(shù)據(jù)的實(shí)時(shí)一致,而是盡早的發(fā)現(xiàn)不一致,并修復(fù)不一致。
最終一致性,是高吞吐互聯(lián)網(wǎng)業(yè)務(wù)一致性的常用實(shí)踐。更具體的,保證數(shù)據(jù)最終一致性的常見方案有三種。
(1) 方法一:線下掃描正反冗余表全部數(shù)據(jù)。

如上圖所示,線下啟動(dòng)一個(gè)離線的掃描工具,不停的比對(duì)正表T1和反表T2,如果發(fā)現(xiàn)數(shù)據(jù)不一致,就進(jìn)行補(bǔ)償修復(fù)。
優(yōu)點(diǎn):
- 比較簡(jiǎn)單,開發(fā)代價(jià)小;
- 線上服務(wù)無需修改,修復(fù)工具與線上服務(wù)解耦;
缺點(diǎn):
- 掃描效率低,會(huì)掃描大量的“已經(jīng)能夠保證一致”的數(shù)據(jù);
- 由于掃描的數(shù)據(jù)量大,掃描一輪的時(shí)間比較長(zhǎng),即數(shù)據(jù)如果不一致,不一致的時(shí)間窗口比較長(zhǎng);
有沒有只掃描“可能存在不一致可能性”的數(shù)據(jù),而不是每次掃描全部數(shù)據(jù),以提高效率的優(yōu)化方法呢?
(2) 方法二:線下掃描增量數(shù)據(jù)。

每次只掃描增量的日志數(shù)據(jù),就能夠極大提高效率,縮短數(shù)據(jù)不一致的時(shí)間窗口,如上圖1-4流程所示:
- 寫入正表T1;
- 第一步成功后,寫入日志log1;
- 寫入反表T2;
- 第二步成功后,寫入日志log2;
當(dāng)然,我們還是需要一個(gè)離線的掃描工具,不停地比對(duì)日志log1和日志log2,如果發(fā)現(xiàn)數(shù)據(jù)不一致,就進(jìn)行補(bǔ)償修復(fù)。
優(yōu)點(diǎn):
- 雖比方法一復(fù)雜,但仍然是比較簡(jiǎn)單的;
- 數(shù)據(jù)掃描效率高,只掃描增量數(shù)據(jù);
缺點(diǎn):
- 線上服務(wù)略有修改(代價(jià)不高,多寫了2條日志);
- 雖然比方法一更實(shí)時(shí),但時(shí)效性還是不高,不一致窗口取決于掃描的周期;
有沒有實(shí)時(shí)檢測(cè)一致性并進(jìn)行修復(fù)的方法呢?
(3) 方法三:實(shí)時(shí)線上“消息對(duì)”檢測(cè)

這次不是寫日志了,而是向消息總線發(fā)送消息,如上圖1-4流程所示:
- 寫入正表T1;
- 第一步成功后,發(fā)送消息msg1;
- 寫入反表T2;
- 第二步成功后,發(fā)送消息msg2;
這次不是需要一個(gè)周期掃描的離線工具了,而是一個(gè)實(shí)時(shí)訂閱消息的服務(wù)不停的收消息。
假設(shè)正常情況下,msg1和msg2的接收時(shí)間應(yīng)該在3s以內(nèi),如果檢測(cè)服務(wù)在收到msg1后沒有收到msg2,就嘗試檢測(cè)數(shù)據(jù)的一致性,不一致時(shí)進(jìn)行補(bǔ)償修復(fù)。
優(yōu)點(diǎn):效率高,實(shí)時(shí)性高。
缺點(diǎn):
- 方案比較復(fù)雜,上線引入了消息總線這個(gè)組件;
- 線下多了一個(gè)訂閱總線的檢測(cè)服務(wù);
however,技術(shù)方案本身就是一個(gè)投入產(chǎn)出比的折衷,可以根據(jù)業(yè)務(wù)對(duì)一致性的需求程度決定使用哪一種方法。
總結(jié)
(1) 關(guān)系鏈業(yè)務(wù)是一個(gè)典型的多對(duì)多關(guān)系,又分為強(qiáng)好友與弱好友;
(2) 數(shù)據(jù)冗余是一個(gè)常見的多對(duì)多業(yè)務(wù)數(shù)據(jù)水平切分實(shí)踐;
(3) 冗余數(shù)據(jù)的常見方案有三種:
- 服務(wù)同步冗余;
- 服務(wù)異步冗余;
- 線下異步冗余;
(4) 數(shù)據(jù)冗余會(huì)帶來一致性問題,高吞吐互聯(lián)網(wǎng)業(yè)務(wù),要想完全保證事務(wù)一致性很難,常見的實(shí)踐是最終一致性。
(5) 最終一致性的常見實(shí)踐是,盡快找到不一致,并修復(fù)數(shù)據(jù),常見方案有三種:
- 線下全量掃描法;
- 線下增量掃描法;
- 線上實(shí)時(shí)檢測(cè)法;
百億關(guān)系鏈架構(gòu)設(shè)計(jì)要點(diǎn),你學(xué)廢了嗎?
知其然,知其所以然。
思路比結(jié)論更重要。


























