放了方便描述,本問(wèn)題討論的是都是page-oriented系統(tǒng)。道理都是一樣的,其他類型的系統(tǒng)也適用。在事務(wù)里,有兩個(gè)最重要的特性:
- 原子性:原子性保證了事務(wù)的多個(gè)操作要么都生效要么都不生效,不會(huì)存在中間狀態(tài)
- 持久性:持久性保證了一旦事務(wù)生效,就不會(huì)再因?yàn)槿魏卧蚨鴮?dǎo)致其修改的內(nèi)容被撤銷或丟失
眾所周知,數(shù)據(jù)必須要成功寫入硬盤等持久化存儲(chǔ)器后才能擁有持久性。實(shí)現(xiàn)原子性和持久性的最大困難是“寫入硬盤”這個(gè)操作并不是原子的,不僅有“寫入”與“未寫入”狀態(tài),還客觀地存在著“正在寫”的中間狀態(tài)。所以如果不做額外保障措施,只是簡(jiǎn)單把內(nèi)存中的數(shù)據(jù)寫入磁盤,并不能保證原子性與持久性。這個(gè)中間態(tài),大致可以歸納成3種情形:
- 未提交事務(wù),部分持久化,程序崩潰:例如修改5個(gè)數(shù)據(jù),已經(jīng)修改了3個(gè),其中2個(gè)已經(jīng)持久化。程序崩潰后重啟,因?yàn)槭聞?wù)未提交,需要把持久化后的數(shù)據(jù)恢復(fù)回去。
- 已提交事務(wù),部分持久化,程序崩潰:例如修改5個(gè)數(shù)據(jù),已經(jīng)修改完,返回調(diào)用方成功,數(shù)據(jù)未持久化。程序就崩潰后重啟,需要把丟失的變更重做回來(lái)。
- 已提交事務(wù),完全沒(méi)持久化,程序崩潰:同2。
我們可以看到,這些問(wèn)題可以歸納成2個(gè)問(wèn)題:
- 把臟數(shù)據(jù)恢復(fù)成之前的數(shù)據(jù)
- 把丟失的數(shù)據(jù)恢復(fù)回來(lái)
為了解決這兩個(gè)問(wèn)題,有3個(gè)流派,我們每個(gè)流派都說(shuō)一下
- Shadow Paging
- Commit Logging
- Write-Ahead Logging
Shadow Paging
基本思想是這樣的,對(duì)于問(wèn)題1,如果我不in-place update,就沒(méi)有臟數(shù)據(jù)了,也就沒(méi)有把臟數(shù)據(jù)恢復(fù)之說(shuō); 對(duì)于問(wèn)題2,如果事務(wù)提交成功的前提,數(shù)據(jù)已經(jīng)持久化,那么就不用丟失數(shù)據(jù)了。因此Shadow Paging在修改數(shù)據(jù)的時(shí)候,會(huì)先copy一份副本,基于這個(gè)副本做修改,如果需要修改多個(gè)數(shù)據(jù),那么就分別copy多個(gè)副本做修改,修改完后把數(shù)據(jù)持久化下來(lái)。事務(wù)提交時(shí),就是把之前引用老數(shù)據(jù)的指針,指向新數(shù)據(jù),然后持久化,持久化完后,事務(wù)就提交成功。如果被修改的指針?lè)植荚趲讉€(gè)頁(yè)面上,那么對(duì)每個(gè)一個(gè)頁(yè)面,也是執(zhí)行shadow paging的策略去更新,是一個(gè)遞歸的過(guò)程。這里大家可以看到有幾個(gè)問(wèn)題:
- copy page本身的開銷,性能問(wèn)題
- 修改引用page也可能走shadow paging,性能問(wèn)題
- 提交事務(wù)需要數(shù)據(jù)頻繁持久化,性能問(wèn)題(是否是隨機(jī)IO,取決于持久化層存儲(chǔ)引擎)
- 一些隔離級(jí)別做起來(lái)成本高,例如支持read commit,那么就有mvcc,保存的是多個(gè)完整的page
可以看到shadow paging策略最大的問(wèn)題,就是性能差。如果要優(yōu)化的化,對(duì)于2、3,一般會(huì)結(jié)合接下來(lái)討論的兩個(gè)流派做優(yōu)化。
Commit Logging
基本思想是這樣的,對(duì)于問(wèn)題1,如果事務(wù)不提交,我就不持久化,那就沒(méi)臟數(shù)據(jù)了,也就沒(méi)有把臟數(shù)據(jù)恢復(fù)之說(shuō);對(duì)于問(wèn)題2,事務(wù)提交成功的前提,是我把所有修改記錄,都append到日志中,提交時(shí)把事務(wù)提交的標(biāo)記也append到log中,然后做日志持久化,這樣就完成的事務(wù)提交。這時(shí)恢復(fù)數(shù)據(jù)就很容易的,而且性能也高,因?yàn)閷?duì)于持久化存儲(chǔ)append的性能很高。因此使用Commit Logging策略的系統(tǒng),每次修改數(shù)據(jù)前,都先append log,log并不要求刷盤,事務(wù)里的所有數(shù)據(jù)都修改完了,append 一個(gè)commit標(biāo)記到log中,然后對(duì)log進(jìn)行刷盤,即事務(wù)完成提交了。對(duì)于事務(wù)提交后的部分持久化問(wèn)題,可以通過(guò)在page處記錄持久化時(shí)對(duì)page做修改的日志序號(hào),避免重復(fù)做即可,或者把操作設(shè)計(jì)成保證冪等性??梢钥吹紺ommit Logging相比Shadow paging有不少優(yōu)點(diǎn):
- 不需要copy page,性能損耗小
- 提交事務(wù),持久化的只是log,并且是append only,性能高
- mvcc好做,對(duì)于page內(nèi)的每個(gè)記錄一個(gè)版本號(hào)即可,不需要額外保存完整的page
但是Commit Logging也有一個(gè)明顯的缺點(diǎn),就是只有事務(wù)提交后,數(shù)據(jù)才能做持久化。這樣在高并發(fā)常見(jiàn)下,可能不用充分利用硬盤的IO,而且對(duì)于大事務(wù),所有數(shù)據(jù)都要在內(nèi)存中hold住。為了改善這兩點(diǎn),就提出了Write-Ahead Logging。
Write-Ahead Logging
基本思想是這樣的,對(duì)于問(wèn)題1,如果事務(wù)不提交,持久化page的前提,是我記錄下修改前的數(shù)據(jù),那么臟數(shù)據(jù)就能恢復(fù)了,這個(gè)log稱為undo log;對(duì)于問(wèn)題2,解決方法和Commit Logging一樣,這個(gè)log稱為redo log。因此使用WAL的系統(tǒng),修改數(shù)據(jù)的流程是這樣的,對(duì)于每個(gè)修改詩(shī)句,修改前先append一條undo log,再append一條redo log,然后修改數(shù)據(jù),事務(wù)未提交前,有數(shù)據(jù)要持久化時(shí)需要保證對(duì)應(yīng)的undo log已經(jīng)持久化。所有數(shù)據(jù)修改完后,append 一個(gè)commit標(biāo)記到log中,然后對(duì)log進(jìn)行刷盤,即事務(wù)完成提交了。系統(tǒng)崩潰服務(wù)重啟,對(duì)數(shù)據(jù)進(jìn)行恢復(fù)時(shí),先不管三七二十一,把redo log里所有明確有提交的事務(wù)重放一邊,然后在redo log里所有沒(méi)提交的事務(wù),去undo log那找log,把臟頁(yè)的數(shù)據(jù)回滾回去??梢钥吹胶虲ommit Logging相比,優(yōu)點(diǎn)是:
- 充分利用硬盤IO,釋放內(nèi)存空間
- 大事務(wù)不需要把所有數(shù)據(jù)都hold在內(nèi)存
理論化
我們將何時(shí)持久化變動(dòng)數(shù)據(jù),按照事務(wù)提交時(shí)點(diǎn)為界,劃分為 FORCE 和 STEAL 兩類情況:
- FORCE:當(dāng)事務(wù)提交時(shí),要求變動(dòng)數(shù)據(jù)必須同時(shí)完成寫入則稱為 FORCE,如果不強(qiáng)制變動(dòng)數(shù)據(jù)必須同時(shí)完成寫入則稱為 NO-FORCE。
-
STEAL:在事務(wù)提交前,允許變動(dòng)數(shù)據(jù)提前寫入則稱為 STEAL,不允許則稱為 NO-STEAL。
image.png
Shadow Paging FORCE + NO-STEAL。
Commit Logging NO-FORCE+ NO-STEAL。
Write-Ahead Logging NO-FORCE + STEAL。
現(xiàn)實(shí)中絕大多數(shù)數(shù)據(jù)庫(kù)采用的都是 NO-FORCE 策略,因?yàn)橹灰辛巳罩?,變?dòng)數(shù)據(jù)隨時(shí)可以持久化,從優(yōu)化磁盤 I/O 性能考慮,沒(méi)有必要強(qiáng)制數(shù)據(jù)寫入立即進(jìn)行。從優(yōu)化磁盤 I/O 性能考慮,允許數(shù)據(jù)提前寫入,有利于利用空閑 I/O 資源,也有利于節(jié)省數(shù)據(jù)庫(kù)緩存區(qū)的內(nèi)存。
