一文熟知存儲(chǔ) – 從磁盤到文件,到數(shù)據(jù)庫,到分布式環(huán)境集中式存儲(chǔ),再到分布式數(shù)據(jù)庫
相信我,認(rèn)真讀完之后,你是業(yè)余數(shù)據(jù)庫選手里靠前的,至少不至于民科。
0.前言
正如各種軟文推送的,我們現(xiàn)在是信息的時(shí)代,而信息從人類社會(huì)誕生就存在了,之所以把現(xiàn)在這個(gè)時(shí)代稱為信息時(shí)代,完全是因?yàn)橛?jì)算機(jī)把信息的傳遞、存儲(chǔ)、處理
效率提升到了一個(gè)前所未有的量級(jí),這也是互聯(lián)網(wǎng)發(fā)展突飛猛進(jìn)的根本原因,我們現(xiàn)在的手機(jī)、各種APP 都是基于這個(gè)關(guān)鍵原因誕生的。
如果有興趣可以想想,如果計(jì)算機(jī)所帶來的“信息傳遞、存儲(chǔ)、處理”效率,每個(gè)方面再進(jìn)一步提升,會(huì)有哪些行業(yè)會(huì)受到影響,會(huì)催生什么形態(tài)的產(chǎn)品,這可能就是下一個(gè)大的風(fēng)口了,接下來從技術(shù)的角度看一下,信息:存儲(chǔ)、傳遞、處理的發(fā)展。

信息傳遞,直白點(diǎn)就是計(jì)算機(jī)網(wǎng)絡(luò),這個(gè)可以參照上一篇文章《一文讀懂網(wǎng)絡(luò),文章巨長(zhǎng),但很詳細(xì)》
信息存儲(chǔ),信息的持久化動(dòng)作,說直白點(diǎn)就是“不丟”,之前是書本,現(xiàn)在是磁盤。這個(gè)就是本文
要說的重點(diǎn),信息時(shí)代信息是怎么存儲(chǔ)的,有怎樣的演進(jìn)過程。
信息處理,數(shù)據(jù)計(jì)算的效率,主要依賴于CPU的計(jì)算效率、CPU的充分使用、數(shù)據(jù)組織結(jié)構(gòu)和處理算法的效率,這個(gè)之后來說。其中網(wǎng)絡(luò)/存儲(chǔ)都會(huì)涉及一定的信息處理(就統(tǒng)一歸屬到信息傳遞、信息存儲(chǔ)域了),這里的信息處理效率單純指數(shù)據(jù)的計(jì)算/分析效率。
本文會(huì)按照這個(gè)順序進(jìn)行敘述:

整體內(nèi)容概要:

1.信息的持久化動(dòng)作
信息持久化動(dòng)作的效率主要是存儲(chǔ)介質(zhì)的效率,外加信息組織形式的效率。存儲(chǔ)介質(zhì)的效率決定了整體效率的上限和下限,而信息組織形式的效率決定了在上下限之間的發(fā)揮空間。
1.1 存儲(chǔ)介質(zhì)
信息的持久化形式是信息記錄的關(guān)鍵效率,最開始腦子記,再后來變成了篆刻和打繩結(jié),到后來書寫書本記錄,到現(xiàn)在計(jì)算機(jī)系統(tǒng)中的磁盤記錄,要想存儲(chǔ)的量大、持久化動(dòng)作的效率高,跟存儲(chǔ)介質(zhì)是密切相關(guān)的。

1.2 信息的管理形式
對(duì)于數(shù)據(jù)的組織形式,最初信息極少,基本不需要組織。出現(xiàn)書本之后,信息密度開始上升,出了索引、書籍管理等形式。現(xiàn)在磁盤存儲(chǔ)是由01表示信息,然后在此之上解碼出對(duì)應(yīng)的人類可讀的文字信息,使用一些數(shù)據(jù)結(jié)構(gòu)來組織信息,單機(jī)是有空間極限的,會(huì)采用分布式的形式來橫向的擴(kuò)展存儲(chǔ)。

2.磁盤讀寫 – 文件讀寫 – 數(shù)據(jù)庫系統(tǒng)操作
對(duì)于信息的存儲(chǔ)站在不同的實(shí)現(xiàn)角度,面向的對(duì)象是不同的:

如果站在硬件開發(fā)的角度,面向的磁盤介質(zhì)的讀寫;如果做操作系統(tǒng)的落地,面向的是磁盤應(yīng)用的讀寫;如果是操作系統(tǒng)使用的角度,是文件的讀寫;如果站在應(yīng)用開發(fā)的角度,是數(shù)據(jù)庫系統(tǒng)的操作。
2.1 磁盤讀寫

2.1.1 磁盤
磁盤是指利用磁記錄數(shù)據(jù)的存儲(chǔ)器,存儲(chǔ)介質(zhì)是可以存儲(chǔ)正負(fù)極的磁性材料,用正負(fù)極來代表0/1。目前市場(chǎng)上的硬盤分為兩類,機(jī)械磁盤、固態(tài)磁盤。固態(tài)磁盤要比機(jī)械磁盤快的多,但是為什么機(jī)械磁盤一直沒被淘汰?核心原因是快的程度遠(yuǎn)沒有超過價(jià)格差異,而且機(jī)械磁盤的速度對(duì)于當(dāng)前的數(shù)據(jù)讀寫的部分場(chǎng)景仍然夠用。如果專門做存儲(chǔ)系統(tǒng),不差錢,那就直接固態(tài)磁盤。
兩者最主要的差異是固態(tài)磁盤完全是基于電信號(hào),而機(jī)械硬盤是機(jī)械結(jié)構(gòu) + 電信號(hào)的結(jié)合。
2.1.1.1 機(jī)械磁盤
機(jī)械詞盤是一個(gè)個(gè)原子存儲(chǔ)單位構(gòu)成了扇面,多個(gè)扇面構(gòu)成了磁道,然后磁道構(gòu)成盤面,然后一個(gè)個(gè)盤面構(gòu)成了一個(gè)磁盤,把磁盤橫切就構(gòu)成了柱面。然后每個(gè)盤面都有一個(gè)磁頭,用磁頭尋找磁道,然后不同盤面并發(fā)找,先定位磁道,然后找到對(duì)應(yīng)的扇區(qū),就完成了對(duì)應(yīng)信息的讀取。(看不到實(shí)物的可以回想一下早年的光盤類比一下)
機(jī)械硬盤讀取的時(shí)間、信息傳遞的時(shí)間,完全是電信號(hào),基本可以忽略不計(jì),真正花時(shí)間的是尋道時(shí)間和旋轉(zhuǎn)時(shí)間,其中尋道時(shí)間要大于旋轉(zhuǎn)時(shí)間。

這就是機(jī)械硬盤隨機(jī)讀寫和順序讀寫速度能差那么多,核心原因就是順序讀寫尋址時(shí)間小,想要提高性能,首先減少磁盤讀寫的次數(shù),另外順序讀寫。
2.1.1.2 固態(tài)磁盤
固態(tài)磁盤基于閃存實(shí)現(xiàn),內(nèi)部是塊裝的存儲(chǔ)結(jié)構(gòu),基本存儲(chǔ)單元是浮柵晶體管,通過充放電子,來對(duì)晶體管進(jìn)行寫入和擦除。然后閃存分為NOR型和NAND型。NOR型閃存芯片具有可靠性高、隨機(jī)讀取速度快的優(yōu)勢(shì),但擦除和編程速度較慢,容量小。NAND閃存容量大,按頁進(jìn)行讀寫,容量大,適合進(jìn)行數(shù)據(jù)存儲(chǔ)。

固態(tài)磁盤并不是完美的,耐用性相對(duì)機(jī)械磁盤要差一些,會(huì)存在比如寫入壽命上限、比特反轉(zhuǎn)等問題。并且由于實(shí)現(xiàn)的差異性,固態(tài)磁盤讀寫不均衡比機(jī)械磁盤要高。而且浮柵晶體管這東西受溫度的影響比較大。
2.2 文件讀寫

如果每次都直接編程訪問磁盤很顯然不太合適,所以操作系統(tǒng)幫你包了一層,我們可以直接訪問操作系統(tǒng),然后操作系統(tǒng)幫你訪問磁盤。在linux中為了使用上的方便,抽象出了文件的概念,并把這些可操作的東西都看作了文件(一切皆文件),除磁盤以外,還有網(wǎng)絡(luò)鏈接這些。
2.2.1 文件讀寫原理
在Linux進(jìn)行文件讀寫時(shí),可以直接透過系統(tǒng)調(diào)用、或者透過函數(shù)庫使用系統(tǒng)調(diào)用直達(dá)操作系統(tǒng),然后到達(dá)虛擬文件系統(tǒng)(VFS),然后文件系統(tǒng)去分發(fā)調(diào)用不同的存儲(chǔ)結(jié)構(gòu),比如基于磁盤的XFS、Ext4,還有管理網(wǎng)絡(luò)鏈接的NFS,又或是基于內(nèi)存的/proc、/sys 文件系統(tǒng)。

通過VFS完成對(duì)應(yīng)文件系統(tǒng)的調(diào)用,這里磁盤讀寫可能就會(huì)進(jìn)行XFS的調(diào)用,然后進(jìn)行磁盤的讀取,讀到的數(shù)據(jù)會(huì)存放在內(nèi)核緩沖區(qū),然后IO線程將對(duì)應(yīng)的數(shù)據(jù)從內(nèi)核緩沖區(qū)中讀出放到用戶態(tài)的內(nèi)存中,然后完成對(duì)應(yīng)的更改之后,再?gòu)挠脩魬B(tài)內(nèi)存透過虛擬文件系統(tǒng)寫入內(nèi)核緩沖,內(nèi)核緩沖刷入磁盤。
注意,這里的文件寫入可能會(huì)提前返回,并非系統(tǒng)調(diào)用完成之后就一定寫入成功了。

fsync
?會(huì)寫到磁盤寫入成功返回,但是大部分磁盤為了性能,會(huì)帶有內(nèi)置寫入緩沖,也就是hardware cache,如果進(jìn)入cache但未刷入磁盤,此時(shí)斷電數(shù)據(jù)還是會(huì)丟的。這種情況發(fā)生的概率極小,目前普遍認(rèn)為寫入hardware就近似不會(huì)丟,用更貴的硬件有更好的效果。
fflush
?寫入內(nèi)核緩沖就認(rèn)為成功,如果此時(shí)斷電,丟的數(shù)據(jù)量級(jí)、丟失的概率都比較高。
mmap
?雖然不是一個(gè)寫入動(dòng)作,但會(huì)把用戶態(tài)內(nèi)存和內(nèi)核緩沖建立一個(gè)映射,避免了內(nèi)核/用戶態(tài)切換,可以通過操作用戶態(tài)內(nèi)存中的對(duì)象,操作內(nèi)核緩沖,省的顯式調(diào)用flush。
各語言常用的write函數(shù)
基本都是調(diào)用fflush,而且較多的語言實(shí)現(xiàn)應(yīng)用層可能還會(huì)有一層緩沖,如果斷電的話,丟失的概率和量級(jí)要更高。如果想要文件持久化的穩(wěn),就fsync吧。
2.2.2 讀寫IO
IO部分,同樣的可以分為同步/異步(是不是同一個(gè)線程/進(jìn)程處理IO操作),阻塞非阻塞(當(dāng)前線程處理時(shí),無可讀/寫是否會(huì)被掛起),這部分同《一文熟知網(wǎng)絡(luò),文章巨長(zhǎng),但是很詳細(xì)》完全一致。
除了這部分基礎(chǔ)的IO分類,還有點(diǎn)民科的分類,還會(huì)根據(jù)有無讀寫緩沖分為緩沖IO、非緩沖IO,根據(jù)是否跳過頁緩存,分為直接IO(直接訪問文件系統(tǒng))和非直接IO(透過頁緩存)

2.2.3 詳解文件系統(tǒng)
2.2.3.1 磁盤 & 文件系統(tǒng) & 操作系統(tǒng)的關(guān)系
由于磁盤是個(gè)物理結(jié)構(gòu),缺少整體的管理機(jī)制,所以針對(duì)這個(gè)方面,磁盤被劃分為幾個(gè)邏輯單元,每個(gè)單元可以被獨(dú)立管理,每個(gè)單元被稱為“分區(qū)”,然后分區(qū)的分配信息存放于分區(qū)表中,Linux下,使用fdisk命令進(jìn)行磁盤分區(qū)(這里會(huì)分配文件系統(tǒng)的類型)
并且,磁盤再進(jìn)行文件系統(tǒng)格式化的時(shí)候,會(huì)分出來 3 個(gè)區(qū):Superblock
、inode blocks
、data blocks
。
Superblock: 存放整個(gè)文件系統(tǒng)的元信息,inode blocks: 存放inode,data blocks: 存放數(shù)據(jù)塊。
分區(qū)是磁盤掛載的第一步,區(qū)分好分區(qū),并確定文件系統(tǒng)類型之后,再注冊(cè)到操作系統(tǒng)中,這是磁盤掛載的第二步。
在linux中,使用目錄樹來進(jìn)行管理,直白的就是以根目錄為入口,向下呈現(xiàn)分枝狀的一種文件結(jié)構(gòu)。linux必須能夠?qū)⒏募到y(tǒng)掛載到根目錄上,當(dāng)根目錄掛載完成之后,我們可以將其它文件系統(tǒng)掛載于樹形結(jié)構(gòu)各種掛載點(diǎn)上。根結(jié)構(gòu)下的任何目錄都可以作為掛載點(diǎn),掛載點(diǎn)實(shí)際上就是linux中的磁盤文件系統(tǒng)的入口目錄。

2.2.3.2 內(nèi)部實(shí)現(xiàn)
磁盤的最小的操作單位是扇區(qū),所以一個(gè)文件最小的操作單位就應(yīng)該等于扇區(qū)的大小,也就是512字節(jié),但由于一個(gè)個(gè)扇區(qū)操作效率比較低,所以會(huì)一次性多個(gè)扇區(qū),多個(gè)扇區(qū)就構(gòu)成了“塊”結(jié)構(gòu),所以文件的最小操作單位就變成了“塊”大小。
每個(gè)文件還有自己的一些元數(shù)據(jù)信息(創(chuàng)建人、時(shí)間、size等),就單獨(dú)起了個(gè)結(jié)構(gòu)來記錄,也就是inode。
所以一個(gè)文件 = 數(shù)據(jù)塊 + inode
還有一個(gè)結(jié)構(gòu):dentry,記錄了文件名字、inode 指針以及與其他 dentry 的關(guān)聯(lián)關(guān)系,dentry 構(gòu)成了整個(gè)文件目錄樹。
首先我們通過目錄相關(guān)dentry,找到了對(duì)應(yīng)的文件,然后透過inode指針操作對(duì)應(yīng)的文件,然后讀寫數(shù)據(jù)塊。
最常用的是ZFS,支持Pool存儲(chǔ)-動(dòng)態(tài)擴(kuò)容、基于copyOnWrite的事務(wù)文件系統(tǒng)、ARC 緩存等等

