使用select for share,for update的場景及死鎖陷阱

SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE語句是innodb事務中的經常使用語句
for share會給表增長一個is鎖,給記錄行增長一個s鎖,for update會給表增長一個ix鎖,給記錄行增長一個x鎖。html

SELECT ... FOR SHARE使用場景

他們的意思就如語法表示的同樣,SELECT ... FOR SHARE,我選擇一些記錄,這些記錄能夠share,其餘事務也能夠讀,可是若是你要修改,很差意思,我加了一個s鎖,你是不能夠修改的。這個語句的應用場景之一是用來讀取到最新的數據。
例如,由於innodb中mvcc機制的存在,在可重複讀隔離級別下,A事務修改某一行的數據,B事務在A事務提交前是看不到A事務對該行的修改的,可是利用SELECT ... FOR SHARE,B事務會等待A事務釋放該行的鎖才能查看到該行數據。
建立一個測試表:mysql

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1');

 

 

SELECT ... FOR UPDATE使用場景

下面再說SELECT ... FOR UPDATE,我選擇一些記錄,這些select的記錄是我下一步要update的,你要讀或者修改這些記錄,很差意思,我加的是x鎖,你讀不了也改不了。只有我當前事務提交了,這些記錄你才能夠讀到或者修改。這個語句的應用場景之一是爲了防止更新丟失。
例如,A事務和B事務同時讀取銀行帳戶餘額,是2元錢,A事務看到2元,消費了1元,將餘額更新爲1元,B事務看到2元,消費了1元,也將餘額更新爲1元,那麼帳戶變爲1元,可是實際應該扣費2元。使用SELECT ... FOR UPDATE讀取記錄,能夠避免這種丟失更新的現象
丟失更新現象:
sql

 

防止丟失更新
數據庫

 

可能有人看到這裏會有疑問:爲何innodb採用MVCC這種多版本併發控制,每次看到的不是最新的數據,而是之前的一個快照呢?
這是由於一個事務的操做有可能成功,也有可能失敗rollback,在一個事務commit以前,被其餘事務讀到還沒提交的變動記錄,會產生數據不同的現象(髒讀),這種狀況就是innodb最低的隔離級別READ UNCOMMITTED,能夠讀到沒有commit的數據。
那麼若是想要不產生髒讀,容易想到的是採用鎖的方式,當一個事務更改某行記錄,就加上鎖,其餘事務等待該事務執行完畢才能讀取到該行記錄,可是這樣作的話會產生大量的鎖佔用與等待,效率是很是低下的,所以innoDB採用了MVCC的方式。簡單的說,A事務變動某行記錄,innodb會產生對應的redo log,若是接下來A事務進行回滾,innodb能夠根據redo log將記錄回滾到事務開始以前的狀態。在A事務沒有結束時,若是B事務來查詢該行記錄,B事務會根據A事務變動後的記錄值(在內存中)加上redo log「計算」出A事務開始前的該行記錄值,從而讀取到該行記錄的一個快照,其中並不會產生鎖與等待。
若是是可重複讀REPEATABLE READ的隔離級別(默認隔離級別),B事務進行過程當中看到的始終會是B事務開始前的記錄行快照信息,無論B事務進行過程當中A事務有沒有完成;若是是提交讀READ COMMITTED級別,B事務進行過程當中,能夠看到A事務提交對記錄行修改值(即若是A事務沒有完成,B查詢到的是A事務開始前的記錄值,若是A事務完成了,B事務查詢到的是A事務完成後的記錄值),在這種狀況下會產生不可重複讀的現象,即同一次事務中屢次查詢看到的結果會不同。

併發

使用select for share,for update的陷阱

再說使用select for share,for update的陷阱,for share會給記錄行增長一個s鎖,for update會給記錄行增長一個x鎖。若是此時有另外一個事務B也想給這些記錄行加s鎖或者x鎖,此時就會產生等待,即事務B等待事務A,此時,若是事務A對這些記錄行想加上另外一個類型的鎖,就會產生死鎖,用等待圖來表示就是,事務B在等待事務A釋放資源,接下來,事務A又必須等待事務B釋放資源,如此造成了一個有向的環。讓咱們舉例說明,爲了方便觀察,咱們將鎖等待超時時間設置長一點,首先,來看一個互相佔用資源的例子:mvc

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1'); INSERT INTO `test_tab` VALUES ('2', '1');

 

死鎖示例1:

 

上面的示例中,A等B,只要B釋放資源,A就能夠進行下去,可是B接下來的操做是去等待A,造成了一個環,產生死鎖。
這種互相佔有不一樣資源的例子等待對方釋放應該是最多見的死鎖場景了,下面,咱們來看一下不常見的函數

 

死鎖示例2:

 

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1');

 

上述兩個事務並無互相佔有不一樣資源,B事務甚至沒有實際佔有資源,可是也產生了死鎖,緣由是在第二步中B事務等待A事務釋放資源,而且B事務要求分配一個x鎖,接下來A事務須要一個f1=1的x鎖,可是此時B事務已經在等待x鎖,A事務只有一個s鎖,並不能升級成x鎖,所以A事務須要等待B。最終造成B等A,A又等B的環狀圖,產生死鎖。
若是第一步中A事務使用的是for update呢?那麼這種死鎖狀況就不會發生,由於for update語句已經申請到一個x鎖,A事務此時持有x鎖就能夠直接在第3步執行刪除操做,並不須要等待B事務的任何資源。測試

 

死鎖示例3:

下面是一個因插入致使產生的死鎖,數據庫建立及數據同上
url

 

上面這個例子能夠看作innoDB中「幻行」的解決方案,使用for share或者for update語句將鎖定記錄及記錄之間的空白區間,阻止任何其餘事務在該區間中插入數據(若是其餘事務容許插入,這將致使同一個事務中屢次讀取到不同的數據,如A事務select,B事務insert提交,A事務select for share,能夠讀取到B事務剛剛提交的記錄)spa

此外,根據測試,在mysql8.0中若是next key鎖區間重合,那麼只能第一個事務擁有該區間的鎖,其餘事務不是等待該區間的鎖,而是等待該區間第一個數據的鎖,這方面的緣由不明。若是再配合max等函數的話,又會出現一些神奇的死鎖現象,例如插入意向鎖的衝突。這些方面估計只有查看innodb的源碼才能知道緣由了,這裏不深刻探究了。

總之,明白死鎖的緣由是因爲事務之間互相等待對方佔有的資源,在等待圖中造成了環便可,分析死鎖有如下方式:
查看當前事務
SELECT * FROM information_schema.INNODB_TRX;
查看當前鎖
SELECT * FROM `performance_schema`.data_locks;
查看當前鎖等待
SELECT * FROM `performance_schema`.data_lock_waits;
分析死鎖日誌:
show ENGINE INNODB STATUS;
在日誌中搜索「LATEST DETECTED DEADLOCK」

咱們看到,使用for update或者for share時有可能發生死鎖狀況,雖然死鎖並不可怕,mysql擁有死鎖檢測的機制打破死鎖而且咱們能夠從新選擇執行該事物,當時當死鎖頻繁出現時,仍是應當注意並加以排查的。最好的狀況是不出現死鎖,所以若是快照數據知足要求時,少用for share或者for update語句,雖然有時你看起來只是在一行記錄上加鎖,可是因爲間隙鎖和下一個鍵鎖的存在,鎖住的可能不止是一行記錄。

參考資料:mysql8.0官方文檔

相關文章
相關標籤/搜索