Mysql Innodb 中的鎖

本文主要涉及如下三個部分:mysql

1. 爲何要加鎖sql

2. 鎖的分類數據庫

3. 常見語句的加鎖分析服務器

4. 如何分析死鎖session

5. 如何預防死鎖數據結構

 

先列出我本地的運行環境併發

數據庫版本是5.7,隔離級別是Repeatable-Read(可重複讀),不一樣的數據庫版本和隔離級別對語句的執行結果影響很大。討論鎖的時候不指明版本和隔離級別,都是耍流氓。spa

1、爲何要加鎖線程

數據庫是一個多用戶使用的共享資源。當多個用戶併發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的狀況。若對併發操做不加控制就可能會讀取和存儲不正確的數據,破壞數據庫的一致性。日誌

鎖是用於管理對公共資源的併發控制。也就是說併發的狀況下,會出現資源競爭,因此須要加鎖。

舉個例子,轉帳操做。簡單來講,張三給李四轉帳x元,能夠分爲三步:


1,先查詢張三的帳戶餘額y是否大於x

2,張三的餘額 y = y - x元

3,李四的餘額 x = z + x元

假設張三帳戶餘額有1000元,李四餘額也有1000元,若是不加鎖的話,同時有兩個請求,A要求轉500元,B要求轉600元,第一步查詢餘額都是足夠的,第2步和第3步也能執行成功,可是最終結果倒是錯誤,第二個請求可能會覆蓋掉第一個請求。

這種問題叫作 丟失更新: 多個事務操做同一行,後面的更新覆蓋了前面的更新值。須要在應用級別加鎖來避免。

 

數據庫有ACID原則,其中I是隔離性,標準SQL規範中定義了四種隔離級別:

越往下,隔離級別越高,問題越少,同時併發度也越低。隔離級別和併發度成反比的。

  1. 髒讀:事務A讀取了事務B未提交的數據
  2. 不可重複讀:對於一條記錄,事務A兩次讀取的數據變了
  3. 幻讀:事務A按照相同的查詢條件,讀取到了新增的數據

MySQL中的隔離級別以下:

和標準SQL規範相比,MySQL中可重複讀解決了幻讀,實現了串行化隔離級別的功能,同時沒有嚴重影響併發。是經過加鎖、阻止插入新數據,來解決幻讀的。

2、鎖的分類

咱們據說過讀鎖、寫鎖、共享鎖、互斥鎖、行鎖等等各類名詞,根據本身的理解,簡單對這些鎖進行了分類。

加鎖機制:

一、樂觀鎖:先修改,保存時判斷是夠被更新過,應用級別

二、悲觀鎖:先獲取鎖,再操做修改,數據庫級別

鎖粒度:

表級鎖:開銷小,加鎖快,粒度大,鎖衝突機率大,併發度低,適用於讀多寫少的狀況。

頁級鎖:BDB存儲引擎

行級鎖:Innodb存儲引擎,默認選項

兼容性:

S鎖,也叫作讀鎖、共享鎖,對應於咱們經常使用的 select * from users where id =1 lock in share mode


X鎖,也叫作寫鎖、排它鎖、獨佔鎖、互斥鎖,對應對於select * from users where id =1 for update

下面這個表格是鎖衝突矩陣,能夠看到只有讀鎖和讀鎖之間兼容的,寫鎖和讀鎖、寫鎖都是衝突的。

衝突的時候會阻塞當前會話,直到拿到鎖或者超時

這裏要提到的一點是,S鎖 和 X鎖是能夠是表鎖,也能夠是行鎖

索引組織表

先理解下索引組織表。

 

輔助索引

 

彙集索引

Innodb中的索引數據結構是 B+ 樹,數據是有序排列的,從根節點到葉子節點一層層找到對應的數據。普通索引,也叫作輔助索引,葉子節點存放的是主鍵值。主鍵上的索引叫作彙集索引,表裏的每一條記錄都存放在主鍵的葉子節點上。當經過輔助索引select 查詢數據的時候,會先在輔助索引中找到對應的主鍵值,而後用主鍵值在彙集索引中找到該條記錄。舉個例子,用name=Alice來查詢的時候,會先找到對應的主鍵值是18 ,而後用18在下面的彙集索引中找到name=Alice的記錄內容是 77 和 Alice。

表中每一行的數據,是組織存放在彙集索引中的,因此叫作索引組織表。

瞭解索引數據結構的目的是爲了說明,行鎖是加在索引上的。

1.select * from user where id=10 for update

一條簡單的SQL。在user表中查找id爲10的記錄,並用for update加X鎖。

