1. 程式人生 > >死鎖:多執行緒同時刪除唯一索引上的同一行

死鎖:多執行緒同時刪除唯一索引上的同一行

3    總結    7

  1. 死鎖問題背景

做MySQL程式碼的深入分析也有些年頭了,再加上自己10年左右的資料庫核心研發經驗,自認為對於MySQL/InnoDB的加鎖實現瞭如指掌,正因如此,前段時間,還專門寫了一篇洋洋灑灑的文章,專門分析MySQL的加鎖實現細節:《MySQL加鎖處理分析》。

但是,昨天”潤潔”同學在《MySQL加鎖處理分析》這篇博文下諮詢的一個MySQL的死鎖場景,還是徹底把我給難住了。此死鎖,完全違背了本人原有的鎖知識體系,讓我百思不得其解。本著機器不會騙人,既然報出死鎖,那麼就一定存在死鎖的原則,我又重新深入分析了InnoDB對應的原始碼實現,進行多次實驗,配合恰到好處的靈光一現,還真讓我分析出了這個死鎖產生的原因。這篇博文的餘下部分的內容安排,首先是給出”潤潔”同學描述的死鎖場景,然後再給出我的剖析。對個人來說,這是一篇十分有必要的總結,對此博文的讀者來說,希望以後碰到類似的死鎖問題時,能夠明確死鎖的原因所在。

  1. 一個不可思議的死鎖

“潤潔”同學,給出的死鎖場景如下:

表結構:

