Mysql爲什麼使用可重複讀(Repeatable read)爲默認隔離級別?

事務的特性(ACID)

羣裏有小夥伴面試時,碰到面試官提了個很刁鑽的問題:java

Mysql爲什麼使用可重複讀(Repeatable read)爲默認隔離級別???mysql

下面進入正題:面試

咱們都知道事務的幾種性質 :原子性一致性隔離性持久性 (ACID)sql

爲了維持一致性和隔離性,通常使用加鎖這種方式來處理,可是加鎖相對帶來的是併發處理能力的下降數據庫

而數據庫是個高併發的應用,所以對於加鎖的處理是事務的精髓.緩存

下面咱們來了解一下封鎖協議,以及事務在數據庫中作了什麼安全

封鎖協議(Locking Protocol)

MySQL的鎖系統:shared lock 和 exclusive lock 即共享鎖和排他鎖,也叫讀鎖(S)和寫鎖(X),共享鎖和排他鎖都屬於悲觀鎖。排他鎖又能夠能夠分爲行鎖和表鎖。session

封鎖協議(Locking Protocol): 在使用X鎖或S鎖對數據加鎖時,約定的一些規則.例如什麼時候申請X或S鎖,持續時間,什麼時候釋放鎖等.併發

一級、二級、三級封鎖協議

對封鎖方式規定不一樣的規則,就造成了各類不一樣的封鎖協議,不一樣的封鎖協議,爲併發操做的正確性提供不一樣程度的保證分佈式

一級封鎖協議

一級封鎖協議定義:事務T在修改數據R以前必須先對其加X鎖(排他鎖),直到事務結束才釋放。事務結束包括正常結束(COMMIT)和非正常結束(ROLLBACK)。

一級封鎖協議能夠防止丟失修改,並保證事務T是可恢復的。使用一級封鎖協議能夠解決丟失修改問題。

在一級封鎖協議中,若是僅僅是讀數據不對其進行修改,是不須要加鎖的,它不能保證可重複讀和不讀「髒」數據。  

二級封鎖協議