這裏User表中,有3個字段, 主鍵id 和 另一個字段name。下面的表格是B+樹索引的簡化表達。第一行id是索引的節點,第二行和第三行是這行記錄,包含了姓名和性別。

如圖所示,經過鎖住彙集索引中的節點來鎖住這條記錄。

彙集索引上的鎖,比較好理解,鎖住id=10的索引,即鎖住了這條記錄。

2. select * from user where name=‘b’ for update

查詢user表中name爲d的記錄,並用for update加X鎖

這裏的name上加了惟一索引,惟一索引本質上是輔助索引,加了惟一約束。因此會先在輔助索引上找到name爲d的索引記錄,在輔助索引中加鎖,而後查找彙集索引,鎖住對應索引記錄。

爲何聚簇索引上的記錄也要加鎖?試想一下,若是有併發的另一個SQL,是直接經過主鍵索引id=30來更新,會先在彙集索引中請求加鎖。若是隻在輔助索引中加鎖的話,兩個併發SQL之間是互相感知不到的。

3. select * from user where name=‘b’ for update

查詢user表中name爲b的記錄,並用for update加X鎖。這裏name上加了普通的索引,不是惟一索引。普通索引的值是能夠重複的。會先在輔助索引中找到name爲b的兩條記錄,加X鎖,而後獲得主鍵值7和30,到彙集索引中加X鎖。

事情並無那麼簡單,若是這時有另外一個事務,插入了name=b,id=40的記錄,卻發現是能夠插入的。

位置在途中紅色線條標註的間隙內,這樣就會出現幻讀,兩次查詢獲得的結果是不一致的,第一次查到兩條數據,插入以後獲得三條數據。

爲了防止這種狀況,出現了另外一種鎖,gap lcok 間隙鎖。鎖住的是索引的間隙。

即圖中,紅色線條標識的空隙。由於新插入name=b的記錄,可能出如今這三個間隙內。

這張圖裏出現了三種鎖

記錄鎖:單行記錄上的鎖

間隙鎖:鎖定記錄之間的範圍,但不包含記錄自己。

Next Key Lock: 記錄鎖+ 間隙鎖,鎖定一個範圍,包含記錄自己。

 

4. 意向鎖( Intention Locks )

InnoDB爲了支持多粒度(表鎖與行鎖)的鎖並存,引入意向鎖。意向鎖是表級鎖,

IS: 意向共享鎖
IX: 意向排他鎖

事務在請求某一行的S鎖和X鎖前,須要先得到對應表的IS、IX鎖。

意向鎖產生的主要目的是爲了處理行鎖和表鎖之間的衝突,用於代表「某個事務正在某一行上持有了鎖,或者準備去持有鎖」。好比,表中的某一行上加了X鎖,就不能對這張表加X鎖。

若是不在表上加意向鎖,對錶加鎖的時候,都要去檢查表中的某一行上是否加有行鎖,多麻煩。

意向鎖的兼容性矩陣

5. 插入意向鎖(Insert Intention Lock)

Gap Lock中存在一種插入意向鎖,在insert操做時產生。

有兩個做用:

  • 和next-key互斥,阻塞next-key 鎖,防止插入數據,這樣就不會幻讀。
  • 插入意向鎖互相是兼容的,容許相同間隙、不一樣數據的併發插入

3、常見語句的加鎖分析

