在INSERT INTO SELECT

时间:2016-06-20 10:04:14

标签: mysql innodb deadlock

我有一个java应用程序,它在下面的sql上发生死锁异常:

insert into voucher (
    id,
    order_id,
    voucher_code
) SELECT
    #{id},
    #{orderId},
    #{voucherCode}
    FROM DUAL WHERE NOT EXISTS (SELECT id FROM voucher where order_id =  #{orderId})

order_id是唯一键。 而且我确信当sql在并发上执行时它将会死锁。 但是,我没有足够的权限来执行show engine innodb status,所以我无法获得有关死锁异常的信息。

我尝试在实验室环境中重现问题。 表测试如下:

Create Table: CREATE TABLE `test` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`info` varchar(128) NOT NULL DEFAULT '',
`order_id` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_order_id` (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8

我在两个不同的会话中执行两个sqls:

insert into test(info,order_id) select '12345',sleep(10) from dual where not exists (select info from test where info='12345');  
insert into test(info,order_id) select '12345',234 from dual where not exists (select info from test where info='12345'); 

死锁日志如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2016-06-20 17:26:54 700000a83000
*** (1) TRANSACTION:
TRANSACTION 2321, ACTIVE 9 sec inserting
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 1, OS thread handle 0x700000a3f000, query id 29 localhost root executing
insert into test(info,order_id) select '12345',234 from dual where not exists (select info from test where info='12345')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 9 page no 3 n bits 72 index `PRIMARY` of table `test`.`test` trx id 2321 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) TRANSACTION:
TRANSACTION 2320, ACTIVE 10 sec setting auto-inc lock
mysql tables in use 2, locked 2
3 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 2, OS thread handle 0x700000a83000, query id 28 localhost root User sleep
insert into test(info,order_id) select '12345',sleep(10) from dual where not exists (select info from test where info='12345')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 9 page no 3 n bits 72 index `PRIMARY` of table `test`.`test` trx id 2320 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `test`.`test` trx id 2320 lock mode AUTO-INC waiting
*** WE ROLL BACK TRANSACTION (2)

我认为子查询select info from test where info=‘12345’可以持有SLock,insert into … select想要XLock。 但我没有找到支持我的观点的官方文件。

所以我的问题如下:
1.我的复制品设计对吗? 2.是我的supposal(子查询select info from test where info=‘12345’可能持有SLock)对吗?任何官方文件都可以支持我的支持吗?

2 个答案:

答案 0 :(得分:1)

运行INSERT ... SELECT时,默认情况下,MySQL会锁定SELECT中的所有行。 如果将隔离级别更改为READ-COMMITTED,则SELECT中的行不会被锁定。在你的情况下,这应该解决死锁问题。

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

PS:我建议阅读有关隔离级别并了解它们之间的差异。

http://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html

  

INSERT INTO T SELECT ... FROM S WHERE ...设置一个独占索引   记录锁定(没有间隙锁定)插入T的每一行。如果   事务隔离级别是READ COMMITTED,或   启用innodb_locks_unsafe_for_binlog和事务   隔离级别不是SERIALIZABLE,InnoDB在S上进行搜索   一致读(无锁)。否则,InnoDB设置共享的下一个密钥   锁定来自S.的InnoDB必须在后一种情况下设置锁定:In   从备份前滚恢复,每个SQL语句必须是   以与原先完成相同的方式执行。

     

CREATE TABLE ... SELECT ...使用共享的下一个键执行SELECT   INSERT ... SELECT。

     

在构造中使用SELECT时REPLACE INTO t SELECT ... FROM   s WHERE ...或UPDATE t ... WHERE col IN(SELECT ... FROM s ...),   InnoDB在表的行上设置共享的下一键锁。

答案 1 :(得分:0)

你工作太辛苦了。我认为做同样的事情:

[.]txt

如果orderId已经在表格中,则INSERT IGNORE INTO voucher ( id, order_id, voucher_code ) VALUES ( #{id}, #{orderId}, #{voucherCode} ) 键会投诉,但UNIQUE会使其静音。

但是......你还有另外一个问题......你的查询(和我的)检查IGNORE是否有重复。也就是说,id可以有两件事,但你只检查一件。

通常,人们只会遗漏UNIQUE

id

但是,您可能希望在插入发生时获得新的INSERT IGNORE INTO voucher ( order_id, voucher_code ) VALUES ( #{orderId}, #{voucherCode} ) ?所以......

id

另一方面,如果你真的想要使用两个唯一键,请考虑这个混乱的陈述:

INSERT INTO voucher
    ( order_id, voucher_code )
    VALUES
    ( #{orderId}, #{voucherCode} )
    ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);
SELECT LAST_INSERT_ID();

那将

  1. REPLACE INTO voucher ( id, order_id, voucher_code ) VALUES ( #{id}, #{orderId}, #{voucherCode} ) 符合 唯一键(id或order_id)的任何行
  2. DELETE值。 (由于您指定了INSERTid无效。)
  3. 现在,让我们更进一步。为什么有两个独特的键?你不能完全摆脱AUTO_INCREMENT吗?然后将id设为order_id

    现在,您已经简化了架构,简化了查询,并且很可能消除了死锁。