關(guān)系型數(shù)據(jù)庫(kù)工作原理簡(jiǎn)述
在我撰寫(xiě) High-Performance Java Persistence training 一書(shū)時(shí),我逐步認(rèn)識(shí)到讓讀者明白關(guān)系型數(shù)據(jù)庫(kù)工作原理會(huì)是了解如何構(gòu)建高性能的 Java 持久化存儲(chǔ)的重要基石。不過(guò)關(guān)系型數(shù)據(jù)庫(kù)中的事務(wù)相關(guān)的重要概念:原子性、持久性以及檢查點(diǎn)等等也是相當(dāng)繞人。而本文中我希望以相對(duì)高屋建瓴的方式來(lái)解釋關(guān)系型數(shù)據(jù)庫(kù)內(nèi)部工作原理,也會(huì)涉及一些數(shù)據(jù)庫(kù)實(shí)現(xiàn)細(xì)節(jié)。
Data Pages
訪問(wèn)磁盤(pán)中的數(shù)據(jù)往往速度較慢,換言之,內(nèi)存中數(shù)據(jù)的訪問(wèn)速度還是遠(yuǎn)快于 SSD 中的數(shù)據(jù)訪問(wèn)速度。基于這個(gè)考量,基本上所有數(shù)據(jù)庫(kù)引擎都盡可能地避免訪問(wèn)磁盤(pán)數(shù)據(jù)。并且無(wú)論數(shù)據(jù)庫(kù)表還是數(shù)據(jù)庫(kù)索引都被劃分為了固定大小的數(shù)據(jù)頁(yè)(譬如 8 KB)。當(dāng)我們需要讀取表或者索引中的數(shù)據(jù)時(shí),關(guān)系型數(shù)據(jù)庫(kù)會(huì)將磁盤(pán)中的數(shù)據(jù)頁(yè)映射入存儲(chǔ)緩沖區(qū)。當(dāng)我們需要修改數(shù)據(jù)時(shí),關(guān)系型數(shù)據(jù)庫(kù)首先會(huì)修改內(nèi)存頁(yè)中的數(shù)據(jù),然后利用 fsync) 這樣的同步工具將改變同步回磁盤(pán)中。
Undo log
由于同時(shí)可能由多個(gè)事務(wù)并發(fā)地對(duì)內(nèi)存中的數(shù)據(jù)進(jìn)行修改,因此關(guān)系型數(shù)據(jù)庫(kù)往往需要依賴(lài)于某個(gè)并發(fā)控制機(jī)制(2PL 或者 MVCC)來(lái)保證數(shù)據(jù)一致性。因此,當(dāng)某個(gè)事務(wù)需要去更改數(shù)據(jù)表中某一行時(shí),未提交的改變會(huì)被寫(xiě)入到內(nèi)存數(shù)據(jù)中,而之前的數(shù)據(jù)會(huì)被追加寫(xiě)入到 undo log 文件中。
Oracle 或者 MySQL 中使用了所謂 undo log 數(shù)據(jù)結(jié)構(gòu),而SQL Server 中則是使用 transaction log 完成此項(xiàng)工作。PostgreSQL 并沒(méi)有 undo log,不過(guò)其內(nèi)建支持所謂多版本的表數(shù)據(jù),即同一行的數(shù)據(jù)可能同時(shí)存在多個(gè)版本。總而言之,任何關(guān)系型數(shù)據(jù)庫(kù)都采用的類(lèi)似的數(shù)據(jù)結(jié)構(gòu)都是為了允許回滾以及數(shù)據(jù)的原子性。
如果當(dāng)前運(yùn)行的事務(wù)發(fā)生了回滾,undo log 會(huì)被用于重建事務(wù)起始階段時(shí)候的內(nèi)存頁(yè)。
Redo Log
某個(gè)事務(wù)提交之后,內(nèi)存中的改變就需要同步到磁盤(pán)中。不過(guò)并不是所有的事務(wù)提交都會(huì)立刻觸發(fā)同步,過(guò)高頻次的同步反而會(huì)對(duì)應(yīng)用性能造成損傷。不過(guò)根據(jù) ACID 原則,提交之后的事務(wù)必須要保證持久性,也就是即使此時(shí)數(shù)據(jù)庫(kù)引擎宕機(jī)了,提交之后的更改也應(yīng)該被持久化存儲(chǔ)下來(lái)。這里關(guān)系型數(shù)據(jù)庫(kù)就是依靠 redo log 來(lái)達(dá)成這一點(diǎn),它是一個(gè)僅允許追加寫(xiě)入的基于磁盤(pán)的數(shù)據(jù)結(jié)構(gòu),它會(huì)記錄所有尚未執(zhí)行同步的事務(wù)操作。相較于一次性寫(xiě)入固定數(shù)目的數(shù)據(jù)頁(yè)到磁盤(pán)中,順序地寫(xiě)入到 redo log 會(huì)比隨機(jī)訪問(wèn)快上很多。因此,關(guān)于事務(wù)的 ACID 特性的保證與應(yīng)用性能之間也就達(dá)成了較好的平衡。該數(shù)據(jù)結(jié)構(gòu)在 Oracle 與 MySQL 中就是叫 redo log,而 SQL Server 中則是由 transaction log 執(zhí)行,在 PostgreSQL 中則是使用 Write-Ahead Log( WAL )。
下面我們繼續(xù)回到上面的那個(gè)問(wèn)題,應(yīng)該在何時(shí)將內(nèi)存中的數(shù)據(jù)寫(xiě)入到磁盤(pán)中。關(guān)系型數(shù)據(jù)庫(kù)系統(tǒng)往往使用檢查點(diǎn)來(lái)同步內(nèi)存的臟數(shù)據(jù)頁(yè)與磁盤(pán)中的對(duì)應(yīng)部分。為了避免 IO 阻塞,同步過(guò)程往往需要等待較長(zhǎng)的時(shí)間才能完成。因此,關(guān)系型數(shù)據(jù)庫(kù)需要保證即使在所有內(nèi)存臟頁(yè)同步到磁盤(pán)之前引擎就崩潰的時(shí)候不會(huì)發(fā)生數(shù)據(jù)丟失。同樣地,在每次數(shù)據(jù)庫(kù)重啟的時(shí)候,數(shù)據(jù)庫(kù)引擎會(huì)基于 redo log 重構(gòu)那些***一次成功的檢查點(diǎn)以來(lái)所有的內(nèi)存數(shù)據(jù)頁(yè)。
總結(jié)
上面我們簡(jiǎn)要討論的這些原則與考慮都是為了保證基于磁盤(pán)的存儲(chǔ)的較高吞吐量的同時(shí)保證數(shù)據(jù)一致性。其中,undo lo 主要用于提供原子性(允許回滾),而 redo log 則是保證磁盤(pán)頁(yè)的不可變性。
【本文是51CTO專(zhuān)欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)51CTO與作者聯(lián)系】