CREATE TABLE dltask (

id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT ‘auto id’,

a varchar(30) NOT NULL COMMENT ‘uniq.a’,

b varchar(30) NOT NULL COMMENT ‘uniq.b’,

c varchar(30) NOT NULL COMMENT ‘uniq.c’,

x varchar(30) NOT NULL COMMENT ‘data’,

PRIMARY KEY (id),

UNIQUE KEY uniq_a_b_c (a, b, c)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=’deadlock test’;

a,b,c三列,組合成一個唯一索引,主鍵索引為id列。

事務隔離級別:

RR (Repeatable Read)

每個事務只有一條SQL:

delete from dltask where a=? and b=? and c=?;

SQL的執行計劃:

執行計劃

死鎖日誌:

死鎖日誌

  1. 初步分析

併發事務,每個事務只有一條SQL語句:給定唯一的二級索引鍵值,刪除一條記錄。每個事務,最多隻會刪除一條記錄,為什麼會產生死鎖?這絕對是不可能的。但是,事實上,卻真的是發生了死鎖。產生死鎖的兩個事務,刪除的是同一條記錄,這應該是死鎖發生的一個潛在原因,但是,即使是刪除同一條記錄,從原理上來說,也不應該產生死鎖。因此,經過初步分析,這個死鎖是不可能產生的。這個結論,遠遠不夠!

  1. 如何閱讀死鎖日誌

在詳細給出此死鎖產生的原因之前,讓我們先來看看,如何閱讀MySQL給出的死鎖日誌。

以上打印出來的死鎖日誌,由InnoDB引擎中的lock0lock.c::lock_deadlock_recursive()函式產生。死鎖中的事務資訊,通過呼叫函式lock_deadlock_trx_print()處理;而每個事務持有、等待的鎖資訊,由lock_deadlock_lock_print()函式產生。

例如,以上的死鎖,有兩個事務。事務1,當前正在操作一張表(mysql tables in use 1),持有兩把鎖(2 lock structs,一個表級意向鎖,一個行鎖(1 row lock)),這個事務,當前正在處理的語句是一條delete語句。同時,這唯一的一個行鎖,處於等待狀態(WAITING FOR THIS LOCK TO BE GRANTED)。

事務1等待中的行鎖,加鎖的物件是唯一索引uniq_a_b_c上頁面號為12713頁面上的一行(注:具體是哪一行,無法看到。但是能夠看到的是,這個行鎖,一共有96個bits可以用來鎖96個行記錄,n bits 96:lock_rec_print()方法)。同時,等待的行鎖模式為next key鎖(lock_mode X)。(注:關於InnoDB的鎖模式,可參考我早期的一篇PPT:《InnoDB 事務/鎖/多版本 實現分析》。簡單來說,next key鎖有兩層含義,一是對當前記錄加X鎖,防止記錄被併發修改,同時鎖住記錄之前的GAP,防止有新的記錄插入到此記錄之前。)

同理,可以分析事務2。事務2上有兩個行鎖,兩個行鎖對應的也都是唯一索引uniq_a_b_c上頁面號為12713頁面上的某一條記錄。一把行鎖處於持有狀態,鎖模式為X lock with no gap(注:記錄鎖,只鎖記錄,但是不鎖記錄前的GAP,no gap lock)。一把行鎖處於等待狀態,鎖模式為next key鎖(注:與事務1等待的鎖模式一致。同時,需要注意的一點是,事務2的兩個鎖模式,並不是一致的,不完全相容。持有的鎖模式為X lock with no gap,等待的鎖模式為next key lock X。因此,並不能因為持有了X lock with no gap,就可以說next key lock X就一定能夠加上。)。

分析這個死鎖日誌,就能發現一個死鎖。事務1的next key lock X正在等待事務2持有的X lock with no gap(行鎖X衝突),同時,事務2的next key lock X,卻又在等待事務1正在等待中的next key鎖(注:這裡,事務2等待事務1的原因,在於公平競爭,杜絕事務1發生飢餓現象。),形成迴圈等待,死鎖產生。

死鎖產生後,根據兩個事務的權重,事務1的權重更小,被選為死鎖的犧牲者,回滾。

根據對於死鎖日誌的分析,確認死鎖確實存在。而且,產生死鎖的兩個事務,確實都是在運行同樣的基於唯一索引的等值刪除操作。既然死鎖確實存在,那麼接下來,就是抓出這個死鎖產生原因。

  1. 死鎖原因深入剖析

  1. Delete操作的加鎖邏輯

在《MySQL加鎖處理分析》一文中,我詳細分析了各種SQL語句對應的加鎖邏輯。例如:Delete語句,內部就包含一個當前讀(加鎖讀),然後通過當前讀返回的記錄,呼叫Delete操作進行刪除。在此文的 組合六:id唯一索引+RR 中,可以看到,RR隔離級別下,針對於滿足條件的查詢記錄,會對記錄加上排它鎖(X鎖),但是並不會鎖住記錄之前的GAP(no gap lock)。對應到此文上面的死鎖例子,事務2所持有的鎖,是一把記錄上的排它鎖,但是沒有鎖住記錄前的GAP(lock_mode X locks rec but not gap),與我之前的加鎖分析一致。

其實,在《MySQL加鎖處理分析》一文中的 組合七:id非唯一索引+RR 部分的最後,我還提出了一個問題:如果組合五、組合六下,針對SQL:select * from t1 where id = 10 for update; 第一次查詢,沒有找到滿足查詢條件的記錄,那麼GAP鎖是否還能夠省略?針對此問題,參與的朋友在做過試驗之後,給出的正確答案是:此時GAP鎖不能省略,會在第一個不滿足查詢條件的記錄上加GAP鎖,防止新的滿足條件的記錄插入。

其實,以上兩個加鎖策略,都是正確的。以上兩個策略,分別對應的是:1)唯一索引上滿足查詢條件的記錄存在並且有效;2)唯一索引上滿足查詢條件的記錄不存在。但是,除了這兩個之外,其實還有第三種:3)唯一索引上滿足查詢條件的記錄存在但是無效。眾所周知,InnoDB上刪除一條記錄,並不是真正意義上的物理刪除,而是將記錄標識為刪除狀態。(注:這些標識為刪除狀態的記錄,後續會由後臺的Purge操作進行回收,物理刪除。但是,刪除狀態的記錄會在索引中存放一段時間。) 在RR隔離級別下,唯一索引上滿足查詢條件,但是卻是刪除記錄,如何加鎖?InnoDB在此處的處理策略與前兩種策略均不相同,或者說是前兩種策略的組合:對於滿足條件的刪除記錄,InnoDB會在記錄上加next key lock X(對記錄本身加X鎖,同時鎖住記錄前的GAP,防止新的滿足條件的記錄插入。) Unique查詢,三種情況,對應三種加鎖策略,總結如下:

  • 找到滿足條件的記錄,並且記錄有效,則對記錄加X鎖,No Gap鎖(lock_mode X locks rec but not gap);
  • 找到滿足條件的記錄,但是記錄無效(標識為刪除的記錄),則對記錄加next key鎖(同時鎖住記錄本身,以及記錄之前的Gap:lock_mode X);
  • 未找到滿足條件的記錄,則對第一個不滿足條件的記錄加Gap鎖,保證沒有滿足條件的記錄插入(locks gap before rec);

