当前位置: 首页 > 科技观察

我的INSERT还能被你的UPDATE卡住?

时间:2023-03-22 11:17:03 科技观察

很多语言和工具都使用锁来保证并发场景下数据和逻辑的正确性,MySQL也不例外。除了行锁和表锁的范围粒度外,还有用于读写的S-lock共享锁和X-lock排它锁。加锁作用域不同,锁之间的相互影响也大不相同,很好理解。例如,一个操作加表锁后,另一个操作要加行锁就得等待;而一个行锁一般不会影响锁定另一行的行锁。除了书籍和八股文,你有遇到过与这些锁有关的问题吗?让我从最近的一次相遇说起。现象某天,项目中出现了好几个监控告警,都是写库时获取锁超时导致的。在某个特定场景下,业务会出现MySQL获取锁超时、事务回滚等异常。org.springframework.dao.CannotAcquireLockException:###更新数据库时出错。原因:com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException:xxxLockwaittimeoutexceeded;tryrestartingtransaction查看错误对应的日志,发现MySQL要执行一个INSERT操作,等待了50秒,超时事务被回滚。同样的代码有好有坏,肯定和触发条件有关系。对应正在执行的操作是一个INSERT2022-xx-xx15:x:x.380[elapse:50674][sql:INSERTINTOxxx_table....]按照之前的固有思维,INSERT的一行数据快要执行了,理论上跟别人没有冲突,为什么我拿不到锁呢?问题无法明确定位到代码逻辑上,只能请DBA帮忙转储事务日志的相关信息。但是内容中并没有死锁信息,事务日志中只有Transactionwaitinglocks的信息。看起来是这样的,我也看了一眼。它不像死锁日志中的HoldLocks信息。这种常见的情况,具体锁在谁的手里,还是一眨眼的功夫。----------------------TRANSACTION13934594,ACTIVE41secinsertingmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize360,1行锁,撤消日志条目1MySQL线程ID6695850,操作系统线程句柄0x7ef74b2c0700,查询ID123xxxabcupdateINSERTINTOxxx_table(col,col1,...)------TRX已等待41秒授予此锁:记录锁空间id1057页号3724n位312索引`xxx_id_idx`表`test`.`xxx_table`trxid13934594lock_modeX锁在rec插入意图等待记录锁之前的间隙,堆号241物理记录:n_fields5;紧凑格式;信息位00:len8;十六进制800000008d8faf7d;升序};;1:长度5;十六进制....123;升序12_34;;2:长度17;十六进制111111111111113732333338;升序111111111111111272338;;3:长度1;十六进制80;升序;;4:长度8;十六进制8000000000012adf;asc*;;------------------没有它的办法,只好回来过仔细看事务日志。仔细看这里这个锁的WAITINGxx,会提到等待锁的类型:RECORDLOCKSindex`xxx_id_idx`oftable`test`.`xxx_table`lock_modeXlocksgapbeforerecinsertintentionwaitingperiod提到lock_modeXlocksrecinsertintentionwaiting之前的gap是GAP锁,那么gap有多大呢?再往前看,提到了索引。是因为表中有索引,而后面的RecodLock正好是这个索引对应的字段,对应索引的定义,发现里面有一个字段恰好是这个索引某个业务属性的id。以前对事务日志不熟悉,这是一个重要的发现。当我根据这个id继续查数据库的时候,发现这条记录是刚才写入库的。一个刚刚写入库的字段和要INSERT的新数据是什么关系?这时候仔细想了一下业务逻辑,想起我们有一个异步操作,就是在数据执行完之后,在一定条件下会更新这条记录。因为这个更新操作涉及更新多张表,所以增加了一个事务。只是因为不是用户要求,我也没有放在一起统一看。而我们之前的INSERT也是在一个事务中,先执行INSERT再执行UPDATE操作,可以这样理解:Session1先执行:BEGIN;1.更新xxx_tableSETupdate_time='xxx'WHEREid='123';3.执行另一个操作Session2Execution:BEGIN;2。INSERTINTO`xxx_table`(col1,col2)...4.执行另一个操作此时,我们看到两次交叉使用锁,导致无法完成,最后直到超时。为什么?那么为什么INSERT会受到先前不相关的UPDATE操作的影响呢?这就不得不提到MySQL中的间隙锁(GAPLock)。业务中的id就是索引中用到的id,是某个服务产生的。并且已经写入库的那个比我们新的INSERT里面的id大。GAPLock只是将新写入的id锁定到写入成功的ID上。而这个写入成功的ID之前正在UPDATE,所以这两个操作冲突了。离线模拟可以通过MySQL自带的几个表查看锁的占用信息。可以明显看出两次操作的锁数据是同一个数据,不冲突才怪。mysql>SELECT*FROM`information_schema`.INNODB_LOCKS\G;******************************1.行***************************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.行***************************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,2552rowsinset(0.04sec)mysql>select*from`information_schema`.INNODB_LOCK_WAITS\G;******************************1.行***************************requesting_trx_id:225753412//申请资源的事情IDrequested_lock_id:225753412:5845:5:253blocking_trx_id:225751488//阻塞的事情事务IDblocking_lock_id:225751488:5845:5:2531rowinset(0.04sec)mysql>select*from`information_schema`.INNODB_TRX\G;***************************1.行***************************trx_id:225751488trx_state:锁定等待trx_started:2022-0xxxtrx_requested_lock_id:225851026:5874:4:1trx_wait_started:2022-05-2xxxtrx_weight:3trx_mysql_thread_id:1875826trx_query:insertintoxxx_tablevalues(...)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:可重复读取trx_unique_checks:1trx_foreign_key_checks:1trx_last_foreign_key_error:NULLtrx_adaptive_hash_latched:0trx_0timeout_0s_read_only:0trx_autocommit_non_locking:0******************************2.行******************************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:REPEATABLEREADtrx_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:02rowsinset(0.03sec)有些时候,我们主要看上我认为INSERT正在写入新数据。理论上,除了表锁,其他的不会有冲突。毕竟还没写完,谁也更新不了。但是这些细节,比如gaplock,NextKeyLock等等,还是会影响到具体的执行。如果你也遇到类似的情况,在有权限的情况下,可以通过上面MySQL自带的'information_schema'.INNODB_三个表找到锁的冲突信息。如果没有权限,可以想办法先拿到事务日志,再进一步分析。通过SHOWENGINEINNODBSTATUS可以获取到事务日志,为一头雾水的分析增加了一点思考。