文章目錄
- 事務隔離:從鎖實現到MVCC實現
- 事務
- 四大特性
- 事務隔離級別
- 鎖實現
- 概念
- 實現事務隔離
- MVCC實現
- 當前讀與快照讀
- 實現事務隔離
- Read View
- 總結
事務隔離:從鎖實現到MVCC實現
面試的時候被面試官問到:你這個項目為什么使用了可重復讀而不選擇讀已提交事務隔離級別。思考了一會發現我對事務、鎖、事務隔離級別的理解還是有所欠缺,今天來整理一下。
本文的梳理都基于下面這張簡單的表。
建表語句:
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, -- 用戶ID,自增主鍵username VARCHAR(50) NOT NULL, -- 用戶名,最多50字符,不能為空age INT, -- 年齡created_at DATETIME DEFAULT CURRENT_TIMESTAMP -- 創建時間,默認當前時間
);
事務
事務就是數據庫中的一系列操作組成的一個整體,我們在實際業務中,需要這一系列操作必須全部執行,不能有些操作執行了,有些操作執行失敗了。
例如,小紅向小明轉賬了500元,我們需要扣減小紅500元的余額,增加小明500元的余額。
UPDATE users
SET balance = balance - 500
WHERE username = '小紅';UPDATE users
SET balance = balance + 500
WHERE username = '小明';
如果不加事務控制,這兩條語句可能會出現第一條執行了,第二條未執行的操作,就會導致500元不翼而飛了。
使用BEGIN開啟一個事務,使用COMMIT提交一個事務。
START TRANSACTION;UPDATE users
SET balance = balance - 500
WHERE username = '小紅';UPDATE users
SET balance = balance + 500
WHERE username = '小明';COMMIT;
對于單條sql語句,數據庫會自動將其看成事務執行,叫隱式事務。
四大特性
事務的四大特性:
- A:Atomicity,原子性,將所有SQL作為原子工作單元執行,要么全部執行,要么全部不執行;
- C:Consistency,一致性,事務完成后,所有數據的狀態都是一致的,即A賬戶只要減去了100,B賬戶則必定加上了100;
- I:Isolation,隔離性,如果有多個事務并發執行,每個事務作出的修改必須與其他事務隔離;
- D:Durability,持久性,即事務完成后,對數據庫數據的修改被持久化存儲。
我們所使用的鎖、MVCC、日志等一系列機制其實都是為了保證事務的者四大特性,使得事務在實際業務中使用起來不會出錯。
事務隔離級別
事務之間有四個隔離級別,分別是讀未提交,讀已提交(解決臟讀),可重復讀(解決不可重復讀),串行化(解決幻讀)。
鎖實現
鎖常用于解決并發請求導致的一系列問題。數據庫鎖也是用來處理當同時有多個事務或者請求來并發地訪問數據庫時,可能會出現的問題。
概念
數據庫鎖主要分為兩種類型:
共享鎖(Shared Lock): 也就是讀鎖,讀鎖和讀鎖不會互斥,讀鎖和寫鎖之間互斥。
排他鎖(Exclusive Lock): 也就是寫鎖,寫鎖和任何讀鎖和寫鎖都是互斥的。
數據庫中具體的鎖:
由于本文只涉及到行鎖,所以這里只簡單介紹一下行鎖。
行鎖鎖定數據庫中的某一條記錄,單個行。數據庫不同行直接可以同時進行訪問,因此提高了并發性。
共享行級鎖:多個事務可以同時獲取共享鎖,用于讀取行數據。
排他行級鎖:只允許一個事務持有排他鎖,用于修改行數據。
實現事務隔離
接下來我們梳理一下如何用鎖來實現事務隔離級別。
首先我們要知道只依靠加鎖來實現事務隔離會帶來性能降低的問題,但是了解一下會對我們更好地去理解MVCC有幫助。
**一級封鎖協議:**事務在修改一條記錄之前必須先對它添加寫鎖,直到事務(提交或者回滾)結束才釋放。
不能解決臟讀問題,因為當事務T1修改記錄,添加了寫鎖的時候,其它事務的讀是不加鎖的,依舊可以讀到數據。
二級封鎖協議: 一級封鎖協議的規則+事務在讀取記錄之前必須先對它加讀鎖,讀完后就可以釋放鎖。
可以解決臟讀問題,但是不能解決不可重復讀問題,因為讀鎖在讀完就釋放了,在到下一次讀的這中間的間隙可能就會出現別的事務將數據修改了。
三級封鎖協議: 一級封鎖協議的規則+務在讀取記錄之前必須先對它加讀鎖,但是事務結束才釋放。
可以解決臟讀問題,可以解決不可重復讀問題。
鎖協議 | 事務隔離級別 |
---|---|
一級封鎖協議 | Read Uncommitted(讀未提交) |
二級封鎖協議 | Read Committed(讀已提交) |
三級封鎖協議 | Repeatable Read(可重復讀) |
表鎖 | Serializable (可串行化) |
MVCC實現
可以看到上面我們只使用了鎖機制來實現了事務隔離,接下來介紹一種無鎖化的、性能更高的實現事務隔離的方法,MVCC(Multi-Version Concurrency Control),即多版本并發控制。
當前讀與快照讀
在了解MVCC實現事務隔離之前,我們先了解一下當前讀和快照讀。
當前讀: Mysql使用鎖來實現當前讀,共享鎖+排他鎖+next-key lock(間隙鎖)實現。
下面這些語法都是當前讀:
語法 |
---|
SELECT … LOCK IN SHARE MODE |
SELECT … FOR UPDATE |
UPDATE |
DELETE |
INSERT |
select語句加上LOCK IN SHARE MODE就是加上共享鎖進行讀,加上FOR UPDATE就是加上排他鎖進行讀。
快照讀: 快照讀是在讀取數據的時候讀取一致性視圖中的數據,Mysql使用MVCC來實現快照讀。
具體而言,每個事務在開始時會創建一個一致性視圖(Consistent View),該視圖反映了事務開始時刻數據庫的快照。這個一致性視圖會記錄當前事務開始時已經提交的數據版本。
當執行查詢操作時,MySQL會根據事務的一致性視圖來決定可見的數據版本。只有那些在事務開始之前已經提交的數據版本才是可見的,未提交的數據或在事務開始后修改的數據則對當前事務不可見。
實現事務隔離
那么MVCC在MySql中又是怎么樣實現事務隔離的呢?
對于讀未提交隔離級別,不做任何控制,相當于是一級封鎖協議,修改語句會默認添加排他鎖,并且在事務結束時才會釋放。
對于讀已提交隔離級別,MVCC通過Read View來實現。(具體看Read View)
對于可重復讀隔離級別,MVCC通過Read View來實現。(具體看Read View)
對于串行化隔離級別,通過加臨健鎖(行鎖+間隙鎖)來實現的。
Read View
首先我們先要了解數據庫的表記錄,除了原來的數據列以外,還維護了3個隱藏列,和Read View相關的只有兩個隱藏列,我們只關注這兩個,一個是DB_TRX_ID,還有一個是DB_ROLL_PTR,其中DB_TRX_ID表明這條記錄所屬的事務id,DB_ROLL_PTR指向這條事務上一個事務所保存的這條記錄的快照。所以對于一條記錄,我們有一個多版本快照鏈(由DB_ROLL_PTR串聯成鏈,讀取選擇讀哪條的時候靠的是DB_TRX_ID來選擇)。有了這個多版本快照鏈,事務在進行快照讀的時候,就會結合Read View所記錄的活躍的事務信息,選擇當前隔離級別下可見的最新的記錄了。
Read View就是mvcc實現快照讀的核心機制,我們借助它就可以去undo_log中尋找要讀的這條記錄在當前事務隔離級別下"可見"的那個版本。下圖就是一個Read View,它其實就是記錄了事務的信息。
使用這個Read View的時候有一些規則:
(首先我們要知道每次讀取記錄的時候其實都是去當前這個記錄的undo_log中去讀取的,每次都先讀取最新的版本,然后結合Read View進行判斷,如果最新版本的不可讀就會沿著版本鏈讀取下一個版本直到可讀。)
-
如果版本的記錄的事務id是當前執行讀取操作的事務id,則直接讀取。
-
如果版本的記錄的事務id小于Read View中的min_trx_id,那么表明該版本是在當前的讀取事務開始之前就提交了的,可以讀。
-
如果版本的記錄的事務id大于Read View中保存的max_trx_id,那么表明該版本是在當前讀取操作的事務開始之后提交的,不能讀取。
-
如果當前讀取操作的事務id位于Read View中記錄的min_trx_id到max_trx_id之間,那么就表明這個版本是在當前活躍的但是還未提交的版本中的,那么只要在讀已提交版本之上,就也不可以讀。
讀已提交 和 可重復讀 有一個很大的區別就是讀已提交在每次讀取操作的時候都會創建一個新的當前狀態的Read View,而可重復讀只會再事務第一次讀取操作之后創建一個Read View,并且使用這個Read View一直到事務結束。就是這個區別使得 讀已提交 可能導致 不可重復讀 的問題。因為第二次讀取使用的是一個更新后的新 Read View,可能讀到了其他事務剛剛提交的新值。
總結
因此,有了MVCC,有了Read View,我們可以無鎖地實現事務隔離級別,在讀取操作地時候不上鎖(沒有MVCC的話,讀取的時候要使用共享鎖來進行控制),只有在修改操作的時候正常加上排他鎖,大大地提高了并發事務的性能。