事務管理
- 1、什么是事務?
- 2、事務常見操作方式
- 3、事務隔離級別
- 4、數據庫并發場景
- 4.1、讀-寫
- 4.2、RR與RC的本質區別
1、什么是事務?
mysql是基于CS模式的,是一套網絡服務,所以我們是可以在本地連接上遠程服務器的mysql服務端的。mysql服務內是多線程并發處理客戶端請求的,所以如果CURD不加以控制,會出問題。如下:
現在有兩個mysql客戶端,搶票有兩個步驟,首先要獲取票數判斷是否大于0,然后進行update操作讓票數減1,現在客戶端A獲取票數判斷完后線程切換,客戶端B也獲取然后進行判斷,這時候兩個線程就都進入了if語句內部,所以一張票被賣了兩次,這就是線程安全問題。
CURD滿足什么屬性可以解決上述問題?
1、買票的過程得是原子的。
2、買票互相應該不能影響。
3、買完票應該要永久有效。
4、買前和買后都要是確定的狀態。
什么是事務?
事務就是一組DML語句組成,這些語句在邏輯上存在相關性,這一組DML語句要么全部成功,要么全部失敗,是一個整體。MySQL提供一種機制,保證我們達到這樣的效果。事務還規定不同的客戶端看到的數據是不相同的。
事務就是要做的或所做的事情,主要用于處理操作量大,復雜度高的數據。假設一種場景:你畢業了,學校的教務系統后臺 MySQL 中,不在需要你的數據,要刪除你的所有信息(一般不會),那么要刪除你的基本信息(姓名,電話,籍貫等)的同時,也刪除和你有關的其他信息,比如:你的各科成績,你在校表現,甚至你在論壇發過的文章等。這樣,就需要多條 MySQL 語句構成,那么所有這些操作合起來,就構成了一個事務。
正如我們上面所說,一個 MySQL 數據庫,可不止你一個事務在運行,同一時刻,甚至有大量的請求被包裝成事務,在向 MySQL 服務器發起事務處理請求。而每條事務至少一條SQL,最多很多SQL,這樣如果大家都訪問同樣的表數據,在不加保護的情況,就絕對會出現問題。甚至,因為事務由多條SQL構成,那么,也會存在執行到一半出錯或者不想再執行的情況,那么已經執行的怎么辦呢?
所以,一個完整的事務,絕對不是簡單的sql集合,還需要滿足如下四個屬性:
- 原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- 一致性:在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。
- 隔離性:數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交( Read uncommitted )、讀提交( read committed )、可重復讀( repeatable read )和串行化( Serializable )
- 持久性:事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
上面四個屬性,可以簡稱為 ACID 。
原子性(Atomicity,或稱不可分割性)
一致性(Consistency)
隔離性(Isolation,又稱獨立性)
持久性(Durability)
為什么會出現事務?
事務被MySQL編寫者設計出來,本質是為了當應用程序訪問數據庫的時候,事務能夠簡化我們的編程模型,不需要我們去考慮各種各樣的潛在錯誤和并發問題。可以想一下當我們使用事務時,要么提交,要么回滾,我們不會去考慮網絡異常了,服務器宕機了,同時更改一個數據怎么辦對吧?因此事務本質上是為了應用層服務的。而不是伴隨著數據庫系統天生就有的。
備注:我們后面把 MySQL 中的一行信息,稱為一行記錄。
事務的版本支持:
在 MySQL
中只有使用了 Innodb
數據庫引擎的數據庫或表才支持事務, MyISAM
不支持。
2、事務常見操作方式
事務的提交方式有兩種:自動提交、手動提交。
查看事務提交方式:
show variables like 'autocommit';
可以看到自動提交默認是打開的。
使用 set
來改變 MySQL
的自動提交模式。
set autocommit=0; # 禁止自動提交
set autocommit=1; # 開啟自動提交
為了方便后續演示,將事務隔離級別設置為讀未提交。
set global transaction isolation level read uncommitted;
之后重啟mysql客戶端,使用以下命令進行查看。
select @@transaction_isolation;
準備工作:創建測試表。
create table if not exists account(id int primary key,name varchar(50) not null default '',balance decimal(10,2) not null default 0.0
)ENGINE=InnoDB;
正常演示-證明事務的開始與回滾
我們自動提交首先設置為開啟。
開始事務有兩種方式:
start transaction;
begin;
開啟事務后執行sql時,可以設置保存點,方便后續回滾。
savepoint s1;
當我們在左側插入一條語句后,右邊就可以看到插入的語句。
接著我們再設置兩個保存點,然后插入兩條數據,右邊查詢也可以看到新插入的兩條數據。
那么現在我們不想要新插入的王五這條數據了,我們可以使用rollback to進行回滾。
當我們回滾到s3,右邊再次查看的時候久發現王五這條數據已經沒有了。
如果說上面的操作我都全部回滾,那么我們可以直接使用rollback命令回滾。
此時再次查看發現剛才插入的記錄都沒有了。
非正常演示1-未commit,客戶端崩潰,MySQL會自動回滾
現在我們左側插入了兩條數據,現在我們讓左側的MySQL客戶端異常退出,看看會發生什么?
在左側MySQL客戶端直接輸入ctrl + \。
可以看到當MySQL異常退出后,MySQL未提交的事務會自動回滾到最初的情況。
非正常演示2-commit之后,客戶端崩潰,MySQL數據不會再受到影響,已經持久化。
當commit之后,數據已經持久化到MySQL中了,這時候客戶端崩潰退出,我們查看account表中的數據也是存在的。所以一旦一個事務提交了,就無法回滾了。
我們查看自動提交發現是ON打開的,而這里事務的自動提交跟我們上面的begin開啟一個事務是沒有關系的,一旦我們begin手動開啟了一個事務,就需要手動commit提交,跟是否自動提交無關系。
非正常演示3-驗證單條SQL和事務的關系
首先關閉自動提交:
下面驗證關閉自動提交執行單條SQL的效果:
剛開始有四條數據,我們開啟一個事務,刪除了id=1的記錄并提交。接著我們再直接執行delete語句刪除id=3的記錄。之后右側查看發現兩條記錄都不存在了,然后我們這時候異常退出MySQL客戶端,再次查詢account,發現id=3的記錄恢復了。
這是因為刪除id=1的時候,我們開啟了一個事務,并且提交了,所以已經持久化到MySQL中了。而后面delete的時候,異常退出了,由于autocommit沒有開啟,所以進行回滾。
下面看autucommit打開的情況:
演示了事務和單條sql刪除的情況,手動開啟事務并提交,即可客戶端崩潰也不會回滾,因為已經持久化了。而使用單條sql,由于開啟了autocommit,所以執行單條sql就是一個事務,執行完單條sql會自動提交,因此崩潰了再次查看數據也是被刪除得了,不會進行回滾。
因此開啟了autocommit,執行單條sql就是一個事務,以前執行的單條sql都是事務。
結論:
- 只要輸入begin或者start transaction,事務便必須要通過commit提交,才會持久化,與是否設置set autocommit無關。
- 事務可以手動回滾,同時,當操作異常,MySQL會自動回滾
- 對于 InnoDB 每一條 SQL 語言都默認封裝成事務,自動提交。(select有特殊情況,因為MySQL有MVCC)
- 從上面的例子,我們能看到事務本身的原子性(回滾),持久性(commit)
- 那么隔離性?一致性?
事務操作注意事項:
- 如果沒有設置保存點,也可以回滾,只能回滾到事務的開始。直接使用rollback(前提是事務還沒有提交)
- 如果一個事務被提交了(commit),則不可以回退(rollback)
- 可以選擇回退到哪個保存點
- InnoDB支持事務,MyISAM不支持事務
- 開始事務可以使用 start transaction 或者 begin
3、事務隔離級別
如何理解隔離性:
MySQL服務可能會同時被多個客戶端進程(線程)訪問,訪問的方式以事務方式進行。一個事務可能由多條SQL構成,也就意味著,任何一個事務,都有執行前,執行中,執行后的階段。而所謂的原子性,其實就是讓用戶層,要么看到執行前,要么看到執行后。執行中出現問題,可以隨時回滾。所以單個事務,對用戶表現出來的特性,就是原子性。
但,畢竟所有事務都要有個執行過程,那么在多個事務各自執行多個SQL的時候,就還是有可能會出現互相影響的情況。比如:多個事務同時訪問同一張表,甚至同一行數據。就如同你媽媽給你說:你要么別學,要學就學到最好。至于你怎么學,中間有什么困難,你媽媽不關心。那么你的學習,對你媽媽來講,就是原子的。那么你學習過程中,很容易受別人干擾,此時,就需要將你的學習隔離開,保證你的學習環境是健康的。
數據庫中,為了保證事務執行過程中盡量不受干擾,就有了一個重要特征:隔離性。
數據庫中,允許事務受不同程度的干擾,就有了一種重要特征:隔離級別。
隔離級別:
- 讀未提交【Read Uncommitted】: 在該隔離級別,所有的事務都可以看到其他事務沒有提交的執行結果。(實際生產中不可能使用這種隔離級別的),但是相當于沒有任何隔離性,也會有很多并發問題,如臟讀,幻讀,不可重復讀等,我們上面為了做實驗方便,用的就是這個隔離性。
- 讀提交【Read Committed】 :該隔離級別是大多數數據庫的默認的隔離級別(不是 MySQL 默認的)。它滿足了隔離的簡單定義:一個事務只能看到其他的已經提交的事務所做的改變。這種隔離級別會引起不可重復讀,即一個事務執行時,如果多次 select, 可能得到不同的結果。
- 可重復讀【Repeatable Read】: 這是 MySQL 默認的隔離級別,它確保同一個事務,在執行中,多次讀取操作數據時,會看到同樣的數據行。但是會有幻讀問題。
- 串行化【Serializable】: 這是事務的最高隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決了幻讀的問題。它在每個讀的數據行上面加上共享鎖,。但是可能會導致超時和鎖競爭(這種隔離級別太極端,實際生產基本不使用)
隔離級別如何實現:隔離,基本都是通過鎖實現的,不同的隔離級別,鎖的使用是不同的。常見有,表鎖,行鎖,讀鎖,寫鎖,間隙鎖(GAP),Next-Key鎖(GAP+行鎖)等。不過,我們目前現有這個認識就行,先關注上層使用。
查看和設置隔離性:
# 查看隔離級別
select @@global.transaction_isolation;
select @@session.transaction_isolation;
select @@transaction_isolation;
第一種查看方式是全局配置的,當我們使用mysql客戶端連接服務端,會從global全局配置中讀取對應的隔離信息,然后將當前會話設置為對應的隔離級別。第二種方式就是從global中讀取的,第二種方式和第三種方式本質上是一樣的。
設置全局的,當我們重新登錄客戶端,由于session是從global中獲取的,所以每次登錄都是global設置的隔離級別,而設置session只對本次登錄有效。
# 設置隔離級別
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}
演示:設置全局隔離級別為串行化,當前隔離級別為讀提交。
讀未提交——read uncommitted
將事務隔離級別設置為讀未提交,然后我們啟動兩個mysql客戶端,分別運行兩個事務,首先在右邊先查看account表的所有信息,發現只有王五,之后左側事務插入兩條數據,并且事務還未提交,右側再次查看,發現多了剛才插入的兩條數據,這就是讀未提交。
當一個事務插入數據或者更新數據的時候,該事務還沒有提交,另一個并發執行的事務讀取可以看到,這就是讀未提交。很明顯這種方式是有問題的。
該方式幾乎沒有加鎖,雖然效率高,但是問題很多,嚴重不推薦。
一個事務在執行中,讀取到另一個事務的更新(或其他操作)但是未commit的數據,這種現象叫做臟讀。
讀提交——read committed
同樣啟動兩個事務,首先右側查看account表,然后左側事務進行插入和更新操作但是不commit,然后右側再次查看,發現并沒有讀取到左側事務更新的數據,而當左側事務commit之后,右側事務再次查看就可以查到了。這就是讀提交。
同時注意到,當左側事務提交后,右側事務并沒有提交。此時在右側事務中,在同一個事務中,同樣的讀取,讀取到了不同的值,這種現象叫做不可重復讀 not repeatable read。
不可重復讀是問題嗎?
設想存在這樣一張員工表emp,里面包含了員工id、員工姓名、員工薪水。現在年終了,老板需要根據薪資的高低來獎勵員工,比如設置了如下的獎勵方式:
現在小張負責處理數據,所以他開啟了一個事務,根據薪資水平查找出對應的員工信息。好巧不巧,小王這時候給Tom修改薪資,假設Tom原來薪資是3200,現在修改為4500。當左邊事務執行到第三條sql,查詢出了tom,而這時候換另一個事務執行,另一個事務執行完并提交。然后繼續查詢第四條sql,這時候又查出了tom,也就是tom既出現在3000->4000,也出現在4000->5000。
因此不可重復讀是有問題的。
可重復讀——repeatable read
MySQL默認采用的隔離級別就是可重復讀。
我們重新啟動MySQL服務,然后登錄查看,發現隔離級別就是可重復讀。
下面演示效果:
啟動兩個事務,右邊先做一次查詢,接著左邊插入數據后再做一次查詢,發現并沒有發生變化。左邊提交事務后邊繼續查詢,發現還是不變。
當我們把右邊事務提交后再次查詢就發現數據變了。
我們發現在左邊終端insert數據,在右邊終端的事務周期中并沒有影響,符合可重復的特點。但是一般的數據庫在可重復讀情況下,無法屏蔽其他事務insert的數據。因為隔離性實現是對數據加鎖完成的,而insert待插入的數據并不存在,那么一般的加鎖無法屏蔽這類問題。會造成雖然大部分內容是可重復讀的,但是insert的數據在可重復讀的情況被讀取出來,導致多次查找時會找出新的記錄,就如同產生了幻覺,這種現象叫做幻讀(phantom read)。而MySQL在RR級別的時候,是解決了幻讀問題的。解決方式:Next-Key鎖,GAP+行鎖解決的。
串行化——serializable
啟動兩個事務,左邊進行查詢,右邊也進行查詢,發現并沒有阻塞,都能正確讀取數據。接著左邊刪除第一條數據,發現直接卡住了。
左邊卡住后,右邊查詢數據時還是可以看到。而當過了一段時間后,左邊timeout超時了。
另外一種情況,啟動兩個事務并且分別進行查詢數據,然后左側刪除卡住,右邊的事務直接提交,提交后發現左邊的sql語句也執行了。
這時候再去查看數據還是不變的,而當左側事務提交后再查詢就發現id=1的數據已被刪除。
這時候兩個事務執行就是串行化執行的,對所有操作進行加鎖,不會有問題,但是效率很低。
如上圖,串行化后執行select直接獲取鎖然后讀取,而進行增刪改會放到MySQL的等待隊列中。
總結:
- 其中隔離級別越嚴格,安全性越高,但數據庫的并發性能也就越低,往往需要在兩者之間找一個平衡點。
- 不可重復讀的重點是修改和刪除:同樣的條件,你讀取過的數據,再次讀取出來發現值不一樣了。
幻讀的重點在于新增:同樣的條件,第1次和第2次讀出來的記錄數不一樣。 - 說明: mysql 默認的隔離級別是可重復讀,一般情況下不要修改。
- 上面的例子可以看出,事務也有長短事務這樣的概念。事務間互相影響,指的是事務在并行執行的時候,即都沒有commit的時候,影響會比較大。
一致性:
- 事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。當數據庫只包含事務成功提交的結果時,數據庫處于一致性狀態。如果系統運行發生中斷,某個事務尚未完成而被迫中斷,而該未完成的事務對數據庫所做的修改已被寫入數據庫,此時數據庫就處于一種不正確(不一致)的狀態。因此一致性是通過原子性來保證的。
- 其實一致性和用戶的業務邏輯強相關,一般MySQL提供技術支持,但是一致性還是要用戶業務邏輯做支撐,也就是一致性是由用戶決定的。
- 而技術上,通過AID保證C。
4、數據庫并發場景
數據庫并發的場景有三種:
- 讀-讀:不存在任何問題,也不需要并發控制。
- 讀-寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀。
- 寫-寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失(后面補充)
4.1、讀-寫
多版本并發控制( MVCC
)是一種用來解決 讀-寫沖突
的無鎖并發控制。
為事務分配單向增長的事務ID,為每個修改保存一個版本,版本與事務ID關聯,讀操作只讀該事務開始前的數據庫的快照。 所以 MVCC
可以為數據庫解決以下問題:
- 在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫并發讀寫的性能
- 同時還可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
每個事務都有自己的事務ID,可以根據事務ID的大小來決定事務到來的先后順序。mysqld可能面臨處理多個事務的情況,事務也有生命周期,所以mysqld也要對事務進行管理——先描述,再組織。事務在我們看來就是mysqld中的一個/一套結構體對象。
理解 MVCC
需要知道三個前提知識:3個記錄隱藏字段、undo
日志、 Read View
先看隱藏字段,數據庫中的表結構有如下隱藏字段:
DB_TRX_ID
:6byte,最近修改(修改/插入)事務ID,記錄創建這條記錄/最后一次修改該記錄的事務IDDB_ROLL_PTR
:7byte,回滾指針,指向這條記錄的上一個版本(簡單理解成,指向歷史版本就行,這些數據一般在undo log
中)DB_ROW_ID
:6byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB
會自動以DB_ROW_ID
產生一個聚簇索引- 補充:實際還有一個刪除flag隱藏字段, 即記錄被更新或刪除并不代表真的刪除,而是刪除flag標志變了
現在假設我們的測試表結構如下:
create table if not exists student(name varchar(11) not null,age int not null
);insert into student (name, age) values ('張三', 28);
現在我們查表:
我們發現表中只有兩列屬性,但實際上不是這樣的,實際上表的結構應該如下圖
再來看 undo
日志:
MySQL
將來是以服務進程的方式,在內存中運行。我們之前所講的所有機制:索引,事務,隔離性,日志等,都是在內存中完成的,即在 MySQL
內部的相關緩沖區(buffer pool)中,保存相關數據,完成各種判斷操作。然后在合適的時候,將相關數據刷新到磁盤當中的。而 undo
日志也是在buffer pool中的,我們可以理解為一段內存緩沖區,用來保存日志數據。
接著我們模擬 MVCC
:
我們已經在student表中插入的一條數據,所以現在的數據為
我們假設這次插入的事務ID就是9,接下來有一個事務10,對student表中數據進行update操作,將name從張三改成李四,具體過程如下:
- 事務10因為要修改,所以要先給該記錄加行鎖。
- 修改前,先將該行記錄拷貝到undo log中,所以undo log中就有了一行副本數據。(原理就是寫時拷貝)
- 所以現在MySQL中有兩行同樣的記錄。現在修改原始記錄中的name,改成 ‘李四’。并且修改原始記錄的隱藏字段DB_TRX_ID為當前事務10的ID,我們默認從10開始,之后遞增。而原始記錄的回滾指針DB_ROLL_PTR列,里面寫入undo log中副本數據的地址,從而指向副本記錄,表示我的上一個版本就是它。
- 事務10提交,釋放鎖。
接著又來一個事務11,也是進行update操作,將age從28改為38,過程如下:
- 事務11因為也要修改,所以要先給該記錄加行鎖。
- 修改前,先將該行記錄拷貝到undo log中,所以undo log中就又有了一行副本數據。此時,新的副本,我們采用頭插方式,插入undo log。現在修改原始記錄中的age,改成38。并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前事務11的ID。而原始記錄的回滾指針DB_ROLL_PTR列,里面寫入undo log中副本數據的地址,從而指向副
本記錄,既表示我的上一個版本就是它。 - 事務11提交,釋放鎖。
這樣我們就形成了基于鏈表記錄的歷史版本鏈,所謂回滾就是用歷史數據覆蓋當前數據。
而上面一個一個的版本,我們又稱為快照。
一些思考:
1、上面是以更新update講的,如果是刪除delete呢?
不要忘了,還有個隱藏的字段flag,當刪除可以把flag字段置為0,而1表示未刪除,那么在buffer pool中page中,多行記錄就不一定連續了,因為可能中間某行flag置為0表示刪除。但是當數據刷盤時,存儲到磁盤中數據一定是連續的,這樣當恢復的時候讀取也是連續的了。那么有了flag字段,刪除delete也可以形成版本鏈。
2、如果是插入insert呢?
insert之前是沒有記錄的,那么也就無法形成版本鏈,但是為了保證在不同隔離級別下能否看到數據,所以也需要將insert的數據保存在undo log中。另外還要記錄一條相反的delete語句,這樣當需要回滾的時候直接執行delete刪除即可回滾。當事務commit之后,undo log中的數據就可以刪除了。
3、如果是查詢select呢?
select并不會修改數據,所以維護select的多個版本沒有意義。但是有個問題,select讀取的是當前最新版本還是歷史的版本呢?
當前讀:讀取最新的記錄就是當前讀。增刪改都叫做當前讀。select也可能當前讀,比如select lock in share mode,select for update。
快照讀:讀取歷史版本就叫做快照讀。
當多個事務同時增刪改的時候,都是當前讀所以需要加鎖。如果有select過來,并且select也要讀取最新版,那么也需要加鎖,這就是串行化。但如果select讀取的是快照讀,這時候就不需要加鎖了,可以并行。此時就提高了效率,這就是MVCC存在的意義。那么什么決定select當前讀還是快照讀?——隔離級別。為什么要有隔離級別?——事務是原子的。事務要經過:begin->CURD->commit,是有一個階段的,多個事務執行CURD可能會交叉,為了保證事務有先有后,應該讓不同事務看到他該看的內容,這就是隔離性和隔離級別解決的問題。
最后來看 Read View
:
Read View
就是事務進行快照讀操作的時候產生的讀視圖 (Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID,這個ID是遞增的,所以最新的事務,ID值越大)
Read View
在 MySQL
源碼中,就是一個類,本質是用來進行可見性判斷的。 即當我們某個事務執行快照讀的時候,對該記錄創建一個 Read View
讀視圖,把它比作條件,用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的 undo log
里面的某個版本的數據。
下面是Read View結構的一些數據:
class ReadView {
// 省略...
private:/** 高水位,大于等于這個ID的事務均不可見*/trx_id_t m_low_limit_id/** 低水位:小于這個ID的事務均可見 */trx_id_t m_up_limit_id;/** 創建該 Read View 的事務ID*/trx_id_t m_creator_trx_id;/** 創建視圖時的活躍事務id列表*/ids_t m_ids;/** 配合purge,標識該視圖不需要小于m_low_limit_no的UNDO LOG,* 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 標記視圖是否被關閉*/bool m_closed;// 省略...
};
而我們重點關注以下四個字段:
這里可以解釋一下:比如當事務5進行快照讀的時候,會產生讀視圖結構,本質上就是malloc或者new了一個結構體/類,m_dis保存當前活躍的事務,比如有3、4、6。然后up_limit_id就是3了,low_limit_id表示尚未分配的下一個事務ID,假設當前最新的事務ID就是6,那么該值就是7。creator_trx_id就是創建該讀視圖的id,也就是5。
我們在實際讀取數據版本鏈的時候,是能讀取到每一個版本對應的事務ID的,即:當前記錄的DB_TRX_ID
那么,我們現在手里面有的東西就有,當前快照讀的 ReadView
和版本鏈中的某一個記錄的 DB_TRX_ID
所以現在的問題就是,當前快照讀,應不應該讀到當前版本記錄。
現在有四個字段:
m_ids:一張列表,用來維護Read View生成時刻,系統正活躍的事務ID
up_limit_id:記錄m_ids列表中事務ID最小的ID(沒有寫錯)
low_limit_id:ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1(也沒有寫錯)
creator_trx_id:創建該ReadView的事務ID
如圖:最上面是版本鏈,下方是時間線。
我們先看時間線的最左側,如果說讀視圖中的 creator_trx_id
等于 DB_TRX_ID
說明讀取的就是我這個事務本身的操作,因此是可以被看到的。如果說隱藏字段的 DB_TRX_ID
小于 up_limit_id
說明這個事務比我當前活躍事務列表里面最小的ID都來的小,也就是在我快照之前就已經提交了,那么也是可以看到的。
接著我們看最右側,如果說隱藏字段 DB_TRX_ID
大于等于 low_limit_id
說明這個事務ID在我快照創建讀視圖的時候,還沒有分配出來使用,也就是快照之后才啟動的事務并且提交,因此不應該被看到。
最后看中間這一塊,此時我們需要先解決一個誤區,m_ids
集合中存在的活躍事務ID并不一定是連續的,可能先啟動的事務晚結束,晚啟動的事務由于是短事務所以已經提交了,因此 m_ids
并不一定是連續的事務ID集合。這時候如果 DB_TRX_ID
不在 m_ids
中,說明該事務在讀視圖創建之前已經提交了,所以可以看到。如果 DB_TRX_ID
在 m_ids
中,說明該事務和當前事務一樣,是活躍事務,因此不應該看到。
我們分析完之后再來看一下源碼:
源碼的邏輯,首先判斷DB_TRX_ID也就是函數中的id,是否小于活躍事務中最小的事務ID,或者等于創建該讀視圖的事務ID,如果滿足說明可以看到返回true。
接著判斷id是否大于等于下一個分配事務ID,如果是說明不應該被看到返回false。如果當前活躍事務集合為空,說明當前就只有我一個事務,那么也是可見的。
最后直接返回判斷id是否在獲取事務id集合m_ids中的邏輯反。如果id在m_ids中返回true,邏輯反之后就是false,表示不可見。如果id不在m_ids中返回false,邏輯反之后就是true,表示可見。
說完理論,我們走一次真實的流程看看:
當前數據庫表記錄如圖,然后有四個事務并發執行,其中事務4表示將name從張三改為李四,事務4最先提交,接著事務2進行快照讀,事務1和3暫時不管,只要知道這三個事務并發執行即可。當事務2快照讀會創建讀視圖,此時讀視圖中的數據如下:
//事務2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成時刻,系統尚未分配的下一個事務ID
creator_trx_id // 2
因為事務4先提交,事務2再形成快照讀,所以從DB_TRX_ID=4開始找,拿該行記錄的DB_TRX_ID去跟
up_limit_id,low_limit_id和活躍事務ID列表(trx_list) 進行比較,判斷當前事務2能看到該記錄的版本。
同時我們注意到,事務1、2、3、4剛開始是并發執行的,然后事務4提交后事務2快照讀,事務2可以看到事務4提交的數據,因此隔離級別為:讀提交。
Read View
是事務可見性的一個類,不是事務創建出來就有 Read View
,而是當這個事務已經存在,首次進行快照讀的時候,mysql
形成 Read View
4.2、RR與RC的本質區別
首先設置事務隔離級別為可重復讀repeatable read。然后創建如下user表:
create table if not exists user(id int primary key,age int,name varchar(20)
);
insert into user (id, age, name) values (1, 15,'黃蓉');
按照如下方式進行第一次測試:
首先分別啟動左右兩個事務A和B,然后分別進行讀取user表。接著事務A更新年齡并提交,事務B進行兩次查詢。我們發現第一次查詢的age還是15不變,而第二次查詢確是18。這里的select * from user lock in share mode以加共享鎖方式進行讀取,對應的就是當前讀。
接著我們再看第二次測試,步驟如下:
還是先啟動事務A和B,但是事務B什么也不做,事務A進行查詢、更新、提交,之后事務B采用快照讀和當前讀,發現讀取到的age都是28。
為什么會這樣呢?
當我們執行select的時候,是快照讀,進行快照讀的時候會創建 Read View
,第一次測試事務B一開始就創建了 Read View
,此時活躍事務集合m_ids里面就有事務A,所以哪怕事務A更新并提交后,后續事務B再進行快照讀的時候也讀取不到了,因為事務A一直在m_ids里面,此時只有當前讀才能看到。
第二次事務B是在事務A提交之后才進行快照讀的,當進行快照讀才會創建 Read View
,此時由于事務A已經提交了,所以對于事務B來說就是舊事務,因此就是可見的。
結論就是:事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,即某個事務首次出現快照讀決定該事務后續快照讀結果的能力。
RR 和 RC 的本質區別:
正是 Read View
生成時機的不同,從而造成RC、RR級別下快照讀的結果的不同。在RR級別下的某個事務的對某條記錄的第一次快照讀會創建一個快照及 Read View
,將當前系統活躍的其他事務記錄起來。此后在調用快照讀的時候,還是使用的是同一個 Read View
,所以只要當前事務在其他事務提交更新之前使用過快照讀,那么之后的快照讀使用的都是同一個 Read View
,所以對之后的修改不可見。即RR級別下,快照讀生成 Read View
時, Read View
會記錄此時所有其他活動事務的快照,這些事務的修改對于當前事務都是不可見的。而早于 Read View
創建的事務所做的修改均是可見。
而在RC級別下的,事務中,每次快照讀都會新生成一個快照和 Read View
,這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因。總之在RC隔離級別下,是每個快照讀都會生成并獲取最新的 Read View
。而在RR隔離級別下,則是同一個事務中的第一個快照讀才會創建 Read View
,之后的快照讀獲取的都是同一個 Read View
。正是RC每次快照讀,都會形成 Read View
,所以RC才會有不可重復讀問題。