本文主要涉及如下三個部分: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規範中定義了四種隔離級別:
越往下,隔離級別越高,問題越少,同時併發度也越低。隔離級別和併發度成反比的。
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操做時產生。
有兩個做用:
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,舉了幾個例子,可能不少都是上線以後才發現的,最好能在開發階段就避免死鎖。