CTO 問我,為什么不按照教材上的 3NF 來設(shè)計(jì)數(shù)據(jù)庫?
有水友問我說,學(xué)校學(xué)數(shù)據(jù)庫,都講究“范式設(shè)計(jì)”,為什么很多互聯(lián)網(wǎng)公司數(shù)據(jù)庫都搞“反范式”設(shè)計(jì)呢?
什么是數(shù)據(jù)庫范式設(shè)計(jì)?
- 1NF:字段原子性;
- 2NF:所有字段必須依賴主鍵;
- 3NF:所有字段必須直接依賴主鍵;
為什么要搞數(shù)據(jù)庫范式設(shè)計(jì)?
減少數(shù)據(jù)冗余,減少數(shù)據(jù)依賴,確保數(shù)據(jù)一致性與完整性。
為什么很多互聯(lián)網(wǎng)公司數(shù)據(jù)庫都搞“反范式”設(shè)計(jì)?
任何脫離業(yè)務(wù)的數(shù)據(jù)庫設(shè)計(jì)都是耍流氓。
很多互聯(lián)網(wǎng)業(yè)務(wù)場(chǎng)景,數(shù)據(jù)的一致性與完整性并不是主要矛盾,大數(shù)據(jù)量與高并發(fā)量才是瓶頸,針對(duì)這兩個(gè)要素的設(shè)計(jì)才是核心,常見的典型“反范式”設(shè)計(jì)有:
- 字段拆分,提升性能;
- 放棄外鍵,減少JOIN,提升性能;
畫外音:數(shù)據(jù)庫范式設(shè)計(jì),大量依賴JOIN。
- 最終一致性,提升性能;
- 放棄事務(wù),犧牲一致性與完整性,提升性能;
- 異步更新,犧牲一致性,提升性能;
- 數(shù)據(jù)冗余,犧牲一致性,提升性能;
- ...
特別是數(shù)據(jù)冗余,在大數(shù)據(jù)量與高并發(fā)量的數(shù)據(jù)庫設(shè)計(jì)中使用極其廣泛,今天重點(diǎn)講講冗余表的設(shè)計(jì)。
為什么會(huì)需要冗余表?
數(shù)據(jù)量很大的時(shí)候,數(shù)據(jù)庫往往要進(jìn)行水平切分,水平切分會(huì)有一個(gè)patition key,通過patition key的查詢能夠直接定位到庫,但是非patition key上的查詢可能就需要掃描多個(gè)庫了。
例如訂單表,業(yè)務(wù)上對(duì)用戶和商家都有訂單查詢需求:
- Order(oid, info_detail)
- T(buyer_id, seller_id, oid)
如果用buyer_id來分庫,seller_id的查詢就需要掃描多庫;如果用seller_id來分庫,buyer_id的查詢就需要掃描多庫。
這類業(yè)務(wù)“高吞吐量低延時(shí)”的查詢需求,往往是通過“數(shù)據(jù)冗余”的方式來滿足的,就是所謂的“冗余表”:
- T1(buyer_id, seller_id, oid)
- T2(seller_id, buyer_id, oid)
同一個(gè)數(shù)據(jù),冗余兩份,一份以buyer_id來分庫,滿足買家的查詢需求;一份以seller_id來分庫,滿足賣家的查詢需求。
冗余表如何實(shí)現(xiàn)?
常見的方案有三種。
方案一:服務(wù)同步寫法。
顧名思義,由服務(wù)層同步寫冗余數(shù)據(jù):
- 業(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ì)較高;
缺點(diǎn):
- 要插入兩次,請(qǐng)求的處理時(shí)間增加;
- 數(shù)據(jù)仍可能不一致,寫入T1完成后服務(wù)重啟,則數(shù)據(jù)不會(huì)寫入T2;
如果系統(tǒng)對(duì)處理時(shí)間比較敏感,引出常用的第二種方案。
方案二:服務(wù)異步寫法。
數(shù)據(jù)的雙寫并不再由服務(wù)來完成,服務(wù)層異步發(fā)出一個(gè)消息,通過MQ發(fā)送給一個(gè)專門的數(shù)據(jù)復(fù)制服務(wù)來寫入冗余數(shù)據(jù),如上圖1-6流程:
1....
2.服務(wù)先插入T1數(shù)據(jù);
3.服務(wù)向MQ發(fā)送一個(gè)異步消息;
...
6. 異步插入T2數(shù)據(jù);
優(yōu)點(diǎn):服務(wù)只插入1次,請(qǐng)求處理時(shí)間短。
缺點(diǎn):
- 系統(tǒng)的復(fù)雜性增加了,多引入了兩個(gè)新組件,MQ與異步服務(wù);
- 業(yè)務(wù)線返回成功時(shí),數(shù)據(jù)還不一定異步插入到T2中,因此數(shù)據(jù)有一個(gè)不一致時(shí)間窗口,這個(gè)窗口很短,最終是一致的;
- 在消息總線丟失消息時(shí),冗余表數(shù)據(jù)仍可能不一致;
如果想解除“數(shù)據(jù)冗余”對(duì)系統(tǒng)的耦合,引出常用的第三種方案。
方案三:線下異步寫法。
數(shù)據(jù)的雙寫不再由服務(wù)層來完成,而是由線下的一個(gè)服務(wù)或者任務(wù)來完成,最常見的,就是利用DTS這類異步數(shù)據(jù)同步服務(wù),完成數(shù)據(jù)的冗余。
優(yōu)點(diǎn):
- 數(shù)據(jù)雙寫與業(yè)務(wù)完全解耦;
- 服務(wù)只插入1次,請(qǐng)求處理時(shí)間短;
缺點(diǎn):
- 業(yè)務(wù)線返回成功時(shí),數(shù)據(jù)還不一定異步插入到T2中,因此數(shù)據(jù)有一個(gè)不一致時(shí)間窗口,這個(gè)窗口很短,最終是一致的;
- 數(shù)據(jù)的一致性依賴于線下服務(wù)或者任務(wù)的可靠性;
可以看到,由于冗余表的插入不具備事務(wù)性,不管哪一種方案,都有可能出現(xiàn)T1插入成功,T2插入失敗的情況,從而喪失“最終一致性”特性,那怎么辦呢?
如何保證冗余表數(shù)據(jù)的最終一致性?
常見的有四種方案。
方案一:線下定期掃描正反冗余表全部數(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à)?。?/li>
- 線上服務(wù)無需修改,修復(fù)工具與線上服務(wù)解耦;
缺點(diǎn):
- 掃描效率低,會(huì)掃描大量的“已經(jīng)能夠保證一致”的數(shù)據(jù);
- 由于掃描的數(shù)據(jù)量大,掃描一輪的時(shí)間比較長(zhǎng),即數(shù)據(jù)如果不一致,不一致的時(shí)間窗口比較長(zhǎng);
優(yōu)化思路:定期掃描全量數(shù)據(jù)太低效,有沒有一種只掃描“可能存在不一致可能性”的增量數(shù)據(jù),以提高效率的優(yōu)化方法呢?
方法二:線下掃描增量數(shù)據(jù)。
每次只掃描增量的日志數(shù)據(jù),就能夠極大提高效率,縮短數(shù)據(jù)不一致的時(shí)間窗口,如上圖1-4流程所示:
1. 寫入正表T1;
2. 寫入日志log1;
3. 寫入反表T2;
4. 寫入日志log2;
然后通過一個(gè)離線的掃描工具,不停的比對(duì)日志log1和日志log2,如果發(fā)現(xiàn)數(shù)據(jù)不一致,就進(jìn)行補(bǔ)償修復(fù)。
優(yōu)點(diǎn):
- 比較簡(jiǎn)單,開發(fā)代價(jià)小;
- 數(shù)據(jù)掃描效率高,只掃描增量數(shù)據(jù);
缺點(diǎn):
- 線上服務(wù)略有修改,但代價(jià)不高,多寫了2條日志;
- 雖然比方法一更實(shí)時(shí),但時(shí)效性還是不高,不一致窗口取決于掃描的周期;
優(yōu)化思路:有沒有實(shí)時(shí)檢測(cè)一致性并進(jìn)行修復(fù)的方法呢?
方法三:實(shí)時(shí)線上“消息對(duì)”檢測(cè)。
這次不是寫日志了,而是向消息總線發(fā)送消息,如上圖1-4流程所示:
1. 寫入正表T1;
2. 發(fā)送消息msg1;
3. 寫入反表T2;
4. 發(fā)送消息msg2;
正常情況下,msg1和msg2的接收時(shí)間應(yīng)該在N秒以內(nèi),如不然,則進(jìn)行補(bǔ)償修復(fù)。
優(yōu)點(diǎn):效率高,實(shí)時(shí)性高。
缺點(diǎn):相對(duì)復(fù)雜。
方案四:人工修復(fù)法。
項(xiàng)目上線時(shí)間太緊,沒時(shí)間搞一致性設(shè)計(jì)哇!
雖然插入不是原子的,奈何出現(xiàn)的概率低啊!
即使出現(xiàn)了,用戶也不一定能發(fā)現(xiàn)呀!
用戶發(fā)現(xiàn)了,找客服也不是找我呀!
找我,一個(gè)DBA工單就修復(fù)啦!
于是,大量的公司,不考慮正表和反表的數(shù)據(jù)一致性,事后發(fā)現(xiàn),事后人工修復(fù)。
總結(jié)
(1) 數(shù)據(jù)庫范式設(shè)計(jì),是為減少數(shù)據(jù)冗余,減少數(shù)據(jù)依賴,確保數(shù)據(jù)一致性與完整性而提出的;
(2) 很多互聯(lián)網(wǎng)業(yè)務(wù)場(chǎng)景,大數(shù)據(jù)量與高并發(fā)量才是瓶頸,故經(jīng)常采用“數(shù)據(jù)冗余”這類反范式設(shè)計(jì);
(3) 數(shù)據(jù)冗余的常見方式有三種:
- 服務(wù)同步寫
- 服務(wù)異步寫
- 線下異步寫
(4) 修復(fù)冗余數(shù)據(jù)一致性的常見方案有四種:
- 線下定期掃全量
- 線下定期掃增量
- 線上實(shí)時(shí)“消息對(duì)”檢測(cè)
- 躺平,人工修復(fù)
知其然,知其所以然。
思路比結(jié)論更重要。