2.2.3.3 文件描述符
linux中,當(dāng)進(jìn)程打開現(xiàn)有文件或創(chuàng)建新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符,文件描述符就是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用來指向被打開的文件,所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都會(huì)通過文件描述符。
文件描述符、文件、進(jìn)程間的關(guān)系:
每個(gè)文件描述符會(huì)與一個(gè)打開的文件相對(duì)應(yīng);
不同的文件描述符也可能指向同一個(gè)文件;
相同的文件可以被不同的進(jìn)程打開,也可以在同一個(gè)進(jìn)程被多次打開;
我們進(jìn)行文件操作時(shí),其實(shí)就是持有文件描述透過系統(tǒng)調(diào)用進(jìn)行文件讀寫,一個(gè)進(jìn)程能持有的文件描述符數(shù)量通常是1024個(gè),不過這個(gè)值可改。
另外,linux文件被并發(fā)修改時(shí),會(huì)有問題的,出現(xiàn)錯(cuò)數(shù)據(jù)。
2.3 數(shù)據(jù)庫系統(tǒng)
文件存儲(chǔ)滿足了早期應(yīng)用的大多數(shù)場(chǎng)景,比如說結(jié)構(gòu)化文本、展示文本文件、圖片、音頻存儲(chǔ)、源碼文件等等,但隨著應(yīng)用的復(fù)雜度逐漸上升,早期相對(duì)完整及簡(jiǎn)單的文件結(jié)構(gòu)已經(jīng)逐漸不滿足應(yīng)用場(chǎng)景了。在數(shù)據(jù)量極大、數(shù)據(jù)類型眾多、數(shù)據(jù)用途千差萬別的情況下,我們面臨的常常是修改若干文件中其中一個(gè)文件,其中的一段數(shù)據(jù)或者一塊數(shù)據(jù)。

所以我們開始寫代碼嘗試組織各種文件,然后用一些數(shù)據(jù)結(jié)構(gòu)來組織數(shù)據(jù),并相對(duì)通用的方法讀寫其中的“子數(shù)據(jù)”。導(dǎo)致在這個(gè)場(chǎng)景下出現(xiàn)了大量的相似的 文件操作&數(shù)據(jù)結(jié)構(gòu)操作代碼,而且隨著數(shù)據(jù)量指數(shù)級(jí)膨脹,大部分程序員已經(jīng)無法編寫高效的程序了。
這個(gè)時(shí)候,數(shù)據(jù)庫系統(tǒng)就孕育而生了,專門去做數(shù)據(jù)的組織與存儲(chǔ),以技術(shù)中間件的形式向大眾提供結(jié)構(gòu)化數(shù)據(jù)的存儲(chǔ)&操作服務(wù),隱藏了文件管理&數(shù)據(jù)操作的復(fù)雜度。
這些中間件就是我們現(xiàn)在所熟知的數(shù)據(jù)庫系統(tǒng)(應(yīng)用),數(shù)據(jù)庫按照數(shù)據(jù)結(jié)構(gòu)的差異可以大致分為:
本文要說的主要是關(guān)系型數(shù)據(jù)庫,鍵值數(shù)據(jù)庫幾年前寫過一篇,可以大體看看《談一談若干的K-V NoSQL應(yīng)用:LevelDB、Redis、Tair、RockesDB》
關(guān)于其他類型的數(shù)據(jù)庫,這塊會(huì)在后面《看這一大鍋中間件》(之后會(huì)更新鏈接)進(jìn)行簡(jiǎn)介
接下來詳細(xì)看看,關(guān)于關(guān)系型數(shù)據(jù)的應(yīng)用落地。
3.關(guān)系型數(shù)據(jù)庫系統(tǒng)詳解
關(guān)系型數(shù)據(jù)庫的核心能力:定義數(shù)據(jù)、操作數(shù)據(jù)。所有的關(guān)系型數(shù)據(jù)庫都是圍繞這兩點(diǎn)進(jìn)行打磨精進(jìn)的,我用的最多的是mysql及mysql 相關(guān)的變形應(yīng)用,就站在mysql的角度看關(guān)系型數(shù)據(jù)庫的落地吧,大致實(shí)現(xiàn)都相似,但是某些技術(shù)點(diǎn)的具體差異比較大。

關(guān)系型數(shù)據(jù)庫還挺多的,比如oracle、db2、SQL server等等,有興趣大家可以去了解下~

3.1 一個(gè)數(shù)據(jù)庫應(yīng)該有哪幾部分構(gòu)成
一個(gè)mysql 應(yīng)用通常由連接層、SQL層、存儲(chǔ)引擎層構(gòu)成。

連接層負(fù)責(zé)mysql的連接處理,及相關(guān)的鑒權(quán)等功能,內(nèi)部通常會(huì)維護(hù)一個(gè)TCP連接池。
SQL層,具有一個(gè)SQL解析器,進(jìn)行SQL語句的處理,包含詞法分析、語法分析等操作,根據(jù)語法樹生成對(duì)應(yīng)的執(zhí)行計(jì)劃。SQL優(yōu)化部分就發(fā)生在這里,對(duì)應(yīng)的SQL緩存(鍵值緩存)這里在這一層發(fā)生的。
下一層是引擎層,負(fù)責(zé)具體的執(zhí)行計(jì)劃的處理者,核心文件/數(shù)據(jù)的處理都發(fā)生在這一層,可以按照當(dāng)前場(chǎng)景的需要選擇不同的數(shù)據(jù)庫引擎,拿mysql來說,常見的有Innodb、MyISAM。

3.2 如何組織&存儲(chǔ)數(shù)據(jù)
3.2.1 文件存儲(chǔ)結(jié)構(gòu) – mysql 文件結(jié)構(gòu)
mysql 里面有這么幾類文件:
數(shù)據(jù)目錄:存儲(chǔ)所有數(shù)據(jù)庫對(duì)象和數(shù)據(jù)文件的根目錄,然后再數(shù)據(jù)目錄下每個(gè)數(shù)據(jù)庫有一個(gè)字目錄,里面包含了該數(shù)據(jù)庫的數(shù)據(jù)文件
表結(jié)構(gòu)文件:這個(gè)文件里存儲(chǔ)了每個(gè)數(shù)據(jù)庫中表的定義和結(jié)構(gòu)信息,這些文件以 .frm結(jié)尾
數(shù)據(jù)文件:真證存儲(chǔ)數(shù)據(jù)的文件,以.ibd結(jié)尾(innodb中)
日志文件:用戶記錄邏輯操作記錄(binlog)、物理變更記錄(redolog)、現(xiàn)場(chǎng)日志(undolog)、錯(cuò)誤日志(errorlog)、慢查詢?nèi)罩荆╯low query log)
配屬文件:存放mysql 相關(guān)的配置信息,這部分信息通常會(huì)直接夾在到內(nèi)存中,不太會(huì)直接讀寫。my.conf
臨時(shí)文件:主要保存臨時(shí)數(shù)據(jù)和中間結(jié)果,通常在臨時(shí)文件夾中
3.2.2 數(shù)據(jù)組織結(jié)構(gòu) – 頁、區(qū)、段、表空間
上面看到的是mysql的“物理”文件存儲(chǔ)結(jié)構(gòu),接下來看下mysql 內(nèi)部是如何對(duì)于數(shù)據(jù)進(jìn)行組織的。
mysql 中基本的操作單位是頁,1頁默認(rèn)16K(操作系統(tǒng)一次讀取的大?。?,16k實(shí)際上是四個(gè)磁盤頁,一個(gè)磁盤頁16k,讀的時(shí)候會(huì)預(yù)讀3頁 + 目標(biāo)頁,也就變成了16K。
數(shù)據(jù)庫對(duì)于數(shù)據(jù)的基本存儲(chǔ)就是用頁來存儲(chǔ)的,頁的種類有很多,比如數(shù)據(jù)頁用來存數(shù),undo頁 來存undo日志,常見的還有系統(tǒng)頁、事務(wù)數(shù)據(jù)頁、插入緩沖位圖頁、插入緩沖空閑列表頁、未壓縮的二進(jìn)制大對(duì)象頁、壓縮的二進(jìn)制大對(duì)象頁。
然后頁構(gòu)成了區(qū),一個(gè)區(qū)通常有64個(gè)連續(xù)的頁,并且相鄰的頁構(gòu)成一個(gè)雙向鏈表,便于順序訪問,一個(gè)區(qū)的大小是100 * 16k 也就是1m,劇透下,有沒發(fā)現(xiàn),這個(gè)就是兩次寫,寫緩沖的大小。
若干個(gè)區(qū)構(gòu)成了段,段中的區(qū)是隨機(jī)分布的,段是mysql對(duì)于磁盤的分配單位,我們創(chuàng)建一個(gè)表、索引的時(shí)候就會(huì)創(chuàng)建一些段結(jié)構(gòu)來存儲(chǔ)具體的數(shù)據(jù)信息,比如表段、索引段

然后表空間是一個(gè)邏輯空間,一個(gè)表一個(gè)表空間,或者多個(gè)表共用同一個(gè)表空間,拿獨(dú)立表空間來說,一個(gè)表空間了就包含了具體的數(shù)據(jù)段、索引段等,表空間是最高的邏輯結(jié)構(gòu),主要用于具體的管理職責(zé),可以看作是一個(gè)管理結(jié)構(gòu)。
3.2.3 邏輯數(shù)據(jù)結(jié)構(gòu) – B+樹
我們感知到的數(shù)據(jù)是一行一行的,而這些行就存放于頁中,我們的操作對(duì)象是行,數(shù)據(jù)庫的處理對(duì)象是頁。
行記錄的格式通常有四種:Compact,Redundant,Dynamic,Compressed,壓縮方法不同而已,常用的5.7默認(rèn)是Dynamic,之后都是Compact。
如果要找某一行該怎么找呢?最直觀的方式是直接一行行找,在mysql 內(nèi)部就是一頁頁的查,然后業(yè)內(nèi)遍歷每一行,很顯然效率比較低,如果按照非主鍵查詢算法是O(n)的,并且沒法把所有頁都灌進(jìn)內(nèi)存來避免IO。
3.2.3.1 主鍵索引
為了解決這個(gè)問題,引入了索引結(jié)構(gòu)(也是一種頁),先找到對(duì)應(yīng)的索引,然后按照索引找到對(duì)應(yīng)的數(shù)據(jù)頁,在進(jìn)行數(shù)據(jù)頁訪問,并且由于索引的量級(jí)、和大小都比數(shù)據(jù)頁小的多,可以進(jìn)行緩存。
這樣就做到了1、2次IO就可以完成數(shù)據(jù)的訪問,多級(jí)-索引頁 + 數(shù)據(jù)頁 就構(gòu)成了整個(gè)查詢樹(b+樹)
整個(gè)過程就變成:
訪問最高層的索引頁,找到下一級(jí)的索引頁,以此類推,查到最底層的根節(jié)點(diǎn)之后,再訪問數(shù)據(jù)頁,執(zhí)行數(shù)據(jù)訪問,和行記錄修改,修改的過程中打log,寫緩沖,然后后臺(tái)線程把臟頁刷回磁盤。
索引頁不一定是全緩存的哈,索引本身也很大,但是“常用”的索引頁會(huì)因?yàn)榫彺娌呗员焕刖彺?,innodb_buffer_pool_size 就是這個(gè)緩存的大小,某幾行比較熱,大概率數(shù)據(jù)頁常駐內(nèi)存,某些段比較熱,大概率索引頁常駐內(nèi)存。這個(gè)參數(shù)決定了查詢的IO次數(shù),也就導(dǎo)致這個(gè)參數(shù)變成了mysql 最大的性能影響參數(shù)。
3.2.3.2 為什么選擇 B+樹

順序遍歷為啥不行?
這個(gè)主要跟數(shù)據(jù)量強(qiáng)相關(guān),理論上沒啥問題,數(shù)據(jù)量小,就是完全遍歷也沒啥問題,數(shù)據(jù)量稍大點(diǎn)就用樹結(jié)構(gòu)去組織數(shù)據(jù)頁(比如多路平衡搜索樹)。
十萬級(jí)數(shù)據(jù),可能順序查就扛不住了,而大概率我們要面對(duì)的是千萬級(jí)的數(shù)據(jù)。所以開始考慮索引結(jié)構(gòu),效率最高的Hash結(jié)構(gòu),然后就是樹,但是Hash不支持范圍查詢,這點(diǎn)很頭疼,所以對(duì)于這種SQL數(shù)據(jù)庫就用樹來做索引了。
索引結(jié)構(gòu)為什么用B+樹呢,其他樹不行?
索引的作用是快速定位數(shù)據(jù)頁,并且盡可能少的IO產(chǎn)生。所以索引頁要盡可能的存放更多信息,然后以這個(gè)為基準(zhǔn)去看各種樹。
二叉樹
數(shù)據(jù)量的增大必然導(dǎo)致高度的快速增加,對(duì)那種逐漸增大的數(shù)據(jù)查詢相當(dāng)于鏈表查詢,效率低下,顯然這個(gè)不適合作為大量數(shù)據(jù)存儲(chǔ)的基礎(chǔ)結(jié)構(gòu)。
二叉平衡樹
同樣數(shù)據(jù)量的增大必然導(dǎo)致高度的快速增加,每次插入數(shù)據(jù)索引的變更成本太高了,不合適。二叉樹都有深度這個(gè)問題
m階b樹
平衡的多路搜索樹,要求根節(jié)點(diǎn)至少有兩個(gè)子節(jié)點(diǎn),除根節(jié)點(diǎn)以外的所有節(jié)點(diǎn)(不包括失敗節(jié)點(diǎn))至少有[m/2]個(gè)子節(jié)點(diǎn),所有的搜索路徑一樣長(zhǎng)。由于索引不重復(fù),數(shù)據(jù)節(jié)點(diǎn)可以理解為是根葉都有。
m階b樹比較擅長(zhǎng)隨機(jī)檢索,但是如果想要順序搜索就比較難了。
b+樹
就是在b樹上做了一點(diǎn)變更,允許索引冗余,并且索引中只存放索引信息,具體的數(shù)據(jù)放在葉子結(jié)點(diǎn)中,這樣就做到了索引信息足夠小,一頁索引能存更多的索引信息,并且繼承了b樹的優(yōu)勢(shì)點(diǎn),搜索路徑均衡,面對(duì)巨大數(shù)據(jù)量級(jí)是還能足夠扁平,搜索效率較高。并且把葉子結(jié)點(diǎn)組成了雙向鏈表,能夠順序查找。
3.2.3.3 非聚集索引
非聚集索引又稱輔助索引,同聚集索引的差異是,是否直接索引到數(shù)據(jù)節(jié)點(diǎn)。