此處,我們看到了next key鎖,是否很眼熟?對了,前面死鎖中事務1,事務2處於等待狀態的鎖,均為next key鎖。明白了這三個加鎖策略,其實構造一定的併發場景,死鎖的原因已經呼之欲出。但是,還有一個前提策略需要介紹,那就是InnoDB內部採用的死鎖預防策略。

  1. 死鎖預防策略

InnoDB引擎內部(或者說是所有的資料庫內部),有多種鎖型別:事務鎖(行鎖、表鎖),Mutex(保護內部的共享變數操作)、RWLock(又稱之為Latch,保護內部的頁面讀取與修改)。

InnoDB每個頁面為16K,讀取一個頁面時,需要對頁面加S鎖,更新一個頁面時,需要對頁面加上X鎖。任何情況下,操作一個頁面,都會對頁面加鎖,頁面鎖加上之後,頁面記憶體儲的索引記錄才不會被併發修改。

因此,為了修改一條記錄,InnoDB內部如何處理:

  1. 根據給定的查詢條件,找到對應的記錄所在頁面;
  2. 對頁面加上X鎖(RWLock),然後在頁面內尋找滿足條件的記錄;
  3. 在持有頁面鎖的情況下,對滿足條件的記錄加事務鎖(行鎖:根據記錄是否滿足查詢條件,記錄是否已經被刪除,分別對應於上面提到的3種加鎖策略之一);
  4. 死鎖預防策略:相對於事務鎖,頁面鎖是一個短期持有的鎖,而事務鎖(行鎖、表鎖)是長期持有的鎖。因此,為了防止頁面鎖與事務鎖之間產生死鎖。InnoDB做了死鎖預防的策略:持有事務鎖(行鎖、表鎖),可以等待獲取頁面鎖;但反之,持有頁面鎖,不能等待持有事務鎖。
  5. 根據死鎖預防策略,在持有頁面鎖,加行鎖的時候,如果行鎖需要等待。則釋放頁面鎖,然後等待行鎖。此時,行鎖獲取沒有任何鎖保護,因此加上行鎖之後,記錄可能已經被併發修改。因此,此時要重新加回頁面鎖,重新判斷記錄的狀態,重新在頁面鎖的保護下,對記錄加鎖。如果此時記錄未被併發修改,那麼第二次加鎖能夠很快完成,因為已經持有了相同模式的鎖。但是,如果記錄已經被併發修改,那麼,就有可能導致本文前面提到的死鎖問題。
  1. 以上的InnoDB死鎖預防處理邏輯,對應的函式,是row0sel.c::row_search_for_mysql()。感興趣的朋友,可以跟蹤除錯下這個函式的處理流程,很複雜,但是集中了InnoDB的精髓。
  1. 剖析死鎖的成因

做了這麼多鋪墊,有了Delete操作的3種加鎖邏輯、InnoDB的死鎖預防策略等準備知識之後,再回過頭來分析本文最初提到的死鎖問題,就會手到拈來,事半而功倍。

首先,假設dltask中只有一條記錄:(1, ‘a’, ‘b’, ‘c’, ‘data’)。三個併發事務,同時執行以下的這條SQL:

delete from dltask where a=’a’ and b=’b’ and c=’c’;

並且產生了以下的併發執行邏輯,就會產生死鎖:

