MySQL事務(wù)詳解
什么是事務(wù)?
- 事務(wù)是一個不可分割的工作單元,工作單元要么工作完成,要么什么也不做。
 - 從應(yīng)用層面來說一個事務(wù)對應(yīng)了一個完整的業(yè)務(wù)功能。
 - 從數(shù)據(jù)庫層面的來講事務(wù)就是由一批DML語句構(gòu)成。
 
事務(wù)的分類
MySQL的InnoDB存儲引擎支持扁平事務(wù)、帶有保存點的事務(wù)、鏈?zhǔn)聞?wù)、分布式事務(wù)。
- 扁平事務(wù)(Flat Transactions)
 
扁平事務(wù)應(yīng)用最為廣泛,實現(xiàn)最為簡單,扁平事務(wù)的所有操作都是在同一個層級,這些操作要么全部成功,要么全部回滾,不能存在部分提交或者部分回滾的的場景。

扁平事務(wù)
- 帶保存點的扁平事務(wù)(Flat Transactions with Sacepoint)
 
扁平事務(wù)的限制就在于不能部分回滾或者提交,而有的場景是這么做是代價非常大的。比如我們舉個例子:
我們玩生存類游戲,如果我們意外失敗就必須從出生地開始玩,那么這會是讓人崩潰的,我們希望有一個游戲存檔,如果游戲失敗我們可以從最近的一個存檔重新加載游戲。
帶保存點的扁平事務(wù)就是,除了支持扁平事務(wù)的操作外,允許事務(wù)執(zhí)行過程中回滾到該事務(wù)較早的一個狀態(tài),而這個較早的狀態(tài)就是保存點來記錄的。

帶保存點的扁平事務(wù)
- 鏈?zhǔn)聞?wù)(Chained Transaction)
 
鏈?zhǔn)聞?wù)是一種保存點事務(wù)的變種,兩者的最大區(qū)別是,帶保存點的事務(wù)可以回滾到較早前的任意保存點,而鏈?zhǔn)绞聞?wù)只能回滾到最近一個保存點;帶保存點的事務(wù)因為需要回滾到任意保存點,固其事務(wù)執(zhí)行期間所占用的資源是不會被釋放的,而鏈?zhǔn)聞?wù)則在執(zhí)行完成當(dāng)前節(jié)點后會釋放掉不需要的資源,并將下一個節(jié)點需要的資源隱士傳遞下去。鏈?zhǔn)聞?wù)可以參考Flink流式計算的Checkpoint機制,兩者非常的相似。

鏈?zhǔn)聞?wù)
- 嵌套事務(wù)(Nested Transaction)
 
嵌套事務(wù)顧名思義,事務(wù)結(jié)構(gòu)看上去就像一棵樹,根節(jié)點就是一個頂層事務(wù),所有的葉子節(jié)點都是扁平事務(wù)(也就是說葉子節(jié)點才是真正干活兒的),事務(wù)的嵌套層級不受限制。子事務(wù)可以提交也可以回滾,但是其提交不會立即生效,只有在頂層事務(wù)提交之后所有子事務(wù)才會被真正的提交。

嵌套事務(wù)
- 分布式事務(wù)(Distributed Transactions)
 
分布式事務(wù)是指一個在分布式環(huán)境下運行的扁平事務(wù),在本章中主要介紹本地事務(wù),分布式事務(wù)我會在后續(xù)章節(jié)是介紹。
事務(wù)的ACID特性
- A(Atomicity)原子性:整個事務(wù)操作是一個完整的不可分割的整體,只有事務(wù)中的所有操作都執(zhí)行成功,事務(wù)才算執(zhí)行成功,否則就要回滾到事務(wù)執(zhí)行前的狀態(tài),即要么全部都做,要么全都不做。
 
例如:轉(zhuǎn)賬場景,自己賬戶扣除轉(zhuǎn)賬額度與對方賬戶收到轉(zhuǎn)賬這兩個操作必須是原子的。
- C(Consistency)一致性:事務(wù)將數(shù)據(jù)庫從一種狀態(tài)轉(zhuǎn)變?yōu)榱硪环N狀態(tài),在事務(wù)執(zhí)行前和事務(wù)執(zhí)行后,數(shù)據(jù)庫的完整性約束沒有被破壞。
 