比如主鍵索引就是聚集索引,而非主鍵唯一索引雖然能1:1定位到數(shù)據(jù),但是不直接索引到數(shù)據(jù)節(jié)點(diǎn),所以是非聚集索引。
mysql InnoDB中的非聚集索引的實(shí)現(xiàn)同主鍵索引基本類似,同樣也是b+樹結(jié)構(gòu),差異點(diǎn)在于,葉子結(jié)點(diǎn)存儲(chǔ)的是主鍵值,所以查找過程基本是先走非主鍵索引,找到對(duì)應(yīng)的主鍵值,然后使用對(duì)應(yīng)的主鍵值去查找對(duì)應(yīng)的數(shù)據(jù)頁,然后找到對(duì)應(yīng)的行數(shù)據(jù)。(很顯然會(huì)有一次回表動(dòng)作,再走一次查詢過程)
非聚集索引的查找過程基于左匹配原則(比較、模糊匹配會(huì)中斷匹配),所以寫sql 語句的時(shí)候要注意下,不過新版本的mysql已經(jīng)能夠?qū)τ趙here 條件根據(jù)索引順序進(jìn)行優(yōu)化。(值匹配也是左匹配(左前綴))
肯定會(huì)有人好奇,為什么存的是主鍵值,而不是直接指向具體的數(shù)據(jù)頁。因?yàn)橥砝锊迦霐?shù)據(jù)是可能會(huì)導(dǎo)致主索引結(jié)構(gòu)發(fā)生變化,會(huì)導(dǎo)致數(shù)據(jù)地址發(fā)生變化,也就要求需要把每一個(gè)索引都要同步更新,成本較大,尤其是在非主鍵索引較多的情況下。
但這里同樣要區(qū)分場(chǎng)景,myisam就是直接指向的數(shù)據(jù)頁,核心還是事務(wù)操作/查詢 的占比情況。
然后mysql InnoDB的插入緩沖除了為了插入性能考慮,還有一個(gè)就是為了解決非聚集索引插入的性能問題,因?yàn)榉蔷奂饕⒉皇窍裰麈I那樣有順序性,插入過程完全是隨機(jī)讀寫,所以需要的時(shí)間比較多,如果同步寫,性能會(huì)差很多。
當(dāng)插入一個(gè)新的索引時(shí),首先會(huì)在insert buffer中查找對(duì)應(yīng)的索引頁,如果存在則插入;不存在則初始化一個(gè)新的索引頁。通常多個(gè)插入緩存能夠被合并為一個(gè)操作再與輔助索引頁合并,所以大大提高了性能。
但是能主鍵查就主鍵插,回表過程開銷整體來說比較大。
3.2.4 鎖
mysql 中常見的鎖有:表鎖、頁鎖、行鎖、間隙鎖、臨鍵鎖、還有意向鎖等,可以大致分為共享鎖和排他鎖。鎖粒度從大到小:表鎖、頁鎖、行鎖(記錄鎖、間隙鎖、臨鍵鎖)
行鎖都是基于索引實(shí)現(xiàn)的,直接鎖索引段,然后按照屬性加共享或排他,除了主鍵索引會(huì)上鎖,輔助索引也會(huì)。
“
這種情況會(huì)死鎖
where index = 1 and primary_key = 2;
where primary_key = 2 and index = 1;
“
死鎖的情況,有非常多,我踩的最多的就是插入意向鎖導(dǎo)致的各種死鎖。
gap 鎖和插入意向鎖(本質(zhì)也是個(gè)間隙鎖):比如5.7里面 大量的insert on duplicate key update會(huì)導(dǎo)致死鎖,還有類似的select for update,沒有就插入,有就更新(像不像余額的初始化動(dòng)作,很危險(xiǎn)哦)
死鎖寫代碼的時(shí)候完全避免可能對(duì)水平要求有點(diǎn)高,上線前模擬真實(shí)流量壓測(cè)吧,然后一個(gè)個(gè)報(bào),對(duì)著死鎖日志,對(duì)著鎖表挨著看吧,之前做賬務(wù)的經(jīng)歷,可太精彩了。
3.3 OLAP & OLTP
OLAP 在線分析系統(tǒng),OLTP在線事務(wù)系統(tǒng),分析和事務(wù)處理面臨的數(shù)據(jù)操作差異是相當(dāng)大的,同時(shí)對(duì)應(yīng)的數(shù)據(jù)庫的底層實(shí)現(xiàn)差異也較大。
OLAP 側(cè)重于分析,使用場(chǎng)景更多的大量的查詢功能,并且是超大規(guī)模查詢和計(jì)算。
OLTP 側(cè)重于數(shù)據(jù)的記錄&變更,增刪改查相對(duì)均衡,對(duì)實(shí)時(shí)性要求通常比較高。
對(duì)于我們?nèi)粘5膱?chǎng)景來看,在線的用戶行為操作,比如支付行為、發(fā)個(gè)朋友圈之類的都是OLTP,比如你刷朋友圈時(shí)給你插了一條廣告,這條廣告計(jì)算得出的過程就是OLAP。
本文側(cè)重要說的OLTP,除了常規(guī)的增刪改查,還要看看更復(fù)雜的改動(dòng) “事務(wù)操作”,事務(wù)的四大特性,想必大家已經(jīng)爛熟于心了:原子性、隔離性、持久性、一致性,前三個(gè)是手段,最后一個(gè)是目的。事務(wù)的本質(zhì)就是利用原子性、隔離性、持久性實(shí)現(xiàn)數(shù)據(jù)的一致性保障。
這個(gè)一致性說的直白點(diǎn),就是計(jì)算機(jī)系統(tǒng)里面的數(shù)據(jù)和客觀世界的預(yù)期變化保持一致,保持客觀規(guī)律和事實(shí)。
看懂所謂的一致性,然后在這幾個(gè)特性上增加一個(gè)去中心化操作,防止篡改,這不就是去中心化存儲(chǔ)了嘛。
扯遠(yuǎn)了,本文要說的就是我們常見的數(shù)據(jù)庫,比如說mysql InnoDB是怎么落地事務(wù)的,怎么實(shí)現(xiàn) 原子性、隔離性、持久性的。

3.3.1 幾大log:binlog、redolog、undolog
binlog 是個(gè)二進(jìn)制log,保存了所有的數(shù)據(jù)庫邏輯操作日志,數(shù)據(jù)庫層面,于數(shù)據(jù)庫存儲(chǔ)引擎無關(guān),包含了每條語句執(zhí)行的時(shí)間、所消耗資源、還有相關(guān)的事務(wù)信息,常用于主從同步。-刷入binlog 文件,有這么幾個(gè)選項(xiàng):
binlog 詳盡可參照:MySQL Binlog 源碼入門
redolog 保存了具體的數(shù)據(jù)變更,記錄了具體是哪個(gè)數(shù)據(jù)頁,偏移量是多少,進(jìn)行了什么修改,偏物理層面的修改。比如:xx 表空間,xx 頁,xx 位置,xx 值-刷入redolog 文件,有這么幾個(gè)選項(xiàng):
redolog詳盡可參照:
庖丁解InnoDB之REDO LOG
MySQL · 引擎特性 · Redo Log record編碼格式
undolog 記錄了數(shù)據(jù)更新前的版本,用于數(shù)據(jù)回滾是快速回退到上一版本的數(shù)據(jù),可以把undolog 看成另一份數(shù)據(jù)。
undolog的結(jié)構(gòu)相對(duì)復(fù)雜可參照:
MySQL · 引擎特性· InnoDB之UNDO LOG介紹
或
庖丁解InnoDB之Undo LOG
3.3.2 看一下事務(wù)的執(zhí)行過程
圍繞這個(gè)過程來看一下具體的實(shí)現(xiàn)原理。
3.3.3 持久性實(shí)現(xiàn)
持久性說白了就是不丟,前面提到過,從應(yīng)用程序到系統(tǒng)調(diào)用,再到磁盤的寫入,都是存在丟失可能的。
如果直接fsync、關(guān)掉hardware緩沖,性能會(huì)劣化到極差。要一點(diǎn)性能我們就沒法兒去保證單次寫入的的可靠性。所以需要一種機(jī)制來保證數(shù)據(jù)不丟,同時(shí)能稍微兼顧一下性能。
mysql InnoDB 采用的持久性方式很大眾化,先寫log,再寫數(shù)據(jù)文件,也就經(jīng)常聽到的WAL(write ahead log),這是幾乎所有有持久化訴求應(yīng)用采用的方式,一種經(jīng)典的crash-safe 策略。

mysql 保存的這份日志是redo log。redo log分為兩部分,日志緩沖和日志文件。整體的過程會(huì)先寫binlog緩沖,然后再寫入redolog 緩沖,再根據(jù)對(duì)應(yīng)的策略在這個(gè)過程中或者過程外將redo log刷入磁盤,然后整體操作完成,redolog刷入提交。
整個(gè)過程保證了事務(wù)提交時(shí),redolog 一定先刷入磁盤,redolog 寫入成功,就認(rèn)定事務(wù)操作成功。對(duì)啦,redolog 是循環(huán)寫的哈,不是個(gè)純流水。

整個(gè)redolog的寫入過程可以被劃分為prepare、commit,做了一個(gè)兩階段提交。
3.3.3.1 為什么需要兩階段
為什么這么實(shí)現(xiàn)呢,首先WAL機(jī)制保證了數(shù)據(jù)不丟。那為什么還需要兩階段提交,這種XA事務(wù)存在的意義是什么呢?這不是分布式事務(wù)的協(xié)議嗎,不少人看到這兒都會(huì)有這個(gè)疑問。
其實(shí)很簡(jiǎn)單,一次數(shù)據(jù)提交,要保證兩個(gè)文件的一致性,這不就是分布式數(shù)據(jù)節(jié)點(diǎn)嘛,只不過沒有網(wǎng)絡(luò)IO發(fā)生而已,原理上是相通的。
前面提到過binlog、redolog各有職責(zé),兩者就實(shí)踐意義上來看都是不可或缺的,并且兩者具有一致性要求,任何一個(gè)少了都會(huì)誘發(fā)一些問題,比如少了binlog主從一致會(huì)受影響;少了redolog事務(wù)就無法重做等等。那不用兩階段可以嗎,不可以,因?yàn)槌霈F(xiàn)故障時(shí)會(huì)很麻煩。

假如只寫一次,不做兩階段提交,這兩種功能的日志有一定概率發(fā)生不一致。
假如先寫redolog,redolog寫成功了,redolog是相對(duì)完整的,binlog還未寫入,此時(shí)宕機(jī),binlog就少了一些,這就導(dǎo)致從庫丟失更新。
假如先寫binlog,只有binlog是相對(duì)完整的,但是binlog這種邏輯操作日志里并沒有記錄具體的事務(wù)檢查點(diǎn),恢復(fù)不了。
而實(shí)現(xiàn)了兩階段,可以等兩者都完成寫入之后(到達(dá)一致),才真正的commit。宕機(jī)恢復(fù)時(shí),到達(dá)commit狀態(tài)的可以直接提交。prepare狀態(tài)的才去檢查binlog,binlog缺失就回滾,binlog完整就直接提交。
這里的WAL和2PC思想很重要,分布式事務(wù)也是這么玩的,后面會(huì)具體分析哈。
3.3.3.2 為什么這么實(shí)現(xiàn)
主要對(duì)于性能的考量,前面提到過,磁盤寫入是一個(gè)重消耗操作,對(duì)性能影響極大,而且磁盤順序?qū)?、磁盤隨機(jī)寫 性能差異很大,出于各種功能,我們要寫數(shù)據(jù)文件(若干次隨機(jī)IO)、binlog(少量順序IO)、redolog(少量順序IO)、undolog(少量順序IO),如果每次都fsync強(qiáng)制刷盤,性能會(huì)差到難以想象,一次sql 操作幾十毫秒、甚至上秒肯定是不可能接受的。

所以把redolog做成了輕量級(jí)記錄,只記錄變更點(diǎn),并且磁盤順序?qū)懀阅茌^好,redolog 寫成功了,然后后臺(tái)線程異步刷磁盤。保證持久性的同時(shí),兼顧了性能。并提供緩沖機(jī)制,并且根據(jù)具體的場(chǎng)景選擇不同的可靠性配置。
對(duì)于binlog,同樣提供了緩沖機(jī)制,可以選擇不同的模式,來平衡redolog、binlog的一致性問題。
3.3.3.3 宕機(jī)時(shí)怎么恢復(fù)
InnoDB 為 redo log 記錄了序列號(hào),這被稱為 LSN(Log Sequence Number),可以理解為偏移量,越新的日志 LSN 越大。InnoDB 用檢查點(diǎn)(checkpoint_lsn)標(biāo)記未被刷臟頁的 redolog 數(shù)據(jù)從這里開始,用 lsn 指示下一個(gè)應(yīng)該被寫入日志的位置。
Recover過程:故障恢復(fù)包含三個(gè)階段:Analysis,Redo和Undo。
3.3.3.4 有redolog 為什么還需要數(shù)據(jù)頁兩次寫?
兩次寫主要是針對(duì)Innodb 對(duì)于WAL設(shè)計(jì)的引入,通常來說理論當(dāng)中的WAL(write ahead log),WAL的原理通常是沒問題的,先寫日志再寫數(shù)據(jù)文件,但是innodb中的WAL實(shí)現(xiàn)實(shí)際上是存在問題的,這里存在一個(gè)叫做部分寫的問題,通常來說數(shù)據(jù)頁是16k(前面提到過),但是磁盤的頁大小通常是4k的(讀16K、寫4K),這就存在問題了,容易出現(xiàn)部分失效的情況,redo log + 舊頁是無法重演數(shù)據(jù)的,所以引入了兩次寫。

