事務的特性(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寶典