deadlock

上面分析的這個併發流程,完整展現了死鎖日誌中的死鎖產生的原因。其實,根據事務1步驟6,與事務0步驟3/4之間的順序不同,死鎖日誌中還有可能產生另外一種情況,那就是事務1等待的鎖模式為記錄上的X鎖 + No Gap鎖(lock_mode X locks rec but not gap waiting)。這第二種情況,也是”潤潔”同學給出的死鎖用例中,使用MySQL 5.6.15版本測試出來的死鎖產生的原因。

  1. 總結

行文至此,MySQL基於唯一索引的單條記錄的刪除操作併發,也會產生死鎖的原因,已經分析完畢。其實,分析此死鎖的難點,在於理解MySQL/InnoDB的行鎖模式,針對不同情況下的加鎖模式的區別,以及InnoDB處理頁面鎖與事務鎖的死鎖預防策略。明白了這些,死鎖的分析就會顯得清晰明瞭。

最後,總結下此類死鎖,產生的幾個前提:

  • Delete操作,針對的是唯一索引上的等值查詢的刪除;(範圍下的刪除,也會產生死鎖,但是死鎖的場景,跟本文分析的場景,有所不同)
  • 至少有3個(或以上)的併發刪除操作;
  • 併發刪除操作,有可能刪除到同一條記錄,並且保證刪除的記錄一定存在;
  • 事務的隔離級別設定為Repeatable Read,同時未設定innodb_locks_unsafe_for_binlog引數(此引數預設為FALSE);(Read Committed隔離級別,由於不會加Gap鎖,不會有next key,因此也不會產生死鎖)
  • 使用的是InnoDB儲存引擎;(廢話!MyISAM引擎根本就沒有行鎖)

相關推薦

:執行同時刪除唯一索引一行

3    總結    7 死鎖問題背景 做MySQL程式碼的深入分析也有些年頭了,再加上自己10年左右的資料庫核心研發經驗,自認為對於MySQL/InnoDB的加鎖實現瞭如指掌,正因如此,前段時間,還專門寫了一篇洋洋灑灑的文章,專門分析MyS

執行執行的通訊

產生背景 不同執行緒分別佔用對方需要的同步資源部放棄,都在等待對方放棄自己需要的同步資源,形成了執行緒的死鎖 解決方法 專門的演算法,原則 儘量減少同步資源的定義 案例 package com.zyd.thread; public class TestDeadLoc

Java 執行同時執行

    我們建立三個任務與三個執行緒,讓三個執行緒啟動,同時執行三個任務。     任務類必須實現 Runable 介面,而 Runable 介面只包含一個 run 方法。需要實現 這個方法來告訴系統

面試題之----寫個函式來解決執行同時讀寫一個檔案的問題

一般的方案: //fopen():開啟檔案或者 URL,返回resource型別資料 。 $fp = fopen('./tmp/lock.txt', 'a+'); if (flock($fp, LOCK_EX)) {//取得獨佔鎖定 fwrite($fp, "Write something h

Hbase批量匯入資料,支援執行同時操作

/** * HBase操作工具類:快取模式多執行緒批量提交作業到hbase * * @Auther: ning.zhang * @Email: [email protected] * @CreateDate: 2018/7/30 */ public c

java執行工具類,可用該執行同時處理相同且數量的任務