先寫入兩次寫buffer,然后刷到共享表空間的兩次寫緩沖區(qū)、最后再刷數(shù)據(jù)文件。
整體來看Innodb使用redolog+double write來保障數(shù)據(jù)的寫入可靠,實(shí)現(xiàn)事務(wù)的持久性。某種視角來看異步刷新臟頁中doublewrite,一定程度實(shí)際充當(dāng)了物理日志的功能。
3.3.4 原子性實(shí)現(xiàn)
原子性是指要么全做,要么一點(diǎn)不做,不存在中間狀態(tài),可能早年人們認(rèn)為原子是不可再分的,所以起了這么個(gè)名字吧,叫不可分割性更形象吧。前面提到的undolog就是來做這個(gè)事兒的,改數(shù)據(jù)之前記錄一下改之前的值。
有了undolog我們就有能力將數(shù)據(jù)恢復(fù)到事務(wù)開啟時(shí)的版本,同時(shí)undolog的持久化也需要redolog來做保障。
3.3.5 隔離性實(shí)現(xiàn)
大家常背的八股,事務(wù)的隔離性:“讀未提交”、“讀已提交”、“可重復(fù)讀”、“串行化”,這幾種隔離級(jí)別實(shí)際上描述的就是并發(fā)修改的粒度,主要用來解決并發(fā)修改下,臟讀、不可重復(fù)讀、幻讀、丟失更新(一類、二類)等問題。
隔離性要解決的核心就是并發(fā)控制的問題,也就是保證并發(fā)執(zhí)行的事務(wù)在某一個(gè)隔離級(jí)別上能夠正確的執(zhí)行。
關(guān)于并發(fā)控制如果想要更深入的了解,可參照:
析數(shù)據(jù)庫并發(fā)控制機(jī)制
如果想了解隔離級(jí)別的發(fā)展史,可參照
數(shù)據(jù)庫事務(wù)隔離發(fā)展歷史
3.3.5.1 并發(fā)修改下的問題
臟讀
是指讀出了其他事務(wù)正在修改的中間值,而不是最終值
不可重復(fù)讀
,一個(gè)事務(wù)內(nèi)多次讀取數(shù)值不同,會(huì)有新提交的事務(wù)結(jié)果被讀到。
幻讀
,其他事務(wù)新插入的數(shù)據(jù)被讀到了。
一類丟失更新
,事務(wù)撤銷時(shí),會(huì)導(dǎo)致之后執(zhí)行的事務(wù)被覆蓋,導(dǎo)致丟失。(可以理解為版本控制的指定快照回滾)
二類丟失更新
,兩個(gè)事務(wù)同時(shí)開啟,但提交延遲較大,較早提交的事務(wù),會(huì)被之后提交的事務(wù)給覆蓋。(經(jīng)典的并發(fā)下的竟態(tài)條件)

3.3.5.2 MVCC機(jī)制
接下來看下,這些數(shù)據(jù)并發(fā)修改的控制是怎么實(shí)現(xiàn)的。從程序?qū)崿F(xiàn)角度,并發(fā)下要解決問題,首先想到的是不要并發(fā),要么串行,要么互不干涉,數(shù)據(jù)操作的這個(gè)場(chǎng)景下,互不干涉是不可能了,那就可以直接串行操作,讓有沖突的操作串行化。
但是掛鎖串行太粗暴了,對(duì)于沖突的情況,毫無并發(fā)度可言,為了性能,能不能無鎖化處理,事務(wù)間互不影響。
畢竟很多時(shí)候我們只需要讀一個(gè)“有效的快照”即可,可重復(fù)讀、讀已提交、讀未提交 這些就是有效的定義,怎么做呢?
mysql innodb提供了MVCC機(jī)制(多版本并發(fā)控制)來實(shí)現(xiàn)讀已提交、可重復(fù)讀兩種級(jí)別。
MVCC本質(zhì)上就是創(chuàng)建數(shù)據(jù)的一致性視圖,讓讀操作切到這個(gè)一致性視圖上:
讀未提交:和MVCC沒啥關(guān)系,就是不創(chuàng)建視圖,直接讀行記錄
讀已提交:在SQL執(zhí)行開始的時(shí)候記錄下已經(jīng)提交的版本視圖,每次讀這個(gè)視圖
可重復(fù)讀:在事務(wù)開始的時(shí)候記錄下已經(jīng)提交的版本視圖,事務(wù)內(nèi)讀這個(gè)視圖
串行化:和MVCC沒啥關(guān)系
舉個(gè)例子:最常用的可重復(fù)讀模式
其他的模式類似,隔離級(jí)別決定的就是視圖創(chuàng)建的時(shí)機(jī),拿到視圖之后,接下來開始判斷
3.3.5.3 回頭看問題
讀未提交、串行化,這個(gè)不需要任何機(jī)制支持,本來就是這樣的。
讀已提交,可以直接讀取MVCC sql執(zhí)行時(shí)創(chuàng)建的一致性視圖,每次讀到的就是已提交的數(shù)據(jù)。
可重復(fù)讀,可以直接讀取MVCC 事務(wù)開啟時(shí)創(chuàng)建的一致性視圖,每次讀到的就是已提交的數(shù)據(jù)。

MVCC把臟讀、不可重復(fù)讀都已經(jīng)解決了,而在事務(wù)的回滾機(jī)制中,一類更新丟失不允許發(fā)生。剩下的還有幻讀和第二類更新丟失,記住一點(diǎn),MVCC是實(shí)現(xiàn)讀已提交、可重復(fù)讀機(jī)制的,討論的幻讀、更新丟失等問題都是在這兩種模式之下進(jìn)行討論。
3.3.5.4 幻讀問題的解決
首先幻讀這個(gè)問題是指可重復(fù)讀級(jí)別下,當(dāng)前讀,讀出其他事務(wù)新提交的數(shù)據(jù)的。如果是讀快照(事務(wù)開啟時(shí)的一致性數(shù)據(jù)視圖)是出現(xiàn)不了幻讀現(xiàn)象的。
可重復(fù)讀級(jí)別下,幻讀問題是通過next-key lock(可粗暴的理解為行鎖 + 間隙鎖)來解決的,不同的版本實(shí)現(xiàn)都有差異。
當(dāng)InnoDB掃描索引記錄的時(shí)候,會(huì)首先對(duì)索引記錄加上行鎖(Record Lock),再對(duì)索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。加上間隙鎖之后,其他事務(wù)就不能在這個(gè)間隙修改或者插入記錄。
當(dāng)然啦,串行化級(jí)別下,當(dāng)然沒有幻讀問題。
3.3.5.5 第二類更新丟失
第二類更新丟失更多是事務(wù)間的協(xié)同能力,本質(zhì)上是一個(gè)多線程問題,最佳的方式是根據(jù)業(yè)務(wù)的需求來上鎖,使其串行化,而mysql 也是這么來做的。

在可重復(fù)讀場(chǎng)景下,讀的都是一致性視圖的快照數(shù)據(jù),事務(wù)開啟時(shí)讀的是相同值,然后進(jìn)行更改,理論上會(huì)發(fā)生后提交事務(wù)覆蓋先提交事務(wù)的情況。但是如果你去實(shí)際實(shí)驗(yàn)一下,不難發(fā)現(xiàn),沒有出現(xiàn)所謂的丟失更新呀。
核心原因是,mysql對(duì)于當(dāng)前讀場(chǎng)景,會(huì)進(jìn)行上鎖動(dòng)作,相當(dāng)于在可重復(fù)讀場(chǎng)景下,讓這幾個(gè)事務(wù)串行化了
網(wǎng)上各種blog 各種搬運(yùn)各種copy,一多半都是錯(cuò)誤信息,不要被迷惑哈,mysql可重復(fù)讀之下,對(duì)于同一行記錄的原地更新是沒有問題的,不存在更新丟失,每次設(shè)置值都會(huì)去上鎖的。
舉個(gè)例子:有id、balance 兩個(gè)字段,表名是t_user_balance;
如果用的是快照讀,那沒法了,會(huì)出現(xiàn)第二類更新丟失
這個(gè)事兒怎么說呢,mysql 對(duì)于MVCC實(shí)現(xiàn)有點(diǎn)瑕疵,PostgreSQL、Oracle這些應(yīng)該對(duì)于可重復(fù)讀場(chǎng)景直接做了校驗(yàn),如果數(shù)據(jù)發(fā)生了變更,自動(dòng)檢測(cè)更新丟失,會(huì)直接報(bào)錯(cuò)扔出來,而不是上鎖。
mysql InnoDB 并不是嚴(yán)格意義上的MVCC,也就是說不會(huì)直接存儲(chǔ)多個(gè)版本的數(shù)據(jù),而是所有更改操作利用行鎖做并發(fā)控制,這樣對(duì)某一行的更新操作是串行化的,然后用Undo log記錄串行化的結(jié)果。當(dāng)快照讀的時(shí)候,利用undolog重建需要讀取版本的數(shù)據(jù),從而實(shí)現(xiàn)讀寫并發(fā)。
3.4 嘗試階段性總結(jié)下
嘗試總結(jié)下,說了這么多實(shí)現(xiàn)和原理,那對(duì)我們使用mysql、或者實(shí)現(xiàn)和mysql類似功能的應(yīng)用,有啥幫助沒,看到這里,可以嘗試回憶總結(jié)下。
如果要有持久性保證,那就WAL,但是要注意部分寫問題。
如果要控“分布”的數(shù)據(jù)節(jié)點(diǎn),經(jīng)典的2PC是一個(gè)不錯(cuò)的解決方案。
要做原子性,就要支持完整且可靠的回滾機(jī)制,記錄修改前的版本,并且對(duì)于這份記錄做持久性保障。
要做事務(wù)動(dòng)作,持久性(解決不丟)、原子性(支持回滾)、隔離性(并發(fā)下的控制) 缺一不可。
并發(fā)問題不好解了,要么不要并發(fā),直接串行(上鎖),或者以串行的方式去并發(fā)(數(shù)據(jù)不共享)。
犧牲可靠性,使用緩沖批次讀寫,對(duì)于吞吐的提升真的很大,規(guī)避IO時(shí),相當(dāng)好用。
順序讀寫、隨機(jī)讀寫性能差異很大,尤其是在大量IO的場(chǎng)景下,整體的差異會(huì)更大,WAL 中日志順序?qū)?,?shù)據(jù)文件異步寫,然后寫入加緩沖,性能比次次fsync要快太多太多。
要寫好where,得對(duì)索引門清,能用主鍵查的就用主鍵。
盡可能避免null,會(huì)導(dǎo)致索引、索引統(tǒng)計(jì)、值比較很麻煩,然后要做索引,區(qū)分度最好高一些。
范圍查詢會(huì)導(dǎo)致索引失效,所以優(yōu)先使用 = in等操作,避免失效,記住最左。
優(yōu)化索引是能修改當(dāng)前的,就不要新增新的索引,會(huì)增加維護(hù)成本和速度(索引頁會(huì)多,插入緩沖的消費(fèi)也會(huì)更慢),并且需要更大的空間。
推薦使用聯(lián)合索引,減少輔助索引的數(shù)量,一次輔助索引查找就找到主鍵值。
where 子句中對(duì)字段進(jìn)行表達(dá)式操作或者函數(shù)操作,都會(huì)導(dǎo)致引擎放棄使用索引而進(jìn)行全表掃描
4.分布式環(huán)境下的數(shù)據(jù)庫系統(tǒng)

看完單機(jī)的mysql 應(yīng)該已經(jīng)十分清楚怎么能夠保證數(shù)據(jù)不丟、不錯(cuò),計(jì)算機(jī)的數(shù)據(jù)存儲(chǔ)和操作能跟我們的預(yù)期保持一致,但是單機(jī)是有極限的,無論是由于CPU算力上限和內(nèi)存上限導(dǎo)致的網(wǎng)絡(luò)極限、磁盤IO極限,又或者磁盤的大小上限,每一個(gè)極限都是制約單機(jī)性能的因素。哪怕我們把軟件層面能做的事兒拉滿了,異步批處理緩沖、緩存、并發(fā)度拉滿,單機(jī)還是有著一個(gè)較低的上限。

在面臨上限的存在,由于數(shù)據(jù)存儲(chǔ)是個(gè)狀態(tài)型應(yīng)用,導(dǎo)致無法像無狀態(tài)應(yīng)用一樣做集群處理。比如nginx,可以堆機(jī)器,請(qǐng)求落到哪臺(tái)機(jī)器上都是可以的,比如執(zhí)行計(jì)算任務(wù),帶著輸入放在哪臺(tái)機(jī)器上算也都是一樣的,而狀態(tài)型應(yīng)用要求只能把流量放在存在目標(biāo)數(shù)據(jù)的機(jī)器上。
那如果每個(gè)機(jī)器都放一樣的數(shù)據(jù),然后把流量散到每臺(tái)機(jī)器上呢?結(jié)果就是:需要保證每臺(tái)機(jī)器的數(shù)據(jù)是完全一樣的,才能保證整個(gè)系統(tǒng)的一致性。最終的結(jié)果必然導(dǎo)致每臺(tái)機(jī)器抗整個(gè)集群的流量(實(shí)際操作 + 一致性流量),單機(jī)極限仍然是系統(tǒng)的極限,并且還浪費(fèi)了大量人力物力。
ps:這其實(shí)就是多活的方式,但是多活其實(shí)是做了時(shí)空差,才不會(huì)演化成上面說的最壞的情況,換個(gè)角度不難發(fā)現(xiàn),多活是為了容災(zāi)和性能,而不是為了突破極限
4.1 拆流量
要突破極限,最直接的方法就是拆流量,把流量拆到每一臺(tái)機(jī)器上,有兩種策略,四個(gè)思路:

