MySQL - 两次调用触发器导致死锁

时间:2016-09-21 20:49:28

标签: php mysql triggers innodb deadlock

我有一个包含数百万行的表格,我必须使用它除以组的计数。

CREATE TABLE `customers` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `group_id` INT(10) UNSIGNED NULL DEFAULT NULL
)

所以我经常打电话

SELECT COUNT(*) FROM customers WHERE group_id=XXX

但遗憾的是,当计算数十亿行的表时,MySQL的速度非常慢(一次调用大约10秒)。

所以我决定创建一个新表来保留计数器:

CREATE TABLE `customer_stats` (
    `group_id` INT(11) NOT NULL,
    `value` INT(11) NOT NULL,
)

我可以保留当前的计数器,并确保它使用触发器是最新的。

所以我有插入/更新/删除的触发器,这里是插入一的例子:

CREATE TRIGGER `customers_insert` AFTER INSERT ON `customers` FOR EACH ROW 
BEGIN
    UPDATE customer_stats
    SET
      `value` = `value` + 1
    WHERE
      customer_stats.group_id = NEW.group_id;
END

它适用于大多数情况,但在高负载(每秒数十次调用)时,我遇到了死锁。

2016-09-21T20:14:30.639907Z 2057 [Note] InnoDB: Transactions deadlock detected, dumping detailed information.
2016-09-21T20:14:30.639926Z 2057 [Note] InnoDB:
*** (1) TRANSACTION:

TRANSACTION 10390, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 10 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 1
MySQL thread id 2059, OS thread handle 140376644818688, query id 85330 test_test-php-fpm_1.test_default 172.19.0.12 root updating
UPDATE customer_stats
SET
  `value` = `value` + 1
WHERE
  customer_stats.group_id = NEW.group_id;
2016-09-21T20:14:30.639968Z 2057 [Note] InnoDB: *** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 85 page no 3 n bits 72 index customer_stats_key_group_id_unique of table `test`.`customer_stats` trx id 10390 lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 21; hex 637573746f6d657264657461696c735f636f756e74; asc customerdetails_count;;
 1: len 4; hex 80000002; asc     ;;
 2: len 6; hex 000000002890; asc     ( ;;
 3: len 7; hex 34000002341224; asc 4   4 $;;
 4: len 4; hex 80000666; asc    f;;

2016-09-21T20:14:30.640302Z 2057 [Note] InnoDB: *** (2) TRANSACTION:

TRANSACTION 10391, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
10 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 1
MySQL thread id 2057, OS thread handle 140376513820416, query id 85333 test_test-php-fpm_1.test_default 172.19.0.12 root updating
UPDATE customer_stats
SET
  `value` = `value` + 1
WHERE
  customer_stats.group_id = NEW.group_id;
2016-09-21T20:14:30.640334Z 2057 [Note] InnoDB: *** (2) HOLDS THE LOCK(S):

2016-09-21T20:14:30.640850Z 2057 [Note] InnoDB: *** WE ROLL BACK TRANSACTION (2)

它仅在高负载下存在,我想知道是否有一些简单的方法来更改触发器以确保它们不会在同一时间尝试执行UPDATE customer_stats,因为这是造成僵局。因此必须同时创建两个客户记录以引发死锁。

我拥有的触发器表和系统有点复杂,但我尽量简化它,我可以解释你的问题。

2 个答案:

答案 0 :(得分:1)

您需要以任何顺序组合INDEX(key, group_id)

让我们简化触发器:第1步:VALUESSELECT更简单:

BEGIN
    DECLARE originalGroupId INT;
    SET originalGroupId = NEW.group_id;
    INSERT IGNORE INTO table_stats(`key`, value, group_id)
        VALUES ('customers_count', 0, originalGroupId);   -- line changed
    UPDATE table_stats
    SET
      table_stats.`value` = table_stats.`value` + 1
    WHERE
      table_stats.`key` = "customers_count"
      AND table_stats.group_id = originalGroupId;
END

第2步:使用IODKU。此时,需要UNIQUE(key, group_id),无论是哪种顺序。

BEGIN
    DECLARE originalGroupId INT;
    SET originalGroupId = NEW.group_id;
    INSERT INTO table_stats(`key`, value, group_id)
        VALUES ('customers_count', 1, originalGroupId)  -- note 1 not 0
        ON DUPLICATE KEY UPDATE
            `value` = `value` + 1;
END

步骤1和2使其运行得更快,从而降低了死锁的频率。

第3步:处理死锁!它们完全可以预防。因此,计划在发生死锁时重复整个事务。

答案 1 :(得分:0)

好吧,我想我发现了什么问题。

我试图简化问题来向您展示,但看起来我觉得它变得更简单了 - 问题不再存在了。

我原来的触发器是:

BEGIN
    DECLARE originalGroupId INT;
    SET originalGroupId = NEW.group_id;
    INSERT IGNORE INTO table_stats(`key`, value, group_id)
    SELECT 'customers_count', 0, originalGroupId;
    UPDATE table_stats
    SET
      table_stats.`value` = table_stats.`value` + 1
    WHERE
      table_stats.`key` = "customers_count"
      AND table_stats.group_id = originalGroupId;
END

我看起来死锁是由INSERT IGNORE或变量引起的,就像我删除它一样 - 它开始工作没有任何问题。谢谢!