二級封鎖協議定義:一級封鎖協議加上事務T在讀取數據R以前必須先對其加S鎖(共享鎖),讀完後釋放S鎖。事務的加鎖和解鎖嚴格分爲兩個階段,第一階段加鎖,第二階段解鎖。

  • 加鎖階段: 在對任何數據進行讀操做以前要申請並得到S鎖(共享鎖,其它事務能夠繼續加共享鎖,但不能加排它鎖),在進行寫操做以前要申請並得到X鎖(排它鎖,其它事務不能再得到任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。

  • 解鎖階段:當事務釋放了一個封鎖之後,事務進入解鎖階段,在該階段只能進行解鎖操做不能再進行加鎖操做。

   二級封鎖協議除防止了丟失修改,還能夠進一步防止讀「髒」數據。但在二級封鎖協議中,因爲讀完數據後釋放S鎖,因此它不能保證可重複讀。

​ 二級封鎖的目的是保證併發調度的正確性。就是說,若是事務知足兩段鎖協議,那麼事務的併發調度策略是串行性的。保證事務的併發調度是串行化(串行化很重要,尤爲是在數據恢復和備份的時候)   

三級封鎖協議

三級封鎖協議定義:一級封鎖協議加上事務T在讀取數據R以前必須先對其加S鎖(共享鎖),直到事務結束才釋放。在一級封鎖協議(一級封鎖協議:修改以前先加X鎖,事務完成釋放)的基礎上加上S鎖,事務結束後釋放S鎖

  三級封鎖協議除防止了丟失修改和不讀「髒」數據外,還進一步防止了不可重複讀。 上述三級協議的主要區別在於什麼操做須要申請封鎖,以及什麼時候釋放。

事務四種隔離級別

在數據庫操做中,爲了有效保證併發讀取數據的正確性,提出的事務隔離級別。上面提到的封鎖協議 ,也是爲了構建這些隔離級別存在的。

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可 不可 可能
可串行化(Serializable ) 不可能 不可能 不可能

對於事務併發訪問會產生的問題,以及各隔離級別的詳細介紹在個人上一篇文章

一文搞懂事務

爲何是RR

通常的DBMS系統,默認都會使用讀提交(Read-Comitted,RC)做爲默認隔離級別,如Oracle、SQL Server等,而MySQL卻使用可重複讀(Read-Repeatable,RR)。要知道,越高的隔離級別,能解決的數據一致性問題越多,理論上性能的損耗更大,且併發性越低。隔離級別依次爲: SERIALIZABLE > RR > RC > RU

咱們能夠經過如下語句設置和獲取數據庫的隔離級別:

查看系統的隔離級別:

mysql> select @@global.tx_isolation isolation;
+-----------------+
| isolation       |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

查看當前會話的 隔離級別:

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set, 1 warning (0.00 sec)

設置會話的隔離級別,隔離級別由低到高設置依次爲:

set session transacton isolation level read uncommitted;
set session transacton isolation level read committed;
set session transacton isolation level repeatable read;
set session transacton isolation level serializable;

設置當前系統的隔離級別,隔離級別由低到高設置依次爲:

set global transacton isolation level read uncommitted;
set global transacton isolation level read committed;
set global transacton isolation level repeatable read;
set global transacton isolation level serializable;

可重複讀(Repeated Read):可重複讀。基於鎖機制併發控制的DBMS須要對選定對象的讀鎖(read locks)和寫鎖(write locks)一直保持到事務結束,但不要求「範圍鎖(range-locks)」,所以可能會發生「幻影讀(phantom reads)」 在該事務級別下,保證同一個事務從開始到結束獲取到的數據一致。是Mysql的默認事務級別。

下面咱們先來思考2個問題

  • 在讀已提交(Read Commited)級別下,出現不可重複讀問題怎麼辦?須要解決麼?

不用解決,這個問題是能夠接受的!畢竟你數據都已經提交了,讀出來自己就沒有太大問題!Oracle ,SqlServer 默認隔離級別就是RC,咱們也沒有更改過它的默認隔離級別.

  • 在Oracle,SqlServer中都是選擇讀已提交(Read Commited)做爲默認的隔離級別爲何Mysql不選擇讀已提交(Read Commited)做爲默認隔離級別,而選擇可重複讀(Repeatable Read)做爲默認的隔離級別呢?

歷史緣由,早階段Mysql(5.1版本以前)的Binlog類型Statement是默認格式,即依次記錄系統接受的SQL請求;5.1及之後,MySQL提供了Row,Mixed,statement 3種Binlog格式, 當binlog爲statement格式,使用RC隔離級別時,會出現BUG所以Mysql將可重複讀(Repeatable Read)做爲默認的隔離級別!

Binlog簡介

Mysql binlog是二進制日誌文件,用於記錄mysql的數據更新或者潛在更新(好比DELETE語句執行刪除而實際並無符合條件的數據),在mysql主從複製中就是依靠的binlog。能夠經過語句「show binlog events in 'binlogfile'」來查看binlog的具體事件類型。binlog記錄的全部操做實際上都有對應的事件類型的

MySQL binlog的三種工做模式: Row (用到MySQL的特殊功能如存儲過程、觸發器、函數,又但願數據最大化一直則選擇Row模式,咱們公司選擇的是row) 簡介:日誌中會記錄每一行數據被修改的狀況,而後在slave端對相同的數據進行修改。 優勢:能清楚的記錄每一行數據修改的細節 缺點:數據量太大

Statement (默認) 簡介:每一條被修改數據的sql都會記錄到master的bin-log中,slave在複製的時候sql進程會解析成和原來master端執行過的相同的sql再次執行。在主從同步中通常是不建議用statement模式的,由於會有些語句不支持,好比語句中包含UUID函數,以及LOAD DATA IN FILE語句等 優勢:解決了 Row level下的缺點,不須要記錄每一行的數據變化,減小bin-log日誌量,節約磁盤IO,提升新能 缺點:容易出現主從複製不一致

Mixed(混合模式) 簡介:結合了Row level和Statement level的優勢,同時binlog結構也更復雜。

咱們能夠簡單理解爲binlog是一個記錄數據庫更改文件,主從複製時須要此文件,具體細節先略過

主從不一致實操

binlog爲STATEMENT格式,且隔離級別爲**讀已提交(Read Commited)**時,有什麼bug呢? 測試表:

mysql> select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | NULL | NULL |
|  2 | NULL | NULL |
|  3 | NULL | NULL |
|  4 | NULL | NULL |
|  5 | NULL | NULL |
|  6 | NULL | NULL |
+----+------+------+
6 rows in set (0.00 sec)
Session1 Session2
mysql> set tx_isolation = 'read-committed';
Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> set tx_isolation = 'read-committed';
Query OK, 0 rows affected, 1 warning (0.00 sec)
begin;<br />Query OK, 0 rows affected (0.00 sec) begin;<br />Query OK, 0 rows affected (0.00 sec)
delete from test where 1=1;
Query OK, 6 rows affected (0.00 sec)
insert into test values (null,'name',100);
Query OK, 1 row affected (0.00 sec)
commit;
Query OK, 0 rows affected (0.01 sec)
commit;
Query OK, 0 rows affected (0.01 sec)

Master此時輸出

select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  7 | name |  100 |
+----+------+------+
1 row in set (0.00 sec)

可是,你在此時在從(slave)上執行該語句,得出輸出

mysql> select * from test;
Empty set (0.00 sec)

在master上執行的順序爲先刪後插!而此時binlog爲STATEMENT格式,是基於事務記錄,在事務未提交前,二進制日誌先緩存,提交後再寫入記錄的,所以順序爲先插後刪!slave同步的是binglog,所以從機執行的順序和主機不一致!slave在插入後刪除了全部數據.

解決方案有兩種! (1)隔離級別設爲可重複讀(Repeatable Read),在該隔離級別下引入間隙鎖。當Session 1執行delete語句時,會鎖住間隙。那麼,Ssession 2執行插入語句就會阻塞住! (2)將binglog的格式修改成row格式,此時是基於行的複製,天然就不會出現sql執行順序不同的問題!奈何這個格式在mysql5.1版本開始才引入。所以因爲歷史緣由,mysql將默認的隔離級別設爲可重複讀(Repeatable Read),保證主從複製不出問題!

RU和Serializable

項目中不太使用**讀未提交(Read UnCommitted)串行化(Serializable)**兩個隔離級別,緣由:

讀未提交(Read UnCommitted)

容許髒讀,也就是可能讀取到其餘會話中未提交事務修改的數據 一個事務讀到另外一個事務未提交讀數據

串行化(Serializable)

使用的悲觀鎖的理論,實現簡單,數據更加安全,可是併發能力很是差。若是你的業務併發的特別少或者沒有併發,同時又要求數據及時可靠的話,可使用這種模式。通常是使用mysql自帶分佈式事務功能時才使用該隔離級別

RC和 RR

此時咱們糾結的應該就只有一個問題了:隔離級別是用讀已提交仍是可重複讀

接下來對這兩種級別進行對比的第一種狀況:

在RR隔離級別下,存在間隙鎖,致使出現死鎖的概率比RC大的多!

實現一個簡單的間隙鎖例子

select * from test where id <11 ;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | NULL | NULL |
|  2 | NULL | NULL |
|  3 | NULL | NULL |
|  4 | NULL | NULL |
|  5 | NULL | NULL |
|  6 | NULL | NULL |
|  7 | name |   7  |
+----+------+------+
7 rows in set (0.00 sec)
session1 session2
mysql> set tx_isolation = 'repeatable-read';
Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> set tx_isolation = 'repeatable-read';
Query OK, 0 rows affected, 1 warning (0.00 sec)
Begin;
select * from test where id <11 for update;
insert into test values(null,'name',9); //被阻塞!
commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (12.23 sec) //鎖釋放後完成了操做

在RR隔離級別下,能夠鎖住(-∞,10] 這個間隙,防止其餘事務插入數據! 而在RC隔離級別下,不存在間隙鎖,其餘事務是能夠插入數據!

ps:在RC隔離級別下並非不會出現死鎖,只是出現概率比RR低而已

鎖表和鎖行

在RR隔離級別下,條件列未命中索引會鎖表!而在RC隔離級別下,只鎖行

select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  8 | name |   11 |
|  9 | name |    9 |
| 10 | name |   15 |
| 11 | name |   15 |
| 12 | name |   16 |
+----+------+------+

鎖表的例子:

session1 session2
Begin;
update test set age = age+1 where age = 15;
Rows matched: 2 Changed: 2 Warnings: 0
insert into test values(null,'test',15);
ERROR 1205 (HY000): Lock wait timeout exceeded;
Commit;

session2插入失敗 查詢 數據顯示:

select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  8 | name |   11 |
|  9 | name |    9 |
| 10 | name |   16 |
| 11 | name |   16 |
| 12 | name |   16 |
+----+------+------+

半一致性讀(semi-consistent)特性

在RC隔離級別下,半一致性讀(semi-consistent)特性增長了update操做的併發性!

在5.1.15的時候,innodb引入了一個概念叫作「semi-consistent」,減小了更新同一行記錄時的衝突,減小鎖等待。 所謂半一致性讀就是,一個update語句,若是讀到一行已經加鎖的記錄,此時InnoDB返回記錄最近提交的版本,判斷此版本是否知足where條件。若知足則從新發起一次讀操做,此時會讀取行的最新版本並加鎖!

建議

在RC級別下,用的binlog爲row格式,是基於行的複製,Innodb的創始人也是建議binlog使用該格式

互聯網項目請用:讀已提交(Read Commited)這個隔離級別

總結

因爲歷史緣由,老版本Mysql的binlog使用statement格式,不使用RR隔離級別會致使主從不一致的狀況

目前(5.1版本以後)咱們使用row格式的binlog 配合RC隔離級別能夠實現更好的併發性能.

關注公衆號:java寶典

求關注

相關文章
相關標籤/搜索