一般来说,insert语句是一个很轻量级的操作,不过,这个结论对于“普通的insert语句”才有效。也就是说,对于那些“特殊情况”的insert,在执行过程中需要给其他资源加锁,或者无法再申请到自增id后就立马释放自增锁。
insert ... select 语句
在可重复读隔离级别下,binlog_format=statement时执行:
insert into t2(c,d) select c,d from t;
这个语句,需要对表t的所有行和间隙加锁。
否则可能会出现准备不一致的情况。
当然,执行insert ... select的时候,对目标表也不是锁全表,而是只锁住需要访问的资源。
insert循环写入
explain insert into t2(c,d) (select c+1,d from t force index(c) order by c desc limit 1);
这个语句比较简单
insert into t(c,d) (select c+1, d from t force index(c) order by c desc limit 1);
这个语句就不同了(在自己的表的操作):
使用了临时表。需要把表t的所有记录都读出来,写入临时表。rows=1是因为受到了limit 1的影响。
语句执行前后,Innodb_rows_read的值增加了4,因为默认临时表是使用Memory引擎的,所以这4行查的都是表t(从表t查出所有记录然后写入临时表),也就是说对对表t做了全表扫描。
执行过程:
- 创建临时表,表里有两个字段c和d;
- 按照索引c扫描表t,依次取出c=4、3、2、1,然后回表,读到c和d的值写入临时表。这时,Rows_examined=4。
- 由于语义里面有limit 1,所以只取了临时表的第一行,再插入到表t中。这时,Rows_exmined的值加1,变成了5。
就是说,这个语句会导致在表t上做全表扫描,并且会给索引c上的所有间隙都加上共享的next-key lock。所以,这个语句执行期间,其他事务不能在这个表上插入数据。
为什么要用临时表,是因为这类一遍遍历数据,一遍更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与逻辑,就跟语义不符了。
可以通过下面的方法优化:
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;
insert唯一键冲突
可重复读隔离级别下,session B要执行的insert语句进入了所等待状态。
session A执行insert语句,发生唯一键冲突的时候,并不只是简单的报错返回,还在冲突的索引c上加了(5,10]共享next-key lock。
这个锁加的没有合理的解释。。。
经典死锁的场景:
session A执行rockback语句回滚的时候,session C几乎同时发现死锁并返回。
死锁逻辑是这样产生的:
- 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁。
- 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样,session C也在索引c上,c=5这一个记录上,加了读锁。
- T3时刻,session A回滚。这时候,session B和session C都视图继续插入操作,都要加上写锁。两个session都要等待对方的行锁,所以就出现了死锁。
insert into ... on duplicate key update
前面的语句改成这样:
insert into t values(11,10,10) on duplicate key update d=100;
这样会给索引c上(5,10]加一个排他的next-key lock(写锁)。
这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。