我一個(gè)INSERT 還能被你 UPDATE 給卡???
許多語(yǔ)言和工具都通過(guò)鎖,來(lái)保證并發(fā)場(chǎng)景下數(shù)據(jù)和邏輯的正確性,MySQL 也不例外。除了行鎖、表鎖這種范圍粒度外,還有這種針對(duì)讀和寫(xiě)的 S鎖共享鎖 和 X鎖獨(dú)占鎖。
隨著鎖定范圍的不同,鎖與鎖之間的互相影響也差異很大,這一點(diǎn)很好理解。比如一個(gè)操作加了表鎖之后,另一個(gè)想加行鎖就得等待;而一個(gè)行鎖一般并不會(huì)影響鎖另一行的行鎖。
除了書(shū)本上和八股文,你有沒(méi)有遇到過(guò)這些鎖相關(guān)的問(wèn)題呢?
我先來(lái)說(shuō)一個(gè)最近遇到的。
現(xiàn)象
某天,項(xiàng)目出現(xiàn)幾條監(jiān)控報(bào)警,都是在寫(xiě)庫(kù)的時(shí)候獲取鎖超時(shí)導(dǎo)致。業(yè)務(wù)會(huì)在某種特定的場(chǎng)景下,出現(xiàn)如下的 MySQL 獲取鎖超時(shí),事務(wù)回滾的異常。
org.springframework.dao.CannotAcquireLockException:### Error updating database.Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: xxxLock?wait?timeout?exceeded;?try?restarting?transaction
看了下錯(cuò)誤對(duì)應(yīng)的日志,發(fā)現(xiàn)當(dāng)時(shí) MySQL 要執(zhí)行一條 INSERT 操作,等了50秒超時(shí)事務(wù)回滾了。同樣的代碼時(shí)好時(shí)壞,那就一定和觸發(fā)條件有關(guān)系了。
對(duì)應(yīng)正在執(zhí)行的是一個(gè) INSERT 的操作2022-xx-xx?15:x:x.380?[elapse:50674]?[sql:INSERT?INTO?xxx_table?....]
按照前面的固有思路,即將執(zhí)行 INSERT 的一行數(shù)據(jù),理論上和別人沒(méi)什么的沖突,為啥會(huì)拿不到鎖呢?
在代碼邏輯里也不能明確定位問(wèn)題,只能求助 DBA 幫忙? dump 事務(wù)日志相關(guān)信息。
但內(nèi)容里也沒(méi)有死鎖信息,事務(wù)日志里也僅有 Transaction 在等待鎖的信息,像這個(gè)樣子,大概看了一眼,不像死鎖日志里有一個(gè) Hold Locks 信息,這種普通的情況,具體鎖在誰(shuí)手里,還是兩眼一抹眼。
---------------------TRANSACTION 13934594, ACTIVE 41 sec insertingmysql tables in use 1, locked 1LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s), undo log entries 1MySQL thread id 6695850, OS thread handle 0x7ef74b2c0700, query id 123 xxx abc updateINSERT?INTO?xxx_table(col,col1,...)------- TRX HAS BEEN WAITING 41 SEC FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 1057 page no 3724 n bits 312 index `xxx_id_idx` of table `test`.`xxx_table` trx id 13934594 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 241 PHYSICAL RECORD: n_fields 5; compact format; info bits 00: len 8; hex 800000008d8faf7d; asc };;?1:?len?5;?hex?....123;?asc?12_34;;2: len 17; hex 111111111111113732333338; asc 111111111111111272338;;3: len 1; hex 80; asc ;;?4:?len?8;?hex?8000000000012adf;?asc???????*?;;------------------
沒(méi)有其它辦法,只好回過(guò)來(lái)仔細(xì)看事務(wù)日志。仔細(xì)看這里的 WAITING xx FOR THIS LOCK 下面,會(huì)提到這個(gè)等待鎖的類(lèi)型
RECORD?LOCKS?index?`xxx_id_idx`?of?table?`test`.`xxx_table`?lock_mode?X?locks?gap?before?rec?insert?intention?waiting期中提到了lock_mode?X?locks?gap?before?rec?insert?intention?waiting
是 GAP ?鎖,那這個(gè)間隙有多大?
再向后看,提到了索引。是因?yàn)楸砝锏倪@個(gè)索引,而后面的 Recod Lock 剛好就是這個(gè)索引對(duì)應(yīng)的各個(gè)字段,那對(duì)應(yīng)到索引的定義,發(fā)現(xiàn)里面有一個(gè)字段剛好是某個(gè)業(yè)務(wù)屬性的 id。
之前對(duì)事務(wù)日志不熟悉,這算一個(gè)比較重要的發(fā)現(xiàn),根據(jù)這個(gè) id 繼續(xù)去查庫(kù)時(shí),發(fā)現(xiàn)這條記錄是在前一刻剛剛寫(xiě)到庫(kù)里。
一個(gè)剛寫(xiě)到庫(kù)里的字段,和新的要 INSERT 的數(shù)據(jù)有什么關(guān)聯(lián)呢?
此時(shí)仔細(xì)回想了一下業(yè)務(wù)邏輯,想起我們有一個(gè)異步的操作,會(huì)在數(shù)據(jù)執(zhí)行之后,在某些條件下,去做更新這條記錄的操作。因?yàn)檫@個(gè)更新操作涉及到更新多個(gè)表,還加了個(gè)事務(wù)。只是因?yàn)椴皇怯脩粽?qǐng)求,不曾放在一起統(tǒng)一看過(guò)。
而我們前面的 INSERT 這個(gè),也是在一個(gè)事務(wù)里,先執(zhí)行 INSERT 再執(zhí)行一個(gè) UPDATE 的操作,可以這樣理解:
會(huì)話 1先執(zhí)行:
BEGIN;
1.?UPDATE xxx_table? SET update_time='xxx'? ??WHERE id = '123' ;
3.再執(zhí)行一個(gè)其他操作
會(huì)話2 執(zhí)行:
BEGIN;
2.INSERT INTO `xxx_table` (col1,col2)...
4.再執(zhí)行一個(gè)操作
此時(shí),我們看到兩個(gè)因?yàn)殒i的交叉使用,導(dǎo)致誰(shuí)都沒(méi)法完成,最終直到超時(shí)。
為什么?
那為什么一個(gè) INSERT 會(huì)受到前面不相關(guān)的 UPDATE 操作的影響呢?
這就不得不提 MySQL 里的間隙鎖?(GAP Lock)。業(yè)務(wù)里的 id,就是在索引 里使用到的那個(gè),是通過(guò)某個(gè)服務(wù)生成的。而已經(jīng)寫(xiě)入到庫(kù)里的那條,id 要比我們新 INSERT 的這條,要大。GAP Lock 剛好鎖定的是新寫(xiě)的 id 到成功寫(xiě)入的這條 ID。而這個(gè)寫(xiě)入成功的 ID,在前面正在被 UPDATE,所以?xún)蓚€(gè)操作就沖突了。
在線下模擬的話,可以通過(guò) MySQL 自帶的幾個(gè)表,來(lái)查看鎖的占用信息,可以清晰的看出來(lái),兩個(gè)操作的 lock data 是同一個(gè)數(shù)據(jù),不沖突才怪呢。
mysql> SELECT * FROM `information_schema`.INNODB_LOCKS\G;*************************** 1. row ***************************lock_id: 225753412:5845:5:253lock_trx_id: 225753412lock_mode: X,GAPlock_type: RECORDlock_table: `db`.`xxx_table`lock_index: xxx_id_idxlock_space: 5845lock_page: 5lock_rec: 253lock_data: 3094360230, 'abc-01', '111623639', 0, 255*************************** 2. row ***************************lock_id: 225751488:5845:5:253lock_trx_id: 225751488lock_mode: Xlock_type: RECORDlock_table: `db`.`xxx_table`lock_index: xxx_id_idxlock_space: 5845lock_page: 5lock_rec: 253lock_data: 3094360230, 'abc-01', '111623639', 0, 2552 rows in set (0.04 sec)
mysql> select * from `information_schema`.INNODB_LOCK_WAITS\G;*************************** 1. row ***************************requesting_trx_id: 225753412 // 申請(qǐng)資源的事務(wù) IDrequested_lock_id: 225753412:5845:5:253blocking_trx_id: 225751488 // 阻塞的事務(wù) IDblocking_lock_id: 225751488:5845:5:2531 row in set (0.04 sec)
mysql> select * from `information_schema`.INNODB_TRX\G;*************************** 1. row ***************************trx_id: 225751488trx_state: LOCK WAITtrx_started: 2022-0xxxtrx_requested_lock_id: 225851026:5874:4:1trx_wait_started: 2022-05-2xxxtrx_weight: 3trx_mysql_thread_id: 1875826trx_query: insert into xxx_table values(...)trx_operation_state: insertingtrx_tables_in_use: 1trx_tables_locked: 1trx_lock_structs: 2trx_lock_memory_bytes: 360trx_rows_locked: 1trx_rows_modified: 1trx_concurrency_tickets: 0trx_isolation_level: REPEATABLE READtrx_unique_checks: 1trx_foreign_key_checks: 1trx_last_foreign_key_error: NULLtrx_adaptive_hash_latched: 0trx_adaptive_hash_timeout: 10000trx_is_read_only: 0trx_autocommit_non_locking: 0*************************** 2. row ***************************trx_id: 225753412trx_state: RUNNINGtrx_started: 2022-0xxxtrx_requested_lock_id: NULLtrx_wait_started: NULLtrx_weight: 4trx_mysql_thread_id: 1875454trx_query: NULLtrx_operation_state: NULLtrx_tables_in_use: 0trx_tables_locked: 0trx_lock_structs: 3trx_lock_memory_bytes: 360trx_rows_locked: 3trx_rows_modified: 1trx_concurrency_tickets: 0trx_isolation_level: REPEATABLE READtrx_unique_checks: 1trx_foreign_key_checks: 1trx_last_foreign_key_error: NULLtrx_adaptive_hash_latched: 0trx_adaptive_hash_timeout: 10000trx_is_read_only: 0trx_autocommit_non_locking: 02 rows in set (0.03 sec)
有些時(shí)候,我們主觀上認(rèn)為的一個(gè) INSERT ,在新寫(xiě)入數(shù)據(jù),理論上除了表鎖和別人沒(méi)啥沖突,畢竟還沒(méi)寫(xiě),也沒(méi)人能更新它。不過(guò)這些細(xì)節(jié)上,間隙鎖,Next Key Lock 等等,還是會(huì)影響到具體的執(zhí)行。如果你也遇到類(lèi)似的情況,有權(quán)限的時(shí)候,可以通過(guò)上面 MySQL `information_schema`.INNODB_自帶的
三個(gè)表,發(fā)現(xiàn)鎖的沖突信息。如果沒(méi)有權(quán)限,可以先想辦法拿到事務(wù)日志,再進(jìn)一步分析。
事務(wù)日志,可以通過(guò)?SHOW ENGINE INNODB STATUS 拿到,給迷茫的分析加一點(diǎn)思路。
你有遇過(guò)到什么排查問(wèn)題中的「峰回路轉(zhuǎn)」嗎?歡迎分享。
相關(guān)閱讀
免安裝,還原生產(chǎn)環(huán)境,運(yùn)行中切換版本,這不是我認(rèn)識(shí)的MySQL
