Mysql 死鎖問題

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的行,這就有點跳了,當然肯定也有解決的方法,那就是重理業務需求,避免這樣的寫法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章