MySQL事務詳解
什么是事務?
- 事務是一個不可分割的工作單元,工作單元要么工作完成,要么什么也不做。
- 從應用層面來說一個事務對應了一個完整的業務功能。
- 從數據庫層面的來講事務就是由一批DML語句構成。
事務的分類
MySQL的InnoDB存儲引擎支持扁平事務、帶有保存點的事務、鏈事務、分布式事務。
- 扁平事務(Flat Transactions)
扁平事務應用最為廣泛,實現最為簡單,扁平事務的所有操作都是在同一個層級,這些操作要么全部成功,要么全部回滾,不能存在部分提交或者部分回滾的的場景。
扁平事務
- 帶保存點的扁平事務(Flat Transactions with Sacepoint)
扁平事務的限制就在于不能部分回滾或者提交,而有的場景是這么做是代價非常大的。比如我們舉個例子:
我們玩生存類游戲,如果我們意外失敗就必須從出生地開始玩,那么這會是讓人崩潰的,我們希望有一個游戲存檔,如果游戲失敗我們可以從最近的一個存檔重新加載游戲。
帶保存點的扁平事務就是,除了支持扁平事務的操作外,允許事務執行過程中回滾到該事務較早的一個狀態,而這個較早的狀態就是保存點來記錄的。
帶保存點的扁平事務
- 鏈事務(Chained Transaction)
鏈事務是一種保存點事務的變種,兩者的最大區別是,帶保存點的事務可以回滾到較早前的任意保存點,而鏈式事務只能回滾到最近一個保存點;帶保存點的事務因為需要回滾到任意保存點,固其事務執行期間所占用的資源是不會被釋放的,而鏈事務則在執行完成當前節點后會釋放掉不需要的資源,并將下一個節點需要的資源隱士傳遞下去。鏈事務可以參考Flink流式計算的Checkpoint機制,兩者非常的相似。
鏈事務
- 嵌套事務(Nested Transaction)
嵌套事務顧名思義,事務結構看上去就像一棵樹,根節點就是一個頂層事務,所有的葉子節點都是扁平事務(也就是說葉子節點才是真正干活兒的),事務的嵌套層級不受限制。子事務可以提交也可以回滾,但是其提交不會立即生效,只有在頂層事務提交之后所有子事務才會被真正的提交。
嵌套事務
- 分布式事務(Distributed Transactions)
分布式事務是指一個在分布式環境下運行的扁平事務,在本章中主要介紹本地事務,分布式事務我會在后續章節是介紹。
事務的ACID特性
- A(Atomicity)原子性:整個事務操作是一個完整的不可分割的整體,只有事務中的所有操作都執行成功,事務才算執行成功,否則就要回滾到事務執行前的狀態,即要么全部都做,要么全都不做。
例如:轉賬場景,自己賬戶扣除轉賬額度與對方賬戶收到轉賬這兩個操作必須是原子的。
- C(Consistency)一致性:事務將數據庫從一種狀態轉變為另一種狀態,在事務執行前和事務執行后,數據庫的完整性約束沒有被破壞。
例如:用戶表的用戶ID列有unique約束,即用戶ID不可重復,如果事務執行插入了一樣的用戶ID,那么就產生了不一致的狀態。
- I(Isolation)隔離性:隔離性(又稱并發控制)非常好理解,就是兩個事務之間不能相互影響,即當前事務提交之前所作出的修改對其他事務都不可見,上一章我們講到了MySQL鎖,它就可以起到控制并發的作用。
- D(Durability)持久性:持久性是指事務一旦提交,其結果就是永久性的,即使是服務器宕機,數據也必須能夠得到恢復,除了硬件故障,數據物理損壞,否則必須保證事務執行結果的永久性,也就是保證高可靠性(High Reliablility)。
事務如何實現
事務的原子性、一致性、持久性通過redo log與undo log來完成,事務的隔離性由鎖與MVCC來完成。
Redo log(重做日志)
Redo log是用來實現事務的持久性,為了更好的讀寫性能,InnoDB會將數據緩存在內存中,對磁盤數據的修改也會落后于內存,如果進程或系統崩潰,則數據面臨丟失的風險,這時重做日志就起到了保證數據的一致性與持久性作用。重做日志主要記錄了以頁為單位的數據修改信息,其結構如下:
redo log 結構
- 重做日志在Buffer中是連續寫入的,Buffer中的數據會適時地刷新到物理文件中;
- 文件順序寫入,每個事務的重做日志追加到文件末尾;
- 單個文件大小固定,寫滿以后會切回到文件組的下一個文件;
- 重做日志文件組的文件個數是固定的,寫完最后一個文件則繼續回到第一個文件開始寫入;
- 每個重做文件有固定2K的文件頭,文件頭的之后是以一個個512bytes的Block,每個Block有16bytes的頭尾信息;
- 重做日志有一個全局的日志序列號(LSN:Log Sequence Number),單調遞增,表示事務寫入的重做日志的字節總量,也就是一個日志偏移量。
Undo log(回滾日志)
重做日志記錄了事務的行為,可以在需要的時候對頁進行“重做”,但是事務有時是需要被回滾的,當語句執行失敗或者用戶請求回滾,就可以通過undo log將數據回滾到修改前的樣子,undo log是存儲了行記錄的變更。其主要包含兩類undo log:
兩種undo log結構
- insert undo log:insert操作時產生,只對當前事務本身可見,在事務提交之后可直接刪除。
- update undo log:delete與update操作產生,需要提供歷史版本,為后續章節要講到的MVCC服務,其交由purge線程統一刪除。
- undo log需要通過group commit 操作將數據fsync到磁盤,以保證事務的持久性。
下面是一個事務與undo log的關系結構:
事務與undo log關系結構
事務隔離
事務在并發場景下很難保證事務的隔離性一致性,主要有以下一些事務的并發一致性問題。
事務并發問題
- 臟讀(Dirty Read):事務A讀取了另外一個并行事務B未提交的數據。
- 不可重復讀(Non-Repeatable Read):在解決臟讀問題之后,能夠保證讀事務讀取到的數據都是持久的數據,如果事務A多次讀取同一數據,正好在兩次讀取之間,另外一個并行事務B提交了這一數據的修改,這就導致事務A多次讀取到的同一數據內容不一樣。
- 幻讀(Phantom):與不可重復讀類似,事務A多次查詢一個范圍,另外一個并行事務B向該范圍內插入或刪除了數據并提交,當事務A再次查詢時發現記錄變多或者丟失。
- 更新丟失(Lost Updates):兩個事務A和B修改了同一數據,由于未提交事務之間看不到對方的修改,因此都以一個舊的前提去更新了同一數據。
- 寫偏差(Write Skew):與更新丟失類似,都是寫前提被改變,寫偏差則是事務A讀取某些數據,作為另一些寫入的前提條件(更新丟失是針對同一數據),但這時另外一個事務B對事務A已讀取的數據做了修改并提交,從而導致事務A做了錯誤的commit操作。
- 讀偏差(Read Skew):如事務A讀取某兩個數據求和,事務B在事務A讀取期間對已讀取數據做了增減,此時事務A求和得到的結果就會與實際的結果不一致。
針對上面的并發問題,InnoDB存儲引擎通過MVCC(當然MVCC本質上也是一種樂觀鎖)與鎖(關于鎖的介紹可以閱讀我的上一篇文章)來解決事務的隔離性一致性問題。
事務隔離級別
事務隔離級別是MySQL對ACID的實現程度上的分級,分為了四個等級,等級越高數據庫越安全,每種隔離級別解決了不同事務并發一致性的問題,具體如下:
- READ UNCOMMITTED(讀未提交):這是一個最差的隔離級別,該級別下事務可以讀到其它事務未提交的數據,也就是說在該事務隔離級別下會發生上述的所有并發一致性問題。
- READ COMMITTED(讀已提交):事務只能讀取到已提交的修改,也就是說多個并發的事務之間的修改是相互不可見的,該事務隔離級別解決了臟讀問題。
- REPEATABLE READ(可重復讀):該級別保證同一個事務中多次讀取同一數據的結果是一致的,該級別是InnoDB默認的隔離級別,該隔離級別解決了臟讀與不可重復讀問題,但是仍可能出現幻讀的情況(InnoDB存儲引擎在該隔離級別下使用了Next-Key Lock解決了幻讀問題)。
- SERIALIZABLE(串行化):強制事務串行化執行,沒有并發,那么并發問題自然就不存在了,當然在該級別下的事務性能非常低。
關于事務隔離的實現會在后續文章詳細講解,本文不在展開。
事務的執行過程
事務的執行過程
- 查詢數據,若數據不存在于buffer,則從磁盤加載;
- 數據更新前,先將當前數據記錄到undo log重,以便后續可能出現的回滾做準備;
- 更新Buffer Pool中的數據;
- 將更新的數據寫入到Redo Log Buffer中;
- 準備提交事務,調用fsync將Redo Log Buffer的數據寫入到redo log文件中,狀態記為prepared;
- 準備提交事務,binlog寫入到磁盤中;
- binlog寫入成功后,將redo log的狀態更新為commit;
binlog的開啟時會存在一個內部XA的問題(binlog是在MySQL層,而redo log在存儲引擎層),這里引入了2PC(二階段提交):
- prepare階段:redo log持久化到磁盤,同時設置狀態為prepared,binlog此時不錯任何操作。
- commit階段:存儲引擎釋放鎖,是否回滾段,然后binlog持久化到磁盤,然后存儲引擎層提交,更改redo log的狀態為commit。