邏輯拆分:
按照數(shù)據(jù)的橫向和縱向分別進(jìn)行拆分。
水平拆分
:按照流量來源做拆分,比如說用戶維度拆,不同流量來源的數(shù)據(jù)落到不同的機(jī)器上。垂直拆分
:相同流量來源,但不同功能的數(shù)據(jù)落到不同的機(jī)器上。
犧牲一致性拆分:
做讀寫分離,按照讀寫,然后按照各自特性做復(fù)制,做拆分。
熱點(diǎn)復(fù)制
:同一份數(shù)據(jù)復(fù)制多份做分布節(jié)點(diǎn),犧牲數(shù)據(jù)一致性,數(shù)據(jù)復(fù)制后組集群、掛緩存、掛緩存集群(短時(shí)間不一致可接受,最終一致就好)構(gòu)成低數(shù)據(jù)一致性讀數(shù)據(jù)源,然后強(qiáng)一致性讀仍然走源數(shù)據(jù),這種拆分方式,主要是讀場(chǎng)景熱點(diǎn)拆分
:把熱點(diǎn)數(shù)據(jù)拆分多份,或者多通道緩沖,通常是計(jì)算場(chǎng)景,根據(jù)場(chǎng)景強(qiáng)行容忍短暫的不一致,做sharding和batch。主要是寫場(chǎng)景
通常來說這幾步是一個(gè)遞進(jìn)過程,首先是水平拆,水平拆完,垂直拆,垂直拆完,按照一致性做拆分,面臨熱點(diǎn)大于極限時(shí),根據(jù)場(chǎng)景,要么拒絕,要么犧牲一致性,繼續(xù)拆。對(duì)于各種有狀態(tài)應(yīng)用的極限應(yīng)對(duì)思路往往都是這樣。
下面,詳細(xì)來看
4.1.1 水平拆分
按照流量來源做拆分,比如說用戶維度拆,不同流量來源的數(shù)據(jù)落到不同的機(jī)器上。每臺(tái)機(jī)器上的流量和數(shù)據(jù)量都降低了很多,整個(gè)集群能扛住大流量了,并且由于數(shù)據(jù)的減少,讀寫的性能也更好了。

就具體實(shí)踐,我們可以在一堆mysql 實(shí)例前面掛一個(gè)接入層,然后按照一定的規(guī)則,穩(wěn)定散列到每一臺(tái)機(jī)器上。如果出現(xiàn)不均衡的情況,可以強(qiáng)制干預(yù)某些大流量源進(jìn)行進(jìn)一步的散列。
關(guān)于數(shù)據(jù)分布的方法可以參照淺談分布式存儲(chǔ)系統(tǒng)數(shù)據(jù)分布方法
4.1.2 垂直拆分
水平拆分通常能解決大部分問題,但是某些情況下水平拆也解決不了問題。由于單行數(shù)據(jù)過大,受限于mysql的實(shí)現(xiàn)原因,表的極限、庫的極限會(huì)提前到來。

很多時(shí)候我們的訴求只是改一大行中的某幾個(gè)字段,但是對(duì)于mysql而言,操作的是整個(gè)數(shù)據(jù)頁。如果行數(shù)據(jù)過大,會(huì)導(dǎo)致一頁內(nèi)能存放的行數(shù)過少,必然導(dǎo)致磁盤IO次數(shù)增多(索引頁多 + 數(shù)據(jù)頁多),而IO多帶來的就是響應(yīng)時(shí)間很長(zhǎng),也就導(dǎo)致單機(jī)能抗的吞吐變小。犧牲性能,但由于單機(jī)吞吐太小,機(jī)器數(shù)過多不可接受;保性能,一臺(tái)機(jī)器少存點(diǎn)行,機(jī)器數(shù)過多也不可接受。而且就算硬堆機(jī)器,本質(zhì)上是在浪費(fèi),mysql 并沒有發(fā)揮最大的性能。
數(shù)據(jù)庫里表越拆越多,帶來的是文件數(shù)量的大規(guī)模增加,而單機(jī)的文件描述符是有上限的。并且數(shù)據(jù)庫的單庫的維護(hù)成本也會(huì)直線上升。而且單數(shù)據(jù)庫的能處理的鏈接也是有上限的,水平拆完之后,流量依舊可能很大,但是單表已經(jīng)足夠窄了。
總結(jié)來看:
1:讓流量只影響到最小的操作單元是最佳的性能處理思路,所以需要按照功能用途進(jìn)行數(shù)據(jù)拆分,那就別讓無關(guān)列
、無關(guān)表
?一起被操作、一起受影響了,這樣才能發(fā)揮最佳性能,提升吞吐
2:水平拆分后,哪怕就極少的數(shù)據(jù),流量仍然可能很大,可以按照功能維度進(jìn)行流量的進(jìn)一步拆離,也就意味著把不同功能的數(shù)據(jù)拆分
3:并且出于容災(zāi)、維護(hù)、故障屏蔽等方面的考慮,做功能隔離,也有著較大的拆分傾向。
4:再加上康威定律導(dǎo)致的組織上的分工,按照功能拆分也是種必然的趨勢(shì);
垂直拆分的意義就很突出了,按照功能字段-拆表,然后組成新的表,突破表極限;按照功能表-拆庫,組成新的庫,突破單庫極限;并且以此發(fā)揮數(shù)據(jù)庫的最佳性能。上層應(yīng)用層按照功能做路由,拆到不同的數(shù)據(jù)源,拆分到不同的表。
4.1.3 熱點(diǎn)復(fù)制
熱點(diǎn)問題很頭疼,原則上要保持一致性,由于有單點(diǎn)極限存在,熱點(diǎn)問題是解不了的。但是為了可用性,我們可以犧牲一致性,做數(shù)據(jù)分布,換取有效吞吐上限的增加(可以理解為可用性的極限),這就是經(jīng)典CAP理論的思路。所有面臨有效吞吐難以提升的場(chǎng)景,不妨都試試這個(gè)思路,把數(shù)據(jù)拆分或者復(fù)制為不同的數(shù)據(jù)節(jié)點(diǎn)(犧牲整體數(shù)據(jù)的一致性)。
流量的一致性要求是不同的,有的要求低、有的要求高。比如說一些評(píng)論信息、用戶個(gè)人介紹、當(dāng)前展示的庫存等等,對(duì)于一致性要求并沒有那么高,最終是沒問題的就夠了。而像賬戶余額、商品扣減時(shí)的庫存、計(jì)費(fèi)數(shù)據(jù)這些則要求丁點(diǎn)兒不錯(cuò)。
當(dāng)完成流量的一致性分類之后,不難發(fā)現(xiàn),讀流量更多是低一致性要求的,寫流量和寫時(shí)讀是高一致流量的。那么我們首先可以做讀寫分離。
用熱點(diǎn)數(shù)據(jù)復(fù)制的方式,將讀流量處理的極限突破掉,低一致性要求讀復(fù)制節(jié)點(diǎn),高一致性要求讀源節(jié)點(diǎn)。

比如mysql,可以按照這些將流量進(jìn)行拆分,同樣的把數(shù)據(jù)復(fù)制多份,其中一份為強(qiáng)一致基準(zhǔn)(主),其他的數(shù)據(jù)節(jié)點(diǎn)來復(fù)制這份數(shù)據(jù)(從)。讓一致性要求低的流量指向從庫,強(qiáng)一致的流量指向主庫。
ps:主從的機(jī)制,不單純是為了突破極限,很多時(shí)候是為了容災(zāi)和故障恢復(fù)。
如果還扛不住,對(duì)于一致性再進(jìn)一步犧牲,進(jìn)一步數(shù)據(jù)復(fù)制,比如從庫上面掛緩存。這樣理論上就把往往是流量大頭的一致性要求低的流量給分出來了。
4.1.4 熱點(diǎn)拆分
犧牲讀一致性將流量拆分完成之后,還是有一類問題是解不了的,這就是我們常說的熱點(diǎn)數(shù)據(jù)寫入問題,常見的場(chǎng)景比如:庫存、收款賬戶。
對(duì)于熱點(diǎn)寫入數(shù)據(jù),我們還能拆嗎?理論不能(會(huì)打破時(shí)間上的一致性,寫入成功、但未生效),但是沒有辦法的情況下,就適當(dāng)?shù)臓奚壿嬌系囊恢滦?,將源?shù)據(jù)拆分為多個(gè)子節(jié)點(diǎn)或者緩沖節(jié)點(diǎn),子節(jié)點(diǎn)共同構(gòu)成邏輯節(jié)點(diǎn),子節(jié)點(diǎn)操作時(shí)只關(guān)心內(nèi)部狀態(tài)。
對(duì)于同一時(shí)刻,子節(jié)點(diǎn)規(guī)集得到的邏輯數(shù)據(jù)節(jié)點(diǎn)與父節(jié)點(diǎn)是不一致的,但是父節(jié)點(diǎn)過一段時(shí)間總能達(dá)到邏輯數(shù)據(jù)節(jié)點(diǎn)的狀態(tài),整體來看是最終一致的。但是基于中間的軟狀態(tài),會(huì)導(dǎo)致某些決策上的不一致。

ps:因?yàn)橐WC整體邏輯上的一致性,當(dāng)前讀本質(zhì)上屬于寫流量,不可能去讀父節(jié)點(diǎn)當(dāng)前數(shù)據(jù),所以當(dāng)前讀應(yīng)該也被拆分到具體的子節(jié)點(diǎn)中。
就拿庫存來說,看兩種常見的實(shí)現(xiàn)方案:
把一份庫存數(shù)據(jù)拆成多份,然后按照一定的策略流到不同的子庫存上,可能會(huì)導(dǎo)致檢查庫存時(shí)無庫存,但是實(shí)際還有。
把庫存拆成一個(gè)個(gè)的令牌,然后預(yù)先分發(fā)給一臺(tái)令牌下發(fā)server(純內(nèi)存),或者直接下發(fā)給每臺(tái)業(yè)務(wù)Server,同樣對(duì)于單個(gè)請(qǐng)求邏輯看來是有一致性問題的。
或者給庫存的寫入動(dòng)作掛個(gè)通道,異步批處理,相當(dāng)于繞過了熱點(diǎn)極限,異步通知用戶中沒中。
比如商家收款:
把商家賬戶拆分為多個(gè)子戶,拆分熱點(diǎn)流量,子戶規(guī)集至主戶,犧牲主余額實(shí)時(shí)狀態(tài)下的一致性。
把付款流量做緩沖,批量入戶,高性能可靠寫存儲(chǔ)換取更高的寫極限,同樣犧牲主余額的一致性。
更甚者,只記流水?dāng)?shù)據(jù),高性能可靠寫換取更高的寫極限,異步對(duì)賬,日結(jié)即可。
我們?nèi)粘?shí)踐的過程中,如果水平、垂直拆分完,流量還是過大,還是難以解決,不妨試試這種方式。
需要注意的是:在犧牲一致性時(shí),要根據(jù)具體的場(chǎng)景進(jìn)行單方面的犧牲,比如庫存能少發(fā)、但不能多發(fā),再比如余額能犧牲一定時(shí)效性,但不允許錯(cuò)
ps:對(duì)于某些場(chǎng)景,可以直接犧牲可用性的,比如抽獎(jiǎng)直接不中獎(jiǎng),秒殺直接無庫存,背后其實(shí)可能直接隨機(jī)數(shù)過濾掉大部分流量。但是對(duì)于某些場(chǎng)景無法去拒絕流量、無法傷害用戶體驗(yàn),那不妨試試上面的思路,只要保證數(shù)據(jù)邏輯上整體的最終一致就好,單個(gè)請(qǐng)求的一致性就犧牲吧。
4.2 關(guān)于分布式環(huán)境下,一致性的保障思路
前面提到了如果使用分布式的方式突破單機(jī)極限,但是如果數(shù)據(jù)做分布了,我們?cè)趺慈ケWC數(shù)據(jù)的一致性呢,前面提到的最終一致、強(qiáng)一致、CAP又是些什么。
4.2.1 一致性只能是強(qiáng)一致嘛
我們前面提到了什么是數(shù)據(jù)一致性:“計(jì)算機(jī)系統(tǒng)中的數(shù)據(jù)與客觀世界的預(yù)期變化保持一致”,而這個(gè)一致性通常由原子性、隔離性、持久性來保障,由于就一份數(shù)據(jù),依賴于事務(wù)機(jī)制變更完成是一瞬時(shí)的,是一個(gè)時(shí)間點(diǎn)而非時(shí)間段。
但是數(shù)據(jù)分布之后,對(duì)于邏輯數(shù)據(jù)的一致性保證就比較復(fù)雜了,數(shù)據(jù)變更往往是一個(gè)時(shí)間段。就現(xiàn)實(shí)情況來看,這個(gè)保持一致性的時(shí)間長(zhǎng)度并沒有很強(qiáng)的界定,比如保持一瞬間到達(dá)就是結(jié)果數(shù)據(jù),還是5分鐘之后是結(jié)果數(shù)據(jù),還是只要我用這個(gè)數(shù)的時(shí)候就是當(dāng)前的結(jié)果數(shù)據(jù)。
我們可以根據(jù)時(shí)間這個(gè)維度,把一致性劃分為:線性一致(強(qiáng)一致)
?最終一致
?順序一致

線性一致
就是變更完成的瞬時(shí)完成變化,以提交為臨界點(diǎn),之前是修改前的,之后是修改后的,在任意時(shí)刻分布下的所有的數(shù)據(jù)節(jié)點(diǎn)總是一致的。最終一致
是允許在變更的時(shí)間跨度內(nèi)有中間狀態(tài)的存在,相當(dāng)于線性一致變更的瞬間是一個(gè)時(shí)間段,時(shí)間段內(nèi)觀察到的是中間狀態(tài),完成之后觀察到的一致的。順序一致
是指一次數(shù)據(jù)的更改是順序的,總能按照更改順序讀到每個(gè)節(jié)點(diǎn)最后一次修改的數(shù)據(jù)。
還有一些其他的基于以上演變后的分類,比如說順序一致變種一下就是因果一致
(只對(duì)必要的讀順序一致),讀寫一致
就是對(duì)自己線性一致,對(duì)其他最終一致。
這些分布式下的數(shù)據(jù)一致性適用于各種不同的場(chǎng)景。
4.2.2 CAP & BASE
CAP 是指分區(qū)容錯(cuò)(因?yàn)橥ㄐ艡C(jī)制導(dǎo)致出現(xiàn)多個(gè)子網(wǎng)絡(luò),重點(diǎn)是互不知死活)、可用性、數(shù)據(jù)一致性只能取其二。

