<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么數(shù)據(jù)庫不應(yīng)該使用外鍵

          共 5657字,需瀏覽 12分鐘

           ·

          2020-08-09 05:41

          為什么這么設(shè)計(jì)(Why’s THE Design)是一系列關(guān)于計(jì)算機(jī)領(lǐng)域中程序設(shè)計(jì)決策的文章,我們?cè)谶@個(gè)系列的每一篇文章中都會(huì)提出一個(gè)具體的問題并從不同的角度討論這種設(shè)計(jì)的優(yōu)缺點(diǎn)、對(duì)具體實(shí)現(xiàn)造成的影響。如果你有想要了解的問題,可以在文章下面留言。

          當(dāng)我們想要持久化地存儲(chǔ)數(shù)據(jù)時(shí),使用關(guān)系型數(shù)據(jù)庫往往都是最穩(wěn)妥的選擇,這不僅因?yàn)榻裉斓年P(guān)系型數(shù)據(jù)庫種類非常豐富并且穩(wěn)定,還因?yàn)椴煌鐓^(qū)對(duì)關(guān)系型數(shù)據(jù)庫的支持都非常完備。我們?cè)谇懊娴奈恼轮性?jīng)分析過 為什么 MySQL 的自增主鍵不單調(diào)也不連續(xù),這篇文章我們來分析關(guān)系型數(shù)據(jù)庫中另一個(gè)重要的概念 — 外鍵(Foreign Key)。

          在關(guān)系型數(shù)據(jù)庫中,外鍵也被稱為關(guān)系鍵,它是關(guān)系型數(shù)據(jù)庫中提供關(guān)系表之間連接的多個(gè)列[^1],這一組數(shù)據(jù)列是當(dāng)前關(guān)系表中的外鍵,也必須是另一個(gè)關(guān)系表中的候選鍵(Candidate Key),我們可以通過候選鍵在當(dāng)前表中找到唯一的元素[^2]。在通常情況下,我們都會(huì)使用關(guān)系表中的主鍵作為其他表中的外鍵,這樣才可以滿足關(guān)系型數(shù)據(jù)庫對(duì)外鍵的約束。

          圖 1 - 關(guān)系型數(shù)據(jù)庫與外鍵

          外鍵不僅僅是數(shù)據(jù)庫表中的一個(gè)整數(shù),它還提供了額外的一致性保證。因?yàn)閿?shù)據(jù)庫往往是整個(gè)系統(tǒng)的真理之源(Source of Truth),所以保證數(shù)據(jù)的一致性和正確性非常重要,關(guān)系型數(shù)據(jù)庫雖然提供了外鍵、觸發(fā)器等特性保證一致性,但是在今天的生產(chǎn)環(huán)境中卻很少被使用。

          引用完整性(Referential Integrity)是數(shù)據(jù)的屬性,如果數(shù)據(jù)擁有該屬性,那么數(shù)據(jù)中所有的引用都是合法的,在關(guān)系型數(shù)據(jù)庫的上下文中,這就意味著關(guān)系型數(shù)據(jù)庫中引用另一個(gè)表中的值必須存在[^3]。

          ALTER TABLE postsADD CONSTRAINT FOREIGN KEY (author_id)REFERENCES authors(id);

          上述 SQL 語句可以向關(guān)系表中增加外鍵約束,該 SQL 語句的執(zhí)行前提是 posts 表中存在 author_id 字段。從 SQL 語句中的 CONSTRAINT 關(guān)鍵字我們也能推測(cè)出外鍵不是一種數(shù)據(jù)類型,它是不同關(guān)系表之間的約束。

          圖 2 - 無狀態(tài)服務(wù)與數(shù)據(jù)庫

          不使用外鍵的原因其實(shí)很簡(jiǎn)單,MySQL、PostgreSQL 等關(guān)系型數(shù)據(jù)庫很難水平擴(kuò)容,但是無狀態(tài)的服務(wù)往往都可以很容易地?cái)U(kuò)容。由于外鍵等特性需要數(shù)據(jù)庫執(zhí)行額外的工作,而這些操作會(huì)占用數(shù)據(jù)庫的計(jì)算資源,所以我們可以將大部分的需求都遷移到無狀態(tài)的服務(wù)中完成以降低數(shù)據(jù)庫的工作負(fù)載。

          根據(jù)更新和刪除時(shí)的行為不同,我們可以將外鍵分成 RESTRICT、CASCADESET NULL 等幾種[^4],當(dāng)我們?yōu)殛P(guān)系表中的字段增加外鍵約束時(shí),需要指定外鍵的類型,最常見的也就是 RESTRICTCASCADE 兩種,其中 RESTRICT 為外鍵的默認(rèn)類型,不同類型的外鍵會(huì)帶來不同的額外開銷,而這些額外開銷就是我們不使用外鍵的理由:

          • 使用 RESTRICT 會(huì)在更新或者刪除記錄時(shí)對(duì)外鍵對(duì)應(yīng)的記錄是否存在進(jìn)行一致性檢查;
          • 使用 CASCADE 會(huì)在更新或者刪除記錄時(shí)觸發(fā)級(jí)聯(lián)更新或者刪除操作;

          注意:MySQL 中的 NO ACTIONRESTRICT 具有相同的語義[^5]。

          接下來我們會(huì)詳細(xì)介紹關(guān)系型數(shù)據(jù)庫如何處理上述兩種不同類型的外鍵,而我們應(yīng)該如何在應(yīng)用中模擬這些功能。

          一致性檢查

          當(dāng)我們使用默認(rèn)的外鍵類型 RESTRICT 時(shí),在創(chuàng)建、修改或者刪除記錄時(shí)都會(huì)檢查引用的合法性。想要在 MySQL 等數(shù)據(jù)庫中觸發(fā)外鍵的一致性檢查其實(shí)非常容易,假設(shè)我們的數(shù)據(jù)庫中包含 posts(id, author_id, content)authors(id, name) 兩張表,在執(zhí)行如下所示的操作時(shí)都會(huì)觸發(fā)數(shù)據(jù)庫對(duì)外鍵的檢查:

          • posts 表中插入數(shù)據(jù)時(shí),檢查 author_id 是否在 authors 表中存在;
          • 修改 posts 表中的數(shù)據(jù)時(shí),檢查 author_id 是否在 authors 表中存在;
          • 刪除 authors 表中的數(shù)據(jù)時(shí),檢查 posts 中是否存在引用當(dāng)前記錄的外鍵;

          作為專門用于管理數(shù)據(jù)的系統(tǒng),數(shù)據(jù)庫與應(yīng)用服務(wù)相比能夠更好地保證完整性,而上述的這些操作都是引入外鍵帶來的額外工作,不過這也是數(shù)據(jù)庫保證數(shù)據(jù)完整性的必要代價(jià)。上述的這些分析都是理論上的定性分析,我們其實(shí)可以簡(jiǎn)單的定量分析一下引入外鍵對(duì)性能的影響。

          在這里我們?cè)跀?shù)據(jù)庫中同時(shí)創(chuàng)建 authors、postsforeign_key_posts 三種表,如下所示,其中 postsforeign_key_posts 兩個(gè)表中的列完全相同,只是 foreign_key_posts 表為 author_id 字段增加了 RESTRICT 類型的外鍵約束:

          圖 3 - 外鍵性能測(cè)試關(guān)系圖

          我們先在 authors 表中插入一條記錄,隨后分別在 postsforeign_key_posts 中插入多條新數(shù)據(jù)列引用該條記錄,前者不會(huì)檢查外鍵的合法性,而后者會(huì)做額外的檢查。你可以在 這里 找到作者用來測(cè)試外鍵額外開銷的 Go 語言代碼[^6],經(jīng)過多次基準(zhǔn)測(cè)試,我們可以得到如下所示的結(jié)果:

          BenchmarkBaseline-8     	    3770	    309503 ns/opBenchmarkForeignKey-8   	    3331	    317162 ns/op
          BenchmarkBaseline-8 3192 315506 ns/opBenchmarkForeignKey-8 3381 315577 ns/op
          BenchmarkBaseline-8 3298 312761 ns/opBenchmarkForeignKey-8 3829 345342 ns/op
          BenchmarkBaseline-8 3753 291642 ns/opBenchmarkForeignKey-8 3948 325239 ns/op

          作者執(zhí)行了 4 次外鍵的基準(zhǔn)測(cè)試,雖然 4 次測(cè)試的結(jié)果不是特別穩(wěn)定,但是使用外鍵的用例在每次測(cè)試中都明顯弱于不使用外鍵的用例,外鍵帶來的額外開銷分別為 ~2.47%、~0.02%、~10.41% 和 ~11.52%。這里的基準(zhǔn)測(cè)試只是一個(gè)比較簡(jiǎn)單的定量分析,但是我們也可以從結(jié)果中看到大概的趨勢(shì) — 外鍵的完整性檢查確實(shí)會(huì)帶來額外的性能開銷,而這些開銷在高并發(fā)的服務(wù)中需要慎重考慮。

          想要在應(yīng)用程序中模擬數(shù)據(jù)庫外鍵的功能其實(shí)比較容易,我們只需要遵循以下的幾個(gè)準(zhǔn)則:

          • 向表中插入數(shù)據(jù)或者修改表中的數(shù)據(jù)時(shí),都應(yīng)該執(zhí)行額外的 SELECT 語句確保它引用的數(shù)據(jù)在數(shù)據(jù)庫中存在;
          • 在刪除數(shù)據(jù)之前需要執(zhí)行額外的 SELECT 語句檢查是否存在當(dāng)前記錄的引用;

          需要注意的是為了保證一致性,我們需要在事務(wù)中執(zhí)行上述的查詢和修改語句,這樣才能完整模擬外鍵的功能;當(dāng)我們向 posts 表中插入或者修改數(shù)據(jù)時(shí),需要的處理相對(duì)比較簡(jiǎn)單,我們只需要執(zhí)行有限的 SELECT 語句并按照如下所示的模式執(zhí)行對(duì)應(yīng)的操作就可以了:

          BEGINSELECT * FROM authors WHERE id =  FOR UPDATE;-- INSERT INTO posts ... / UPDATE posts ...END

          但是如果我們要?jiǎng)h除 authors 表中的數(shù)據(jù),就需要查詢所有引用 authors 數(shù)據(jù)的表;如果有 10 個(gè)表都有指向 authors 表的外鍵,我們就需要在 10 個(gè)表中查詢是否存在對(duì)應(yīng)的記錄,這個(gè)過程相對(duì)比較麻煩,不過也是為了實(shí)現(xiàn)完整性的必要代價(jià),不過這種模擬外鍵方法其實(shí)遠(yuǎn)比使用外鍵更消耗資源,它不僅需要查詢關(guān)聯(lián)數(shù)據(jù),還要通過網(wǎng)絡(luò)發(fā)送更多的數(shù)據(jù)包。

          級(jí)聯(lián)操作

          當(dāng)我們?cè)陉P(guān)系型數(shù)據(jù)庫中創(chuàng)建外鍵約束時(shí),如果使用如下所示的 SQL 語句指定更新或者刪除記錄時(shí)使用 CASCADE 行為,那么在客戶端更新或者刪除數(shù)據(jù)時(shí)就會(huì)觸發(fā)級(jí)聯(lián)操作:

          ALTER TABLE postsADD CONSTRAINT FOREIGN KEY (author_id)REFERENCES authors(id)ON UPDATE CASCADEON DELETE CASCADE;
          • 當(dāng)客戶端更新 authors 表中記錄的主鍵時(shí),數(shù)據(jù)庫會(huì)同時(shí)更新 posts 表中所有引用該記錄的外鍵;
          • 當(dāng)客戶端刪除 authors 表中的記錄時(shí),數(shù)據(jù)庫會(huì)刪除所有與 authors 表關(guān)聯(lián)的記錄;

          不過無論是執(zhí)行更新還是刪除操作,數(shù)據(jù)庫都可以保證各個(gè)關(guān)系表之間引用的一致性和合法性不會(huì)出現(xiàn)引用到不存在記錄的情況,與 RESTRICT 行為一樣,所有外鍵的更新和刪除行為都可以通過執(zhí)行額外的檢查和操作保證數(shù)據(jù)的一致。

          圖 4 - 復(fù)雜的級(jí)聯(lián)操作

          雖然級(jí)聯(lián)刪除的出發(fā)點(diǎn)也是保證數(shù)據(jù)的完整性,但是在設(shè)計(jì)關(guān)系表之間的不同關(guān)系時(shí),我們也需要注意級(jí)聯(lián)刪除引起的數(shù)據(jù)大規(guī)模刪除的問題。如上圖所示,當(dāng)客戶端想要在數(shù)據(jù)庫中刪除 authos 表中的數(shù)據(jù)時(shí),如果我們同時(shí)在 authorsposts 中指定了級(jí)聯(lián)刪除的行為,那么數(shù)據(jù)庫會(huì)同時(shí)刪除所有關(guān)聯(lián)的 posts 記錄以及與 posts 表關(guān)聯(lián)的 comments 數(shù)據(jù)。

          這種涉及多級(jí)的級(jí)聯(lián)刪除行為在數(shù)據(jù)量較小的數(shù)據(jù)庫中不會(huì)導(dǎo)致問題,但是在數(shù)據(jù)量較大的數(shù)據(jù)庫中刪除關(guān)鍵數(shù)據(jù)可能會(huì)引起雪崩,一條記錄的刪除可能會(huì)被放大到幾十倍甚至上百倍,這些對(duì)磁盤的隨機(jī)讀寫會(huì)帶來巨大的開銷,是我們想要盡可能避免的情況。如果我們能夠較好地設(shè)計(jì)各個(gè)表之間的關(guān)系并且慎用 CASCADE 行為,這對(duì)于保證數(shù)據(jù)庫中數(shù)據(jù)的合法性有著很重要的意義,使用該特性可以避免數(shù)據(jù)庫中出現(xiàn)過期的、不合法的數(shù)據(jù),但是在使用時(shí)也要合理預(yù)估可能造成的最壞情況。

          手動(dòng)實(shí)現(xiàn)數(shù)據(jù)庫的級(jí)聯(lián)刪除操作是可行的,如果我們?cè)谝粋€(gè)事務(wù)中按照順序刪除所有的數(shù)據(jù),確實(shí)可以保證數(shù)據(jù)的一致性,但是這與外鍵的級(jí)聯(lián)刪除功能沒有太大的區(qū)別,反而會(huì)有更差的表現(xiàn)。如果我們能夠接受在一個(gè)時(shí)間窗口內(nèi)的數(shù)據(jù)不一致,就可以將一個(gè)大號(hào)的刪除任務(wù)拆成多個(gè)子任務(wù)分批執(zhí)行,降低對(duì)數(shù)據(jù)庫影響的峰值。

          DELETE FROM posts WHERE author_id = 1 LIMIT 100;DELETE FROM posts WHERE author_id = 1 LIMIT 100;...DELETE FROM authors WHERE id = 1;

          與數(shù)據(jù)庫外鍵的 CASCADE 相比,這種方式會(huì)帶來更大的額外開銷,只是我們能降低對(duì)數(shù)據(jù)庫性能的瞬時(shí)影響。

          總結(jié)

          外鍵提供的幾種在更新和刪除時(shí)的不同行為都可以幫助我們保證數(shù)據(jù)庫中數(shù)據(jù)的一致性和引用合法性,但是外鍵的使用也需要數(shù)據(jù)庫承擔(dān)額外的開銷,在大多數(shù)服務(wù)都可以水平擴(kuò)容的今天,高并發(fā)場(chǎng)景中使用外鍵確實(shí)會(huì)影響服務(wù)的吞吐量上限。在數(shù)據(jù)庫之外手動(dòng)實(shí)現(xiàn)外鍵的功能是可能的,但是卻會(huì)帶來很多維護(hù)上的成本或者需要我們?cè)跀?shù)據(jù)一致性上做出一些妥協(xié)。我們可以從可用性、一致性幾個(gè)方面分析使用外鍵、模擬外鍵以及不使用外鍵的差異:

          • 不使用外鍵犧牲了數(shù)據(jù)庫中數(shù)據(jù)的一致性,但是卻能夠減少數(shù)據(jù)庫的負(fù)載;
          • 模擬外鍵將一部分工作移到了數(shù)據(jù)庫之外,我們可能需要放棄一部分一致性以獲得更高的可用性,但是為了這部分可用性,我們會(huì)付出更多的研發(fā)與維護(hù)成本,也增加了與數(shù)據(jù)庫之間的網(wǎng)絡(luò)通信次數(shù);
          • 使用外鍵保證了數(shù)據(jù)庫中數(shù)據(jù)的一致性,也將全部的計(jì)算任務(wù)全部交給了數(shù)據(jù)庫;

          在大多數(shù)不需要高并發(fā)或者對(duì)一致性有較強(qiáng)要求的系統(tǒng)中,我們可以直接使用數(shù)據(jù)庫提供的外鍵幫助我們對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),但是在對(duì)一致性要求不高的、復(fù)雜的場(chǎng)景或者大規(guī)模的團(tuán)隊(duì)中,不使用外鍵也確實(shí)可以為數(shù)據(jù)庫減負(fù),而大團(tuán)隊(duì)也有更多的時(shí)間和精力去設(shè)計(jì)其他的方案,例如:分布式的關(guān)系型數(shù)據(jù)庫。

          當(dāng)我們考慮應(yīng)不應(yīng)該在數(shù)據(jù)庫中使用外鍵時(shí),需要關(guān)注的核心我們的數(shù)據(jù)庫承擔(dān)這部分計(jì)算任務(wù)后會(huì)不會(huì)影響系統(tǒng)的可用性,在使用時(shí)也不應(yīng)該一刀切的決定用或者不用外鍵,應(yīng)該根據(jù)具體的場(chǎng)景做決策,我們?cè)谶@里介紹了兩個(gè)使用外鍵時(shí)可能遇到的問題:

          • RESTRICT 外鍵會(huì)在更新和刪除關(guān)系表中的數(shù)據(jù)時(shí)對(duì)外鍵約束的合法性進(jìn)行檢查,保證外鍵不會(huì)引用到不存在的記錄;
          • CASCADE 外鍵會(huì)在更新和刪除關(guān)系表中的數(shù)據(jù)時(shí)觸發(fā)對(duì)關(guān)聯(lián)記錄的更新和刪除,在數(shù)據(jù)量較大的數(shù)據(jù)庫中可能會(huì)有數(shù)量級(jí)的放大效果;

          我們?cè)诤芏鄷r(shí)候其實(shí)并不能選擇是否使用外鍵,大多數(shù)公司的 DBA 都會(huì)對(duì)數(shù)據(jù)庫系統(tǒng)的使用有比較明確的規(guī)定,但是我們要清楚做出使用外鍵和不使用外鍵這一抉擇的原因。到最后,我們還是來看一些比較開放的相關(guān)問題,有興趣的讀者可以仔細(xì)思考一下下面的問題:

          • 數(shù)據(jù)庫中還有哪些特性是我們?cè)谏a(chǎn)環(huán)境中不會(huì)使用的?為什么?
          • 分布式的關(guān)系型數(shù)據(jù)庫與 MySQL 等傳統(tǒng)數(shù)據(jù)庫有哪些區(qū)別?

          如果對(duì)文章中的內(nèi)容有疑問或者想要了解更多軟件工程上一些設(shè)計(jì)決策背后的原因,可以在博客下面留言,作者會(huì)及時(shí)回復(fù)本文相關(guān)的疑問并選擇其中合適的主題作為后續(xù)的內(nèi)容。



          推薦閱讀



          學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進(jìn)群」即可


          站長(zhǎng) polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識(shí)

          Go愛好者值得關(guān)注


          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人无码精免费视频 | 黄片网站进入 | 国内色图| 黄片视频一区二区三区 | 国产A片电影 |