MySQL 插入引发触发器的死锁
MySQL Deadlock with an an insert that raises a trigger
我发现发生了一些罕见的死锁错误。我知道当两个查询作业相互依赖时会发生死锁,因此 MySQL 回滚其中一个。但在我的情况下 MySQL 处于自动提交模式,我正在插入一条触发触发器的新记录。所以我不明白它产生死锁情况的原因。
这是我的 table 架构:
---- 用户 Table ----
CREATE TABLE `users` (
`insta_id` bigint(20) unsigned NOT NULL,
`name` varchar(50) NOT NULL,
`password` varchar(60) NOT NULL,
`gem` int(10) unsigned DEFAULT '20',
`coin` int(10) unsigned DEFAULT '20',
PRIMARY KEY (`insta_id`),
UNIQUE KEY `insta_id` (`insta_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
---like_requests Table---
CREATE TABLE `like_requests` (
`req_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`insta_id` bigint(20) unsigned NOT NULL,
`media_id` varchar(50) NOT NULL,
`remaining_like` int(10) unsigned NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`count` int(10) unsigned NOT NULL,
PRIMARY KEY (`req_id`),
KEY `insta_id` (`insta_id`),
KEY `media_id` (`media_id`),
CONSTRAINT `like_requests_ibfk_1` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=103902 DEFAULT CHARSET=latin1
---喜欢Table---
CREATE TABLE `likes` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`insta_id` bigint(20) unsigned NOT NULL,
`media_id` varchar(50) NOT NULL,
`req_id` bigint(20) unsigned DEFAULT NULL,
`date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `req_id` (`req_id`),
KEY `insta_id` (`insta_id`),
KEY `media_id` (`media_id`),
CONSTRAINT `likes_ibfk_1` FOREIGN KEY (`req_id`) REFERENCES `like_requests`(`req_id`),
CONSTRAINT `likes_ibfk_2` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1704209 DEFAULT CHARSET=latin1
我有一个点赞触发器 table,定义如下:
CREATE TRIGGER `after_insert_likes` AFTER INSERT ON `likes`
FOR EACH ROW BEGIN
UPDATE users SET users.coin=users.coin+1
WHERE users.insta_id = NEW.insta_id LIMIT 1;
IF NEW.req_id IS NOT NULL THEN
UPDATE like_requests
SET like_requests.remaining_like = like_requests.remaining_like-1
WHERE like_requests.req_id = NEW.req_id
AND like_requests.remaining_like > 0
LIMIT 1;
END IF;
END
做一些简单的插入:
$sql = "INSERT INTO likes (insta_id,media_id,req_id) VALUES (?,?,?);";
$pdo = $this->db;
$statement = $pdo->prepare($sql);
$statement->bindValue(1,$data['id'],PDO::PARAM_INT);
$statement->bindValue(2,$data['media_id']);
$statement->bindValue(3,$data['req_id'],PDO::PARAM_INT);
try
{
$statement->execute();
return GetOkResponseWithMessage($response,"Like was submitted");
}
catch (PDOException $exc)
{
return GetErrorResponseWithMessage($response,$exc->getMessage(),500);
}
我收到以下死锁错误日志:
*** (1) TRANSACTION:
TRANSACTION 29031910, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
LOCK WAIT 7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264238, OS thread handle 0x7f6522c6eb00, query id 753506 localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id LIMIT 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031910 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0
*** (2) TRANSACTION:
TRANSACTION 29031909, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264237, OS thread handle 0x7f65209f8b00, query id 753507 localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id LIMIT 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031909 lock mode S locks rec but not gap lock hold time 0 wait time before grant 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031909 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0
*** WE ROLL BACK TRANSACTION (2)
这不应该以锁等待而不是死锁结束吗?
如何在不重新启动事务的情况下解决这个问题?
线程 2 持有用户 table 中行的共享锁。
然后线程1尝试获取同一行的独占锁,进入锁等待
但是线程 1 不会有机会超时,因为线程 2 然后试图将他的锁升级为独占...但是要做到这一点,他必须等待处于锁等待状态的线程 1,但它正在等待线程 2。
他们互相挡住了对方。
这是一个僵局。
服务器选择要终止的事务,这样它们就不会不必要地相互阻塞。
死锁检测允许一个线程以另一个线程为代价立即成功。否则他们都会被困在锁里等待,直到其中一个因等待太久而死亡。
您处于自动提交模式,当然,这并不意味着您不在事务中。使用 InnoDB 的每个查询仍然在事务中处理,但使用自动提交时,事务在查询开始执行时隐式启动,并在查询成功时隐式提交。
在likes
中,(insta_id, media_id)
是独一无二的吗?或者 (insta_id, req_id)
?或者也许所有 3???如果是这样,请将其设为 PRIMARY KEY
并删除 id all together. If you must keep
id, get rid of
UNIQUE(id), since
PRIMARY KEY(id)` 提供该功能。
同样,去掉UNIQUE(insta_id)
。
想想autocommit
和TRIGGER
结合起来就是一个由几个命令组成的交易:
BEGIN;
INSERT INTO likes... -- Includes 2 uniqueness checks, 1 FK check
UPDATE users ...
if... UPDATE like_requests ...
COMMIT;
我建议的索引更改可能会加快速度,从而减少死锁的可能性。变化可能甚至把死锁变成等待,但我怀疑。
防止死锁的最佳方法是与死锁共存,捕获它们并重放事务(在本例中为INSERT
)。
(无关:) media_id
似乎被冗余存储。
我发现发生了一些罕见的死锁错误。我知道当两个查询作业相互依赖时会发生死锁,因此 MySQL 回滚其中一个。但在我的情况下 MySQL 处于自动提交模式,我正在插入一条触发触发器的新记录。所以我不明白它产生死锁情况的原因。
这是我的 table 架构:
---- 用户 Table ----
CREATE TABLE `users` (
`insta_id` bigint(20) unsigned NOT NULL,
`name` varchar(50) NOT NULL,
`password` varchar(60) NOT NULL,
`gem` int(10) unsigned DEFAULT '20',
`coin` int(10) unsigned DEFAULT '20',
PRIMARY KEY (`insta_id`),
UNIQUE KEY `insta_id` (`insta_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
---like_requests Table---
CREATE TABLE `like_requests` (
`req_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`insta_id` bigint(20) unsigned NOT NULL,
`media_id` varchar(50) NOT NULL,
`remaining_like` int(10) unsigned NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`count` int(10) unsigned NOT NULL,
PRIMARY KEY (`req_id`),
KEY `insta_id` (`insta_id`),
KEY `media_id` (`media_id`),
CONSTRAINT `like_requests_ibfk_1` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=103902 DEFAULT CHARSET=latin1
---喜欢Table---
CREATE TABLE `likes` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`insta_id` bigint(20) unsigned NOT NULL,
`media_id` varchar(50) NOT NULL,
`req_id` bigint(20) unsigned DEFAULT NULL,
`date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `req_id` (`req_id`),
KEY `insta_id` (`insta_id`),
KEY `media_id` (`media_id`),
CONSTRAINT `likes_ibfk_1` FOREIGN KEY (`req_id`) REFERENCES `like_requests`(`req_id`),
CONSTRAINT `likes_ibfk_2` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1704209 DEFAULT CHARSET=latin1
我有一个点赞触发器 table,定义如下:
CREATE TRIGGER `after_insert_likes` AFTER INSERT ON `likes`
FOR EACH ROW BEGIN
UPDATE users SET users.coin=users.coin+1
WHERE users.insta_id = NEW.insta_id LIMIT 1;
IF NEW.req_id IS NOT NULL THEN
UPDATE like_requests
SET like_requests.remaining_like = like_requests.remaining_like-1
WHERE like_requests.req_id = NEW.req_id
AND like_requests.remaining_like > 0
LIMIT 1;
END IF;
END
做一些简单的插入:
$sql = "INSERT INTO likes (insta_id,media_id,req_id) VALUES (?,?,?);";
$pdo = $this->db;
$statement = $pdo->prepare($sql);
$statement->bindValue(1,$data['id'],PDO::PARAM_INT);
$statement->bindValue(2,$data['media_id']);
$statement->bindValue(3,$data['req_id'],PDO::PARAM_INT);
try
{
$statement->execute();
return GetOkResponseWithMessage($response,"Like was submitted");
}
catch (PDOException $exc)
{
return GetErrorResponseWithMessage($response,$exc->getMessage(),500);
}
我收到以下死锁错误日志:
*** (1) TRANSACTION:
TRANSACTION 29031910, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
LOCK WAIT 7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264238, OS thread handle 0x7f6522c6eb00, query id 753506 localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id LIMIT 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031910 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0
*** (2) TRANSACTION:
TRANSACTION 29031909, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264237, OS thread handle 0x7f65209f8b00, query id 753507 localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id LIMIT 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031909 lock mode S locks rec but not gap lock hold time 0 wait time before grant 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table `insta_star`.`users` trx table locks 4 total table locks 4 trx id 29031909 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0
*** WE ROLL BACK TRANSACTION (2)
这不应该以锁等待而不是死锁结束吗?
如何在不重新启动事务的情况下解决这个问题?
线程 2 持有用户 table 中行的共享锁。
然后线程1尝试获取同一行的独占锁,进入锁等待
但是线程 1 不会有机会超时,因为线程 2 然后试图将他的锁升级为独占...但是要做到这一点,他必须等待处于锁等待状态的线程 1,但它正在等待线程 2。
他们互相挡住了对方。
这是一个僵局。
服务器选择要终止的事务,这样它们就不会不必要地相互阻塞。
死锁检测允许一个线程以另一个线程为代价立即成功。否则他们都会被困在锁里等待,直到其中一个因等待太久而死亡。
您处于自动提交模式,当然,这并不意味着您不在事务中。使用 InnoDB 的每个查询仍然在事务中处理,但使用自动提交时,事务在查询开始执行时隐式启动,并在查询成功时隐式提交。
在likes
中,(insta_id, media_id)
是独一无二的吗?或者 (insta_id, req_id)
?或者也许所有 3???如果是这样,请将其设为 PRIMARY KEY
并删除 id all together. If you must keep
id, get rid of
UNIQUE(id), since
PRIMARY KEY(id)` 提供该功能。
同样,去掉UNIQUE(insta_id)
。
想想autocommit
和TRIGGER
结合起来就是一个由几个命令组成的交易:
BEGIN;
INSERT INTO likes... -- Includes 2 uniqueness checks, 1 FK check
UPDATE users ...
if... UPDATE like_requests ...
COMMIT;
我建议的索引更改可能会加快速度,从而减少死锁的可能性。变化可能甚至把死锁变成等待,但我怀疑。
防止死锁的最佳方法是与死锁共存,捕获它们并重放事务(在本例中为INSERT
)。
(无关:) media_id
似乎被冗余存储。