MySQL鎖系統總結

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"innoDB鎖簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"innoDb支持多種粒度的鎖,按照粒度來分,可分爲"},{"type":"codeinline","content":[{"type":"text","text":"表鎖(LOCK_TABLE)"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"行鎖(LOCK_REC)"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般的鎖系統都會有"},{"type":"codeinline","content":[{"type":"text","text":"共享鎖"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"排他鎖"}]},{"type":"text","text":"的分類,"},{"type":"codeinline","content":[{"type":"text","text":"共享鎖"}]},{"type":"text","text":"也叫"},{"type":"codeinline","content":[{"type":"text","text":"讀鎖"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"排他鎖"}]},{"type":"text","text":"也叫"},{"type":"codeinline","content":[{"type":"text","text":"寫鎖"}]},{"type":"text","text":"。加在同一個資源上,寫鎖會阻塞另外一把寫鎖或讀鎖的獲取,讀鎖則允許另外一把讀鎖的獲取,也就是讀讀之間允許併發,讀寫或者寫寫會阻塞,innodb中表鎖和行鎖都支持"},{"type":"codeinline","content":[{"type":"text","text":"共享鎖(簡寫S)"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"排他鎖(簡寫X)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲innoDB支持多粒度的鎖,允許表鎖和行鎖的並存,爲了方便多粒度鎖衝突的判斷,innoDB中還存在一種名叫"},{"type":"codeinline","content":[{"type":"text","text":"意向鎖(Intention Locks)"}]},{"type":"text","text":"的鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,還有一種特殊的表鎖,自增鎖,主要用來併發安全的生成自增id,一種特殊的意向鎖,插入意向鎖,用來防止幻讀問題"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"表鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表鎖,鎖定的粒度是整個表,也分共享鎖和排他鎖。不同於行鎖,表鎖MySQL Server層就有實現(所以MyISAM支持表鎖,也只支持表鎖),innoDb則在存儲引擎層面也實現了一遍表鎖(後面會介紹具體結構)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哪些時候會觸發表鎖呢?在執行某些ddl時,比如"},{"type":"codeinline","content":[{"type":"text","text":"alter table"}]},{"type":"text","text":"等操作,會對整個表加鎖,也可以手動執行鎖表語句:"},{"type":"codeinline","content":[{"type":"text","text":"LOCK TALBES table_name [READ | WRITE]"}]},{"type":"text","text":",READ爲共享鎖,WRITE爲排他鎖,手動解鎖的語句爲:"},{"type":"codeinline","content":[{"type":"text","text":"UNLOCK TABLES"}]},{"type":"text","text":",會直接釋放當前會話持有的所有表鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一些需要注意的地方:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲MySQL Server層和InnoDB都實現了表鎖,僅當"},{"type":"codeinline","content":[{"type":"text","text":"autocommit=0"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"innodb_table_lock=1"}]},{"type":"text","text":"(默認設置)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖,這種情況下,InnoDB才能自動識別涉及表級鎖的死鎖,否則,InnoDB將無法自動檢測並處理這種死鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在用"},{"type":"codeinline","content":[{"type":"text","text":"LOCK TALBES"}]},{"type":"text","text":"顯式獲取鎖後,只能訪問加鎖的這些表,並且如果加的是共享鎖,那麼只能執行查詢操作,而不能執行更新操作,如果獲得的是排他鎖,則可以進行更新操作。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開始事務時會自動UNLOCK之前的表鎖,COMMIT或ROLLBACK都不能釋放用"},{"type":"codeinline","content":[{"type":"text","text":"LOCK TABLES"}]},{"type":"text","text":"加的表級鎖。"},{"type":"codeinline","content":[{"type":"text","text":"LOCK TALBES"}]},{"type":"text","text":"時會先隱式提交事務,再鎖表,"},{"type":"codeinline","content":[{"type":"text","text":"UNLOCK TALBES"}]},{"type":"text","text":"也會隱式提交事務。所以,事務中需要的表鎖必須在事務開頭一次性獲取,無法再事務中間獲取,因爲不管是"},{"type":"codeinline","content":[{"type":"text","text":"LOCK TALBES"}]},{"type":"text","text":"還是"},{"type":"codeinline","content":[{"type":"text","text":"UNLOCK TALBES"}]},{"type":"text","text":"都會提交事務"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"官網上建議的表鎖的使用方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"SET autocommit=0;\nLOCK TABLES t1 WRITE, t2 READ, ...;\n... do something with tables t1 and t2 here ...\nCOMMIT;\nUNLOCK TABLES;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際業務中,沒有特殊理由,不建議使用表鎖,因爲鎖的粒度太大了,極大的影響併發。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"意向鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"意向鎖是一種特殊的表級鎖,意向鎖是爲了讓InnoDB多粒度的鎖能共存而設計的。取得行的共享鎖和排他鎖之前需要先取得表的意向共享鎖(IS)和意向排他鎖(IX),意向共享鎖和意向排他鎖都是系統自動添加和自動釋放的,整個過程無需人工干預。 主要是用來輔助表級和行級鎖的衝突判斷,因爲Innodb支持行級鎖,如果沒有意向鎖,則判斷表鎖和行鎖衝突的時候需要遍歷表上所有行鎖,有了意向鎖,則只要判斷表是否存在意向鎖就可以知道是否有行鎖了。表級別鎖的兼容性如下表:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ba\/ba9e6a9da8feda1a02e2e377c1caace5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖片可以看到,意向鎖與意向鎖兼容,IX、IS自身以及相互都兼容,不互斥,因爲意向鎖僅表示下一層級加什麼類型的鎖,不代表當前層加什麼類型的鎖;IX和表級X、S互斥;IS和表級X鎖互斥。其兼容性正好體現了其作用。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"自增鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自增鎖是一種特殊的表級別鎖,如果一個表的某個行具有AUTO_INCREMENT的列,則一個事務在插入記錄到這個表的時候,會先獲取自增鎖。如果一個事務持有自增鎖,會阻塞其他事物對該表的插入操作,保證自增連續。innodb_autoinc_lock_mode變量定義了不同的自增算法,在MySql8.0之前默認值是1,MySql8.0之後默認值是2,具體區別參考官方文檔(https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/innodb-auto-increment-handling.html)"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"行鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Innodb中的行鎖種類繁多,可以分爲:記錄鎖(record locks)、間隙鎖(gap locks)、臨鍵鎖(next-key locks),插入意向鎖(insert intention locks)。行鎖在邏輯上都可以看作作用於索引或者索引間隙之上,索引分爲主鍵索引和非主鍵索引兩種,如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很多語句都會加行鎖,比如Update、Delete、Insert等操作,或者使用SELECT ... FOR SHARE | UPDATE [NOWAIT |SKIP LOCKED]來進行當前讀(Locking Reads),其中SHARE表示加共享鎖,UPDATE表示加排他鎖。當要加的鎖與當前行已有鎖互斥時,會一直阻塞等待一段時間(innodb_lock_wait_timeout定義了等待時間)。加上NOWAIT參數則不會阻塞,會立即返回,並顯示一個錯誤,加上SKIP LOCKED則會在結果集中跳過這些衝突的記錄(慎用)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在不同的語句,不同的事務隔離級別下,甚至不同的索引類型下,行鎖會表現成不同的形式,下面介紹這些形式:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"記錄鎖(record locks)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在邏輯上,記錄鎖可以理解爲鎖定的是某個具體的索引,當SQL執行按照唯一性(Primary key、Unique key)索引進行數據的檢索時,查詢條件等值匹配且查詢的數據是存在,這時 SQL 語句加上的鎖即爲記錄鎖。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"圖片間隙鎖(gap locks)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在邏輯上,間隙鎖可以理解爲鎖住的是索引之間的間隙,是一個左開右開的區間。當SQL執行按照索引進行數據的檢索時,查詢條件的數據不存在,這時SQL語句加上的鎖即爲間隙鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖片如上圖,因爲這些語句查詢的值都不存在,所以鎖住的都是間隙。並且在 InnoDb 存儲引擎裏,每個數據頁中都會有兩個虛擬的行記錄,用來限定記錄的邊界,分別是:Infimum Record 和 Supremum Record,Infimum 是比該頁中任何記錄都要小的值,而 Supremum 比該頁中最大的記錄值還要大,這兩條記錄在創建頁的時候就有了,並且不會刪除。所以當查詢的值比當前已有記錄最大值還大時候,鎖住的會是最大值到Supremum之間的間隙。比如第一條語句,查詢的時候就算是等值匹配,只要這個不存在的數據落在兩個索引節點之間,就算不是一個範圍,也會鎖住索引節點間的所有數據即gap3,範圍(7,11)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"間隙鎖是可以共存的,共享間隙鎖與獨佔間隙鎖之間是沒有區別的,兩者之間並不衝突。其存在的目的都是防止其他事務往間隙中插入新的紀錄,故而一個事務所採取的間隙鎖是不會去阻止另外一個事務在同一個間隙中加鎖的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"間隙鎖是設計用來防止幻讀的,當鎖定一個gap時,其他事務沒有辦法再往這個gap中插入數據,PostgreSQL沒有這種機制,所以PostgreSQl沒有辦法鎖住不存在的行,無法防止幻讀"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"臨鍵鎖(next-key locks)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在邏輯上,臨鍵鎖可以理解爲鎖住的是索引本身以及索引之前的間隙,是一個左開右閉的區間。當SQL執行按照非唯一索引進行數據的檢索時,會給匹配到行上加上臨鍵鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖片如上圖,當執行select * from table_name where id = 3 for update時會鎖定(-∞,3)區間,因爲按照這個SQL的語義,即是爲了鎖住id=3的數據,不允許其他操作,如果只是鎖住記錄本身,肯定是沒有辦法保證的,因爲這是非唯一索引,還有可能插入其他id=3的數據,如果把間隙都給鎖住,則其他對這個間隙的插入操作都會被阻塞,從而保證了一致性,這也是臨鍵鎖的用意。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果加鎖時,查詢條件沒有命中索引(非ICP的查詢),則InnoDB會嘗試給全表每一條記錄都加上臨鍵鎖,效果相當於鎖表了"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"插入意向鎖(insert intention locks)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插入意向鎖是一種間隙鎖形式的意向鎖,在真正執行INSERT操作之前設置。當執行插入操作時,總會檢查當前插入操作的下一條記錄(已存在的主索引節點)上是否存在鎖對象,判斷是否鎖住了gap,如果鎖住了,則判定和插入意向鎖衝突,當前插入操作就需要等待,也就是配合上面的間隙鎖或者臨鍵鎖一起防止了幻讀操作。 因爲插入意向鎖是一種意向鎖,意向鎖只是表示一種意象,所以插入意向鎖之間不會互相沖突,多個插入操作同時插入同一個gap時,無需互相等待,比如當前索引上有記錄4和8,兩個併發session同時插入記錄6,7。他們會分別爲(4,8)加上GAP鎖,但相互之間並不衝突。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"INSERT語句在執行插入之前,會先在gap中加入插入意向鎖,如果是唯一索引,還會進行Duplicate Key判斷,如果存在相同Key且該Key被加了互持鎖,則還會加共享鎖,然後等待(因爲這個相同的Key之後有可能會回滾刪除,這裏非常容易死鎖)。等到成功插入後,會在這條記錄上加排他記錄鎖。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"行鎖小結"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/43\/43f71cbb3cbb29b4206ad9ae1d3f3932.png","alt":"圖片","title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"行鎖在不同的語句中和環境條件下可以表現成:記錄鎖(record locks)、 間隙鎖(gap locks)、臨鍵鎖(next-key locks)和插入意向鎖(insert intention locks)。記錄鎖鎖住具體的記錄,間隙鎖鎖住記錄之間的間隙,臨鍵鎖鎖住記錄和記錄前面的間隙,插入意向鎖則是特殊的間隙鎖,在插入前判斷行將要插入的間隙是否會有衝突。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上說的各種行鎖的加鎖情況都是在可重複讀(REPEATABLE READ)隔離級別下,這個級別也是innoDB默認的事務隔離級別,是最常用的隔離級別,但是其實不同語句在不同隔離級別下加鎖的情況會有非常大的區別,以下會簡單說明。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"不同語句和隔離級別對加鎖的影響"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏先排除讀未提交(READ UNCOMMITTED)這種隔離級別的情況,這種級別在生產上幾乎無法使用,會出現髒讀的情況,不一致讀,無法保證事務的ACID。然後先看下串行化(SERIALIZABLE)隔離級別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"串行化隔離級別和可重複讀隔離級別最大的區別應該是,innoDB會隱式的轉換所有的SELECT語句,給其加共享鎖,變成SELECT ... FOR SHARE,這樣讀操作會阻塞其他寫操作,使得讀寫無法併發,只能串行,從而保證嚴格的一致性。不過這種行爲也受到autocommit變量的影響:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果禁用了autocommit,如上所述,則innoDB會隱式的轉換所有的SELECT語句,給其加共享鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果開啓了autocommit,不會進行隱式轉換,因爲每條語句構成一個事務,所有快照讀語句(也就是沒有FOR UPDATE|SHARE的SELECT語句)可以被認爲是隻讀的事務,是可以安全併發,不需要阻塞其他事務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可重複讀(READ COMMITTED)隔離級別下,和可重複讀隔離級別在行鎖方面主要的區別是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可重複讀隔離級別下取消了間隙鎖,臨鍵鎖也退化成了記錄鎖。間隙鎖定和臨鍵鎖僅用於外鍵約束檢查和重複鍵檢查,也就是說加鎖時,如果沒有符號條件的查詢並不加鎖,有符合條件的查詢也只會給記錄加上記錄鎖。因爲沒有了間隙鎖,所以會出現幻讀問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在可重複讀隔離級別下,加鎖時如果查詢條件沒有命中索引(非ICP的查詢),則會給表中每條記錄都加上臨鍵鎖。而不可重複讀隔離級別下因爲沒有間隙鎖,則會退化成給表中每條數據加上記錄鎖,並且還會把沒有匹配的行上的鎖給釋放掉,而不是把全表所有記錄不管有沒有匹配都給鎖上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"7死鎖因爲使用表鎖時,需要一次性申請所有所需表的鎖,所以在只使用表鎖的情況下不會出現死鎖,一般出現死鎖的情況都是行鎖。innoDB有死鎖探測機制,在申請鎖的時候,都會先進行死鎖判斷,採用的算法深度優先搜索,並且如果在搜索過程中發現有環,就說明發生了死鎖,爲了避免死鎖檢測開銷過大,如果搜索深度超過了 200(LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK)也同樣認爲發生了死鎖。出現死鎖時,innoDB會選擇一個回滾代價比較小的事務進行回滾。以下會舉幾個比較典型的死鎖例子(均在可重複度隔離級別下),首先會先建一張測試的表:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"CREATE TABLE `student` (\n`id` int NOT NULL,\n`uuid` varchar(64) NOT NULL,\n`name` varchar(64) NOT NULL,\n`age` int NOT NULL,\nPRIMARY KEY (`id`),\nUNIQUE KEY `uuid_index` (`uuid`),\nKEY `name_index` (`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖例一"}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
語句順序\\事務事務一事務二
T1begin;begin;
T2select * from student where id = 1  for update;select * from student where id = 2 for update;
T3select * from student where id = 2  for update;
T4(死鎖發生)
select * from student where id = 1 for update;"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是最簡單最典型的死鎖的情況了,兩個事務互相鎖定持有資源,並且等待對方的資源,最後形成一個環,死鎖出現。最後某個事務回滾,寫業務代碼的時候,應該對併發條件可能出現這種情況的語句有所警覺。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖例二"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前提:事務開始時,student表裏有id=1的記錄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

語句順序\\事務
事務一事務二
T1begin;begin;
T2select * from student where id = 1  for share;select * from student where id = 1 for share;
T3update student set name = 'Tom' where id = 1;
T4(死鎖發生)
update student set name = 'Jack' where id = 1;"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖例三"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前提:事務開始時,student表裏沒有id=100的記錄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

語句順序\\事務
事務一事務二
T1begin;begin;
T2select * from student where id = 100 for update;select * from student where id = 100 for update;
T3insert into student values (100, 'uuid100', 'jack', 18);
T4(死鎖發生)
insert into student values (100, 'uuid100', 'jack', 18);"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖例四"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前提:事務開始時,student表裏沒有uuid=uuid100的記錄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

語句順序\\事務
事務一事務二事務三
T1begin;begin;begin;
T2insert into student values (100, 'uuid100', 'jack', 18);

T3
insert into student values (101, 'uuid100', 'jack', 18);insert into student values (102, 'uuid100', 'jack', 18);
T4(死鎖發生)rollback;

"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這也是MySql官方文檔給出的一個例子。三個事務同時插入一條某個唯一索引屬性(上面的uuid)相同的數據,其中某個事務先一步插入,其他兩個事務會阻塞等待,然後先一步插入的事務回滾,其他兩個事務出現死鎖,其中某個事務會被回滾。官方文檔還提到了另外一種類似的情況,具體可以參考Locks Set by Different SQL Statements in InnoDB"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種死鎖的原因是,INSERT的時候,會對唯一索引進行Duplicate Key判斷,如果唯一鍵衝突,則會加共享鎖等待,也就是T3時候的事務二和事務三,都會獲得共享鎖。T4時,事務一回滾,事務二和事務三都會申請升級排他鎖,這樣就造成類似死鎖案例二的情況,形成死鎖了。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖例五"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

語句順序\\事務
事務一事務二
T1begin;begin;
T2(死鎖發生)update student set age = age + 1 where name = 'jack';update student set name = 'bob' where id > 100;"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用SHOW ENGINE INNODB STATUS語句可以看到最近一次的死鎖信息,在調試的時候很有幫助。 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出現死鎖後某個事務會回滾,其他事務成功,上層業務會捕獲到死鎖錯誤,再重試一般會成功,如果出現大量鎖重試,則說明哪裏出了問題,寫代碼的時候可以注意以下幾點可以減少死鎖出現的概率:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似的業務邏輯儘量以固定的順序訪問表和行"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果業務允許,大事務拆小,大事務持有鎖的時間更長,更容易出現死鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲表添加合理的索引,可以看到可重複讀級別下,如果不走索引(非ICP的查詢)將會爲表的每一行記錄加鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘量少用for share或者for update語句,雖然看起來只是在一行記錄上加鎖,但是由於間隙鎖和臨鍵鎖的存在,鎖住的可能不止是一行記錄"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提前申請足夠強度的鎖,不要先用for share鎖住行,後面再update,很容易死鎖。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"鎖的內部表示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"innoDb"}]},{"type":"text","text":"內部中,用"},{"type":"codeinline","content":[{"type":"text","text":"unsigned long"}]},{"type":"text","text":"類型表示鎖的類型,其中不同的位代表鎖不同的信息,最低的4位表示lock_mode,中間的4位表示lock_type,其餘高位表示record_lock_type,內部使用位操作來設置和判斷是否設置了對應的值 :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| record_lock_type | lock_type  | lock_mode | --- | --- | ---"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"lock_mode"}]},{"type":"text","text":":描述了鎖的基本類型,分爲以下幾種"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_IS: 意向共享鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_IX: 意向排他鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_S: 共享鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_X: 排他鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_AUTO_INC: 自增鎖"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在源碼中有一個lock_mode的枚舉類型,除了以上還有幾個值:LOCK_NONE,用來表示一致性讀,LOCK_NUM用來表示lock_mode的數量,LOCK_NONE_UNSET用來複位低8位  "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"lock_type"}]},{"type":"text","text":":佔用中間的4位,目前只用到了5位和6位,分別表示表鎖(LOCK_TABLE)和 行鎖(LOCK_REC)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"record_lock_type"}]},{"type":"text","text":":對於表鎖類型來說都是空的,對於行鎖目前值有:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_WAIT:表示正在等待鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_ORDINARY:表示臨鍵鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_GAP:表示間隙鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_REC_NOT_GAP:表示記錄鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_INSERT_INTENTION:表示插入意向鎖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOCK_CONV_BY_OTHER:表示鎖是由其它事務創建的(比如隱式鎖轉換)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上說的是鎖的類型的表示,行鎖、表鎖類型相關信息都統一到一個字段了。同類型字段一樣,行鎖、表鎖本身在"},{"type":"codeinline","content":[{"type":"text","text":"innoDb"}]},{"type":"text","text":"中也統一用一個結構體來表示"},{"type":"codeinline","content":[{"type":"text","text":"lock_t"}]},{"type":"text","text":",大體如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"struct lock_t {\n trx_t* trx; \/\/ 鎖所屬的事務\n UT_LIST_NODE_T(lock_t) trx_locks; \/\/ 事務所持鎖的列表\n ulint type_mode; \/\/ 鎖類型\n hash_node_t hash; \/\/ 全局鎖哈希表對應的節點\n dict_index_t* index; \/\/ 行鎖的行記錄索引\n union {\n lock_table_t; \/\/ 表鎖 \n lock_rec_t rec_lock; \/\/ 行鎖\n } un_member; \/\/ 鎖詳情\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"行鎖和表鎖都用一個"},{"type":"codeinline","content":[{"type":"text","text":"lock_t"}]},{"type":"text","text":"結構來表示,差異部分在一個union結構中表示,裏面的type_mode即是上面介紹的鎖類型,行鎖的結構如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"struct lock_rec_t {\n ulint space; \/\/ 鎖的space id\n ulint page_no; \/\/ 鎖的page number\n ulint n_bits; \/\/ 鎖住位置的bitmap\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過("},{"type":"codeinline","content":[{"type":"text","text":"space"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"page_no"}]},{"type":"text","text":")可以確定鎖所在的頁,"},{"type":"codeinline","content":[{"type":"text","text":"innoDb"}]},{"type":"text","text":"內部還會有個字段"},{"type":"codeinline","content":[{"type":"text","text":"heap_no"}]},{"type":"text","text":"來表示記錄在頁上的偏移,也就是說三元組("},{"type":"codeinline","content":[{"type":"text","text":"space"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"page_no"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"heap_no"}]},{"type":"text","text":")可以唯一的確定一行的位置。在分配"},{"type":"codeinline","content":[{"type":"text","text":"lock_rec_t"}]},{"type":"text","text":"結構的時候,還會爲其在最後分配一個大小爲n_bits的bitmap,而記錄偏移的bit即爲"},{"type":"codeinline","content":[{"type":"text","text":"heap_no"}]},{"type":"text","text":",用來快速判斷這頁哪些記錄加了鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"innoDb"}]},{"type":"text","text":"所有的行鎖會插入到一個全局hash表("},{"type":"codeinline","content":[{"type":"text","text":"lock_sys"}]},{"type":"text","text":"->"},{"type":"codeinline","content":[{"type":"text","text":"rec_hash"}]},{"type":"text","text":")中,相同("},{"type":"codeinline","content":[{"type":"text","text":"space"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"page_no"}]},{"type":"text","text":")也就是同一頁的鎖會被Hash到同一個bucket裏,通過"},{"type":"codeinline","content":[{"type":"text","text":"lock_t"}]},{"type":"text","text":"->"},{"type":"codeinline","content":[{"type":"text","text":"hash"}]},{"type":"text","text":"串成鏈表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結一下,就是同一事務,同一類型的行鎖在同一頁上會複用同一個鎖結構"},{"type":"codeinline","content":[{"type":"text","text":"lock_t"}]},{"type":"text","text":",用後面的bitmap來具體表示鎖哪些行,大大節約了空間。同一頁上不同的事物或類型的鎖通過鏈表串起來放在"},{"type":"codeinline","content":[{"type":"text","text":"rec_hash"}]},{"type":"text","text":"的同一個bucket裏,利用hash的結構先定位到頁,然後遍歷同一頁上不同的"},{"type":"codeinline","content":[{"type":"text","text":"lock_t"}]},{"type":"text","text":",就可以得到哪些事物的哪些鎖鎖住了哪些行,這種設計平衡了時間和空間的效率。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"innoDB鎖系統配合MVCC機制一起實現了事務的"},{"type":"codeinline","content":[{"type":"text","text":"一致性"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"隔離性"}]},{"type":"text","text":",innoDB中的鎖總類繁多,並且和事務隔離級別關係密切,不同語句在不同隔離級別下的加鎖情況大有不同,細節尤其多,而瞭解這些對排查死鎖會有很大的幫助。行鎖在innoDB中的實現也頗爲巧妙,值得學習。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"參考鏈接"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/schecterdamien.github.io\/2021\/03\/04\/mysql-lock\/"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/innodb-locking.html"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http:\/\/mysql.taobao.org\/monthly\/2017\/12\/02\/"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http:\/\/mysql.taobao.org\/monthly\/2016\/01\/01\/"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http:\/\/mysql.taobao.org\/monthly\/2018\/05\/04\/"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.aneasystone.com\/archives\/2017\/12\/solving-dead-locks-three.html"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.cnblogs.com\/jojop\/p\/13982679.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:360技術(ID:qihoo_tech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/yq5Erdv5Dft3foJEVE2dxA","title":"xxx","type":null},"content":[{"type":"text","text":"MySQL鎖系統總結"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章