
许多语言和工具都通过锁,来保证并发场景下数据和逻辑的正确性,MySQL 也不例外。除了行锁、表锁这种范围粒度外,还有这种针对读和写的 S锁共享锁 和 X锁独占锁。 随着锁定范围的不同,锁与锁之间的互相影响也差异很大,这一点很好理解。比如一个操作加了表锁之后,另一个想加行锁就得等待;而一个行锁一般并不会影响锁另一行的行锁。 除了书本上和八股文,你有没有遇到过这些锁相关的问题呢? 我先来说一个最近遇到的。 现象某天,项目出现几条监控报警,都是在写库的时候获取锁超时导致。业务会在某种特定的场景下,出现如下的 MySQL 获取锁超时,事务回滚的异常。 复制 org.springframework.dao.CannotAcquireLockException : ### Error updating database . Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: xxxLock wait timeout exceeded; try restarting transaction1.2.3.4.5. 看了下错误对应的日志,发现当时 MySQL 要执行一条 INSERT 操作,b2b信息网等了50秒超时事务回滚了。同样的代码时好时坏,那就一定和触发条件有关系了。 复制 对应正在执行的是一个 INSERT 的操作2022-xx-xx 15:x:x.380 [elapse:50674] [sql:INSERT INTO xxx_table ....]1.2.3. 按照前面的固有思路,即将执行 INSERT 的一行数据,理论上和别人没什么的冲突,为啥会拿不到锁呢? 在代码逻辑里也不能明确定位问题,只能求助 DBA 帮忙 dump 事务日志相关信息。 但内容里也没有死锁信息,事务日志里也仅有 Transaction 在等待锁的信息,像这个样子,大概看了一眼,不像死锁日志里有一个 Hold Locks 信息,这种普通的情况,具体锁在谁手里,还是两眼一抹眼。 复制---------------------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 0 0: 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 * ;; ------------------1.2.3.4.5.6.7.8.9.10.11.12.13.14.15. 没有其它办法,只好回过来仔细看事务日志。仔细看这里的 WAITING xx for this lock 下面,会提到这个等待锁的类型: 复制RECORD LOCKS index `xxx_id_idx` of table `test`.`xxx_table` lock_mode X locks gap before rec insert intention waiting1. 期中提到了 复制lock_mode X locks gap before rec insert intention waiting1. 是 GAP 锁,高防服务器那这个间隙有多大? 再向后看,提到了索引。是因为表里的这个索引,而后面的 Recod Lock 刚好就是这个索引对应的各个字段,那对应到索引的定义,发现里面有一个字段刚好是某个业务属性的 id。 之前对事务日志不熟悉,这算一个比较重要的发现,根据这个 id 继续去查库时,发现这条记录是在前一刻刚刚写到库里。 一个刚写到库里的字段,和新的要 INSERT 的数据有什么关联呢? 此时仔细回想了一下业务逻辑,想起我们有一个异步的操作,会在数据执行之后,在某些条件下,去做更新这条记录的操作。因为这个更新操作涉及到更新多个表,还加了个事务。WordPress模板只是因为不是用户请求,不曾放在一起统一看过。 而我们前面的 INSERT 这个,也是在一个事务里,先执行 INSERT 再执行一个 UPDATE 的操作,可以这样理解: 会话 1先执行: 复制BEGIN ; 1. UPDATE xxx_table SET update_time=xxx WHERE id = 123 ; 3.再执行一个其他操作1.2.3. 会话2 执行: 复制BEGIN ; 2.INSERT INTO `xxx_table` (col1,col2)...4.再执行一个操作1.2.3. 此时,我们看到两个因为锁的交叉使用,导致谁都没法完成,最终直到超时。 为什么? 那为什么一个 INSERT 会受到前面不相关的 UPDATE 操作的影响呢? 这就不得不提 MySQL 里的间隙锁 (GAP Lock)。业务里的 id,就是在索引 里使用到的那个,是通过某个服务生成的。而已经写入到库里的那条,id 要比我们新 INSERT 的这条,要大。GAP Lock 刚好锁定的是新写的 id 到成功写入的这条 ID。而这个写入成功的 ID,在前面正在被 UPDATE,所以两个操作就冲突了。 在线下模拟的话,可以通过 MySQL 自带的几个表,来查看锁的占用信息,可以清晰的看出来,两个操作的 lock data 是同一个数据,不冲突才怪呢。 复制mysql> SELECT * FROM `information_schema`.INNODB_LOCKS\G ; |