分布式環(huán)境下,分區(qū)容錯(cuò)是必須的。首先分布式下協(xié)同機(jī)制(比如網(wǎng)絡(luò))一定是不穩(wěn)定的,如果不容錯(cuò)(不允許有分區(qū)發(fā)生,但是分區(qū)一定會(huì)發(fā)生),就只能不分布,只有單節(jié)點(diǎn)能解決問題。
剩下的就是選可用性和數(shù)據(jù)一致性,發(fā)生網(wǎng)絡(luò)分區(qū)時(shí),就代表有節(jié)點(diǎn)不可達(dá),要么拒絕請(qǐng)求(犧牲可用性),要么不可達(dá)的節(jié)點(diǎn)不操作(犧牲一致性),別無他法。
如果有可靠的協(xié)同機(jī)制,那CAP就能同時(shí)滿足了?可靠的協(xié)同機(jī)制就代表不會(huì)有分區(qū)發(fā)生,多個(gè)數(shù)據(jù)節(jié)點(diǎn)本質(zhì)就是一個(gè)數(shù)據(jù)節(jié)點(diǎn),有沒有分區(qū)容錯(cuò)已經(jīng)不重要了,AC就能同時(shí)滿足。比如同一磁盤頁內(nèi)的兩行數(shù)據(jù)(協(xié)同:磁頭的一次刷入),一次寫入,要么全成功、要么全失敗,肯定是一致的,并且可用性跟一致性沒有互斥關(guān)系,肯定是能共存的。
另外,分區(qū)容錯(cuò),容的錯(cuò)是指的是部分錯(cuò)誤:Partition Tolerance:System continues to work despite message loss or partial failure.
這其實(shí)意味著不只是網(wǎng)絡(luò)分區(qū),就算單機(jī)內(nèi),數(shù)據(jù)呈分布態(tài),本質(zhì)上也會(huì)有CAP問題,我們也可以用CAP和BASE理論去解決問題。
可以回憶一下,同一數(shù)據(jù)變更,redolog、binlog需要都記錄,如果其中一方掛了需要決策還要不要繼續(xù)。比如binlog因?yàn)槟承┰虿豢蛇_(dá)了,是不是也要做決策是拒絕請(qǐng)求犧牲可用性,還是容忍binlog丟失產(chǎn)生不一致,這就代表了有互斥關(guān)系發(fā)生。

并且進(jìn)行決策,mysql選擇一致性之后,還需要2PC機(jī)制來進(jìn)一步保證已經(jīng)寫入的redolog不會(huì)直接生效,來保障整體的數(shù)據(jù)一致性。2PC只是保證了在選擇一致性時(shí),保障數(shù)據(jù)的一致性,而不是讓CA不互斥了,過程中該決策CA,還是要決策的。
而對(duì)于mysql內(nèi)部的不同行(不同數(shù)據(jù)頁)的修改雖然是分布的,但是redolog寫入(單節(jié)點(diǎn)數(shù)據(jù))保證了其一定能操作成功,也就是完全可以把兩行數(shù)據(jù)的兩次落盤看作一次修改,也就是協(xié)同機(jī)制是絕對(duì)可靠的,不存在分區(qū)問題。

就使用上來看,數(shù)據(jù)對(duì)于單機(jī)mysql來看是單節(jié)點(diǎn)的,本身就是強(qiáng)一致的,成功是一致、失敗也是一致,單就使用上可用性跟一致性沒有互斥關(guān)系,是CA的,不需要使用者決策什么。
BASE理論
是在CAP
之上提出:一致性和可用性并非只能干脆的選擇,可以繞點(diǎn)彎路,稍微平衡下,比如分區(qū)發(fā)生時(shí),選擇容忍丟失保證基礎(chǔ)可用性(基本可用),但是存在中間狀態(tài)(軟狀態(tài)),但網(wǎng)絡(luò)分區(qū)最終會(huì)恢復(fù),然后通過一定手段達(dá)成一致(最終一致)
上面這句話可以這么粗暴的理解哈?!?/p>
4.2.3 共識(shí)算法
上面提到了各種一致性的定義,還有相應(yīng)的分布式環(huán)境下,可用性、一致性、分區(qū)容錯(cuò)性之間的關(guān)系。很多人對(duì)這里CAP中的一致性有疑惑,是說跟客觀世界保持一致,還是說所有的分布節(jié)點(diǎn)都是都一樣。
首先,這兩者并不沖突,更像是既要又要,只有每個(gè)節(jié)點(diǎn)對(duì)于變化都是一致的,也就是每個(gè)節(jié)點(diǎn)的變化跟客觀時(shí)間的變化預(yù)期保持一致,才能保證整個(gè)系統(tǒng)數(shù)據(jù)的一致性。
而這里促進(jìn)一致的過程,指的就是共識(shí)。
共識(shí)的定義:共識(shí)是指在分布式系統(tǒng)中,通過節(jié)點(diǎn)之間的相互通信和協(xié)調(diào),使得網(wǎng)絡(luò)中的各個(gè)節(jié)點(diǎn)能夠就某個(gè)值或狀態(tài)達(dá)成一致的過程。共識(shí)的目標(biāo)是確保網(wǎng)絡(luò)中的所有節(jié)點(diǎn)對(duì)某個(gè)事實(shí)的認(rèn)同,并保證數(shù)據(jù)的一致性和可信度。
通俗點(diǎn)來說,分布式一致性,通過共識(shí)算法是讓每個(gè)數(shù)據(jù)節(jié)點(diǎn)均發(fā)生變化,到達(dá)一致狀態(tài),并且每個(gè)節(jié)點(diǎn)的變化跟客觀世界的變化保持一致,最終的結(jié)果就是分布式系統(tǒng)變化完,跟單節(jié)點(diǎn)時(shí)一致,并且跟現(xiàn)實(shí)世界保持一致
分布式一致性 = 單節(jié)點(diǎn)一致性 + 一致性變化
共識(shí)算法是一個(gè)促成分布節(jié)點(diǎn)一致的算法,常見的有:Paxos算法、Raft算法、ZAB算法等等。
放個(gè)觀點(diǎn),能讓分布式系統(tǒng)保證強(qiáng)一致的算法只有Paxos算法。
4.3 邏輯拆分下的數(shù)據(jù)一致性保障
當(dāng)我們把數(shù)據(jù)按照水平、垂直切分之后,數(shù)據(jù)節(jié)點(diǎn)被分布了,沒法像單機(jī)一樣直接本地事務(wù)來保證數(shù)據(jù)的強(qiáng)一致不與可用性發(fā)生沖突。比如余額轉(zhuǎn)賬不同的行記錄需要保持一致,比如商品庫存數(shù)據(jù)和訂單數(shù)據(jù)需要保持一致等。
按照CAP理論,一致性、可用性就要被決策。我們選擇可以選擇CP或者AP,或者以BASE理論選擇AP 容忍軟狀態(tài),通過各種手段到達(dá)最終一致性。
4.3.1 強(qiáng)一致策略 — 剛性事務(wù)
對(duì)于分布式環(huán)境下實(shí)現(xiàn)強(qiáng)一致是不準(zhǔn)確的,我們只能無限逼近強(qiáng)一致。原理同本地事務(wù)相對(duì)類似,同樣要實(shí)現(xiàn)原子性、隔離性、持久性來保證數(shù)據(jù)的一致性。
分布式事務(wù)主要也是建立在本地事務(wù)之上的,在本地事務(wù)的基礎(chǔ)上,增加協(xié)調(diào)多個(gè)本地事務(wù)的能力,讓整個(gè)分布式數(shù)據(jù)修改的動(dòng)作能夠?qū)崿F(xiàn)原子性、持久性、隔離性,而整個(gè)的協(xié)調(diào)機(jī)制就是全局的事務(wù)管理器。可以回憶一下本地事務(wù)的協(xié)調(diào)機(jī)制和執(zhí)行過程。
持久性基本是完全依賴于本地事務(wù)實(shí)現(xiàn);隔離性可以粗暴的理解為一個(gè)本地事務(wù)的寫如果依賴其他事務(wù)的數(shù)據(jù),則透過其他事務(wù)進(jìn)行讀,看作是在一個(gè)事務(wù)內(nèi)部;原子性方面除了實(shí)現(xiàn)本地操作的原子性,還要保證多個(gè)數(shù)據(jù)節(jié)點(diǎn)要么全做、要么全不做(單機(jī)建立在undolog之上,而分布式建立在rollback機(jī)制上)
而如果嘗試進(jìn)行抽象,就得到了經(jīng)典的DTP模型,一個(gè)分布式事務(wù)的落地,至少需要應(yīng)用程序、事務(wù)管理器、資源管理器(本地事務(wù)支持者,就是我們的數(shù)據(jù)庫)、通訊管理器。
4.3.1 XA 事務(wù)
接下來如果選擇CP(分區(qū)容錯(cuò) + 一致性),看基于DTP模型落地的其中一種實(shí)現(xiàn) — XA事務(wù)。分布式事務(wù)的關(guān)鍵是事務(wù)管理器,XA事務(wù)的事務(wù)管理器是基于XA協(xié)議落地的。
4.3.1.1 2PC
XA協(xié)議默認(rèn)使用兩階段(2PC)提交來實(shí)現(xiàn)不同本地事務(wù)的之間資源的提交和回滾,把本地事務(wù)分為準(zhǔn)備階段、提交階段兩部分,具體過程是:

2PC的實(shí)現(xiàn)機(jī)制很理想,是建立在網(wǎng)絡(luò)沒有問題的情況下及事務(wù)管理器不故障的前提下的,2PC是沒問題的,有一方異常,就犧牲可用性拒絕請(qǐng)求。但由于決策者完全依賴于事務(wù)管理器,如果事務(wù)管理器掛了,參與者就一致等著了,完全夯死,而且事務(wù)管理器是單點(diǎn)的,一旦故障,整個(gè)系統(tǒng)就全掛了。
再有就是如果commit消息丟失,如果是rollback還好,要是commit就出現(xiàn)數(shù)據(jù)不一致了。
在單機(jī)數(shù)據(jù)分布式問題也是一樣的,前面提到的mysql 2PC來寫binlog、redolog在同步鏈路里,prepare之后,因?yàn)槟承┰蚝蛔×?,redolog就得等著;commit失敗時(shí),也會(huì)產(chǎn)生不一致。只不過能夠檢查binlog和redolog來搞成一致的。并且單機(jī)上這些問題發(fā)生的可能性近乎認(rèn)為不可能了,2PC在相對(duì)可靠的通信環(huán)境、協(xié)調(diào)穩(wěn)定的情況下,是沒有任何問題的。
但是99%的應(yīng)用都是分布在網(wǎng)絡(luò)環(huán)境中的,通信一定是不可靠的,管理器自身故障或者跟管理器鏈接的故障,一定會(huì)發(fā)生。2PC的瑕疵就被放大了。不過可以使用共識(shí)協(xié)議Paxos或Raft保證協(xié)調(diào)者的高可用來解決單點(diǎn)故障,但是網(wǎng)絡(luò)問題,著實(shí)難解。
4.3.1.2 3PC
于是3PC就出現(xiàn)了,主要就是解決單點(diǎn)問題、同步阻塞問題的,把prepare、commit變成了canCommit、preCommit、doCommit三個(gè)階段,并且引入了超時(shí)機(jī)制。
canCommit用來做類似于2PC的prepare數(shù)據(jù)準(zhǔn)備階段,preCommit是事務(wù)的信息的寫入(redolog、undolog等),然后docommit后全部提交,每一輪的繼續(xù)執(zhí)行依賴于上一輪的結(jié)果。

3PC相對(duì)2PC增加了一個(gè)準(zhǔn)備階段,把大部分錯(cuò)誤屏蔽在第一輪,第二輪才真正的鎖定資源并準(zhǔn)備回滾動(dòng)作,避免的前置大量的資源占用;并且增加了對(duì)于參與者的等待超時(shí),如果長(zhǎng)時(shí)間沒有收到來自于事務(wù)管理器的指令,并且檢察事務(wù)管理器掛掉之后,剩余的參與者會(huì)進(jìn)行決策,會(huì)根據(jù)當(dāng)前狀態(tài)進(jìn)行下一步的流轉(zhuǎn)(自我決策),對(duì)齊了接下來的動(dòng)作,這樣就解掉了事務(wù)管理器故障和參與者夯死的問題。
但是在出現(xiàn)網(wǎng)絡(luò)分區(qū)的狀態(tài)下,仍然會(huì)有不一致問題,前置的canCommit、preCommit如果丟失都還好,事務(wù)并沒有生效。但所有事務(wù)都preCommit了,但是某一個(gè)事務(wù)的回執(zhí)丟了,整體決策需要回滾,但網(wǎng)絡(luò)分區(qū)的這個(gè)事務(wù)自決策可以提交,而其他參與者收到了rollback指令,此時(shí)數(shù)據(jù)就不一致了,網(wǎng)絡(luò)分區(qū)狀態(tài)下,3PC是存在較大一致性風(fēng)險(xiǎn)的。
3PC雖然解決了2PC一些阻塞和單點(diǎn)的風(fēng)險(xiǎn),但是一致性問題仍然存在,并且3PC多了一個(gè)階段,整體的協(xié)議復(fù)雜度更高了,性能也會(huì)因?yàn)轭~外多了一次IO而導(dǎo)致降低。就目前業(yè)界實(shí)現(xiàn)來看,用的相對(duì)較少,但是3PC的這種對(duì)于分布式事務(wù)的探索方式,為分布式事務(wù)的落地探明了許多方向和提供了一些可借鑒的經(jīng)驗(yàn)。
4.3.2 最終一致策略– 柔性事務(wù)
同上面的提到的追求強(qiáng)一致事務(wù)的落地不同,很多方案追求的是最終一致,實(shí)現(xiàn)差異上與上面的方案較大。常見的必入有TCC補(bǔ)償型事務(wù)、最大努力通知:比如本地消息表、事務(wù)型消息、又或者同步重試。ps:不用太過糾結(jié)分類,這里是廣義上的最大努力通知。
業(yè)界系統(tǒng)的落地往往都是以最終一致的方式進(jìn)行落地,尤其是最大努力通知更是最常見的實(shí)踐方式。下面直接說結(jié)論,很重要,重點(diǎn)關(guān)注
1: 對(duì)于常規(guī)的業(yè)務(wù),實(shí)時(shí)性要求、一致性要求都沒那么高
,相對(duì)均衡,圍繞業(yè)務(wù)狀態(tài)機(jī)進(jìn)行冪等重試,同步鏈路最大可能保證成功,然后異步對(duì)賬補(bǔ)單就能解決問題了。
2: 對(duì)于實(shí)時(shí)性不高,但一致性要求比較高
?的業(yè)務(wù)可以使用事務(wù)型消息中間件,做好冪等重試,成本極低。要是實(shí)時(shí)性更高一點(diǎn)就試試本地消息表的方式。
3: 對(duì)于實(shí)時(shí)型要求相對(duì)較高,一致性要求比較高
?的業(yè)務(wù)可以使用本地消息表的方式,盡可能快的推進(jìn)業(yè)務(wù)狀態(tài)。
4: 對(duì)于實(shí)時(shí)性、一致性要求都比較高
,要么快點(diǎn)成功、要么快點(diǎn)失敗,但得保證一致性,TCC就是一個(gè)不錯(cuò)的選擇。
5: 對(duì)于實(shí)時(shí)性、一致性要求都極高
,不妨去試試超級(jí)計(jì)算機(jī)解決方案,別搞什么分布式事務(wù)了,如果膽大點(diǎn),成熟的2PC解決方案也可一試。
之前寫過的一個(gè)業(yè)務(wù)中間件(業(yè)務(wù)事件總線)的設(shè)計(jì)分享,整體一致性保障思路,就是基于最大努力通知-簡(jiǎn)易實(shí)現(xiàn) + 本地消息表(考慮到性能做了下變種,先嘗試通知,然后記錄失敗,圍繞失敗記錄重試)、事務(wù)型消息多組組合模式落地的,可以根據(jù)業(yè)務(wù)場(chǎng)景選擇不同的方式。而之前在螞蟻?zhàn)鲋Ц兜臅r(shí)候,用的XTS是基于TCC落地的。
4.3.2.1 最大努力通知 — 簡(jiǎn)易實(shí)現(xiàn)
就一句話:圍繞業(yè)務(wù)狀態(tài)機(jī)進(jìn)行冪等重試,同步鏈路最大可能保證成功,然后異步對(duì)賬補(bǔ)單就能解決問題。相信我,如果不是做交易、做支付,夠用了。

