前言
事務是訪問資料庫的一個操作序列,資料庫應用系統透過事務集來完成對資料庫的存取。
1. 什麼是事務?
事務必須服從ISO/IEC所制定的ACID原則。ACID是原子性(atomicity)、一致性(consistency)、隔離性(isolation)、永續性(durability)的縮寫,這四種狀態的意思是:
- 原子性(Atomicity):原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,這和前面兩篇部落格介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到資料庫,如果操作失敗則不能對資料庫有任何影響;
- 一致性(Consistency):一致性是指事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態;
- 隔離性(Isolation):在事務正確提交之前,不允許把事務對該資料的改變提供給任何其他事務,即在事務正確提交之前,它可能的結果不應該顯示給其他事務;
- 永續性(Durability):永續性是指一個事務一旦被提交了,那麼對資料庫中的資料的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。
2. 事務的作用
當多個執行緒都開啟事務運算元據庫中的資料時,資料庫系統要能進行隔離操作,以保證各個執行緒獲取資料的準確性。
3. 遇到的併發問題
- 第一類丟失更新:A事務撤銷時,把已經提交的B事務的更新資料改寫了;
- 第二類丟失更新:A事務改寫B事務已經提交的資料,造成B事務所做操作丟失;
- 臟讀:A事務讀取了事務B中未提交的資料;
- 不可重覆讀:A事務多次讀取的值不同。因為該值被B事務修改並提交了;
- 幻讀:A事務兩次讀之間,B事務插入了資料。
4. 如何解決上面的問題呢?
為瞭解決上面的問題,開發者為MySQL資料庫設計了以下四種事務隔離級別:
- Read Uncommitted(未提交讀):允許臟讀,也就是可能讀取到其他會話中未提交事務修改的資料;
- Read Committed(提交讀):只能讀取到已經提交的資料。Oracle等多數資料庫預設都是該級別 (不重覆讀);
- Repeated Read(可重覆讀):可重覆讀。在同一個事務內的查詢都是事務開始時刻一致的,InnoDB預設級別。在SQL標準中,該隔離級別消除了不可重覆讀,但是還存在幻象讀,但是innoDB解決了幻讀;
- Serializable(序列讀):完全序列化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞。
隔離級別 | 臟讀 | 不可重覆度 | 不幻讀 |
---|---|---|---|
Read Uncommitted(未提交讀) | 可能 | 可能 | 可能 |
Read Committed(提交讀) | 不可能 | 可能 | 可能 |
Repeated Read(可重覆讀) | 不可能 | 不可能 | 可能 |
Serializable(序列讀) | 不可能 | 不可能 | 不可能 |
5. 小嘗試
5.1 檢視全域性或會話的事務隔離級別
SELECT @@global.tx_isolation, @@tx_isolation;
5.2 修改全域性或會話的事務隔離級別
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
6. 鎖的基本敘述
資料庫中的鎖是指一種軟體機制,用來控制防止某個使用者(行程會話)在已經佔用了某種資料資源時,其他使用者做出影響本使用者資料操作或導致資料非完整性和非一致性問題發生的手段。
按照鎖級別劃分,鎖可分為共享鎖、排他鎖:
- 共享鎖(讀鎖):針對同一塊資料,多個讀操作可以同時進行而不會互相影響。共享鎖只針對UPDATE時候加鎖,在未對UPDATE操作提交之前,其他事務只能夠獲取最新的記錄但不能夠UPDATE操作;
- 排他鎖(寫鎖):當前寫操作沒有完成前,阻斷其他寫鎖和讀鎖。
按鎖粒度劃分,鎖可分為表級鎖、行級鎖、頁級鎖:
- 行級鎖:開銷大,加鎖慢,會出現死鎖,鎖定力度最小,發生鎖衝突的機率最低,併發度高;
- 表級鎖:開銷小,加鎖快,不會出現死鎖,鎖定力度大,發生衝突所的機率高,併發度低;
- 頁面鎖:開銷和加鎖時間介於表鎖和行鎖之間,會出現死鎖,鎖定力度介於表和行行級鎖之間,併發度一般。
7. 悲觀鎖和樂觀鎖
7.1 悲觀鎖
總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖、表鎖等,讀鎖、寫鎖等,都是在做操作之前先上鎖,所以不管衝突是否真的發生,都會使用鎖機制。
悲觀鎖功能:
- 鎖住讀取的記錄,防止其它事務讀取和更新這些記錄。其它事務會一直阻塞,直到這個事務結束;
- 悲觀鎖是在使用了資料庫的事務隔離功能的基礎上,獨享佔用的資源,以此保證讀取資料一致性,避免修改丟失;
- 悲觀鎖可以使用Repeatable Read事務,它完全滿足悲觀鎖的要求。
7.2 樂觀鎖
總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和CAS演演算法實現。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量。
樂觀鎖是一種思想,樂觀鎖不會鎖住任何東西,也就是說,它不依賴資料庫的事務機制,樂觀鎖完全是應用系統層面的東西。所以它不是一種鎖機制.如果使用樂觀鎖,那麼資料庫就必須加版本欄位,否則就只能比較所有欄位,但因為浮點型別不能比較,所以實際上沒有版本欄位是不可行的。
7.3 版本號機制
一般是在資料表中加上一個資料版本號version欄位,表示資料被修改的次數,當資料被修改時,version值會加一。當執行緒A要更新資料值時,在讀取資料的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
7.4 CAS演演算法
CAS演演算法的核心思想是Compare and Swap,即比較再交換。
假設有A執行緒準備去修改記憶體中變數名為name的值,因此A執行緒會用以前自己讀到的name變數值和此刻name的值做對比,如果一樣,則表明在變數值沒被修改過,因此可以更新修改,否則更新失敗。
8. MySQL的重覆讀(Repeated Read)事務隔離級別
前面說過,MySQL預設實現了可重覆讀的事務隔離級別,但是不能解決幻讀的問題,然而在MySQL資料庫使用可重覆讀的事務隔離條件下,並未發生幻讀。MySQL使用MVCC(多版本併發控制)進行了控制。
8.1名詞簡析:
- 1.MVCC——簡稱multiversion concurrency
control,也就是多版本併發控制,是個很基本的概念。MVCC的作用是讓事務在並行發生時,在一定隔離級別前提下,可以保證在某個事務中能實現一致性讀,也就是該事務啟動時根據某個條件讀取到的資料,直到事務結束時,再次執行相同條件,還是讀到同一份資料,不會發生變化(不會看到被其他並行事務修改的資料); - read view——InnoDB MVCC使用的內部快照的意思。在不同的隔離級別下,事務啟動時(有些情況下,可能是SQL陳述句開始時)看到的資料快照版本可能也不同。在上面介紹的幾個隔離級別下會用到 read view;
- 快照讀——就是所謂的根據read view去獲取資訊和資料,不會加任何的鎖;
- 當前讀——前讀會獲取得到所有已經提交資料,按照邏輯上來講的話,在一個事務中第一次當前讀和第二次當前讀的中間有新的事務進行DML操作,這個時候倆次當前讀的結果應該是不一致的,但是實際的情況卻是在當前讀的這個事務還沒提交之前,所有針對當前讀的資料修改和插入都會被阻塞,主要是因為next-key
lock解決了當前讀可能會發生幻讀的情況。next-key lock當使用主鍵索引進行當前讀的時候,會降級為record lock(行鎖)。
8.2 Read view詳析
InnoDB支援MVCC多版本控制,其中READ COMMITTED和REPEATABLE READ隔離級別是利用consistent
read view(一致讀檢視)方式支援的。所謂的consistent read
view就是在某一時刻給事務系統trx_sys打snapshot(快照),把當時的trx_sys狀態(包括活躍讀寫事務陣列)記下來,之後的所有讀操作根據其事務ID(即trx_id)與snapshot中trx_sys的狀態做比較,以此判斷read
view對事務的可見性。
REPEATABLE READ隔離級別(除了GAP鎖之外)和READ COMMITTED隔離級別的差別是建立snapshot時機不同。REPEATABLE READ隔離級別是在事務開始時刻,確切的說是第一個讀操作建立read view的時候,READ COMMITTED隔離級別是在陳述句開始時刻建立read view的。這就意味著REPEATABLE READ隔離級別下麵一個事務的SELECT操作只會獲取一個read view,但是READ COMMITTED隔離級別下一個事務是可以獲取多個read view的。
建立/關閉read view需要持有trx_sys->mutex,會降低系統效能,5.7版本對此進行最佳化,在事務提交時session會cache只讀事務的read view。
8.3 read view 判斷當前版本資料項是否可見
在InnoDB中,建立一個新事務的時候,InnoDB會將當前系統中的活躍事務串列(trx_sys->trx_list)建立一個副本(read
view),副本中儲存的是系統當前不應該被本事務看到的其他事務id串列。當使用者在這個事務中要讀取該行記錄的時候,InnoDB會將該行當前的版本號與該read
view進行比較。
具體的演演算法如下:
- 設該行的當前事務id為trx_id,read view中最早的事務id為trx_id_min, 最遲的事務id為trx_id_max;如果trx_id< trx_id_min的話,那麼表明該行記錄所在的事務已經在本次新事務建立之前就提交了,所以該行記錄的當前值是可見的。
- 如果trx_id>trx_id_max的話,那麼表明該行記錄所在的事務在本次新事務建立之後才開啟,所以該行記錄的當前值不可見;
- 如果trx_id_min
<= trx_id <= trx_id_max,
那麼表明該行記錄所在事務在本次新事務建立的時候處於活動狀態,從trx_id_min到trx_id_max進行遍歷,如果trx_id等於他們之中的某個事務id的話,那麼不可見。
如圖所示:
從該行記錄的DB_ROLL_PTR指標所指向的回滾段中取出最新的undo-log的版本號的資料,將該可見行的值傳回。
需要註意的是,新建事務(當前事務)與正在記憶體中commit 的事務不在活躍事務連結串列中。
在具體多版本控制中我們先來看下原始碼:
函式:read_view_sees_trx_id。
read_view中儲存了當前全域性的事務的範圍:
【low_limit_id, up_limit_id】
1.當行記錄的事務ID小於當前系統的最小活動id,就是可見的。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
2.當行記錄的事務ID大於當前系統的最大活動id(也就是尚未分配的下一個事務的id),就是不可見的。
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
3.當行記錄的事務ID在活動範圍之中時,判斷是否在活動連結串列中,如果在就不可見,如果不在就是可見的。
for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids - i - 1);
if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}
Read view 圖解:
參考:https://yq.aliyun.com/articles/560506
來源:https://segmentfault.com/a/1190000018652117