Mysql初探:資料庫事務概述
此文為極客時間:MySQL實戰45講的 3、8、18、19節事務相關部分的總結
一、事務的啟動方式
mysql 主要有兩種事務的啟動方式:
begin
或start transaction
顯式啟動事務。對應的提交語句是commit
,回滾是rollback
set autocommit = 0
關閉自動提交,然後在執行第一條 sql 的時候啟動事務,這個事務會一直持續到你主動 commit 或者 rollback,或者斷開連線才會結束。
有一些客戶端連線框架會在連線成功後預設修改設定,這可能導致意外的長事務。因此,顯示啟動事務明顯是比較安全的,但是對於一些需要頻繁使用事務的業務,每次都需要呼叫 begin 然後再 commit。對於這種情況,可以使用 commit work and chain
autocommit = 1
時,使用該語句可以在提交以後自動開啟下一個新事務。
這樣省去了再次執行 begin 語句的開銷,而且可以明確地知道每個語句是否處於事務中。
除此之外,我們還可以使用 sql 去在 information_schema 庫的 innodb_trx 這個表中查詢長事務:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started)) > 60
比如上面這條語句,就是用於查詢持續時間超過 60s 的事務
二、事務的隔離級別
我們知道事務有四大特性(ACID):原子性,一致性,隔離性,永續性。
針對隔離性,我們有:
- 讀未提交:一個事務還沒提交時,它做的變更就能被別的事務看到。
- 讀已提交:一個事務提交之後,它做的變更才會被其他事務看到。
- 可重複讀:一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料是一致的。當然在可重複讀隔離級別下,未提交變更對其他事務也是不可見的。
- 序列化:顧名思義是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。
簡單的理解:
- 讀未提交:別人改資料的事務尚未提交,我在我的事務中也能讀到。
- 讀已提交:別人改資料的事務已經提交,我在我的事務中才能讀到。
- 可重複讀:別人改資料的事務已經提交
- 序列化:我的事務尚未提交,別人就別想改資料。
以這張圖為例:
- 讀未提交: 則 V1 的值就是 2。這時候事務 B 雖然還沒有提交,但是結果已經被 A 看到了。因此,V2、V3 也都是 2。
- 讀已提交:則 V1 是 1,V2 的值是 2。事務 B 的更新在提交後才能被 A 看到。所以, V3 的值也是 2。
- 可重複讀:則 V1、V2 是 1,V3 是 2。之所以 V2 還是 1,遵循的就是這個要求:事務在執行期間看到的資料前後必須是一致的。
- 序列化:則在事務 B 執行“將 1 改成 2”的時候,會被鎖住。直到事務 A 提交後,事務 B 才可以繼續執行。所以從 A 的角度看, V1、V2 值是 1,V3 的值是 2。
我們不難看出,讀已提交和可重複讀,最大的區別在於,當一個查詢的事務尚未提交,另一個修改的事務的提交是否會影響到這次查詢結果。
三、事務隔離的實現
1.髒讀,幻讀,不可重複讀
說起事務,就不得不提到三種錯誤讀:
- 髒讀(讀到了RoolBack):表示一個事務能夠讀取另一個事務中還未提交的資料。這個未提交資料就是髒讀(Dirty Read)。
- 幻讀(讀到了insert):指同一個事務內多次查詢返回的結果集不一樣。
- 不可重複讀(讀到了update):是指在一個事務內,多次讀同一資料。
- 第一類丟失更新:兩個事務更新同一條資料資源,後做的事務撤銷,發生回滾造成已完成事務的更新丟失
- 第二類丟失更新:兩個事務更新同一條資料資源,後完成的事務會造成先完成的事務更新丟失
2.事務隔離的實現
在實現上,資料庫裡面會建立一個檢視,當訪問的時候以檢視的邏輯結果為準。
這裡需要注意一下,這裡的檢視區別於我們自己建立的 View :
innodb 建立的,用於實現 MVCC 時的一致性讀檢視,即 consistent read view,用於支援 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別。
- 讀未提交:直接返回記錄上的最新值,沒有檢視概念;
- 讀已提交:這個檢視是在每個 SQL 語句開始執行的時候建立的。
- 可重複讀:這個檢視是在事務啟動時建立的,整個事務存在期間都用這個檢視。
- 序列化:直接用加鎖的方式來避免並行訪問。
這裡單獨對讀已提交和可重複讀的邏輯做一個區分:
- 在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視;
- 在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的檢視。
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
讀未提交 | √ | √ | √ |
讀已提交 | × | √ | √ |
可重複讀 | × | × | √ |
序列化 | × | × | × |
四、MVCC
1.概述
MVCC 即是併發版本控制。拿可重複讀舉個例子:
我們知道 innodb 有個 undo log ,每條記錄在更新的時候都會在 undo log 中記錄一條回滾操作,通過日誌記錄可以回滾到上一狀態的值。
假設一個值從 1 被按順序改成了 2、3、4,在回滾日誌裡面就會有類似下面的記錄。
當前的值是4,但是對於不同時間段啟動的事務建立的檢視ABC而言,分別為1,2,4,這時就算把4再改成5,對於ABC三個檢視也不會有影響。
同一條記錄在系統中可以存在多個版本,就是資料庫的多版本併發控制(MVCC)。
2.一致性讀檢視的實現
當在可重複讀隔離級別下時,事務在啟動的時候就給整庫“拍了個快照”,這個快照就是我們在事務的隔離提到過一致性讀檢視。這個檢視是邏輯上的,用於描述事務之間的可見性。
在 innodb 裡,每個事務都有獨有的 transaction id,這是在事務開始的時候向系統申請的,是嚴格遞增的。
而每行資料也有多個版本,每次事務更新資料的時候都會把 id 賦給對應版本資料的 row trx_id。
如上圖,我們可以看到這一行資料被三個事務進行了修改,現在有四個版本,每個版本更新前都會記錄一條回滾的語句在 undo log。
事實上,V1,V2這些版本的資料並不是真實存在的,而是在需要的時候才通過 undo log 計算獲取。比如需要 V2,就從 V4 經過 U3 和 U2 獲得。
現在我們知道資料版本是如何跟事務繫結的,那麼事務的隔離就很好理解了:當一個事務啟動的時候,獲取事務 id,事務id比他小的說明是在他之前就產生的,這些事務對應的版本就是被本事務承認的,反之,則這些資料是不被本事務承認的,要向前找到可以承認的資料版本。
為此,innodb 會在事務啟動的時候,為事務建立一個數組,這個數字裡會存放所有當前啟動了但是還沒提交的事務的 id。這個數組裡最小的視為低水位,最大的+1視為高水位,從低水位到高水位中間的這塊區域,就是當前事務的一致性檢視。這段操作是在鎖保護性進行的。
3.資料版的一致性讀
假如我們只在事務裡面進行查詢,而暫時不涉及到更新,那麼基於一致性檢視,當前事務就可以根據資料版本id,也就是 row trx_id 來判斷當前資料版本對於自己而言是否可見:
- 如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個資料是可見的;
- 如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,是肯定不可見的;
- 如果落在黃色部分,那就包括兩種情況
- 若 row trx_id 在陣列中,表示這個版本是由還沒提交的事務生成的,不可見;
- 若 row trx_id 不在陣列中,表示這個版本是已經提交了的事務生成的,可見。
當然,可能存在這麼一種情況:如果有一個事務在未提交事務的區間,但是在當前事務獲
仍然以上圖為例,假如有一個事務,他的低水位是18,也就是說在他啟動的時候,row trx_id 是17 的V3是最新的版本,在他查詢的時候,而最新的版本變成了 row trx_id 是25的V4,那麼對他而言V4是不可見的,於是通過 undo log U3 計算得到V3,V3低於他的低水位,所以V3是可見的,故對於該事務而言值就是V3的值。
可以看到,事務開始前和事務開始後讀到的資料都一致的,這個就是一致性讀。可重複讀依賴這個隔離級別核心依賴於此。
4.資料的當前讀
當事務裡只進行查詢的時候一致性讀可以保證讀取的正確性,但是如果進行的是更新,那麼一致性讀反而會導致錯誤。我們以下圖為例:
原本 k 是2,事務C進行了更新並且率先提交,對於事務C而言,此時k是3,但是事務B又進行了一次更新,那麼等到提交的時候,k該是3還是4?
這裡涉及到一個規則。因為更新總是需要先讀後改,所以更新的讀必須要讀最新的資料,也就是當前讀。
值得一提的是,如果是 select 語句,如果加了讀鎖或者寫鎖,也是當前讀:
# 加讀鎖
select k from t where id=1 lock in share mode;
# 加寫鎖
select k from t where id=1 for update;
然後,我們在前面瞭解了行鎖,而行鎖有一個兩階段鎖的機制:事務裡的有對某一行資料的更新,那麼sql執行前就會去獲取行鎖,然後執行完sql之後不釋放,等到事務提交之後才會去釋放鎖。由於事務C先獲取了行鎖,那麼事務B的更新就會等待事務C釋放鎖以後才會得到鎖。反映到執行上,就是事務B的 update 等到 事務C提交了才會繼續進行。
也就說,而行鎖的兩階段鎖保證了更新的順序進行,當前讀機制保證的更新語句總是能拿到最新的資料。
5.一致性檢視與可重複讀和讀已提交
MVCC 實現的核心在於一致性檢視,可重複讀和讀已提交建立檢視的機制決定了他們實現效果的不同:
- 在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視;
- 在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的檢視。
6.為什麼要避免長事務
- 佔用日誌空間:因為一致性檢視需要通過 undo log 去計算舊版本的資料,而 undo log 只有在沒有比某條日誌更早的一致性檢視時才會刪除。所以如果存在長事務,可能就會導致資料庫的檢視存在很長時間,直到這些檢視刪除前日誌都會一直保留,這將會導致佔用大量儲存空間。
- 影響版本控制計算效能:在可重複讀這個隔離級別下,如果其他事務對某條資料進行了非常多次的操作,最後會導致本事務讀取的時候必須要通過 undo log 計算非常多次才能找到最初的資料版本。
- 佔用鎖資源:長事務還會可能會佔用鎖資源,比如只有等事務提交才能釋放的行鎖。
五、總結
1.事務的啟動:
begin
或start transaction
顯式啟動事務。對應的提交語句是commit
,回滾是rollback
;- set autocommit = 0`關閉自動提交,然後在執行第一條 sql 的時候啟動事務,這個事務會一直持續到你主動 commit 或者 rollback,或者斷開連線才會結束。
2.事務的隔離級別:
- 讀未提交:別人改資料的事務尚未提交,我在我的事務中也能讀到。會髒讀,幻讀,不可重複讀;
- 讀已提交:別人改資料的事務已經提交,我在我的事務中才能讀到。會幻讀,不可重複讀;
- 可重複讀:別人改資料的事務已經提交,我在我的事務中也不去讀。會不可重複讀;
- 序列化:我的事務尚未提交,別人就別想改資料。加鎖,不會錯誤讀。
3.併發版本控制(MVCC):
-
每個事務的更新都會產生一個新版本資料,每個資料版本有自己的 row trx_id,對應更新他們的事務的 transaction id;
-
事務啟動時 innodb 為事務建立一個數組,這個數字裡會存放所有當前啟動了但是還沒提交的事務的 id。這個數組裡最小的視為低水位,最大的+1視為高水位,從低水位到高水位中間的這塊區域,就是當前事務的一致性檢視。根據事務版本 id 從一致性檢視中判斷該版本對本事務是否可見;
-
可重複讀和讀已提交建立檢視的機制決定了他們實現效果的不同:
在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視;
在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的檢視。