4.3.2.2 最大努力通知 — 事務(wù)型消息
消息本身會(huì)幾個(gè)狀態(tài),能保證如果消費(fèi)不成功,則一直消費(fèi)。這樣就做到了能把一個(gè)事務(wù)內(nèi)需要處理的動(dòng)作全部推成功。比如使用rocketMQ,可以看下具體過程:

這種方式需要保證下游在業(yè)務(wù)上一定是可成功的,只能屏蔽技術(shù)錯(cuò)誤導(dǎo)致的失敗。最好把條件動(dòng)作作為發(fā)起方,比如扣款后訂單狀態(tài)變更,比如扣庫存后發(fā)放獎(jiǎng)勵(lì);或者發(fā)起方需要有定期校驗(yàn)機(jī)制,對(duì)業(yè)務(wù)數(shù)據(jù)進(jìn)行兜底,也就是對(duì)賬補(bǔ)單。
并且由于是基于消息隊(duì)列落地的,實(shí)效性上會(huì)比較差。
這個(gè)方式本質(zhì)上2PC思路保證消息一定提交成功,然后最大努力通知消費(fèi)方(事務(wù)參與方)。
4.3.2.3 最大努力通知 — 本地消息表機(jī)制
這個(gè)也跟mysql寫redolog保障多節(jié)點(diǎn)數(shù)據(jù)一致是一樣的思路(WAL,日志是本地消息表,數(shù)據(jù)是具體事務(wù)動(dòng)作),只不過一個(gè)是在本地環(huán)境下,一個(gè)是在網(wǎng)絡(luò)環(huán)境。把分布式事務(wù)轉(zhuǎn)變?yōu)楸镜厥聞?wù),利用本地事務(wù)的一致性保證操作絕對(duì)寫成功。
本地消息表的機(jī)制也更像是自己實(shí)現(xiàn)了一個(gè)rocketMQ的能力,只不過把這部分能力做了一定的簡(jiǎn)化并且放到本地了,然后本地事務(wù)中多了一張本地消息表,一個(gè)事務(wù)內(nèi)處理分布式事務(wù)發(fā)起方的內(nèi)部邏輯、本地消息表的寫入,就不用2PC提交了。其他的原理都一樣,保證通知成功就好了,可以按照一致性的實(shí)效要求,同步通知或者定時(shí)任務(wù)按照一定頻率通知更新,不斷流轉(zhuǎn)事務(wù)處理的全局狀態(tài)機(jī)(虛擬的)。

同樣需要保證下游是業(yè)務(wù)可成功的,也需要進(jìn)行對(duì)賬進(jìn)行數(shù)據(jù)校驗(yàn),做一致性兜底。
并且由于是在本地事務(wù)中處理,會(huì)有單表的上限,可以適當(dāng)?shù)那斜?,但是單機(jī)上限就無能為力了。
4.3.2.4 TCC 補(bǔ)償性事務(wù)
TCC是一種同步鏈路分布式事務(wù)的落地方式,實(shí)現(xiàn)方式跟2PC的思想也比較像,但是不像2PC、3PC這樣要實(shí)現(xiàn)嚴(yán)格遵循ACID要求的事務(wù)(剛性事務(wù)),而是基于BASE理論落地的一種柔性事務(wù),允許軟狀態(tài)的發(fā)生,最終一致即可;并且2PC、3PC是站在數(shù)據(jù)庫層面來進(jìn)行實(shí)踐落地的,TCC是站在應(yīng)用層出發(fā)去落地的。

相對(duì)于通知型事務(wù),TCC是一種補(bǔ)償機(jī)制,更適用于快速成功、快速失敗的場(chǎng)景。這樣的方式可以貼近業(yè)務(wù)去做數(shù)據(jù)一致性的保障,雖然對(duì)業(yè)務(wù)落地有一定的侵入性,但是對(duì)于事務(wù)本身,由于是從業(yè)務(wù)出發(fā)的,并且是追求最終一致,反而落地的效果更好。
接下來看下TCC的實(shí)現(xiàn)思路和執(zhí)行過程:
4.4 犧牲一致性拆分下的一致性保障
按照CAP理論,對(duì)于需要強(qiáng)一致不可再拆的數(shù)據(jù),我們進(jìn)行了強(qiáng)拆或復(fù)制,導(dǎo)致其變成了多個(gè)真實(shí)數(shù)據(jù)節(jié)點(diǎn) + 邏輯數(shù)據(jù),犧牲了一致性換取了有效吞吐的提升(可用性),這種場(chǎng)景下,要保證的一致性是子數(shù)據(jù)節(jié)點(diǎn)和邏輯數(shù)據(jù)節(jié)點(diǎn)的一致性,只要有可靠的數(shù)據(jù)同步機(jī)制、數(shù)據(jù)規(guī)集機(jī)制,數(shù)據(jù)最終肯定能達(dá)成一致,最關(guān)鍵的是怎么去處理這個(gè)軟狀態(tài),讓業(yè)務(wù)可接受。
4.4.1 關(guān)于軟狀態(tài)
軟狀態(tài)是指在到達(dá)最終一致前,數(shù)據(jù)的中間狀態(tài),而這個(gè)狀態(tài)是邏輯上不一致的。但不一致是可以按照不一致的情況來細(xì)分的,比如計(jì)算型數(shù)據(jù):數(shù)據(jù)讀到了少的、數(shù)據(jù)讀到了多的。比如說按時(shí)間分類:過早的讀出了新數(shù)據(jù)、舊數(shù)據(jù)遲遲未更新。
我們是可以按照業(yè)務(wù)傾向去選擇具體的不一致的,所以關(guān)于這個(gè)軟狀態(tài)是可以有業(yè)務(wù)偏向性實(shí)現(xiàn)的,讓這個(gè)軟,硬一點(diǎn),需要根據(jù)具體的業(yè)務(wù)場(chǎng)景來進(jìn)行合理的設(shè)計(jì),不一致業(yè)務(wù)友好。

拿主庫存-子庫存舉例:
扣減場(chǎng)景,不允許超發(fā),那就按照實(shí)際庫存進(jìn)行切分,請(qǐng)求到來時(shí),可能邏輯上仍有庫存,但請(qǐng)求進(jìn)行處理時(shí),當(dāng)前分片已無庫存,邏輯上傾向于多占庫存。
展示場(chǎng)景,請(qǐng)求到來時(shí),不需要展示當(dāng)前的實(shí)時(shí)庫存,展示的相對(duì)多一點(diǎn)問題不大,邏輯上傾向于庫存少占。
拿主賬務(wù)-子賬務(wù)舉例:
大量收款場(chǎng)景,寫多讀少,對(duì)于展示,用戶應(yīng)該馬上看到當(dāng)前的余額和流水,那每次展示的時(shí)候就做一次實(shí)時(shí)規(guī)集進(jìn)行展示,要展示最新的(如果告知用戶入賬延遲,展示老的也沒問題)。
對(duì)于寫入操作,主賬務(wù)和子賬務(wù)是否一致無所謂。
拿評(píng)論寫入熱點(diǎn)舉例:
寫入一條評(píng)論,進(jìn)入其中一個(gè)寫入隊(duì)列,全局?jǐn)?shù)據(jù)未更新問題不大,但個(gè)人寫入記錄需要立即展示,這里全局(對(duì)其他用戶)展示老的,對(duì)個(gè)人展示新的即可。
拿榜單讀熱點(diǎn)舉例:
榜單更新后,由于讀數(shù)據(jù)復(fù)制多份,又是無狀態(tài)請(qǐng)求,榜單分?jǐn)?shù)一會(huì)兒增一會(huì)兒減不可接受,更新過程中讀老數(shù)據(jù),指定時(shí)間點(diǎn)全局生效。
4.4.2 弱一致性要求的甄別
要做上述的設(shè)計(jì),我們最核心的是要知道對(duì)于數(shù)據(jù)的一致性要求,這個(gè)數(shù)據(jù)最終會(huì)被用來干啥,計(jì)算?展示?分析?記錄?
這部分需要深入業(yè)務(wù)去考慮,說幾個(gè)對(duì)C相關(guān)的:用戶所見即所得、變化基于現(xiàn)實(shí)世界的時(shí)間軸、即寫即讀。
5.多活架構(gòu)下的分布式數(shù)據(jù)庫系統(tǒng)

前面從單機(jī)上的磁盤讀寫,再到針對(duì)操作系統(tǒng)的文件讀寫,最后到應(yīng)用層數(shù)據(jù)庫mysql的實(shí)踐落地,再到數(shù)據(jù)切分之后分布式環(huán)境中集中式存儲(chǔ)的使用,接下來看下在多活架構(gòu)下的分布式存儲(chǔ) — 分布式數(shù)據(jù)庫系統(tǒng)。
集中式數(shù)據(jù)庫,指的是將數(shù)據(jù)集中存儲(chǔ)在單一節(jié)點(diǎn)上,雖然拆表、拆庫了,但是對(duì)于一個(gè)原子數(shù)據(jù)(比如一行)來說仍然只有一個(gè)主Server進(jìn)行其存儲(chǔ)和操作,也就是常說的單主。

而分布式數(shù)據(jù)庫,則是將數(shù)據(jù)在多個(gè)節(jié)點(diǎn)進(jìn)行分散存儲(chǔ),每個(gè)節(jié)點(diǎn)就是一個(gè)集中式存儲(chǔ),然后多個(gè)節(jié)點(diǎn)之間按照一定的規(guī)則進(jìn)行組織和訪問,對(duì)于一個(gè)原子數(shù)據(jù)來說,會(huì)有多個(gè)主Server進(jìn)行存儲(chǔ)和操作,也就是常說的多主。
在上一篇章,講集中式存儲(chǔ)時(shí)就提到了這種模式,當(dāng)時(shí)說把數(shù)據(jù)做復(fù)制仍然突破不了流量極限,因?yàn)閷?duì)于單機(jī)來說“一致性流量 + 實(shí)際流量”仍然相當(dāng)于單機(jī)去扛所有流量,并且還浪費(fèi)了這么多機(jī)器去承接復(fù)制數(shù)據(jù)。
但其實(shí)在實(shí)踐過程中,尤其是在具體的某一業(yè)務(wù)場(chǎng)景下,會(huì)出現(xiàn)時(shí)間、空間上的差值,只要我們?cè)谟玫臅r(shí)候把數(shù)據(jù)搞一致就好了,這樣就可以根據(jù)地理位置跨度、時(shí)間差等信息做數(shù)據(jù)復(fù)制分布和數(shù)據(jù)同步。單機(jī)的流量就變成了“少量一致性流量 + 實(shí)際流量”。
突破單機(jī)極限只是一方面,我們更多的時(shí)候做多活并不是為了突破單機(jī)上的極限(做多),更多的時(shí)候是為了系統(tǒng)的時(shí)延縮短、容災(zāi)處理(做活)
多活架構(gòu)下,整個(gè)系統(tǒng)的可擴(kuò)展性、容錯(cuò)能力、可用性都被提升了,但是相應(yīng)的整體系統(tǒng)也變的更加復(fù)雜了。
5.1 兩地三中心 & 異地多活
兩地三中心,本質(zhì)上是一種容災(zāi)架構(gòu),IDC機(jī)房本地+異地、數(shù)據(jù)中心本地主數(shù)據(jù)中心 + 本地備份數(shù)據(jù)中心 + 異地備份數(shù)據(jù)中心。這樣就具備了機(jī)房級(jí)的容災(zāi),但不具備全部城市級(jí)的容災(zāi)能力。在故障時(shí)通過數(shù)據(jù)節(jié)點(diǎn)選主完成故障切換。

這種架構(gòu)下通常我們的服務(wù)會(huì)做雙活(偽活),比如說同城雙活或者異地雙活,但是背后的數(shù)據(jù)時(shí)單主的只做了相應(yīng)的災(zāi)備功能,數(shù)據(jù)庫寫數(shù)據(jù)是存在跨機(jī)房調(diào)用的,但是對(duì)于我們的常規(guī)業(yè)務(wù),可用性/容災(zāi)要求并沒有那么高的業(yè)務(wù)來看,足夠了。
但是對(duì)于時(shí)延要求極高、容災(zāi)要求極高的場(chǎng)景,兩地三中心的模式已經(jīng)不夠用了,通常需要把數(shù)據(jù)庫也進(jìn)行相應(yīng)的拆分,和服務(wù)一起構(gòu)成邏輯意義上的活節(jié)點(diǎn)(單元化能力),單元化的產(chǎn)物是LDC-邏輯數(shù)據(jù)中心。

