文章目錄
- 事務
- 定義
- 并發事務
- 代碼實現
- MVCC
- 定義
- 核心機制
事務
定義
什么是事務? 事務是指一組操作要么全部成功,要么全部失敗的執行單位。
在數據庫中,一個事務通常包含一組SQL語句,系統保證這些語句作為一個整體執行。
為什么引入? 想象一下,若沒有事務銀行中的轉賬操作可能會發生:A對B轉賬,A這邊已扣款但B卻沒收到。這不是虧麻了。再比如,多個線程同時操作一條數據,那么這條數據該聽誰的呢?引入事務就是為了解決這些問題,保證數據的完整性,一致性和可靠性。
特性: 有四大特性:
特性 | 全稱 | 含義 |
---|---|---|
A | Atomicity(原子性) | 操作不可分割,要么全成功,要么全失敗 |
C | Consistency(一致性) | 執行完事務后,數據要從一個一致狀態轉到另一個一致狀態 |
I | Isolation(隔離性) | 并發事務之間互不干擾 |
D | Durability(持久性) | 提交后的數據必須永久保存,即使系統宕機 |
并發事務
并發事務常見的問題: 了解事務的基本原理后,我們在數據庫執行多個事務時,實際上是并發執行。但就像線程并發執行,又會引起很多問題。主要歸為以下三類:
(1)臟讀:指事務1在訪問數據A時,事務2在修改數據A,此時事務1拿到的數據不是最終數據。
如何解決?只需要在事務1中寫的過程加鎖(可以理解為多線程編程的鎖),也就是寫的過程不會受到任何外部的干擾。任何事務讀到的肯定是寫完之后的數據了。
(2)不可重復讀:雖然在寫的過程加鎖可以解決臟讀問題,但是沒說讀的時候不能寫啊? 假設事務1讀數據A時,事務2在寫數據A,事務1讀到一半的時候,事務2寫完了。此時事務1假設繼續讀下去,發現內容與前文不一致了。這就是不可重復讀。
如何解決?顯而易見,給讀的過程也加鎖。這樣讀寫都不被干預,那么這就不管怎樣都安全了嗎?
(3)幻讀:顯然還是不安全,我們考慮一種情況,事務1,2在數據庫中讀寫都加鎖了。事務1讀取數據A,事務2讀取數據A,在事務2讀取A時,事務1不能讀取A,但它可以操縱數據庫的其他的數據。假設事務1新增數據B,事務2在讀完數據A后,再查詢數據庫有多少條數據。發現多了一條,這就是“不敢睜開眼,希望是我的幻覺”(歌詞哈哈),大家理解記憶這就是幻讀!(當然我理解的可能也有偏差,不過大多數文章都以新增數據舉例)。
如何解決?直接徹底串行化,事務2干活的同時,事務1徹底不許做別的了。
問題 | 描述 | 舉例 |
---|---|---|
臟讀(Dirty Read) | 一個事務讀到另一個事務尚未提交的數據 | T1更新數據,T2讀取了這個數據,后來T1回滾,T2讀到的是無效數據 |
不可重復讀(Non-repeatable Read) | 同一事務中多次讀取結果不一致 | T1兩次讀取某條記錄,中間T2修改了這條記錄 |
幻讀(Phantom Read) | 同一事務中兩次查詢數據條數不一致 | T1讀取符合條件的所有記錄,T2新增了一條符合條件的數據,T1再次讀取發現“多出一條” |
隔離級別 | 描述 | 會不會出現臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|---|
READ UNCOMMITTED | 最低,不加鎖 | ? 有 | ? 有 | ? 有 |
READ COMMITTED | 讀已提交(Oracle 默認) | ? 無 | ? 有 | ? 有 |
REPEATABLE READ | 可重復讀(MySQL 默認) | ? 無 | ? 無 | ? 有(InnoDB 用間隙鎖避免) |
SERIALIZABLE | 串行化,最高隔離 | ? 無 | ? 無 | ? 無 |
代碼實現
不做重點,感興趣的可以直接在項目中練習
Connection conn = null;
try {conn = dataSource.getConnection();conn.setAutoCommit(false); // 開啟事務// 執行多個 SQL 語句updateAccount1(conn);updateAccount2(conn);conn.commit(); // 提交事務
} catch (Exception e) {if (conn != null) conn.rollback(); // 回滾事務
} finally {if (conn != null) conn.close(); // 關閉連接
}
MVCC
定義
介紹完事務后,我們可以介紹一種更輕量化的解決事務并發問題的方法。講之前簡單講一下,事務是怎么實現全不做的? 可能會有疑問,事務執行了多個操作,還差幾個操作就執行完了,這個時候突發緊急情況不能做了,怎么把之前的操作取消呢。實際上可以理解為數據庫已經將之前的版本的數據記錄下來,將之前操作所修改的數據全部還原。這就是回滾。
??什么是MVCC? MVCC -多版本并發控制,它允許事務在不加鎖的情況下并發讀寫,通過維護數據的多個版本實現。神奇吧,竟然不用加鎖也可以實現。那么它是怎樣版本控制的呢?
為什么引入? 講原理之前,先要了解之前加鎖方案存在的缺陷以及MVCC的目的
加鎖方案主要有以下兩個問題
傳統機制 | 問題 |
---|---|
讀加共享鎖,寫加排他鎖 | 讀寫互相阻塞,效率低下 |
高并發下,鎖沖突頻繁 | 會造成 鎖等待、死鎖、性能瓶頸 |
MVCC目的:
在 無需加鎖的前提下,實現“讀寫分離”、高并發讀操作的一致性。也就是避免鎖的使用
核心機制
原理
(1)在InnoDB中,MVCC維護每一行的數據多個版本來實現。主要依靠兩個字段和undo log機制。
(2)兩個字段是指數據庫為每條記錄隱藏的維護了兩個字段分別是更新此記錄的事務id和回滾指針。
字段 | 含義 |
---|---|
trx_id | 插入或最后修改該行的事務ID |
roll_pointer | 指向 undo log 中上一個版本的地址,形成版本鏈 |
每個事務啟動時,系統會分配一個全局遞增的事務ID。
(3)undo log
undo log 大家可理解為舊賬本,就像夫妻倆一旦吵架就要翻舊賬。事務也是如此發生沖突,直接翻舊賬。 具體為當一個事務更新記錄時,數據庫首先將未更新的記錄先保存到undo log中,然后事務才可以更新記錄,并將記錄中的roll_pointer 指向此舊帳本對應的記錄。
[當前版本]trx_id: 15roll_pointer --> undo log #1[undo log #1]trx_id: 12roll_pointer --> undo log #2[undo log #2]trx_id: 9
(4)那如何通過翻舊賬實現可見性呢?
這里要注意,我們在這通常針對的讀操作不加鎖,寫操作數據庫一般默認都會加鎖,我們盡可能減少的是讀操作的加鎖。假設有一組并發的事務開始執行時,系統依次給每個事務分配遞增ID。 其中當這里邊的事務讀取記錄時,核心內容就一條即更新這條記錄的事務id必須小于這一組事務id的最小值。讀取時這條記錄才會對事務可見。或者修改這一條記錄事務id是它本身,這種情況也是可見的。其他條件,比如大于這一組事務id的最小值,說明更新這條記錄的事務id在這一組事務中,由于是并發執行,所以對其不可見。此時,記錄的回滾指針會指向之前的版本記錄讓其事務讀取。如下:
判斷條件 | 是否可見 | 原因 |
---|---|---|
trx_id == 當前事務ID | 是 | 當前事務自己創建或修改的記錄 |
trx_id < 最小活躍事務ID | 是 | 創建該記錄的事務已在當前事務啟動前提交 |
trx_id ∈ 活躍事務列表 | 否 | 創建該記錄的事務尚未提交 |
否則 | 否,繼續通過 roll_pointer 回滾舊版本 | 找到對當前事務可見的歷史版本 |
這里解釋一下可能存在的疑問。 事務出現不會直接獲取要讀取記錄的事務id字段。而是當我們在事務中查詢語句執行時,才會獲取更新此記錄的事務ID。這種方式也叫快照讀,就是說讀取的時候會給記錄拍照定格ReadView。不是事務出現拍照定格。
事務T1如果還沒進行讀取,是不會生成Read View的。當它真的去執行 SELECT 操作時,它才會拍下“當前全局事務表中的活躍事務ID快照所以 Read View 中的最大事務ID(up_limit_id)可能遠遠大于當前事務自己的ID
當前活躍的事務創建的 Read View 都是相同的嗎?Read View 是每個事務“第一次執行快照讀”時才生成的。即便兩個事務同時處于“活躍”狀態,它們的 Read View 也可能在不同的時刻拍下,因此內容可能不同。快照讀和其他操作的區別:
操作類型 | 讀寫類型 | 是否生成 Read View | 能否看到未提交數據 | 是否加鎖 |
---|---|---|---|---|
SELECT | 快照讀(Snapshot Read) | ? 是(第一次讀時生成) | ? 否(只能看到歷史版本) | ? 否 |
SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE | 當前讀(Current Read) | ? 否 | ? 是(讀取最新已提交或當前數據) | ? 是(加行鎖) |
UPDATE / DELETE | 當前讀 + 寫操作 | ? 否 | ? 是(讀取最新數據) | ? 是(加排他鎖) |
INSERT | 寫操作 | ? 否 | -(新數據,無歷史版本) | ? 是(加插入意向鎖) |
總結來說,本文面向面試對其基本原理做了一定梳理,希望可以幫助大家通過面試。