後面會有多個SQL語句,先說明一下表結構

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `id_no` varchar(255) DEFAULT NULL COMMENT '身份證號',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `mobile` varchar(255) DEFAULT NULL COMMENT '手機號',
  `age` int(11) DEFAULT NULL COMMENT '年齡',
  `address` varchar(255) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_id_no` (`id_no`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=10002 DEFAULT CHARSET=utf8 COMMENT='用戶表';

這裏有一個user表,5個字段,其中id是主鍵,id_no是身份證號,加了惟一索引,name是用戶姓名,能夠重複的,加了普通索引,手機號、年齡、地址都沒有索引。

 

1. 普通select

select  * from user where id =1;

begin;
select  * from user where id =1;
commit:

普通的select 語句是不加鎖的。select包裹在事務中,一樣也是不加鎖的。where後面的條件無論多少,普通的select是不加鎖的。

2. 顯式加鎖

select  * from user where id =1 lock in share mode;

select  * from user where id =1 for update;

顯式指出要加什麼樣的鎖。上面一個加的是共享鎖,下面的是互斥鎖。

這裏須要強調的一點,須要明確在事務中是用這些鎖,不在事務中是沒有意義的。

 

3. 隱式加鎖

update user set address '北京' where id=1;
delete from user where id=1;

update和delete也會對查詢出的記錄加X鎖,隱式加互斥鎖。加鎖類型和for update 相似

後面只按照顯式加鎖的select for update 舉例子,更新和刪除的加鎖方式是同樣的。

4. 按索引類型

elect  * from user where id =1 for update;

select  * from user where id_no ='a22' for update;

select  * from user where name ='王二' for update;

select  * from user where address ='杭州' for update;

四條SQL,區別在於where條件的過濾列,分別是主鍵、惟一索引、普通索引、無索引。

主鍵:以前提到過索引組織表,這裏會在彙集索引上對查詢出的記錄,加X鎖

惟一索引:會在輔助索引上,把在對應的id_no=a22的索引加X鎖,由於是惟一的,因此不是next-key鎖。而後在主鍵上,也會在這條記錄上加X鎖。

普通索引:由於不是惟一的,會在輔助索引上,把對應的id_no=a22的索引加next-key鎖。而後在主鍵加X鎖。

無索引:首先,是不推薦這種寫法,沒有索引的話,由於會全表掃描,數據量大的話查詢會很慢。這裏討論的是,這種狀況下,會加什麼鎖? 答案: 首先,聚簇索引上的全部記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。在這種狀況下,這個表上,除了不加鎖的快照度,其餘任何加鎖的併發SQL,均不能執行,不能更新,不能刪除,不能插入,全表被鎖死。這是一個很恐怖的事情,請注意。

5. 記錄不存在的狀況

前面幾個例子中,都是能夠查到結果的。若是對應記錄不存在會怎樣?答案是鎖住間隙,不容許插入。mysql要保證沒有其餘人能夠插入,因此鎖住間隙。

6. 普通 insert 語句

在插入以前,會先在插入記錄所在的間隙加上一個插入意向鎖。

insert會對插入成功的行加上排它鎖,這個排它鎖是個記錄鎖,而非next-key鎖(固然更不是gap鎖了),不會阻止其餘併發的事務往這條記錄以前插入 。

7. 先查詢後插入

相似於這樣的insert

insert into target_table select * from source_table ...
create target_table select * from source_table ...

將select查詢的結果集,插入到另外一張表中,或者使用結果集,建立一個新表。

和以前簡單插入的狀況相似,已插入成功的數據加X鎖,間隙加上一個插入意向鎖。

對於select的源表中的記錄,會加共享的 next-key 鎖。這是爲了防止主從同步出問題。

舉個例子:

session1 先開啓事務,而後查詢user2表的結果集,插入到user表中,session2開啓事務,在插入user2中插入數據,所插入的數據恰好是session1能查詢到的數據,若是不加鎖的話,session2能夠插入成功,而後session2提交事務,接着session1提交數據。這樣看起來是沒問題的,可是session2先提交的,因此bin log中會這樣記錄,先在user2表中插入數據,而後在user中插入數據,這樣的bin log在從庫執行的時候,就會出問題。

主庫: user2插入一條數據,user 插入一條數據

從庫: user2插入一條數據,user 插入兩條數據

user表會比主庫多一條數據。因此須要鎖住select查詢表中加next-key鎖,不容許user2表中新增數據。

 

4、分析當前鎖的狀況

先說一下死鎖的定義,死鎖是指兩個或兩個以上的事務在執行過程當中,因爭奪資源而形成的一種互相等待的現象。這個定義適用於數據庫,有幾個重點,兩個或兩個以上的事務,一個事務是不會出現死鎖的。爭奪的資源通常都是表或者記錄。

出現死鎖了會怎樣,正常狀況下,mysql會檢查出死鎖,並回滾某一個事務,讓另外一個事務正常運行。

Mysql 會回滾反作用小的事務,斷定的標準是執行的時間以及影響的範圍。

1.如何知道系統有沒有發生過死鎖,如何去查看發生過的鎖

show status like ‘innodb_row_lock%'; 從系統啓動到如今的數據

Innodb_row_lock_current_waits:當前正在等待鎖的數量;

Innodb_row_lock_time :鎖定的總時間長度,單位ms;

Innodb_row_lock_time_avg :每次等待所花平均時間;

Innodb_row_lock_time_max:從系統啓動到如今等待最長的一次所花的時間;

Innodb_row_lock_waits :從系統啓動到如今總共等待的次數。

平均時間和鎖等待次數比較大的話,說明可能會存在鎖爭用狀況

2. show engine innodb status

展現innodb存儲引擎的運行狀態

經過這個命令顯示的內容比較多,其中有一項lasted detected deadlock 顯示最近發生的死鎖。

圖中紅色線條標註的是執行的SQL,以及加了什麼鎖,能夠看出是在這行記錄上加了X鎖,沒有gap鎖。

3. 錯誤日誌中查看歷史發生過的死鎖

set global innodb_print_all_deadlocks=1;

上一個命令,只能看到最近發生的鎖,若是我想看歷史發生的鎖怎麼辦? 執行這一句,更改innodb 的一個配置,innodb_print_all_deadlocks,打印全部的死鎖。會將死鎖的信息輸出到mysql的錯誤日誌中,默認是不輸出,格式和show engine innodb status 是差很少的。

4. information_schema.innodb_locks

information_schema 數據庫是mysql自帶的,保存着關於MySQL服務器所維護的全部其餘數據庫的信息。其中innodb_locks表,記錄了事務請求可是還沒得到的鎖,即等待得到的鎖。

lock_id:鎖的id,由鎖住的空間id編號、頁編號、行編號組成

lock_trx_id:鎖的事務id。

lock_mode:鎖的模式。S[,GAP], X[,GAP], IS[,GAP], IX[,GAP]

lock_type:鎖的類型,表鎖仍是行鎖

lock_table:要加鎖的表。

lock_index:鎖住的索引。

lock_space:innodb存儲引擎表空間的id號碼

lock_page:被鎖住的頁的數量,若是是表鎖,則爲null值。

lock_rec:被鎖住的行的數量,若是表鎖,則爲null值。

lock_data:被鎖住的行的主鍵值,若是表鎖,則爲null值。

5. information_schema.innodb_lock_waits

查看等待中的鎖

requesting_trx_id:申請鎖資源的事務id。

requested_lock_id:申請的鎖的id。

blocking_trx_id:阻塞的事務id,當前擁有鎖的事務ID。

blocking_lock_id:阻塞的鎖的id,當前擁有鎖的鎖ID

6. information_schema.innodb_trx

查看已開啓的事務

trx_id:innodb存儲引擎內部事務惟一的事務id。

trx_state:當前事務的狀態。

trx_started:事務開始的時間。

trx_requested_lock_id:等待事務的鎖id,如trx_state的狀態爲LOCK WAIT,那麼該值表明當前事務以前佔用鎖資源的id,若是trx_state不是LOCK WAIT的話,這個值爲null。

trx_wait_started:事務等待開始的時間。

trx_weight:事務的權重,反映了一個事務修改和鎖住的行數。在innodb的存儲引擎中,當發生死鎖須要回滾時,innodb存儲引擎會選擇該值最小的事務進行回滾。

trx_mysql_thread_id:正在運行的mysql中的線程id,show full processlist顯示的記錄中的thread_id。

trx_query:事務運行的sql語句

5、預防死鎖

1. 以相同的順序更新不一樣的表

這樣執行的話,會出現鎖等待,但不容易出現死鎖。

假設有這麼兩個接口,增長老師和學生的幸運值、減小老師和學生的幸運值,這個需求是我造出來的,先別管需求是否是合理。

如今有兩個請求,一個增長幸運值,一個下降幸運值,若是更新順序不一樣的話,就是這樣,第一個事務先給老師加幸運值,第二個接口給學生減幸運值,而後第一個事務給學生加幸運值,由於鎖已經被第二個事務持有了,因此第一個事務等待。而後第二個事務給老師幸運值,這時就互相等待鎖,出現了死鎖。

2. 預先對數據進行排序

好比一個接口批量操做數據,若是亂序的話,併發的狀況下,也是有可能出現死鎖的。給學生批量加分的接口,按照表格中的執行順序的話,第一個事務,持有A的鎖,請求B的鎖,第二個事務持有B的鎖,請求A的鎖,出現死鎖。

3. 直接申請足夠級別的鎖,而非先共享鎖,再申請排他鎖。

好比這種狀況,兩個事務,先申請共享鎖,共享鎖是兼容的,而後申請互斥鎖的時候,須要互相等待,就出現了死鎖。

 

4. 事務的粒度及時間儘可能保持小,這樣鎖衝突的機率就小了,也就不容易出現死鎖。不建議在數據庫的事務中執行API調用。

5. 正確加索引。沒有索引會引發全表掃描,相似於鎖表。

 

六:總結:

1,正確的加索引,儘可能先查詢,而後使用主鍵去加鎖,等於操做來加鎖,而儘可能避免輔助索引,或者不是範圍比較來加鎖。

2,出現了鎖的問題,根據數據庫已有的信息,分析死鎖。

3,舉了幾個例子,可能不少都是上線以後才發現的,最好能在開發階段就避免死鎖。

相關文章
相關標籤/搜索