mysql事務中的隔離級別
事務隔離級別及造成的讀影響:
其中,可重復讀
這個隔離級別,有效地防止了臟讀和不可重復讀,但仍然可能發生幻讀,可能發生幻讀就表示可重復讀
這個隔離級別防不住幻讀嗎?
我不管從數據庫方面的教科書還是一些網絡教程上,經常看到RR級別是可以重復讀的,但是無法解決幻讀,只有可串行化(Serializable)才能解決幻讀,這個說法是否正確呢?
在這篇文章中,我將重點圍繞MySQL中
可重復讀(Repeatable read)能防住幻讀嗎?
這一問題展開討論,相信看完這篇文章后,你一定會對事務隔離級別有新的認識.
我們的數據庫中有如下結構和數據的Users
表,下文中我們將對這張表進行操作,
什么是幻讀?
在說幻讀之前,我們要先來了解臟讀和不可重復讀:
臟讀
當一個事務讀取到另外一個事務修改但未提交的數據時,就可能發生臟讀。
在我們的例子中,事務2修改了一行,但是沒有提交,事務1讀了這個沒有提交的數據。現在如果事務2回滾了剛才的修改或者做了另外的修改的話,事務1中查到的數據就是不正確的了,所以這條數據就是臟讀。
不可重復讀
“不可重復讀”現象發生在當執行SELECT 操作時沒有獲得讀鎖或者SELECT操作執行完后馬上釋放了讀鎖; 另外一個事務對數據進行了更新,讀到了不同的結果.
在這個例子中,事務2提交成功,因此他對id為1的行的修改就對其他事務可見了。導致了事務1在此前讀的age=1,第二次讀的age=2,兩次結果不一致,這就是不可重復讀.
幻讀
“幻讀”又叫"幻象讀",是''不可重復讀''的一種特殊場景:當事務1兩次執行''SELECT ... WHERE''檢索一定范圍內數據的操作中間,事務2在這個表中創建了(如[[INSERT]])了一行新數據,這條新數據正好滿足事務1的“WHERE”子句。
如圖事務1執行了兩遍同樣的查詢語句,第二遍比第一遍多出了一條數據,這就是幻讀。
臟讀、不可重復讀和幻讀的區別
三者的場景介紹完,但是一定仍然有很多同學搞不清楚,它們到底有什么區別,我總結一下.
臟讀:指讀到了其他事務未提交的數據.
不可重復讀: 讀到了其他事務已提交的數據(update).
不可重復讀與幻讀都是讀到其他事務已提交的數據,但是它們針對點不同.
不可重復讀:update.
幻讀:delete,insert.
mysql中的四種事務隔離級別
未提交讀
未提交讀(READ UNCOMMITTED)是最低的隔離級別,在這種隔離級別下,如果一個事務已經開始寫數據,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據.
把臟讀的圖拿來分析分析,因為事務2更新id=1的數據后,仍然允許事務1讀取該條數據,所以事務1第二次執行查詢,讀到了事務2更新的結果,產生了臟讀.
已提交讀
由於MySQL的InnoDB默認是使用的RR級別,所以我們先要將該session開啟成RC級別,並且設置binlog的模式
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';(或者是MIXED)
在已提交讀(READ COMMITTED)級別中,讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行,會對該寫鎖一直保持直到到事務提交.
同樣,我們來分析臟讀,事務2更新id=1的數據后,在提交前,會對該對象寫鎖,所以事務1讀取id=1的數據時,會一直等待事務2結束,處於阻塞狀態,避免了產生臟讀。
同樣,來分析不可重復讀,事務1讀取id=1的數據后並沒有鎖住該數據,所以事務2能對這條數據進行更新,事務2對更新並提交后,該數據立即生效,所以事務1再次執行同樣的查詢,查詢到的結果便與第一次查到的不同,所以已提交讀防不了不可重復讀。
可重復讀
在可重復讀(REPEATABLE READS)是介於已提交讀和可串行化之間的一種隔離級別(廢話😅),它是InnoDb的默認隔離級別,它是我這篇文章的重點討論對象,所以在這里我先賣個關子,后面我會詳細介紹.
可串行化
可串行化(Serializable )是高的隔離級別,它求在選定對象上的讀鎖和寫鎖保持直到事務結束后才能釋放,所以能防住上訴所有問題,但因為是串行化的,所以效率較低.
可重復讀(Repeatable read)能防住幻讀嗎?
可重復讀
在講可重復讀之前,我們先在mysql的InnoDB下做下面的實驗.
可以看到,事務A既沒有讀到事務B更新的數據,也沒有讀到事務C添加的數據,所以在這個場景下,它既防住了不可重復讀,也防住了幻讀.
到此為止,相信大家已經知道答案了,這是怎么做到的呢?
悲觀鎖與樂觀鎖
我們前面說的在對象上加鎖,是一種悲觀鎖機制,有很多文章說可重復讀
的隔離級別防不了幻讀, 是認為可重復讀會對讀的行加鎖,導致其他事務修改不了這條數據,直到事務結束,但是這種方案只能鎖住數據行,如果有新的數據進來,是阻止不了的,所以會產生幻讀.
可是MySQL、ORACLE、PostgreSQL等已經是非常成熟的數據庫了,怎么會單純地采用這種如此影響性能的方案呢?
我來介紹一下悲觀鎖和樂觀鎖.
悲觀鎖
正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。讀取數據時給加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。
樂觀鎖
相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。
而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
MySQL、ORACLE、PostgreSQL等都是使用了以樂觀鎖為理論基礎的MVCC(多版本並發控制)來避免不可重復讀和幻讀,MVCC的實現沒有固定的規范,每個數據庫都會有不同的實現方式,這里討論的是InnoDB的MVCC。
MVCC(多版本並發控制)
在InnoDB中,會在每行數據后添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。 在實際操作中,存儲的並不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下:
- SELECT時,讀取創建版本號<=當前事務版本號,刪除版本號為空或>當前事務版本號。
- INSERT時,保存當前事務版本號為行的創建版本號
- DELETE時,保存當前事務版本號為行的刪除版本號
- UPDATE時,插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行
通過MVCC,雖然每行記錄都要額外的存儲空間來記錄version,需要更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多讀操作都不用加鎖,讀取數據操作簡單,性能好.
細心的同學應該也看到了,通過MVCC讀取出來的數據其實是歷史數據,而不是最新數據,這在一些對於數據時效特別敏感的業務中,很可能出問題,這也是MVCC的短板之處,有辦法解決嗎? 當然有.
MCVV這種讀取歷史數據的方式稱為快照讀(snapshot read),而讀取數據庫當前版本數據的方式,叫當前讀(current read).
快照讀
我們平時只用使用select就是快照讀,這樣可以減少加鎖所帶來的開銷.
select * from table ....
當前讀
對於會對數據修改的操作(update、insert、delete)都是采用當前讀的模式。在執行這幾個操作時會讀取最新的記錄,即使是別的事務提交的數據也可以查詢到。假設要update一條記錄,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生沖突,所以在update的時候需要知道最新的數據。讀取的是最新的數據,需要加鎖。以下第一個語句需要加共享鎖,其它都需要加排它鎖。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;
我們再利用當前讀來做試驗.
可以看到在讀提交的隔離級別中,事務1修改了所有class_id=1的數據,當時當事務2 insert后,事務A莫名奇妙地多了一行class_id=1的數據,而且沒有被之前的update所修改,產生了讀提交下的的幻讀.
而在可重復度的隔離級別下,情況就完全不同了.事務1在update后,對該數據加鎖,事務B無法插入新的數據,這樣事務A在update前后數據保持一致,避免了幻讀,可以明確的是,update鎖的肯定不只是已查詢到的幾條數據,因為這樣無法阻止insert,有同學會說,那就是鎖住了整張表唄.
還是那句話, Mysql已經是個成熟的數據庫了,怎么會采用如此低效的方法呢? 其實這里的鎖,是通過next-key鎖實現的.
Next_Key鎖
在Users這張表里面,class_id是個非聚簇索引,數據庫會通過B+樹維護一個非聚簇索引與主鍵的關系,簡單來說,我們先通過class_id=1找到這個索引所對應所有節點,這些節點存儲着對應數據的主鍵信息,即id=1,我們再通過主鍵id=1找到我們要的數據,這個過程稱為回表.
我本想用我們文章中的例子來畫一個B+樹,可是畫得太丑了,為了避免拉低此偏文章B格.所以我想引用上面那邊文章中作者畫的B+樹來解釋Next-key.
假設我們上面用到的User表需要對Name
建立非聚簇索引,是怎么實現的呢?我們看下圖:
B+樹的特點是所有數據都存儲在葉子節點上,以非聚簇索引的秦壽生
為例,在秦壽生
的右葉子節點存儲着所有秦壽生
對應的Id,即圖中的34,在我們對這條數據做了當前讀后,就會對這條數據加行鎖,對於行鎖很好理解,能夠防止其他事務對其進行update
或delete
,但為什么要加GAP鎖呢?
還是那句話,B+樹的所有數據存儲在葉子節點上,當有一個新的叫秦壽生
的數據進來,一定是排在在這條id=34的數據前面或者后面的,我們如果對前后這個范圍進行加鎖了,那當然新的秦壽生
就插不進來了.
那如果有一個新的范統
要插進行呢? 因為范統
的前后並沒有被鎖住,是能成功插入的,這樣就極大地提高了數據庫的並發能力.
馬失前蹄
上文中說了可重復讀能防不可重復讀,還能防幻讀,它能防住所有的幻讀嗎?當然不是,也有馬失前蹄的時候.
比如如下的例子:
1.a事務先select,b事務insert確實會加一個gap鎖,但是如果b事務commit,這個gap鎖就會釋放(釋放后a事務可以隨意操作),
2.a事務再select出來的結果在MVCC下還和第一次select一樣,
3.接着a事務不加條件地update,這個update會作用在所有行上(包括b事務新加的),
4.a事務再次select就會出現b事務中的新行,並且這個新行已經被update修改了.
Mysql官方給出的幻讀解釋是:只要在一個事務中,第二次select多出了row就算幻讀, 所以這個場景下,算出現幻讀了
查詢和修改mysqld的隔離級別
查詢:
# 方式一:查看系統的隔離級別:
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set (0.01 sec)
# 方式二:查看當前會話的 隔離級別:
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
修改:
1.通過sql語句設置隔離級別:
設置會話的隔離級別,隔離級別由低到高設置依次為:
set session transaction isolation level read uncommitted;
set session transaction isolation level read committed;
set session transaction isolation level repeatable read;
set session transaction isolation level serializable;
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set (0.00 sec)
設置當前系統的隔離級別,隔離級別由低到高設置依次為:
set global transaction isolation level read uncommitted;
set global transaction isolation level read committed;
set global transaction isolation level repeatable read;
set global transaction isolation level serializable;
mysql> set global transaction isolation level read committed;
Query OK,0 rows affected (0.00 sec)
注意:mysql默認的事務處理級別是'REPEATABLE-READ',而Oracle和SQL Server是READ_COMMITED
2.在配置文件中添加隔離級別:
打開my.ini文件,在mysqld中設置:transaction-isolation=READ-COMMITTED