【问题标题】:MySQL - trigger called twice causes deadlockMySQL - 触发器调用两次导致死锁
【发布时间】:2016-09-21 20:49:27
【问题描述】:

我有一个包含数百万行的表,我必须使用按组对其进行计数。

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,因为这会导致死锁。所以必须同时创建两个客户记录才能引发死锁。

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

【问题讨论】:

  • 你试过在group_id上建立索引吗?
  • @Solarflare 是的,两个表中的该列都有索引
  • “所以必须同时创建两个客户记录才能引发死锁” 这不是死锁的意思。同时两个是没有问题的。除非每个事务都持有另一个需要的锁,否则事务不会死锁,这表明您在单个事务中执行多个插入。你能证实吗?如果是这样,你为什么要这样做?如果您没有删除某些状态消息,也会更容易解释。
  • @Michael-sqlbot 这里是整个死锁消息pastebin.com/yK2vhEsh,查询有点复杂,因为我使用的不是“customer_stats”表,而是“table_stats”,因为它不是一个表我有一个计数器,但不止一个,而且我正在使用一张桌子将所有计数器放在一张桌子上。
  • 和简化版本触发器的死锁日志(如上面的示例所示):pastebin.com/bWcjhEap 我的想法是死锁是因为我试图在单独的会话中更新 table_stats 表中的同一行,所以他们都得到UPDATE SET value = 100+1,这是因为死锁是,因为MySQL中没有简单的增量函数并且它是基于值的,但我可能错了。

标签: php mysql triggers innodb deadlock


【解决方案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 步:处理死锁!它们不是完全可以预防的。因此,计划在发生死锁时重复整个事务。

【讨论】:

  • 这不是解决方案。 !!
  • @Ouroboros - 并非所有死锁都可以消除。我的答案试图缓解这个问题。届时,重放回滚交易的成本将是可以接受的。
  • 需要清楚地说明辅助密钥为何以及如何导致问题。我会尝试写下我们遇到类似问题的答案
  • @Ouroboros - ((1) 事务处理锁定可能涉及的行。在某些情况下,额外的行(或间隙)被锁定。(对不起,我不知道'没有有用的例子。)(2)最大的好处是索引可以让查询更快地完成,从而更快地“让开”,从而减少死锁的可能性(或“锁定等待超时”)。也就是说,辅助键不会导致问题,但它可能避免问题。
【解决方案2】:

好的,我想我发现了问题所在。

我试图简化问题以在此处向您展示,但看起来在我简化后 - 问题不再存在。

我原来的触发器是:

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 或变量引起的,就像我删除它时一样 - 它开始工作没有任何问题。谢谢!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-05
    • 1970-01-01
    • 1970-01-01
    • 2013-04-28
    • 1970-01-01
    相关资源
    最近更新 更多