package zrh4; public class ThreadModel {private static int maxThread = 4;protected static int currentThread = 0;private static ThreadMode

通過閉鎖方式實現執行同時併發測試

閉鎖是一種同步工具類,可以延遲執行緒的進度直到其達到終止狀態。形象一點就是,閉鎖就是一扇關閉的大門,在閉鎖達到結束條件之前,這扇門是關閉的,沒有任何執行緒可以通過。而一旦條件達到,就像開閘洩洪一樣,萬馬奔騰,瞬間達到高併發。在此我希望模擬高併發的瞬間,而不是依次的啟動執行緒

ios執行同時訪問陣列問題

  錯誤: <__NSArrayM: 0x96be3e0>was mutated while being enumerated.   意思就是陣列在被一個執行緒訪問的時候,另一個數組也對它進行訪問。   原因是這樣的,我的遊戲中,有個掉道具的系統,裡面有一個數組

SurfaceView 練習——執行同時畫圖

為什麼需要 SurfaceView 學習一個東西,首先我們要明白,為什麼要學?在 Android 開發中,我們已經有了 TextView, ImageView, ...等形形色色的 view,如果對Android的實現,我們還的可以繼承他們,新增自定義的行為。

單例模式,執行同時訪問一個例項物件問題的處理,加lock .

多執行緒同時訪問一個例項物件時, 可以給程序加一把鎖來處理。lock是確保當一個執行緒位於程式碼的臨界區時,另一個執行緒不進入臨界區。如果其他執行緒試圖進入鎖定的程式碼,則它將一直等待(即被阻止),直到該物件被釋放。 public class Singleton {    

執行同時載入快取實現

import com.google.common.cache.Cache; import com.google.common.c

java中執行安全,執行執行通訊快速入門

一:多執行緒安全問題 ###1 引入 /* * 多執行緒併發訪問同一個資料資源 * 3個執行緒,對一個票資源,出售 */ public class ThreadDemo { public static void main(String[

刨根問底系列(3)——關於socket api的原子操作性和執行安全性的探究和實驗測試(執行同時send,write)

多個執行緒對同一socket同時進行send操作的結果 1. 概覽 1.1 起因 自己寫的專案裡,為了保證連線不中斷,我起一個執行緒專門傳送心跳包保持連線,那這個執行緒在send傳送資料時,可能會與主執行緒中的send衝突,因此我就想探討一下socket api是否具有執行緒安全性。網上很多說法,但多是推測,

基於TCP/IP協議的執行雙向通訊在OpenWrt的實現

1、TCP/IP協議組 TCP/IP協議(傳輸控制協議)由網路層的IP協議和傳輸層的TCP協議組成。 IP層負責網路主機的定位,資料傳輸的路由,由IP地址可以唯一的確定Internet上的一臺主機。 TCP層負責面向應用的可靠的或非可靠的資料

Java語言程式設計-進階篇(七)執行與並行程式設計【

1.簡單的多執行緒例子package test; public class hello { public static void main(String args[]){ Runnable printA = new PrintChar('a',100);

Android FTP 執行斷點續傳下載\上傳

最近在給我的開源下載框架Aria增加FTP斷點續傳下載和上傳功能,在此過程中,爬了FTP的不少坑,終於將功能實現了,在此把一些核心功能點記錄下載。 FTP下載原理 FTP單執行緒斷點續傳 FTP和傳統的HTTP協議有所不同,由於FTP沒有所謂的標頭

java實現FTP執行斷點續傳,傳下載!

package com.ftp;  import java.io.File;    import java.io.FileOutputStream;    import java.io.IOException;    import java.io.InputStream;    import java.io

比較dbms_job幾個模擬執行方法在呼叫延遲的差異

目的:比較幾個多執行緒方法在呼叫延遲上的差異。 1.dbms_job.submit; 2.dbms_job.isubmit; 3.dbms_job.run; 4.dbms_job.broken; 這幾種都能呼叫新JOB,模擬多執行緒,比較這幾種方式的差異。 實驗內容:連續

Linux下執行程式設計學習【2】——代…

要想一份程式碼在linux下能編譯,在windows下也能編譯,就得應用巨集處理。最初產生這個構想,是在學習opengl的時候,發覺glut庫是跨平臺的,檢視原始碼後發覺glut裡面進行了很多巨集處理。這是第一次知道編譯器在進行編譯的時候也會定義一些巨集關鍵字。 程式結果如下: 在win8系統下,用d

Java 執行實現場景

簡述: 《Java 程式設計思想》  P718 ~ P722 模擬死鎖的場景, 三個人 三根筷子,每個人需要拿到身邊的兩根筷子才能開始吃飯 出現死鎖的場景是,三個人都拿到了右邊的筷子,但是由於筷子都被搶佔,均無法獲得左邊的筷子 Chopstick.java