Mysql死锁“SELECT ... FOR UPDATE”并插入

时间:2015-07-30 00:28:43

标签: mysql multithreading locking deadlock isolation-level

我在下面运行这段代码时遇到了死锁。

代码的目的是在Title表中插入一个新的Title,最终结果是如果没有其他title已经设置了defaultTitle位,我需要设置defaultTitle位。

标题表是产品表的外键(因此在ProductId上有一个非唯一索引)。 Title表如下所示:

CREATE TABLE `Title` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `ProductId` int(11) DEFAULT NULL,
  `Title` varchar(100) NOT NULL,
  `DefaultBit` bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (`ID`),
  KEY `fk_product_Title` (`ProductId`),
  CONSTRAINT `title_fk_1` FOREIGN KEY (`ProductId`) REFERENCES `product` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

在此示例事务中,我想为同一产品的Title表添加2个新标题。如果我同时在2个不同的会话中运行此代码,它总是死锁。

我想要的结果是没有死锁,第一个事务将DefaultBit设置为1,第二个事件将DefaultBit设置为0。

START TRANSACTION;

SET @prodId = 4;

SET @insTitleDefault = (SELECT  IF(COUNT(PT.ID) > 0, 0, 1) as defaultOrNot
FROM    ProductTitle PT
WHERE   PT.ProductId = @prodId);

SELECT SLEEP(2); - Added for testing to pinpoint the deadlock.

INSERT INTO Title
(`ProductId`,`Title`,`DefaultBit`)
VALUES
(@prodId ,"Some title text",@insTitleDefault);

SET @newTitleId = LAST_INSERT_ID();

SELECT * FROM Title WHERE ProductId = @prodId;

-- COMMIT; -- commenting out the commit for testing purposes, Rollback instead
ROLLBACK;

我尝试添加FOR UPDATE

SET @insTitleDefault = (SELECT  IF(COUNT(PT.ID) > 0, 0, 1) as defaultOrNot
FROM    ProductTitle PT
WHERE   PT.ProductId = @prodId FOR UPDATE);

但这也不起作用,仍然是死锁。

我还尝试按照Deadlock using SELECT ... FOR UPDATE in MySQL

中的答案,以不同的隔离模式运行整个事务
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

这不会导致死锁,但它也不会保留正确的DefaultBit,因为它将两个事务值都设置为1(每个产品应该只有1个默认值)。

到目前为止我的分析:

我已经运行了show ENGINE INNODB状态;每次死锁后都会获得以下相关信息:

LATEST DETECTED DEADLOCK
------------------------
2015-07-30 10:05:18 3808
*** (1) TRANSACTION:
TRANSACTION 227273, ACTIVE 6 sec inserting
mysql tables in use 2, locked 2
LOCK WAIT 6 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 93, OS thread handle 0x433c, query id 3098292 localhost 127.0.0.1 user update
INSERT INTO Title(`ProductId`,`Title`,`DefaultBit`)
VALUES(@pId,"Some title text",@insTitleDefault)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 380 page no 132 n bits 752 index `fk_product_Title` of table `globalhq`.`Title` trx id 227273 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 227274, ACTIVE 4 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 2, locked 2
6 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 95, OS thread handle 0x3808, query id 3098300 localhost 127.0.0.1 user update
INSERT INTO Title(`ProductId`,`Title`,`DefaultBit`)
VALUES(@pId,"Some title text",@insTitleDefault)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 380 page no 132 n bits 752 index `fk_product_Title` of table `globalhq`.`Title` trx id 227274 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:
RECORD LOCKS space id 380 page no 132 n bits 752 index `fk_product_Title` of table `globalhq`.`Title` trx id 227274 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;;

*** WE ROLL BACK TRANSACTION (2)

我收集这就是正在发生的事情

  1. TX1正在对select语句
  2. 进行S锁定
  3. TX2也在选择语句
  4. 上设置了S锁
  5. TX1将IX锁定置于Insert语句,但是从第2点
  6. 被阻止
  7. TX2将IX锁定置于Insert语句中,但是从第2点和第3点被阻止,因此TX 2上出现死锁。
  8. 当我在select语句中添加FOR UPDATE时:

    1. TX1在选择语句
    2. 上放置IX锁定
    3. TX2还在选择语句
    4. 上放置了IX锁定
    5. TX1将X锁定置于Insert语句但从第2点
    6. 被阻止
    7. TX2将X锁定置于Insert语句中,但是从第2点和第3点被阻止,因此TX 2上出现死锁。
    8. 我不确定正确的前进方向,因为我尝试过的每个方法都会使程序处于打开状态。任何建议都很棒。

1 个答案:

答案 0 :(得分:0)

我建议你在" PT.ProductId"上添加索引。专栏。
我有同样的问题,如上所述解决 原则是当索引存在时,选择更新'将锁定索引,其他'选择更新'或者'插入'在第一个交易提交之前,其他交易将被阻止。所以僵局不会发生。