1.Mysql中的隔離級別RC與RR
1.1. 數據庫事務ACID特性
數據庫事務的4個特性:
原子性(Atomic): 事務中的多個操作,不可分割,要麼都成功,要麼都失敗; All or Nothing.
一致性(Consistency): 事務操作之後, 數據庫所處的狀態和業務規則是一致的; 比如a,b賬戶相互轉賬之後,總金額不變;
隔離性(Isolation): 多個事務之間就像是串行執行一樣,不相互影響;
持久性(Durability): 事務提交後被持久化到永久存儲.
1.2. 隔離性
其中 隔離性 分爲了四種:
READ UNCOMMITTED:可以讀取未提交的數據,未提交的數據稱爲髒數據,所以又稱髒讀。此時:幻讀,不可重複讀和髒讀均允許;
READ COMMITTED:只能讀取已經提交的數據;此時:允許幻讀和不可重複讀,但不允許髒讀,所以RC隔離級別要求解決髒讀;
REPEATABLE READ:同一個事務中多次執行同一個select,讀取到的數據沒有發生改變;此時:允許幻讀,但不允許不可重複讀和髒讀,所以RR隔離級別要求解決不可重複讀;
SERIALIZABLE: 幻讀,不可重複讀和髒讀都不允許,所以serializable要求解決幻讀;
1.3. 幾個概念
髒讀:可以讀取未提交的數據。RC 要求解決髒讀;
不可重複讀:同一個事務中多次執行同一個select, 讀取到的數據發生了改變(被其它事務update並且提交);
可重複讀:同一個事務中多次執行同一個select, 讀取到的數據沒有發生改變(一般使用MVCC實現);RR各級級別要求達到可重複讀的標準;
幻讀:同一個事務中多次執行同一個select, 讀取到的數據行發生改變。也就是行數減少或者增加了(被其它事務delete/insert並且提交)。SERIALIZABLE要求解決幻讀問題;
這裏一定要區分 不可重複讀 和 幻讀:
不可重複讀的重點是修改:
同樣的條件的select, 你讀取過的數據, 再次讀取出來發現值不一樣了
幻讀的重點在於新增或者刪除:
同樣的條件的select, 第1次和第2次讀出來的記錄數不一樣
從結果上來看, 兩者都是爲多次讀取的結果不一致。但如果你從實現的角度來看, 它們的區別就比較大:
對於前者, 在RC下只需要鎖住滿足條件的記錄,就可以避免被其它事務修改,也就是 select for update, select in share mode; RR隔離下使用MVCC實現可重複讀;
對於後者, 要鎖住滿足條件的記錄及所有這些記錄之間的gap,也就是需要 gap lock。
而ANSI SQL標準沒有從隔離程度進行定義,而是定義了事務的隔離級別,同時定義了不同事務隔離級別解決的三大併發問題:
Isolation Level | Dirty Read | Unrepeatable Read | Phantom Read |
---|---|---|---|
Read UNCOMMITTED | YES | YES | YES |
READ COMMITTED | YES | YES | YES |
READ REPEATABLE | NO | NO | YES |
SERIALIZABLE | NO | NO | NO |
1.4. 數據庫的默認隔離級別
除了MySQL默認採用RR隔離級別之外,其它幾大數據庫都是採用RC隔離級別。
但是他們的實現也是極其不一樣的。Oracle僅僅實現了RC 和 SERIALIZABLE隔離級別。默認採用RC隔離級別,解決了髒讀。但是允許不可重複讀和幻讀。其SERIALIZABLE則解決了髒讀、不可重複讀、幻讀。
MySQL的實現:MySQL默認採用RR隔離級別,SQL標準是要求RR解決不可重複讀的問題,但是因爲MySQL採用了gap lock,所以實際上MySQL的RR隔離級別也解決了幻讀的問題。那麼MySQL的SERIALIZABLE是怎麼回事呢?其實MySQL的SERIALIZABLE採用了經典的實現方式,對讀和寫都加鎖。
1.5. MySQL 中RC和RR隔離級別的區別
MySQL數據庫中默認隔離級別爲RR,但是實際情況是使用RC 和 RR隔離級別的都不少。好像淘寶、網易都是使用的 RC 隔離級別。那麼在MySQL中 RC 和 RR有什麼區別呢?我們該如何選擇呢?爲什麼MySQL將RR作爲默認的隔離級別呢?
1.5.1 RC 與 RR 在鎖方面的區別
1> 顯然 RR 支持 gap lock(next-key lock),而RC則沒有gap lock。因爲MySQL的RR需要gap lock來解決幻讀問題。而RC隔離級別則是允許存在不可重複讀和幻讀的。所以RC的併發一般要好於RR;
2> RC 隔離級別,通過 where 條件過濾之後,不符合條件的記錄上的行鎖,會釋放掉(雖然這裏破壞了“兩階段加鎖原則”);但是RR隔離級別,即使不符合where條件的記錄,也不會是否行鎖和gap lock;所以從鎖方面來看,RC的併發應該要好於RR;
1.5.2 RC 與 RR 在複製方面的區別
1> RC 隔離級別不支持 statement 格式的bin log,因爲該格式的複製,會導致主從數據的不一致;只能使用 mixed 或者 row 格式的bin log; 這也是爲什麼MySQL默認使用RR隔離級別的原因。複製時,我們最好使用:binlog_format=row
2> MySQL5.6 的早期版本,RC隔離級別是可以設置成使用statement格式的bin log,後期版本則會直接報錯;
1.5.3 RC 與 RR 在一致性讀方面的區別
簡單而且,RC隔離級別時,事務中的每一條select語句會讀取到他自己執行時已經提交了的記錄,也就是每一條select都有自己的一致性讀ReadView; 而RR隔離級別時,事務中的一致性讀的ReadView是以第一條select語句的運行時,作爲本事務的一致性讀snapshot的建立時間點的。只能讀取該時間點之前已經提交的數據。
具體可以參加:MySQL 一致性讀 深入研究
1.5.4 RC 支持半一致性讀,RR不支持
RC隔離級別下的update語句,使用的是半一致性讀(semi consistent);而RR隔離級別的update語句使用的是當前讀;當前讀會發生鎖的阻塞。
1> 半一致性讀:
簡單來說,semi-consistent read是read committed與consistent read兩者的結合。一個update語句,如果讀到一行已經加鎖的記錄,此時InnoDB返回記錄最近提交的版本,由MySQL上層判斷此版本是否滿足 update的where條件。若滿足(需要更新),則MySQL會重新發起一次讀操作,此時會讀取行的最新版本(並加鎖)。semi-consistent read只會發生在read committed隔離級別下,或者是參數innodb_locks_unsafe_for_binlog被設置爲true(該參數即將被廢棄)。
對比RR隔離級別,update語句會使用當前讀,如果一行被鎖定了,那麼此時會被阻塞,發生鎖等待。而不會讀取最新的提交版本,然後來判斷是否符合where條件。
半一致性讀的優點:
減少了update語句時行鎖的衝突;對於不滿足update更新條件的記錄,可以提前放鎖,減少併發衝突的概率。
2.Mysql中的鎖
2.1.Mysql都有什麼鎖
MySQL有三種鎖的級別:表級、行級、頁級。
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低。
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般
算法:
行鎖(Record Lock):鎖直接加在索引記錄上面,鎖住的是key。
間隙鎖(Gap Lock):鎖定索引記錄間隙,確保索引記錄的間隙不變。間隙鎖是針對事務隔離級別爲可重複讀或以上級別而已的。
Next-Key Lock :行鎖和間隙鎖組合起來就叫Next-Key Lock。
默認情況下,InnoDB工作在可重複讀隔離級別下,並且會以Next-Key Lock的方式對數據行進行加鎖,這樣可以有效防止幻讀的發生。Next-Key Lock是行鎖和間隙鎖的組合,當InnoDB掃描索引記錄的時候,會首先對索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。加上間隙鎖之後,其他事務就不能在這個間隙修改或者插入記錄。
Gap Lock在InnoDB的唯一作用就是防止其他事務的插入操作,以此防止幻讀的發生。
2.1.1 行鎖(Record Lock)
行鎖鎖定的是索引記錄,而不是行數據,也就是說鎖定的是key。
2.1.2 間隙鎖(Gap Lock)
例如:
create table test(id int,v1 int,v2 int,primary key(id),key idx_v1
(v1
))Engine=InnoDB DEFAULT CHARSET=UTF8;
該表的記錄如下:
+—-+——+——+
| id | v1 | v2 |
+—-+——+——+
| 1 | 1 | 0 |
| 2 | 3 | 1 |
| 3 | 4 | 2 |
| 5 | 5 | 3 |
| 7 | 7 | 4 |
| 10 | 9 | 5 |
間隙鎖(Gap Lock)一般是針對非唯一索引而言的,test表中的v1(非唯一索引)字段值可以劃分的區間爲:
(-∞,1)
(1,3)
(3,4)
(4,5)
(5,7)
(7,9)
(9, +∞)
假如要更新v1=7的數據行,那麼此時會在索引idx_v1對應的值,也就是v1的值上加間隙鎖,鎖定的區間是(5,7)和(7,9)。同時找到v1=7的數據行的主鍵索引和非唯一索引,對key加上鎖。
記錄的GAP的區間如下:是一個左開右閉的空間(原因是默認主鍵的有序自增的特性)
2.1.3後碼鎖(Next-Key Lock)
記錄鎖和間隙鎖的結合,對於InnoDB中,更新非唯一索引對應的記錄(在這裏來說是更新v1字段的值),會加上Next-Key Lock。如果更新記錄爲空,就不能加記錄鎖,只能加間隙鎖。
在默認情況下,mysql的事務隔離級別是可重複讀,並且innodb_locks_unsafe_for_binlog參數爲0,這時默認採用next-key locks。所謂Next-Key Locks,就是Record lock和gap lock的結合,即除了鎖住記錄本身,還要再鎖住索引之間的間隙。
下面我們針對大部分的SQL類型分析是如何加鎖的,假設事務隔離級別爲可重複讀。
select .. from
不加任何類型的鎖
select…from lock in share mode
在掃描到的任何索引記錄上加共享的(shared)next-key lock,還有主鍵聚集索引加排它鎖
select..from for update
在掃描到的任何索引記錄上加排它的next-key lock,還有主鍵聚集索引加排它鎖
update..where delete from..where
在掃描到的任何索引記錄上加next-key lock,還有主鍵聚集索引加排它鎖
insert into..
簡單的insert會在insert的行對應的索引記錄上加一個排它鎖,這是一個record lock,並沒有gap,所以並不會阻塞其他session在gap間隙裏插入記錄。不過在insert操作之前,還會加一種鎖,官方文檔稱它爲insertion intention gap lock,也就是意向的gap鎖。這個意向gap鎖的作用就是預示着當多事務併發插入相同的gap空隙時,只要插入的記錄不是gap間隙中的相同位置,則無需等待其他session就可完成,這樣就使得insert操作無須加真正的gap lock。想象一下,如果一個表有一個索引idx_test,表中有記錄1和8,那麼每個事務都可以在2和7之間插入任何記錄,只會對當前插入的記錄加record lock,並不會阻塞其他session插入與自己不同的記錄,因爲他們並沒有任何衝突。
假設發生了一個唯一鍵衝突錯誤,那麼將會在重複的索引記錄上加讀鎖。當有多個session同時插入相同的行記錄時,如果另外一個session已經獲得改行的排它鎖,那麼將會導致死鎖。
因爲InnoDB對於行的查詢都是採用了Next-Key Lock的算法,鎖定的不是單個值,而是一個範圍,按照這個方法是會和第一次測試結果一樣。但是,當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級爲Record Lock,即僅鎖住索引本身,不是範圍。
2.1.4 意向鎖(Next-Key Lock)
innodb的意向鎖主要用戶多粒度的鎖並存的情況。比如事務A要在一個表上加S鎖,如果表中的一行已被事務B加了X鎖,那麼該鎖的申請也應被阻塞。如果表中的數據很多,逐行檢查鎖標誌的開銷將很大,系統的性能將會受到影響。爲了解決這個問題,可以在表級上引入新的鎖類型來表示其所屬行的加鎖情況,這就引出了“意向鎖”的概念。舉個例子,如果表中記錄1億,事務A把其中有幾條記錄上了行鎖了,這時事務B需要給這個表加表級鎖,如果沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了。如果存在意向鎖,那麼假如事務A在更新一條記錄之前,先加意向鎖,再加X鎖,事務B先檢查該表上是否存在意向鎖,存在的意向鎖是否與自己準備加的鎖衝突,如果有衝突,則等待直到事務A釋放,而無須逐條記錄去檢測。事務B更新表時,其實無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就行了。
說白了意向鎖的主要作用是處理行鎖和表鎖之間的矛盾,能夠顯示“某個事務正在某一行上持有了鎖,或者準備去持有鎖”
2.1.5 鎖選擇
1)如果更新條件沒有走索引,例如執行”update from t1 set v2=0 where v2=5;” ,此時會進行全表掃描,掃表的時候,要阻止其他任何的更新操作,所以上升爲表鎖。
2)如果更新條件爲索引字段,但是並非唯一索引(包括主鍵索引),例如執行“update from t1 set v2=0 where v1=9;” 那麼此時更新會使用Next-Key Lock。使用Next-Key Lock的原因:
a)首先要保證在符合條件的記錄上加上排他鎖,會鎖定當前非唯一索引和對應的主鍵索引的值;
b)還要保證鎖定的區間不能插入新的數據。
3)如果更新條件爲唯一索引,則使用Record Lock(記錄鎖)。
2.2.什麼情況下會造成死鎖
所謂死鎖: 是指兩個或兩個以上的進程在執行過程中,
因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去.
此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等竺的進程稱爲死鎖進程.
表級鎖不會產生死鎖.所以解決死鎖主要還是針對於最常用的InnoDB.
死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。
那麼對應的解決死鎖問題的關鍵就是:讓不同的session加鎖有次序
3.Mysql中發生死鎖的問題分析(默認RR隔離級別)
默認情況下,InnoDB存儲引擎不會回滾超時引發的異常,除死鎖外。
3.1 案例一
一個session通過for循環會有幾條如下的語句:
Select * from xxx where id=’隨機id’ for update
基本來說,程序開啓後不一會就死鎖。
這可以是說最經典的死鎖情形了。
對於這個問題的改進很簡單,直接把所有分配到的id直接一次鎖住就行了。
Select * from xxx where id in (xx,xx,xx) for update
在in裏面的列表值mysql是會自動從小到大排序,加鎖也是一條條從小到大加的鎖
3.2 案例二
在開發中,經常會做這類的判斷需求:根據字段值查詢(有索引),如果不存在,則插入;否則更新。
展開代碼
以id爲主鍵爲例,目前還沒有id=2的行
Session1:
select * from test where id=2 for update;
Empty set (0.00 sec)
session2:
select * from test where id=3 for update;
Empty set (0.00 sec)
Session1:
insert into test values(2,’a’,’b’,now());
鎖等待中……
Session2:
insert into test values(3,’b’,’c’,now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
select的where子句沒有滿足條件的記錄,而對於不存在的記錄 並且在RR級別下,select加鎖類型爲gap lock,gap lock之間是兼容的,所以兩個事務都能成功執行select;關於gap lock可以參考文章加鎖分析。
insert時,其加鎖過程爲先在插入間隙上獲取插入意向鎖,插入數據後再獲取插入行上的排它鎖。又插入意向鎖與gap lock和 Next-key lock衝突,即一個事務想要獲取插入意向鎖,如果有其他事務已經加了gap lock或 Next-key lock,則會阻塞。
場景中兩個事務都持有gap lock,然後又申請插入意向鎖,此時都被阻塞,循環等待造成死鎖。
\ | Gap | Insert Intension | Record | Next-Key |
---|---|---|---|---|
Gap | 兼容 | 兼容 | 兼容 | 衝突 |
Insert Intension | 兼容 | 衝突 | 兼容 | 衝突 |
Record | 兼容 | 兼容 | 衝突 | 衝突 |
Next-Key | 兼容 | 兼容 | 衝突 | 衝突 |
當對存在的行進行鎖的時候(主鍵),mysql就只有行鎖。
當對未存在的行進行鎖的時候(即使條件爲主鍵),mysql是會鎖住一段範圍(有gap鎖)
鎖住的範圍詳細可見2.1.2
對於這種死鎖的解決辦法是:
insert into t3(xx,xx) on duplicate key update xx
=’XX’;
用mysql特有的語法來解決此問題。因爲insert語句對於主鍵來說,插入的行不管有沒有存在,都會只有行鎖。
3.3 案例三
展開代碼
mysql> select * from test where id=9 for update;
+—-+——–+——+———————+
| id | v1 | v2 | ctime |
+—-+——–+——+———————+
| 9 | a | b | 2018-08-05 11:36:30 |
+—-+——–+——+———————+
1 row in set (0.00 sec)
Session2:
mysql> select * from test where id<20 for update;
鎖等待中
Session1:
mysql> insert into test values(7,’ae’,’af’,now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
這個跟案例一其它是差不多的情況,只是session1不按常理出牌了,
Session2在等待Session1的id=9的鎖,session2又持了1到8的鎖(注意9到19的範圍並沒有被session2鎖住),最後,session1在插入新行時又得等待session2,故死鎖發生了。
這種一般是在業務需求中基本不會出現,因爲你鎖住了id=9,卻又想插入id=7的行,這就有點跳了,當然肯定也有解決的方法,那就是重理業務需求,避免這樣的寫法。