以前分析一個死鎖問題,發現本身對數據庫隔離級別理解還不夠清楚,因此趁着這幾天假期,整理一下MySQL事務的四大隔離級別相關知識,但願對你們有幫助~html
事務,由一個有限的數據庫操做序列構成,這些操做要麼所有執行,要麼所有不執行,是一個不可分割的工做單位。mysql
假如A轉帳給B 100 元,先從A的帳戶里扣除 100 元,再在 B 的帳戶上加上 100 元。若是扣完A的100元后,還沒來得及給B加上,銀行系統異常了,最後致使A的餘額減小了,B的餘額卻沒有增長。因此就須要事務,將A的錢回滾回去,就是這麼簡單。sql
事務併發執行存在什麼問題呢,換句話說就是,一個事務是怎麼幹擾到其餘事務的呢?看例子吧~數據庫
假設如今有表:安全
CREATE TABLE `account` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, `balance` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `un_name_idx` (`name`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表中有數據:session
假設如今有兩個事務A、B:數據結構
由上圖能夠發現,事務A、B交替執行,事務A被事務B干擾到了,由於事務A讀取到事務B未提交的數據,這就是髒讀。併發
假設如今有兩個事務A和B:mvc
事務A又被事務B干擾到了!在事務A範圍內,兩個相同的查詢,讀取同一條記錄,卻返回了不一樣的數據,這就是不可重複讀。高併發
假設如今有兩個事務A、B:
事務A查詢一個範圍的結果集,另外一個併發事務B往這個範圍中插入/刪除了數據,並靜悄悄地提交,而後事務A再次查詢相同的範圍,兩次讀取獲得的結果集不同了,這就是幻讀。
既然併發事務存在髒讀、不可重複、幻讀等問題,InnoDB實現了哪幾種事務的隔離級別應對呢?
想學習一個知識點,最好的方式就是實踐之。好了,咱們去數據庫給它設置讀未提交隔離級別,實踐一下吧~
先把事務隔離級別設置爲read uncommitted,開啓事務A,查詢id=1的數據
set session transaction isolation level read uncommitted; begin; select * from account where id =1;
結果以下:
這時候,另開一個窗口打開mysql,也把當前事務隔離級別設置爲read uncommitted,開啓事務B,執行更新操做
set session transaction isolation level read uncommitted; begin; update account set balance=balance+20 where id =1;
接着回事務A的窗口,再查account表id=1的數據,結果以下:
能夠發現,在讀未提交(Read Uncommitted) 隔離級別下,一個事務會讀到其餘事務未提交的數據的,即存在髒讀問題。事務B都還沒commit到數據庫呢,事務A就讀到了,感受都亂套了。。。實際上,讀未提交是隔離級別最低的一種。
爲了不髒讀,數據庫有了比讀未提交更高的隔離級別,即已提交讀。
把當前事務隔離級別設置爲已提交讀(READ COMMITTED),開啓事務A,查詢account中id=1的數據
set session transaction isolation level read committed; begin; select * from account where id =1;
另開一個窗口打開mysql,也把事務隔離級別設置爲read committed,開啓事務B,執行如下操做
set session transaction isolation level read committed; begin; update account set balance=balance+20 where id =1;
接着回事務A的窗口,再查account數據,發現數據沒變:
咱們再去到事務B的窗口執行commit操做:
commit;
最後回到事務A窗口查詢,發現數據變了:
由此能夠得出結論,隔離級別設置爲已提交讀(READ COMMITTED) 時,已經不會出現髒讀問題了,當前事務只能讀取到其餘事務提交的數據。可是,你站在事務A的角度想一想,存在其餘問題嗎?
提交讀的隔離級別會有什麼問題呢?
在同一個事務A裏,相同的查詢sql,讀取同一條記錄(id=1),讀到的結果是不同的,即不可重複讀。因此,隔離級別設置爲read committed的時候,還會存在不可重複讀的併發問題。
若是你的老闆要求,在同個事務中,查詢結果必須是一致的,即老闆要求你解決不可重複的併發問題,怎麼辦呢?老闆,臣妾辦不到?來實踐一下可重複讀(Repeatable Read) 這個隔離級別吧~
哈哈,步驟一、二、6的查詢結果都是同樣的,即repeatable read解決了不可重複讀問題,是否是內心美滋滋的呢,終於解決老闆的難題了~
RR級別是否解決了幻讀問題呢?
再來看看網上的一個熱點問題,有關於RR級別下,是否解決了幻讀問題?咱們來實踐一下:
由圖可得,步驟2和步驟6查詢結果集沒有變化,看起來RR級別是已經解決幻讀問題了~
可是呢,RR級別仍是存在這種現象:
其實,上圖若是事務A中,沒有update account set balance=200 where id=5;
這步操做,select * from account where id>2
查詢到的結果集確實是不變,這種狀況沒有幻讀問題。可是,有了update這個騷操做,同一個事務,相同的sql,查出的結果集不一樣,這個是符合了幻讀的定義~
這個問題,親愛的朋友,你以爲它算幻讀問題嗎?
前面三種數據庫隔離級別,都有必定的併發問題,如今放大招吧,實踐SERIALIZABLE隔離級別。
把事務隔離級別設置爲Serializable,開啓事務A,查詢account表數據
set session transaction isolation level serializable; select @@tx_isolation; begin; select * from account;
另開一個窗口打開mysql,也把事務隔離級別設置爲Serializable,開啓事務B,執行插入一條數據:
set session transaction isolation level serializable; select @@tx_isolation; begin; insert into account(id,name,balance) value(6,'Li',100);
執行結果以下:
由圖可得,當數據庫隔離級別設置爲serializable的時候,事務B對錶的寫操做,在等事務A的讀操做。其實,這是隔離級別中最嚴格的,讀寫都不容許併發。它保證了最好的安全性,性能倒是個問題~
實現隔離機制的方法主要有兩種:
MySql使用不一樣的鎖策略(Locking Strategy)/MVCC來實現四種不一樣的隔離級別。RR、RC的實現原理跟MVCC有關,RU和Serializable跟鎖有關。
官方說法:
SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. Thus, using this isolation level, such reads are not consistent.
讀未提交,採起的是讀不加鎖原理。
官方的說法:
InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.)
SELECT ... FOR SHARE
,即加共享鎖。MVCC,中文叫多版本併發控制,它是經過讀取歷史版本的數據,來下降併發事務衝突,從而提升併發性能的一種機制。它的實現依賴於隱式字段、undo日誌、快照讀&當前讀、Read View,所以,咱們先來了解這幾個知識點。
對於InnoDB存儲引擎,每一行記錄都有兩個隱藏列DB_TRX_ID、DB_ROLL_PTR,若是表中沒有主鍵和非NULL惟一鍵時,則還會有第三個隱藏的主鍵列DB_ROW_ID。
- 事務未提交的時候,修改數據的鏡像(修改前的舊版本),存到undo日誌裏。以便事務回滾時,恢復舊版本數據,撤銷未提交事務數據對數據庫的影響。
- undo日誌是邏輯日誌。能夠這樣認爲,當delete一條記錄時,undo log中會記錄一條對應的insert記錄,當update一條記錄時,它記錄一條對應相反的update記錄。
- 存儲undo日誌的地方,就是回滾段。
多個事務並行操做某一行數據時,不一樣事務對該行數據的修改會產生多個版本,而後經過回滾指針(DB_ROLL_PTR)連一條Undo日誌鏈。
咱們經過例子來看一下~
mysql> select * from account ; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | Jay | 100 | +----+------+---------+ 1 row in set (0.00 sec)
事務B修改後,造成的Undo Log鏈以下:
快照讀:
讀取的是記錄數據的可見版本(有舊的版本),不加鎖,普通的select語句都是快照讀,如:
select * from account where id>2;
當前讀:
讀取的是記錄數據的最新版本,顯示加鎖的都是當前讀
select * from account where id>2 lock in share mode; select * from account where id>2 for update;
爲了下面方便討論Read View可見性規則,先定義幾個變量
- m_ids:當前系統中那些活躍的讀寫事務ID,它數據結構爲一個List。
- min_limit_id:m_ids事務列表中,最小的事務ID
- max_limit_id:m_ids事務列表中,最大的事務ID
注意啦!! RR跟RC隔離級別,最大的區別就是:RC每次讀取數據前都生成一個ReadView,而RR只在第一次讀取數據時生成一個ReadView。
我以爲理解一個新的知識點,最好的方法就是居於目前存在的問題/現象,去分析它的前因後果~ RC的實現也跟MVCC有關,RC是存在重複讀併發問題的,因此咱們來分析一波RC吧,先看一下執行流程
假設如今系統裏有A,B兩個事務在執行,事務ID分別爲100、200,而且假設存在的老數據,插入事務ID是50哈~
事務A 先執行查詢1的操做
# 事務A,Transaction ID 100 begin ; 查詢1:select * from account WHERE id = 1;
事務 B 執行更新操做,id =1記錄的undo日誌鏈以下
begin; update account set balance =balance+20 where id =1;
回到事務A,執行查詢2的操做
begin ; 查詢1:select * from account WHERE id = 1; 查詢2:select * from account WHERE id = 1;
查詢2執行分析:
咱們回到事務B,執行提交操做,這時候undo日誌鏈不變
begin; update account set balance =balance+20 where id =1; commit
再次回到事務A,執行查詢3的操做
begin ; 查詢1:select * from account WHERE id = 1; 查詢2:select * from account WHERE id = 1; 查詢3:select * from account WHERE id = 1;
查詢3執行分析:
因此,這就是RC存在不可重複讀問題的過程啦有不理解的地方能夠多讀幾遍哈
咱們再來分析一波,RR隔離級別是如何解決不可重複讀併發問題的吧~
你可能會以爲兩個併發事務的例子太簡單了,好的!咱們如今來點刺激的,開啓三個事務~
假設如今系統裏有A,B,C兩個事務在執行,事務ID分別爲100、200,300,存量數據插入的事務ID是50~
# 事務A,Transaction ID 100 begin ; UPDATE account SET balance = 1000 WHERE id = 1;
# 事務B,Transaction ID 200 begin ; //開個事務,佔坑先
這時候,account表中,id =1記錄的undo日誌鏈以下:
# 事務C,Transaction ID 300 begin ; //查詢1:select * from account WHERE id = 1;
查詢1執行過程分析:
接着,咱們把事務A提交一下:
# 事務A,Transaction ID 100 begin ; UPDATE account SET balance = 1000 WHERE id = 1; commit;
在事務B中,執行更新操做,把id=1的記錄balance修改成2000,更新完後,undo 日誌鏈以下:
# 事務B,Transaction ID 200 begin ; //開個事務,佔坑先 UPDATE account SET balance = 2000 WHERE id = 1;
回到事務C,執行查詢2
# 事務C,Transaction ID 300 begin ; //查詢1:select * from account WHERE id = 1; //查詢2:select * from account WHERE id = 1;
查詢2:執行分析:
InnoDB 實現了標準的行級鎖,包括兩種:共享鎖(簡稱 s 鎖)、排它鎖(簡稱 x 鎖)。
若是事務 T1 持有行 r 的 s 鎖,那麼另外一個事務 T2 請求 r 的鎖時,會作以下處理:
若是 T1 持有 r 的 x 鎖,那麼 T2 請求 r 的 x、s 鎖都不能被當即容許,T2 必須等待T1釋放 x 鎖才能夠,由於X鎖與任何的鎖都不兼容。
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
記錄鎖的事務數據(關鍵詞:lock_mode X locks rec but not gap
),記錄以下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10078 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;
由於RC是存在幻讀問題的,因此咱們先切到RC隔離級別,分析一波~
假設account表有4條數據。
所以,咱們能夠發現,RC隔離級別下,加鎖的select, update, delete等語句,使用的是記錄鎖,其餘事務的插入依然能夠執行,所以會存在幻讀~
由於RR是解決幻讀問題的,怎麼解決的呢,分析一波吧~
假設account表有4條數據,RR級別。
所以,咱們能夠發現,RR隔離級別下,加鎖的select, update, delete等語句,會使用間隙鎖+ 臨鍵鎖,鎖住索引記錄之間的範圍,避免範圍間插入記錄,以免產生幻影行記錄。