大部分數據庫系統(如Oracle)都將都將讀提交(Read-Commited)做爲默認隔離級別,而MySQL卻選擇可重複讀(Repeatable-Read)做爲其默認隔離級別。這篇文章咱們就分析下MySQL爲什麼會選取不可重複讀隔離級別做爲默認隔離機制以及是如何解決不可重複讀隔離級別的幻讀問題。數據庫
展開分析以前,咱們先來認識下隔離級別的概念。
隔離級別總共有四種:併發
其中,讀未提交和串行化隔離級別通常不會使用到,由於讀未提交會致使髒讀,不可重複讀,幻讀等一系列問題。而串行化是將全部的事務強制串行執行,嚴重影響併發性能。性能
這裏咱們簡單介紹下髒讀,不可重複讀和幻讀的概念:spa
- 髒讀:事務A讀取到了事務B修改但未提交且最後要回滾的數據。
如上圖所示,t3時刻,事務A讀取到了事務B還累加可是還未提交的a值,且在t3時刻,事務B回滾了,那麼事務A基於t3時刻的查詢所作的操做就會出現問題。3d
- 不可重複讀:事務A先後讀取到的數據不一致。
如上圖所示,事務A在t2時刻讀取到a的值,和t4時刻讀取到的a的值不一致,由於事務B在t3時刻對a值進行了更新並提交。日誌
- 幻讀:事務A先後讀取的結果條數不一致。
如上圖所示,事務A在t2時刻和t4時刻獲取到的數據條數不一致,由於事務B在t3時刻新增了一條符合事務A查詢條件的數據並提交了。blog
其中,讀提交(RC)隔離級別能夠避免髒讀的產生,可是會有不可重複讀和幻讀的問題;可重複讀(RR)隔離級別能夠避免髒讀和不可重複讀的問題,可是會有幻讀的問題。
事務
關於MySQL爲什麼會採用可重複讀做爲其默認隔離級別,得從MySQL的binlog提及。
binlog是MySQL的二進制日誌,其記錄數據表結構變動(alter,create)以及表數據更改(update,delete,insert)。
binlog日誌有三種記錄模式並各有優缺點:
MySQL默認的binlog記錄模式爲row。同步
在早期版本的MySQL中,binlog只有statement這一種記錄模式,而此種模式致使的一個致命問題就是,在讀提交(RC)隔離級別下會致使主從數據不一致。
在binlog中,記錄日誌的規則爲:事務commit以後,記錄日誌。it
咱們看下在RC隔離機制下的一個案例:
假設此時binlog記錄模式爲statement。
那麼記錄binlog的順序爲:
t4時刻,記錄t1表的delete語句;
t6時刻,記錄對t表的update語句。
以後主從同步,master將自身的binlog同步給slave,slave執行同步時就會遇到的一個問題:slave會先執行刪除t1表的內容,再執行更新t表的記錄,此時會致使主從不一致。
接下來,咱們在看下在RR隔離機制下的相同案例:
在RR隔離機制下,事務B的操做被阻塞,因此不會使得binlog在statement模式下記錄順序出現顛倒而致使主從數據不一致問題。
因此,因爲早期MySQL版本中binlog只有statement模式,而在讀提交(RC)隔離級別下記錄的binlog使用statement模式會致使主從數據不一致的問題,因此,MySQL選擇使用可重複讀(RR)做爲默認隔離級別以保證主從複製數據一致性。
在高性能MySQL第三版中可重複讀隔離級別的描述中寫到:可重複讀不能避免幻讀的產生。幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍內插入了新行,當以前的事務再次讀取該範圍的記錄時,會產生幻行。InnoDB存儲引擎經過多版本併發控制(MVCC)解決了幻讀問題。
咱們先來看一個可重複讀隔離級別(RR)下的實例:
分析以上實例:
理論上,在t3時刻,事務B插入了一條符合事務A查詢條件的記錄並提交了事務,那麼事務A在t2和t4時刻的查詢應該是不同的,可是實際結果確是:事務A先後查詢結果一致。
實際上,這是MVCC的功勞。MVCC的實現,是經過保存數據在某個時間點的快照來實現的。也就是說,在某個時間點事務開啓時,其看到的數據是該時間點以前已經提交的數據的快照內容,這就保證了事務執行期間看到的數據時一致的。
分析「RR級別避免幻讀」圖示中的事務:
事務A在t2時刻獲取到快照a,此快照將持續到t4時刻事務A提交事務。
事務B在t3時刻插入一條數據,可是事務A的快照a是基於t2時刻的快照,因此事務A並不能獲取到事務B插入的數據。
固然,MySQL的MVCC快照並非每個事務進來就copy一份數據庫信息,而是基於數據表每行信息後面保存的系統版本號去實現的。以下圖所示,一行信息會有多個版本並存,每一個事務可能讀取到的版本不同。
每開啓一個新的事務,系統版本都會自動遞增,事務開始時刻的系統版本號會做爲事務版本號,用來和查詢到的每行記錄的版本號進行比較。
針對select,insert,delete,update操做,InnoDB的MVCC具體操做爲:
select:
InnoDB會根據兩個條件檢查每行記錄值:
一、InnoDB只查找行的系統版本號小於或等於事務的系統版本號的記錄,這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
二、行的刪除版本要麼未定義,要麼大於當前事務版本號,確保能夠讀取到未刪除以前的數據。
insert:
InnoDB爲新插入的行保存當前系統版本號做爲行版本號。
delete:
InnoDB爲刪除的每一行保存當前系統版本號做爲行刪除標識。
update:
InnoDB爲插入的一行新記錄保存當前系統版本號做爲行版本號,同時保存當前系統版本號到原來的行做爲行刪除標識。
文字太抽象,咱們經過圖來了解下:
start transaction with consistent snapshot;表示當即開啓事務。
一、select
假設表T(id,a,b) 有數據[1,1,1],[2,2,2],[3,3,3]
假設當前事務ID爲10:
如上,事務B之因此獲取不到事務A的insert,是由於事務B的事務ID比事務A提交的插入數據的行標誌ID小。
二、update
update的常規commit和select相似,咱們看下未提交的狀況。
假設表T(id,a,b) 有數據[1,1,1],[2,2,2],[3,3,3]
假設當前事務ID爲10,當前id=1行版本號爲10:
咱們根據上圖分析下流程。
首先爲了方便理解,咱們將行版本ID以及當前行記錄內容記爲x{z,z,z}
那麼初始版本爲:10{1,1,1}
事務A:可讀版本爲10,11
事務B:可讀版本爲10,11,12
事務C:可讀版本爲10,11,12,13
執行流程:
a、事務A,B,C依次開啓事務;
b、事務C首先update並commit,那麼此時版本爲13{1,2,1};
c、接下來事務B執行update,此時查詢到當前最新行版本爲13{1,2,1},update須要當前讀的數據,以防數據不一致,因此拿到13{1,2,1}版本數據進行update,此時版本變動爲12{1,3,1};
d、而後事務A執行了select,查詢時,由於事務A的事務版本號爲11,因此只能讀取行版本號小於等於11的版本,因此仍是原始數據。
整個版本變動過程爲:
10{1,1,1} -> 13{1,2,1} -> 12{1,3,1}
以上就是update的一種狀況。
delete就和select,update相似,就再也不詳細說明了。
MySQL之因此選擇可重讀事務隔離機制是由於早期binlog只支持statement格式,而此種格式在讀提交隔離機制下回致使主從不一致。MySQL的可重讀隔離機制解決幻讀的問題關鍵是靠MVCC的實現,事務ID和行版本ID保證了讀取的一致性和隔離性。在MySQL中,經過多版本併發控制(MVCC)去避免幻讀的問題,可是隻是在select的時候能夠避免幻讀,update以後再select仍是可能會出現幻讀現象。