问题解析
insert语句的锁为什么这么多?
MySQL对自增主键锁做了优化,尽量在申请到自增id以后,就释放自增锁。
因此,大部分insert语句是一个很轻量的操作。
不过,也有些insert语句在执行过程中需要给其他资源加锁,或者无法在申请到自增id以后就立马释放自增锁。
如,insert … select 语句在可重复读隔离级别下,binlog_format=statement时执行SQL语句时,需要对表所有行和间隙加锁。
保证并发insert时日志和数据的一致性。
如果session B先执行,由于这个语句对表t主键索引加了(-∞,1]这个next-key lock,会在语句执行完成后,才允许session A的insert语句执行。
但如果没有锁的话,就可能出现session B的insert语句先执行,但是后写入binlog的情况。
1 | CREATE TABLE t̀ ̀( ìd ̀int(11) NOT NULL AUTO_INCREMENT, `c ̀int(11) DEFAULT NULL, `d ̀int(11) DEFAULT NULL, PRIMARY KEY (̀ id )̀, UNIQUE KEY `c ̀(̀ c )̀) ENGINE=InnoDB;insert into t values(null, 1,1);insert into t values(null, 2,2);insert into t values(null, 3,3);insert into t values(null, 4,4);create table t2 like tinsert into t2(c,d) select c,d from t;insert into t values(-1,-1,-1);insert into t2(c,d) select c,d from t; |
create temporary table temp_t(c int,d int) engine=memory;insert into temp_t (select c+1, d from t force index(c) order by c desc limit 1);insert into t select * from temp_t;drop table temp_t;
唯一键冲突加锁这个例子也是在可重复读(repeatable read)隔离级别下执行的。
可以看到,session B要执行的insert语句进入了锁等待状态。
也就是说,session A执行的insert语句,发生唯一键冲突的时候,并不只是简单地报错返回,还在冲突的索引上加了锁。
我们前面说过,一个next-key lock就是由它右边界的值定义的。
这时候,session A持有索引c上的(5,10]共享next-key lock(读锁)。
至于为什么要加这个读锁,其实我也没有找到合理的解释。
从作用上来看,这样做可以避免这一行被别的事务删掉。
这里官方文档有一个描述错误,认为如果冲突的是主键索引,就加记录锁,唯一索引才加next-key lock。
但实际上,这两类索引冲突加的都是next-key lock。
唯一键冲突--死锁在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回。
这个死锁产生的逻辑是这样的:
1. 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。
注意,这个索引是唯一索引,因此退化为记录锁。
2. 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样 ,session C也在索引c上,c=5这一个记录上,加了读锁。
3. T3时刻,session A回滚。
这时候,session B和session C都试图继续执行插入操作,都要加上写锁。
两个session都要等待对方的行锁,所以就出现了死锁。
这个流程的状态变化图如下所示。
图8 状态变化图--死锁insert into … on duplicate key update语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。
insert into t values(11,10,10) on duplicate key update d=100; 如果有多个列违反了唯一性约束,就会按照索引的顺序,修改跟第一个索引冲突的行。
可以看到,主键id是先判断的,MySQL认为这个语句跟id=2这一行冲突,所以修改的是id=2的行。
需要注意的是,执行这条语句的affected rows返回的是2,很容易造成误解。
实际上,真正更新的只有一行,只是在代码实现上,insert和update都认为自己成功了,update计数加了1, insert 计数也加了1。
今天这篇文章,我和你介绍了几种特殊情况下的insert语句。
insert … select 是很常见的在两个表之间拷贝数据的方法。
在可重复读隔离级别下,这个语句会给select的表里扫描到的记录和间隙加读锁。
而如果insert和select的对象是同一个表,则有可能会造成循环写入。
这种情况下,我们需要引入用户临时表来做优化。
insert 语句如果出现唯一键冲突,会在冲突的唯一值上加共享的next-key lock(S锁)。
因此,碰到由于唯一键约束导致报错后,要尽快提交或回滚事务,避免加锁时间过长。