mysql中insert...select引发的死锁

本文解析了MySQL Innodb引擎在高并发情况下因自增列产生的死锁现象,通过分析InnoDB源代码揭示了死锁判定规则,并针对不同autoinc_lock_mode设置下INSERT...SELECT语句对死锁的影响进行了探讨。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考文章:

http://blog.chinaunix.net/uid-9950859-id-181376.html

http://blog.163.com/itjin45@126/blog/static/105107513201442102534166/

mysql5.6用户手册

http://dev.mysql.com/doc/refman/5.6/en/innodb-auto-increment-handling.html

 

作为mysql的新手,被最近在项目中碰到的死锁问题吓了一跳,赶紧记下来备忘.

使用show engine innodb status命令查看到的死锁相关信息如下:

(在navicat中执行该命令时会看到status列的值为空,此时别忘了右键全选-复制)

------------------------
LATEST DETECTED DEADLOCK
------------------------
2015-11-30 09:16:01 7f1e2c554700TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION

*** TRANSACTION:
TRANSACTION 649212, ACTIVE 0 sec setting auto-inc lock
mysql tables in use 2, locked 2
4 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 138630, OS thread handle 0x7f1e2c554700, query id 7715687 10.132.43.55 iapproxy Sending data
INSERT INTO tsp_agt_msg_send (
     c1,

     c2,

     c3,

       ... 

SELECT 

          'c1',

          'c2', 
          'c3', 
          ...
*** WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `diap_proxy`.`tsp_agt_msg_send` trx id 649212 lock mode AUTO-INC waiting
*** WE ROLL BACK TRANSACTION (2)
------------
TRANSACTIONS
------------
Trx id counter 695842
Purge done for trx's n:o < 695818 undo n:o < 0 state: running but idle
History list length 3160
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 695841, not started
MySQL thread id 138912, OS thread handle 0x7f1e1f5d7700, query id 7846296 192.168.180.54 root cleaning up
---TRANSACTION 0, not started
MySQL thread id 138901, OS thread handle 0x7f1e17a62700, query id 7844770 10.132.137.206 root cleaning up
---TRANSACTION 0, not started
MySQL thread id 138875, OS thread handle 0x7f1e1feba700, query id 7846415 10.132.137.206 root init
show engine innodb status
---TRANSACTION 695828, not started
MySQL thread id 138874, OS thread handle 0x7f1e1dd77700, query id 7846253 192.168.180.54 root cleaning up
---TRANSACTION 692142, not started
MySQL thread id 138843, OS thread handle 0x7f1e1ee39700, query id 7846251 192.168.180.54 root cleaning up
---TRANSACTION 673389, not started

 

首先来看下这一行

2015-11-30 09:16:01 7f1e2c554700TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION

这里涉及到一个死锁判定的规则:

在innodb源代码lock/lock0lock.c文件中,定义了两个常量:

/* Restricts the length of search we will do in the waits-for

     graph of transactions */

#define LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK 1000000

 

/* Restricts the recursion depth of the search we will do in the waits-for

    graph of transactions */

#define LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK 200

然后在检查是否产生死锁的函数lock_deadlock_occurs()中有如下代码:

 

ret = lock_deadlock_recursive(trx, trx, lock, &cost, 0);

switch (ret) {

case LOCK_EXCEED_MAX_DEPTH:

        产生死锁
        ...

        break;

}

其中的lock_deadlock_recursive()函数是递归函数,它会检查自身递归深度,其中有如下代码:

 

ibool   too_far

   = depth > LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK

|| *cost > LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK;

...

if (too_far) {

                return(LOCK_EXCEED_MAX_DEPTH);

            }

因此innodb在检查是否产生死锁时调用lock_deadlock_occurs()检查,这个函数再会调用lock_deadlock_recursive()递归检查锁的数目(不知道这么说是否确切?),当递归的深度depth大于了一开始介绍的常量LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK,或者cost(不清楚这个代表什么)大于一开始介绍的常量LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK时,就认为发生了死锁.

如上所述这个死锁并非数据库真正发生了死锁,mysql主观的认为发生了死锁.

下面继续分析死锁日志,注意如下两行,

*** WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `diap_proxy`.`tsp_agt_msg_send` trx id 649212 lock mode AUTO-INC waiting
从5.6的用户手册中查找到AUTO-INC的相关信息:

 InnoDB uses a special lock called the table-level AUTO-INC lock for inserts into tables with AUTO_INCREMENT columns. This lock is normally held to the end of the statement (not to the end of the transaction), to ensure that auto-increment numbers are assigned in a predictable and repeatable order for a given sequence of INSERT statements

InnoDB在为自增列产生值的时候,使用一种叫做AUTO_INC的表级锁来做控制.这种锁是作用于语句的而不是事务(即语句执行完了锁就会被释放).使用这种锁是为了确保自增列的值的可预见性和可重复性.可预见性是说当一条insert语句作用于多行时,这些行的自增列基于第一行来说是可预见的;可重复执行是指基于语句的复制在slave重放时自增列的值与master的一致.

mysql提供参数innodb_autoinc_lock_mode来控制在产生自增列时锁的行为,可取值为0,1,2默认为1

0:对于每一个insert操作,都加AUTO_INC锁来为自增列分配值.

1:对于简单的insert操作,不加AUTO_INC锁,而使用一个轻量级的mutex,分配完毕后立即释放,不需要等到语句结束.

对于批量的insert操作,加AUTO_INC锁.当简单的insert操作检测到其他的事务持有AUTO_INC锁时,也需要等待AUTO_INC锁.

2.从不加AUTO_INC锁,并发性最好,但基于语句的复制和恢复将有问题.

 

insert操作有INSERT, INSERT ... SELECT, REPLACE, REPLACE ... SELECT, and LOAD DATA.(凡是能产生新的一行的语句都是)

简单的insert语句:

在语句正式执行前,所作用的行数是确定的.比如insert子句,不包含子查询的replace子句.

 

批量插入的sql语句:
包括INSERT ... SELECT, REPLACE ... SELECT和LOAD DATA

混合型的sql语句:

INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');即包含指定的又包含需要mysql帮忙自增的.


从以上分析可以得出,在innodb_autoinc_lock_mode=1的情况下insert...select语句会导致自增列的赋值需要加AUTO_INC锁.当在并发作insert的情况下会导致上述死锁.

总结: 在高并发的情况下对有自增列的表做插入操作应避免使用批量insert语句.
 


 

 

<think>嗯,用户想了解MySQL 8.0.25中使用INSERT INTO SELECT语句时涉及的next-key行为以及解决方法。我需要先回忆一下MySQL机制,尤其是InnoDB的next-key。 首先,next-key是InnoDB在可重复读隔离级别下用来防止幻读的一种机制。它结合了记录和间隙定一个范围。当执行INSERT INTO SELECT时,如果源表有数据被扫描,可能会对扫描到的范围加next-key,特别是当使用唯一索引或非唯一索引的时候。 然后,用户提到的版本是8.0.25,需要确认这个版本是否有相关的变化。根据引用[2],MySQL 8.0.22开始,DML操作读取grant表时不加读,但这里讨论的是普通的表操作。可能需要查看MySQL官方文档关于的部分。 接下来,INSERT INTO SELECT在复制数据时,如果目标表有触发器或外键约束,或者源表有索引扫描,可能会导致更长时间的定,从而引发性能问题甚至死锁。比如,当从大表中SELECT数据插入到另一个表时,如果源表的索引范围被定,其他事务可能被阻塞。 解决方法可能包括调整事务隔离级别,比如使用读已提交来减少间隙的使用,或者优化索引,减少扫描范围。另外,分批处理数据可能有助于减少的持有时间。引用[3]提到MySQL 8.0提高了可靠性,可能在管理方面有所优化,但具体到8.0.25需要确认。 用户可能遇到的情况是,在执行INSERT INTO SELECT时,源表被长时间定,影响其他事务。需要解释这种情况下next-key的行为,并提供具体的解决方案,比如使用主键顺序扫描、降低隔离级别、拆分事务等。同时,结合用户提供的引用,可能引用相关版本文档来支持解答。</think>### MySQL 8.0.25中`INSERT INTO SELECT`的Next-Key行为分析 #### 1. Next-Key的基本机制 在InnoDB存储引擎中,`INSERT INTO SELECT`语句在**可重复读(REPEATABLE READ)**隔离级别下,若涉及索引范围扫描,可能触发Next-Key。Next-Key是记录(Record Lock)和间隙(Gap Lock)的组合,用于防止幻读(Phantom Read)[^2]。 例如: ```sql INSERT INTO target_table SELECT * FROM source_table WHERE indexed_column BETWEEN 100 AND 200; ``` 如果`source_table`的`indexed_column`是非唯一索引,InnoDB会对该范围内所有记录及间隙加,阻止其他事务插入符合此范围的新数据。 #### 2. 具体行为表现 - **主键/唯一索引扫描**:若`SELECT`条件使用主键或唯一索引精确匹配(如`WHERE id = 5`),仅加记录- **非唯一索引扫描**:若使用非唯一索引或全表扫描,会对扫描范围加Next-Key- **无索引扫描**:全表扫描时,可能对表中所有间隙加,导致严重竞争[^3]。 #### 3. 典型问题场景 ```sql -- 事务1(长时间运行) INSERT INTO orders_archive SELECT * FROM orders WHERE create_time >= '2023-01-01'; -- 事务2(被阻塞) INSERT INTO orders (create_time) VALUES ('2023-01-02'); ``` 若`orders.create_time`有非唯一索引,事务1会定`2023-01-01`之后的间隙,导致事务2被阻塞。 #### 4. 优化解决方案 ##### 方法1:调整隔离级别 将事务隔离级别改为**读已提交(READ COMMITTED)**,该级别下InnoDB不使用间隙: ```sql SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; INSERT INTO target_table SELECT ...; COMMIT; ``` 注意:需评估幻读风险是否可接受。 ##### 方法2:索引优化 为`SELECT`条件添加高效索引,减少定范围: ```sql ALTER TABLE source_table ADD INDEX idx_column(column); ``` ##### 方法3:分批处理 使用主键范围分批操作,减少单次持有时间: ```sql INSERT INTO target_table SELECT * FROM source_table WHERE id BETWEEN 1 AND 1000; INSERT INTO target_table SELECT * FROM source_table WHERE id BETWEEN 1001 AND 2000; ``` ##### 方法4:使用`LIMIT`与循环 通过应用层控制分批逻辑: ```python last_id = 0 batch_size = 1000 while True: cursor.execute(f""" INSERT INTO target_table SELECT * FROM source_table WHERE id > {last_id} ORDER BY id LIMIT {batch_size} """) last_id = get_last_insert_id() if affected_rows < batch_size: break ``` #### 5. 监控状态 通过以下命令实时监控行为: ```sql -- 查看当前信息 SELECT * FROM performance_schema.data_locks; -- 查看阻塞关系 SELECT * FROM sys.innodb_lock_waits; ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值