许多语言和工具都通过锁,来保证并发场景下数据和逻辑的正确性,MySQL 也不例外。除了行锁、表锁这种范围粒度外,还有这种针对读和写的 S锁共享锁 和 X锁独占锁。 随着锁定范围的不同,锁与锁之间的互相影响也差异很大,这一点很好理解。比如一个操作加了表锁之后,另一个想加行锁就得等待;而一个行锁一般并不会影响锁另一行的行锁。 除了书本上和八股文,你有没有遇到过这些锁相关的问题呢? 我先来说一个最近遇到的。 某天,项目出现几条监控报警,都是在写库的时候获取锁超时导致。业务会在某种特定的场景下,出现如下的 MySQL 获取锁超时,事务回滚的异常。 org.springframework.dao.CannotAcquireLockException: ### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: xxx 看了下错误对应的日志,发现当时 MySQL 要执行一条 INSERT 操作,网站模板等了50秒超时事务回滚了。同样的代码时好时坏,那就一定和触发条件有关系了。 对应正在执行的是一个 INSERT 的操作 按照前面的固有思路,即将执行 INSERT 的一行数据,理论上和别人没什么的冲突,为啥会拿不到锁呢? 在代码逻辑里也不能明确定位问题,只能求助 DBA 帮忙 dump 事务日志相关信息。 但内容里也没有死锁信息,事务日志里也仅有 Transaction 在等待锁的信息,像这个样子,大概看了一眼,不像死锁日志里有一个 Hold Locks 信息,这种普通的情况,具体锁在谁手里,还是两眼一抹眼。 ------------------ ---TRANSACTION 13934594, ACTIVE 41 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s), undo log entries 1 MySQL thread id 6695850, OS thread handle 0x7ef74b2c0700, query id 123 xxx abc update INSERT 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 waiting Record 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 * ;; 没有其它办法,只好回过来仔细看事务日志。仔细看这里的 WAITING xx for this lock 下面,会提到这个等待锁的类型: 期中提到了 是 GAP 锁,云南idc服务商那这个间隙有多大? 再向后看,提到了索引。是因为表里的这个索引,而后面的 Recod Lock 刚好就是这个索引对应的各个字段,那对应到索引的定义,发现里面有一个字段刚好是某个业务属性的 id。 之前对事务日志不熟悉,这算一个比较重要的发现,根据这个 id 继续去查库时,发现这条记录是在前一刻刚刚写到库里。 一个刚写到库里的字段,和新的要 INSERT 的数据有什么关联呢? 此时仔细回想了一下业务逻辑,想起我们有一个异步的操作,会在数据执行之后,在某些条件下,去做更新这条记录的操作。因为这个更新操作涉及到更新多个表,还加了个事务。香港云服务器只是因为不是用户请求,不曾放在一起统一看过。 而我们前面的 INSERT 这个,也是在一个事务里,先执行 INSERT 再执行一个 UPDATE 的操作,可以这样理解: 会话 1先执行: BEGIN; 1. UPDATE xxx_table SET update_time=xxx WHERE id = 123 ; 会话2 执行: BEGIN; 2.INSERT INTO `xxx_table` (col1,col2)... 此时,我们看到两个因为锁的交叉使用,导致谁都没法完成,最终直到超时。 为什么? 那为什么一个 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;