在多活架構(gòu)下,提供服務(wù)時(shí),活節(jié)點(diǎn)之間是不會(huì)進(jìn)行通信的,自己搞自己的,有多活的調(diào)度機(jī)制,負(fù)責(zé)進(jìn)行流量的分發(fā)和數(shù)據(jù)的一致性保障,在故障時(shí)把故障流量切入其他的活節(jié)點(diǎn)即可。三地五中心就是一種落地實(shí)踐,三個(gè)城市,4個(gè)常駐服務(wù)單元化加一個(gè)單元,提供城市級(jí)容災(zāi),并且基于就近訪問策略提升響應(yīng)。
5.2 GZONE RZONE CZONE
并不是所有服務(wù)都需要做單元化部署的,按照數(shù)據(jù)的類型,比如可以分為全局共享數(shù)據(jù)、用戶類數(shù)據(jù)、用戶間共享數(shù)據(jù),依賴這些數(shù)據(jù)再加上對(duì)應(yīng)的服務(wù)可以大致劃分為GZONE RZONE CZONE

GZone(Global Zone):全局單元,意味著全局只有一份。部署了不可拆分的數(shù)據(jù)和服務(wù),比如系統(tǒng)配置等。
CZone(City Zone):顧名思義,這是以城市為單位部署的單元。同樣部署了不可拆分的數(shù)據(jù)和服務(wù),比如用戶賬號(hào)服務(wù),客戶信息服務(wù)等,基于“寫讀時(shí)間差現(xiàn)象”,把全局?jǐn)?shù)據(jù)整了多個(gè)副本。
RZone(Region Zone):最符合理論上單元定義的 zone,每個(gè) RZone 都是自包含的,擁有自己的數(shù)據(jù),能完成所有業(yè)務(wù)。
有沒有發(fā)現(xiàn)GZONE、CZONE很像是之前提到的犧牲數(shù)據(jù)一致的思路,其實(shí)本質(zhì)上就是這個(gè),只不過做了單元化處理并結(jié)合部署架構(gòu),讓這套方案更標(biāo)準(zhǔn)化了。
5.2 多活的核心是RZONE
當(dāng)GZONE 負(fù)責(zé)全局信息寫入,CZONE提供就近讀能力之后,全局?jǐn)?shù)據(jù)的讀寫效率就提上來了,而對(duì)于大部分應(yīng)用來說,用戶狀態(tài)型相關(guān)的數(shù)據(jù)才是最需要進(jìn)行做活的。
RZone 每一個(gè)單元都有一個(gè)用戶維度的邏輯主庫,既承擔(dān)了水平分庫的流量sharding的能力,同時(shí)每個(gè)RZone之間是互備的,當(dāng)一個(gè)zone掛掉之后,是可以把流量且到其他zone而可以進(jìn)行正常讀寫的,每個(gè)zone擁有全量的用戶態(tài)數(shù)據(jù),但是只有一個(gè)zone對(duì)外提供服務(wù)。

這個(gè)機(jī)制是基于數(shù)據(jù)復(fù)制策略實(shí)現(xiàn)的,并且在這種體系下,我們可以動(dòng)態(tài)的掛載新的zone 實(shí)現(xiàn)狀態(tài)數(shù)據(jù)的橫向擴(kuò)容(彈性伸縮)。
mysql 本身是不支持這種機(jī)制的,所以就誕生了新一代應(yīng)用:
分布式數(shù)據(jù)庫
,比如OceanBase、TDSQL等等,提供了多主的能力(多節(jié)點(diǎn)讀寫)
真的就這么強(qiáng)嘛,一點(diǎn)問題都沒有?
不過就之前的做支付的經(jīng)歷來看,彈性伸縮、故障恢復(fù)時(shí)的一致性考驗(yàn)還是相當(dāng)強(qiáng)的,由于要求每一筆都不能錯(cuò),但是我們對(duì)于分布式數(shù)據(jù)庫的使用并沒有那么極致和標(biāo)準(zhǔn),流水型數(shù)據(jù)還好,如果是狀態(tài)型數(shù)據(jù),狀態(tài)丟失、狀態(tài)延遲等問題還是會(huì)誘發(fā)較多的問題。
5.3 OceanBase 實(shí)現(xiàn)原理
OceanBase是螞蟻?zhàn)匝械囊豢罘植际綌?shù)據(jù)庫,并且提供mysql、oracle兩種不同的租戶模式,自身通過LSM存儲(chǔ)結(jié)構(gòu)實(shí)現(xiàn),好奇的話可以看看之前寫的LevelDB的實(shí)現(xiàn)
這里核心要說的ob是怎么實(shí)現(xiàn)多主的,ob里面還提供了很多高性能的操作,比如高效本地事務(wù)、高效分布式事務(wù)、SQL優(yōu)化等等能力,有興趣可以看看ob的官方文檔, 或者OceanBase文檔收集
OceanBase 數(shù)據(jù)庫以分區(qū)為單位組建 Paxos協(xié)議組。每個(gè)分區(qū)都有多份副本,通過維護(hù)成員組關(guān)系,自動(dòng)建立 Paxos組。同時(shí)在以分區(qū)為單位的 Paxos 協(xié)議組的基礎(chǔ)上,自動(dòng)選舉主副本。然后不同的主副本是存在于多個(gè)zone之中的,Paxos 協(xié)議組成員通過redolog的多數(shù)派強(qiáng)同步來確保數(shù)據(jù)的持久化,具體來看:

復(fù)制時(shí)使用日志流(LS、Log Stream)在多副本之間同步狀態(tài)。每個(gè) Tablet 都會(huì)對(duì)應(yīng)一個(gè)確定的日志流,DML 操作寫入 Tablet 的數(shù)據(jù)所產(chǎn)生的 Redo 日志會(huì)持久化在日志流中。日志流的多個(gè)副本會(huì)分布在不同的可用區(qū)中,多個(gè)副本之間維持了共識(shí)算法,選擇其中一個(gè)副本作為主副本,其他的副本皆為從副本。Tablet 的 DML 和強(qiáng)一致性查詢只在其對(duì)應(yīng)的日志流的主副本上進(jìn)行。
日志流使用自研的 Paxos 協(xié)議實(shí)現(xiàn)了將 Redo 日志在本服務(wù)器持久化,同時(shí)通過網(wǎng)絡(luò)發(fā)送給日志流的從副本,從副本在完成各自持久化后應(yīng)答主副本,主副本在確認(rèn)有多數(shù)派副本都持久化成功后確認(rèn)對(duì)應(yīng)的 Redo 日志持久化成功。從副本利用 Redo 日志的內(nèi)容實(shí)時(shí)回放,保證自己的狀態(tài)與主副本一致。
OccanBase主備同步也允許配置為異步模式,支持最終一致性,這種模式一般用來支持異地容災(zāi),有概率丟數(shù)據(jù)。
除此之外,oceanbase 對(duì)于分布式事務(wù)支持強(qiáng)一致的分布式事務(wù),使用兩階段提交協(xié)議來實(shí)現(xiàn),并將將 Paxos 分布式一致性協(xié)議引入到兩階段提交,使得分布式事務(wù)具備自動(dòng)容錯(cuò)能力。
5.4 TDSQL 實(shí)現(xiàn)原理
OceanBase是一種自研的數(shù)據(jù)庫,然后向下兼容了mysql、oracle等,那直接基于mysql做不出來分布式數(shù)據(jù)庫嗎,答案是可以,又不少公司就是這么實(shí)踐的,畢竟完完全全新寫一個(gè)DB成本太高了,但是新寫也有自己的優(yōu)勢(shì),整個(gè)應(yīng)用完全可控&歷史包袱較小。
騰訊的TDSQL就是基于mysql來進(jìn)行分布式數(shù)據(jù)庫實(shí)踐落地的,做了一些適配工作,包括存儲(chǔ)集群化、按需自動(dòng)伸縮、自動(dòng)擴(kuò)容、極致性能優(yōu)化等。
多主的實(shí)現(xiàn)跟oceanbase比較像,同樣是多節(jié)點(diǎn)可提供服務(wù),但是常駐單節(jié)點(diǎn)提供服務(wù),然后節(jié)點(diǎn)之間實(shí)現(xiàn)數(shù)據(jù)復(fù)制同步,然后復(fù)制機(jī)制是基于原聲mysql的binlog復(fù)制能力的。
詳細(xì)了解可參照:騰訊TEG團(tuán)隊(duì)原創(chuàng):基于MySQL的分布式數(shù)據(jù)庫TDSQL十年鍛造經(jīng)驗(yàn)分享
6.回過頭來總結(jié)下啟發(fā)吧

存儲(chǔ)這東西,是真的好復(fù)雜呀,專業(yè)的事情,交給專業(yè)的人去做哈,千萬別自己完花活,log、logstream、mysql、levelDB、mongoDB、neo4j、oceanbase 是真香呀
磁盤,機(jī)械磁盤慢,固態(tài)磁盤夠快但是穩(wěn)定性似乎沒那么好(但也不至于拉胯),有錢,就上固態(tài)硬盤吧。
文件讀寫時(shí),我們需要兼顧性能和數(shù)據(jù)寫入的可靠性,多熟悉幾個(gè)系統(tǒng)調(diào)用及其內(nèi)部實(shí)現(xiàn),沒啥壞處。
順序讀寫、磁盤讀寫性能差異真的很大,尤其是機(jī)械硬盤的場(chǎng)景,尋道時(shí)間太夸張了。
linux是有文件描述符上限的,要額外注意;還有別并發(fā)寫,會(huì)有問題的。
除了磁盤、文件系統(tǒng)的合理使用,磁盤頁的組織形式相當(dāng)重要,好的數(shù)據(jù)結(jié)構(gòu)能幫你減少IO,比如B+樹、LSM等等
當(dāng)面對(duì)并發(fā)的時(shí)候,首先別并發(fā),真需要并發(fā),如果不知道怎么整了,直接掛鎖串行,扛不???那就異步削峰,不讓削?那能直接拒絕流量嗎?
如果要有持久性保證,那就WAL,但是要注意部分寫問題。
如果要控“分布”的數(shù)據(jù)節(jié)點(diǎn),經(jīng)典的2PC是一個(gè)不錯(cuò)的解決方案。
要做原子性,就要支持完整且可靠的回滾機(jī)制,記錄修改前的版本,并且對(duì)于這份記錄做持久性保障。
要做事務(wù)動(dòng)作,持久性(解決不丟)、原子性(支持回滾)、隔離性(并發(fā)下的控制) 缺一不可。
并發(fā)問題不好解了,要么不要并發(fā),直接串行(上鎖),或者以串行的方式去并發(fā)(數(shù)據(jù)不共享)。
犧牲可靠性,使用緩沖批次讀寫,對(duì)于吞吐的提升真的很大,規(guī)避IO時(shí),相當(dāng)好用。
順序讀寫、隨機(jī)讀寫性能差異很大,尤其是在大量IO的場(chǎng)景下,整體的差異會(huì)更大,WAL 中日志順序?qū)懀瑪?shù)據(jù)文件異步寫,然后寫入加緩沖,性能比次次fsync要快太多太多。
要寫好where,得對(duì)索引門清,能用主鍵查的就用主鍵。
盡可能避免null,會(huì)導(dǎo)致索引、索引統(tǒng)計(jì)、值比較很麻煩,然后要做索引,區(qū)分度最好高一些。
范圍查詢會(huì)導(dǎo)致索引失效,所以優(yōu)先使用 = in等操作,避免失效,記住最左。
優(yōu)化索引是能修改當(dāng)前的,就不要新增新的索引,會(huì)增加維護(hù)成本和速度(索引頁會(huì)多,插入緩沖的消費(fèi)也會(huì)更慢),并且需要更大的空間。
推薦使用聯(lián)合索引,減少輔助索引的數(shù)量,一次輔助索引查找就找到主鍵值。
where 子句中對(duì)字段進(jìn)行表達(dá)式操作或者函數(shù)操作,都會(huì)導(dǎo)致引擎放棄使用索引而進(jìn)行全表掃描
做中間件使用的時(shí)候,知道其大致實(shí)現(xiàn),才能更好的使用,很多時(shí)候設(shè)計(jì)者真的沒辦法把用戶當(dāng)小白。
做數(shù)據(jù)存儲(chǔ),能用中間件、就用中間件,別自己瞎整。
當(dāng)碰到極限了就拆分,水平拆、垂直拆 拆到扛得住為準(zhǔn),要是有熱點(diǎn),那就按流量性質(zhì)在拆,做讀寫分離,讀數(shù)據(jù)復(fù)制拆分,寫數(shù)據(jù) 做子節(jié)點(diǎn)、子通道
分布式下保證數(shù)據(jù)的一致性真的很難,做不了強(qiáng)一致,就試試最終一致,柔性事務(wù)挺香的。
CAP理論很重要,很多時(shí)候?yàn)榱藫Q取性能(有效吞吐)可以適當(dāng)?shù)臓奚恢滦?。為了一致性,可用性必然受影響?/p>
BASE理論對(duì)于日常幾乎所有的應(yīng)用都絕對(duì)夠用了。
一致性一定要根據(jù)具體的業(yè)務(wù)場(chǎng)景來判定,很多場(chǎng)景下你并不需要強(qiáng)一致,甚至軟狀態(tài)長(zhǎng)期存在也是沒問題的。
如何低成本實(shí)現(xiàn)分布式事務(wù),最大努力通知是一個(gè)不錯(cuò)的選擇。
分布式數(shù)據(jù)庫能更好的幫你落地多活架構(gòu),就現(xiàn)在來看,現(xiàn)成解決方案還是很多的。
一定要考慮下,你真的需要多活架構(gòu)或者分布式數(shù)據(jù)庫嗎?
多寫幾個(gè)demo試試,再看看官方文檔,比看漫天復(fù)制的文章強(qiáng)得多。
7.寫在最后
這篇文章寫的是真過癮啊,比上一篇寫網(wǎng)絡(luò)更過癮。
原本是想把腦海中的知識(shí)框架以文章的方式呈現(xiàn)給大家,但是寫的過程中不斷查缺補(bǔ)漏,并深究了太多次 “為什么這么實(shí)現(xiàn)”。致使自己因?yàn)檫@幾篇文章、因?yàn)檫@些“為什么”,實(shí)實(shí)在在的打開了計(jì)算機(jī)視野的另一扇大門。之前頂多面試前梳理個(gè)大致概念,知道大致的實(shí)現(xiàn)原理,與存儲(chǔ)這個(gè)領(lǐng)域?qū)嵸|(zhì)上隔了一堵墻,雖然本篇文章深入程度也不夠,但是已經(jīng)基本跨過那堵墻了,讓很多事情,從未知的未知,變成了已知的未知。
技術(shù)是真的奇妙呀,多少人付出了多少青春去搞定的這些事情,才能讓我們現(xiàn)在的應(yīng)用如此的方便。忍不住致敬。
最后再回頭看一下這張圖,是不是都完全清晰了


本文使用?文章同步助手?同步