項目上線 線上遇到大量的deadlock 和wait timeout 但是看程序沒什么問題 問dba也不能給出很好的解決方案!最終自己去了解mysql鎖 以及看mysq鎖日志 如果了解mysql鎖的機制下分析就很好解決
mysql的幾種鎖
X鎖(排他鎖) :
與其他X鎖和S鎖互斥
S鎖(共享鎖):
與X鎖互斥 當一個事物獲得S鎖 別的事物可以繼續獲得S鎖 但是不能加X鎖 X鎖與X鎖和S鎖互斥
IX(意向排他鎖)
IX是表級的 mysql引擎自動控制 在獲得X鎖之前 會先獲得IX鎖 IX只會與表級的S,X鎖互斥. 當mysql對表級進行加鎖(X或者S)的時候不用一行一行對數據判斷是否加了X鎖 直接根據是否有IX鎖來進行判斷,提高了效率
IS(意向共享鎖)
IS是鎖是表級的 mysql引擎自動控制 在獲得S鎖之前會先獲得IS鎖 IS鎖只會與表級X鎖互斥 當mysql鎖對表級進行加X鎖是 不用一行一行對數據判斷是否加了S鎖 直接判斷是否存在表級的IS意向鎖
gap(鎖)
間隙鎖 用於在指定索引位置區間加鎖 (只會在插入是互斥)
id | age |
1 | 10 |
2 | 20 |
3 | 30 |
4 | 40 |
這個時候gap鎖就有[無窮小,10],[10,20],[20,30],[30,40][40,無窮大]
如果在RR模式下delete table where age=20 將不能插入10~20之間 20~30到之間的值
gap鎖 只會在insert的時候互斥 (可以理解為gap在非Insert獲取的都是共享鎖 在Insert時獲取的是排他鎖)
next-key
行鎖和gap鎖的組合
當前讀與快照讀
快照讀
簡單的查詢 select * from student
值得注意的是 在RC模式下 每次讀取都是新的快照 在RR模式下 當一次快照讀后 后面都會讀快照
session1 | session2 |
select * from student; | |
inser into student(name) values('小明') | |
update student set name='小明' where id=1; | |
delete student set name='小明2' where id=2; | |
commit; | |
select * from student; | |
commit; |
RC模式 session1第二次查詢 將會受到session2的操作的影響 因為RC模式每次讀取是讀取新的快照
RR模式 session1第二次查詢不會受到session2的影響 因為后面每次讀取都是讀取快照(session1 update insert delete也會更新快照)
ps:以上結果經過在mysql innodb模式 實踐
當前讀
select * student lock in share mode(共享鎖)
select * student for update(排他鎖)
insert
delete
update
快照讀是不加鎖的 當前讀 都會獲取相應的鎖
聚簇索引以及二級索引
mysql加鎖不是鎖住某一行數據而是在表的索引上面加鎖 理解聚簇索引和二級索引能夠很好的幫助我們理解鎖
什么是聚簇索引?
在innodb下 數據的存儲順序跟索引的存儲存儲順序是一樣 聚簇索引的頁節點就指向數據,每個表都會有一個聚簇索引 默認在主鍵上 如果沒有找到將會在表中的唯一非空的列上加上聚簇索引 如果沒有mysql將自動維護一個隱式的列作為聚簇索引
二級索引(非聚簇索引)
二級索引的 頁節點 存儲的是聚簇索引 當查詢一條非聚簇索引的列 會先根據索引找到聚簇索引 再根據聚簇索引查找數據
mysql RC和RR隔離級別的加鎖方式
現在有student表有以下數據
id | name |
1 | a |
2 | b |
3 | d |
4 | f |
RC模式
delete student where id=1;
如果id列是主鍵
將會在id為1的的聚簇索引上加上X鎖
如果id列為唯一索引
將會在唯一索引上加上X鎖 同時根據唯一索引找到聚簇索引 並加鎖 為什么在唯一索引上面加了X鎖還要在聚簇索引上加鎖 因為如果這個時候另外一個事物根據聚簇索引更新數據 將感知不到鎖
比如上面表 name為聚簇索引 delete student where id=1; id=1的索引將加上鎖 這個但是聚簇索引name沒有加上 update student id=2 where name=a 這個時候 name=a 獲取鎖成功能將能修改成功
上面的解釋但是會有疑惑 既然加鎖都加載聚簇索引上為什么還要多此一舉的在非聚簇索引列上加鎖(個人理解 判斷鎖互斥時 直接根據條件的索引 而不用再次根據索引找到聚簇索引)
如果id列非唯一索引
跟唯一索引加鎖機制一樣
如果id列無索引
將在走聚簇索引 全表掃描不管是否滿足條件的數據都會加上X鎖 但是RC模式有優化 不滿足條件的加鎖之后又會釋放
RR模式
如果id列是主鍵
將會在滿足條件的聚簇索引上加上X鎖
如果id列為唯一索引
在id列上加上X鎖同時在 對應的聚簇索引加上X鎖
如果id列非唯一索引
將在id列上加上next-key鎖 同時在聚簇索引加上x鎖
如果id列無索引
將在走聚簇索引 全表掃描不管是否滿足條件的數據都會加上next-key鎖 跟RC模式不同的是將不會釋放
deadlock分析
環境:RR隔離級別
線上報大量的deadlock 通過命令登錄mysql 通過此SHOW ENGINE INNODB STATUS\G將打印最近一次死鎖
定位到代碼 有這樣一個操作
1.delete from dealer_order_item where dealer_order_code='1111'
2.insert dealer_order_item(dealer_order_code,.....) values('1111',....)
dealer_order_code 沒有索引
通過上面的鎖分析 RR模式下如果當前讀沒有滿足條件的數據 整個表每一條數據都將會加上next-key鎖如下
session1 | session2 |
delete from dealer_order_item where dealer_order_code='1111' | |
delete from dealer_order_item where dealer_order_code='2222' | |
insert dealer_order_item(dealer_order_code,.....) values('1111',....) | |
結果:session1執行insert會死鎖 session2執行delete會等待
1.session1 delete全表掃描獲得所有行的gap鎖和x鎖
2.session2執行 delete全表掃描獲得gap鎖 然后鎖等待session釋放x鎖
3.session2執行 insert 嘗試獲得gap鎖 因為session2已經拿到gap鎖但是未拿到x鎖, 所以不能插入等待 因為session2也在等待session1釋放x鎖(所以死鎖)
情況2(未驗證過 生產環境遇到過2個insert死鎖情況)
如果where條件都不存在 2個delete都拿到gap鎖(鎖無窮大)而沒有行鎖 然后各自執行insert相互等待gap鎖 導致死鎖
waitlock分析
表數據id為主鍵索引 name為非唯一索引
select * from information_schema.innodb_trx;表
trx_id(事物id) | trx_state(事物狀態) | trx_started(事物開始時間) | trx_requested_lock_id | trx_wait_started(事物開始等待時間) | trx_weight | trx_mysql_thread_id(事物線程id) | trx_query(具體sql) | trx_operation_state(事物當前操作狀態) | trx_tables_in_use(事物中多少個表被使用) | trx_tables_locked(事物鎖了多少個表) | trx_lock_structs | trx_lock_memory_bytes(事物鎖住的內存大小) | trx_rows_locked(事物擁有多少個鎖) | trx_rows_modified(事物修改的行數) | trx_concurrency_tickets(事物並發票數) | trx_isolation_level(事物隔離級別) | trx_unique_checks(是否唯一檢查) | trx_foreign_key_checks(是否外鍵檢查) | trx_last_foreign_key_error(最后的外鍵錯誤) | trx_adaptive_hash_latched | trx_adaptive_hash_timeout | trx_is_read_only | trx_autocommit_non_locking |
7842656 | LOCKWAIT | 2019-01-14 10:22:04 | 7842656:25:4:4 | 2019-01-14 10:22:04 | 8 | 3733599 | update demo set name='5555' where name='3333' | updating or deleting | 1 | 1 | 5 | 1136 | 10 | 3 | 0 | REPEATABLE READ | 1 | 1 | NULL | 0 | 0 | 0 | 0 |
7842647 | RUNNING | 2019-01-14 10:21:37 | NULL | NULL | 7 | 3733596 | NULL | NULL | 0 | 1 | 4 | 1136 | 7 | 3 | 0 | REPEATABLE READ | 1 | 1 | NULL | 0 | 0 | 0 | 0 |
select * from information_schema.innodb_lock_waits;表
requesting_trx_id(請求鎖的事物id) | requested_lock_id(請求鎖的鎖id) | blocking_trx_id(當前擁有鎖的事物id) | blocking_lock_id(當前擁有鎖鎖id) |
7842656 | 7842656:25:4:4 | 7842647 | 7842647:25:4:4 |
select * from information_schema.innodb_locks;表
lock_id(鎖id) | lock_trx_id | lock_mode(鎖模式) | lock_type(鎖類型) | lock_table(被做鎖的表) | lock_index(被鎖的索引) | lock_space(被鎖的表空間號) | lock_page(被鎖的頁號) | lock_rec(被鎖的記錄號) | lock_data(被鎖的數據) |
7842656:25:4:4 | 7842656(擁有鎖的事物id) | X,GAP | RECORD | `dms`.`demo` | index_name | 25 | 4 | 4 | '6666', 2 |
7842647:25:4:4 | 7842647 | X | RECORD | `dms`.`demo` | index_name | 25 | 4 | 4 | '6666', 2 |
1. 先看 表1 事物id 7842656等待7842647釋放鎖 trx_rows_locked字段看命名像是鎖了多少數據 不過我根據數據分析應該是獲得了多少個鎖 name索引加上聚簇索引id的鎖+3個gap鎖等於10
ctrl+f 搜索:7842647 看關系更佳
2.分析表2 事物id7842656等待事物id7842647釋放7842647:25:4:4這個鎖
3.分析表3就清晰很多了 事物id7842656是請求這個鎖7842656:25:4:4
4.結論 session 2 嘗試修改name為5555會獲得索引為5555的x鎖和gap鎖 但是被seesion1獲得沒釋放 所以造成鎖等待
mysql 鎖等待分析相關表
information_schema.innodb_trx表
包含了正在InnoDB引擎中執行的所有事務的信息,包括waiting for a lock和running的事務
information_schema.innodb_lock_waits表
包含了blocked的事務的鎖等待的狀態
information_schema.innodb_locks表
主要包含了InnoDB事務鎖的具體情況,包括事務正在申請加的鎖和事務加上的鎖。
如何避免deadlock和wait lock
delete update 避免使用非索引字段為條件
RR隔離級別將會走聚簇索引 全表掃描為每一行加上next-key鎖 注:RC隔離級別會逐行加X鎖 並釋放
可以這樣理解:delete update 掃描一條就會為一條加上鎖 當沒有索引會全表掃描 RR隔離級別掃描一條就會為這條加上鎖 並不會釋放 RC隔離級別掃描一條就會為這條加上鎖 如果不滿足條件的加上鎖之后 會自動釋放
name非索引條件 用於修改條件 雖然有滿足條件數據 也會導致全表掃描並逐行加X鎖
我們為name加上索引條件再進行測試
alter table demo add index index_name(name);
可以發現修改成功並不會等待
避免批量修改刪除避免無序
1.session修改id為1的數據 session2修改id為4的數據各自拿到next-key鎖
2.session1 修改id為4的數據 等待session2釋放next-key鎖 session2修改id為1的數據等待session1釋放next-key鎖 造成死鎖
批量修改或者刪除 統一排序一下 比如這里根據id 就不會出現交叉修改依賴
避免隱式轉換
session1 name為varchar 確傳入number導致隱式轉換走聚簇索引全表掃描 導致整個表都鎖了
記錄一個小插曲
可以看到 name有索引也沒有隱式轉換 也鎖了整個表。因為這個是mysql的優化機制當掃描的數據超過全表的20%~30%時 即便有二級索引也會走掃描整個聚簇索引( 個人測試針對當前讀是這樣 select不受影響)
避免where條件全表掃描
其實總結上面 可以發現 當前讀是根據where條件 掃描一條就加一個鎖
避免操作不存在的數據
修改或者編輯 最好先判斷數據不存在
我們觀察一下鎖的情況
可以發現操作不存在數據會觸發gap對應索引排序的gap鎖 鎖無窮大 影響插入數據(應該在只會在RR模式上出現 不過在不確定數據是否存在 操作之前先判斷是否存在 是個好習慣)