例如:用戶表的用戶ID列有unique約束,即用戶ID不可重復(fù),如果事務(wù)執(zhí)行插入了一樣的用戶ID,那么就產(chǎn)生了不一致的狀態(tài)。
- I(Isolation)隔離性:隔離性(又稱并發(fā)控制)非常好理解,就是兩個事務(wù)之間不能相互影響,即當(dāng)前事務(wù)提交之前所作出的修改對其他事務(wù)都不可見,上一章我們講到了MySQL鎖,它就可以起到控制并發(fā)的作用。
 - D(Durability)持久性:持久性是指事務(wù)一旦提交,其結(jié)果就是永久性的,即使是服務(wù)器宕機,數(shù)據(jù)也必須能夠得到恢復(fù),除了硬件故障,數(shù)據(jù)物理損壞,否則必須保證事務(wù)執(zhí)行結(jié)果的永久性,也就是保證高可靠性(High Reliablility)。
 
事務(wù)如何實現(xiàn)
事務(wù)的原子性、一致性、持久性通過redo log與undo log來完成,事務(wù)的隔離性由鎖與MVCC來完成。
Redo log(重做日志)
Redo log是用來實現(xiàn)事務(wù)的持久性,為了更好的讀寫性能,InnoDB會將數(shù)據(jù)緩存在內(nèi)存中,對磁盤數(shù)據(jù)的修改也會落后于內(nèi)存,如果進(jìn)程或系統(tǒng)崩潰,則數(shù)據(jù)面臨丟失的風(fēng)險,這時重做日志就起到了保證數(shù)據(jù)的一致性與持久性作用。重做日志主要記錄了以頁為單位的數(shù)據(jù)修改信息,其結(jié)構(gòu)如下:

redo log 結(jié)構(gòu)
- 重做日志在Buffer中是連續(xù)寫入的,Buffer中的數(shù)據(jù)會適時地刷新到物理文件中;
 - 文件順序?qū)懭?,每個事務(wù)的重做日志追加到文件末尾;
 - 單個文件大小固定,寫滿以后會切回到文件組的下一個文件;
 - 重做日志文件組的文件個數(shù)是固定的,寫完最后一個文件則繼續(xù)回到第一個文件開始寫入;
 - 每個重做文件有固定2K的文件頭,文件頭的之后是以一個個512bytes的Block,每個Block有16bytes的頭尾信息;
 - 重做日志有一個全局的日志序列號(LSN:Log Sequence Number),單調(diào)遞增,表示事務(wù)寫入的重做日志的字節(jié)總量,也就是一個日志偏移量。
 
Undo log(回滾日志)
重做日志記錄了事務(wù)的行為,可以在需要的時候?qū)撨M(jìn)行“重做”,但是事務(wù)有時是需要被回滾的,當(dāng)語句執(zhí)行失敗或者用戶請求回滾,就可以通過undo log將數(shù)據(jù)回滾到修改前的樣子,undo log是存儲了行記錄的變更。其主要包含兩類undo log:

兩種undo log結(jié)構(gòu)
- insert undo log:insert操作時產(chǎn)生,只對當(dāng)前事務(wù)本身可見,在事務(wù)提交之后可直接刪除。
 - update undo log:delete與update操作產(chǎn)生,需要提供歷史版本,為后續(xù)章節(jié)要講到的MVCC服務(wù),其交由purge線程統(tǒng)一刪除。
 - undo log需要通過group commit 操作將數(shù)據(jù)fsync到磁盤,以保證事務(wù)的持久性。
 
下面是一個事務(wù)與undo log的關(guān)系結(jié)構(gòu):

事務(wù)與undo log關(guān)系結(jié)構(gòu)
事務(wù)隔離
事務(wù)在并發(fā)場景下很難保證事務(wù)的隔離性一致性,主要有以下一些事務(wù)的并發(fā)一致性問題。
事務(wù)并發(fā)問題
- 臟讀(Dirty Read):事務(wù)A讀取了另外一個并行事務(wù)B未提交的數(shù)據(jù)。
 - 不可重復(fù)讀(Non-Repeatable Read):在解決臟讀問題之后,能夠保證讀事務(wù)讀取到的數(shù)據(jù)都是持久的數(shù)據(jù),如果事務(wù)A多次讀取同一數(shù)據(jù),正好在兩次讀取之間,另外一個并行事務(wù)B提交了這一數(shù)據(jù)的修改,這就導(dǎo)致事務(wù)A多次讀取到的同一數(shù)據(jù)內(nèi)容不一樣。
 - 幻讀(Phantom):與不可重復(fù)讀類似,事務(wù)A多次查詢一個范圍,另外一個并行事務(wù)B向該范圍內(nèi)插入或刪除了數(shù)據(jù)并提交,當(dāng)事務(wù)A再次查詢時發(fā)現(xiàn)記錄變多或者丟失。
 - 更新丟失(Lost Updates):兩個事務(wù)A和B修改了同一數(shù)據(jù),由于未提交事務(wù)之間看不到對方的修改,因此都以一個舊的前提去更新了同一數(shù)據(jù)。
 - 寫偏差(Write Skew):與更新丟失類似,都是寫前提被改變,寫偏差則是事務(wù)A讀取某些數(shù)據(jù),作為另一些寫入的前提條件(更新丟失是針對同一數(shù)據(jù)),但這時另外一個事務(wù)B對事務(wù)A已讀取的數(shù)據(jù)做了修改并提交,從而導(dǎo)致事務(wù)A做了錯誤的commit操作。
 - 讀偏差(Read Skew):如事務(wù)A讀取某兩個數(shù)據(jù)求和,事務(wù)B在事務(wù)A讀取期間對已讀取數(shù)據(jù)做了增減,此時事務(wù)A求和得到的結(jié)果就會與實際的結(jié)果不一致。
 
針對上面的并發(fā)問題,InnoDB存儲引擎通過MVCC(當(dāng)然MVCC本質(zhì)上也是一種樂觀鎖)與鎖(關(guān)于鎖的介紹可以閱讀我的上一篇文章)來解決事務(wù)的隔離性一致性問題。
事務(wù)隔離級別
事務(wù)隔離級別是MySQL對ACID的實現(xiàn)程度上的分級,分為了四個等級,等級越高數(shù)據(jù)庫越安全,每種隔離級別解決了不同事務(wù)并發(fā)一致性的問題,具體如下:
- READ UNCOMMITTED(讀未提交):這是一個最差的隔離級別,該級別下事務(wù)可以讀到其它事務(wù)未提交的數(shù)據(jù),也就是說在該事務(wù)隔離級別下會發(fā)生上述的所有并發(fā)一致性問題。
 - READ COMMITTED(讀已提交):事務(wù)只能讀取到已提交的修改,也就是說多個并發(fā)的事務(wù)之間的修改是相互不可見的,該事務(wù)隔離級別解決了臟讀問題。
 - REPEATABLE READ(可重復(fù)讀):該級別保證同一個事務(wù)中多次讀取同一數(shù)據(jù)的結(jié)果是一致的,該級別是InnoDB默認(rèn)的隔離級別,該隔離級別解決了臟讀與不可重復(fù)讀問題,但是仍可能出現(xiàn)幻讀的情況(InnoDB存儲引擎在該隔離級別下使用了Next-Key Lock解決了幻讀問題)。
 - SERIALIZABLE(串行化):強制事務(wù)串行化執(zhí)行,沒有并發(fā),那么并發(fā)問題自然就不存在了,當(dāng)然在該級別下的事務(wù)性能非常低。
 
關(guān)于事務(wù)隔離的實現(xiàn)會在后續(xù)文章詳細(xì)講解,本文不在展開。
事務(wù)的執(zhí)行過程

事務(wù)的執(zhí)行過程
- 查詢數(shù)據(jù),若數(shù)據(jù)不存在于buffer,則從磁盤加載;
 - 數(shù)據(jù)更新前,先將當(dāng)前數(shù)據(jù)記錄到undo log重,以便后續(xù)可能出現(xiàn)的回滾做準(zhǔn)備;
 - 更新Buffer Pool中的數(shù)據(jù);
 - 將更新的數(shù)據(jù)寫入到Redo Log Buffer中;
 - 準(zhǔn)備提交事務(wù),調(diào)用fsync將Redo Log Buffer的數(shù)據(jù)寫入到redo log文件中,狀態(tài)記為prepared;
 - 準(zhǔn)備提交事務(wù),binlog寫入到磁盤中;
 - binlog寫入成功后,將redo log的狀態(tài)更新為commit;
 
binlog的開啟時會存在一個內(nèi)部XA的問題(binlog是在MySQL層,而redo log在存儲引擎層),這里引入了2PC(二階段提交):
- prepare階段:redo log持久化到磁盤,同時設(shè)置狀態(tài)為prepared,binlog此時不錯任何操作。
 - commit階段:存儲引擎釋放鎖,是否回滾段,然后binlog持久化到磁盤,然后存儲引擎層提交,更改redo log的狀態(tài)為commit。
 















 
 
 









 
 
 
 