MySQL 触发器导致死锁,通过锁表解决

标签 mysql innodb database-deadlocks mysql-innodb-cluster

一段时间以来,我一直在与 MySQL 死锁问题作斗争。我们有很多记录数据的表,然后有插入后触发器,可以提取每分钟的统计数据/汇总数据并将其保存到另一个汇总表中。 显然,这会导致多个插入影响汇总表中的同一行。但由于没有任何内容等待插入结果继续,因此这不应导致死锁。插入是批量完成的 - 每隔几毫秒使用批量插入。它们可以同时从不同的应用程序完成。 由于这些批量插入语句从来都不是较大事务的一部分,因此我不太明白为什么它会导致死锁。如果有人能解释为什么会发生这种情况,我们将不胜感激!从错误日志中,我只看到多行:

RECORD LOCKS space id 118597 page no 67 n bits 80 index PRIMARY of table `logschema`.`table_summary_stats` /* Partition `p_2020_11_02` */ trx id 7600352476 lock_mode X locks rec but not gap
Record lock, heap no 11 PHYSICAL RECORD: n_fields 13; compact format; info bits 0

现在,我似乎终于在批量插入之前通过使用“锁定表”语句手动执行mysql表锁定来摆脱死锁。 我知道在 innodb 表上进行表级锁是非常不受欢迎的,但是自从我添加了这个表锁以来,我还没有看到死锁发生。

表级锁可以解决这样的死锁问题吗?这是解决此类问题的可接受方法吗?还是在使用 innodb 表时应该不惜一切代价避免表锁?

编辑:汇总表如下所示:

 CREATE TABLE `table_summary_stats` (
  `id` bigint DEFAULT NULL,
  `DateAndTime` datetime NOT NULL,
  `address` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `group` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `result` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `count` int DEFAULT NULL,
  PRIMARY KEY (`DateAndTime`,`group`,`result`,`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
/*!50100 PARTITION BY RANGE (to_days(`DateAndTime`))
(PARTITION p_2020_10_26 VALUES LESS THAN (738090) ENGINE = InnoDB,
 PARTITION p_2020_11_10 VALUES LESS THAN (738105) ENGINE = InnoDB,
 PARTITION overflow VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */;

触发器执行以下操作:

    INSERT INTO table_summary_stats
SET 
    DateAndTime = date_format(from_unixtime(NEW.appEpochMilli/1000), '%Y-%m-%d %H:%i:00'),
    address = NEW.address, 
    group = NEW.group,
    result = NEW.result,
    count = 1
on duplicate key
update
    count = count + 1

以下是相关死锁信息:

------------------------
 LATEST DETECTED DEADLOCK
 ------------------------
 2020-11-02 20:00:53 0x7f0cc032a700
 *** (1) TRANSACTION:
 TRANSACTION 7600352761, ACTIVE 0 sec inserting
 mysql tables in use 2, locked 2
 LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 3
 MySQL thread id 874850, OS thread handle 139654885635840, query id 3299800570 10.15.0.91 cdrwriter update
    INSERT INTO table_summary_stats
    SET 
        DateAndTime = date_format(from_unixtime(NEW.appEpochMilli/1000), '%Y-%m-%d %H:%i:00'),
        address = NEW.address, 
        group = NEW.group,
        result = NEW.result,
        count = 1
    on duplicate key
    update
        count = count + 1
 
 *** (1) HOLDS THE LOCK(S):
 RECORD LOCKS space id 118597 page no 67 n bits 80 index PRIMARY of table `sms_cdr`.`table_summary_stats` /* Partition `p_2020_11_02` */ trx id 7600352761 lock_mode X locks rec but not gap
 Record lock, heap no 10 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
  0: len 5; hex 99a7c53ec0; asc    > ;;
  1: len 4; hex 74657374; asc test;;
  2: len 30; hex 7b0a202022737461747573223a20226572726f72222c0a202022636f6465; asc {   "status": "error",   "code; (total 76 bytes);
  3: len 11; hex 3933373931303130353131; asc 93791010511;;
  4: len 6; hex 0001c5042df9; asc     - ;;
  5: len 7; hex 01000053520238; asc    SR 8;;
  6: SQL NULL;
  7: len 4; hex 80057c22; asc   |";;
  8: len 8; hex 80000000642f4d05; asc     d/M ;;
  9: len 8; hex 8000000000c03473; asc       4s;;
  10: len 8; hex 800000001a7e7aee; asc      ~z ;;
  11: len 8; hex 8000000000f2b5b1; asc         ;;
  12: len 8; hex 800000008060b217; asc      `  ;;
 
 
 *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
 RECORD LOCKS space id 118597 page no 67 n bits 80 index PRIMARY of table `sms_cdr`.`table_summary_stats` /* Partition `p_2020_11_02` */ trx id 7600352761 lock_mode X locks rec but not gap waiting
 Record lock, heap no 11 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
  0: len 5; hex 99a7c54000; asc    @ ;;
  1: len 4; hex 74657374; asc test;;
  2: len 30; hex 7b0a202022737461747573223a20226572726f72222c0a202022636f6465; asc {   "status": "error",   "code; (total 76 bytes);
  3: len 11; hex 3933373931303130353131; asc 93791010511;;
  4: len 6; hex 0001c5042cdc; asc     , ;;
  5: len 7; hex 02000004ea07ff; asc        ;;
  6: SQL NULL;
  7: len 4; hex 8003095b; asc    [;;
  8: len 8; hex 8000000036a3a0bb; asc     6   ;;
  9: len 8; hex 8000000000785507; asc      xU ;;
  10: len 8; hex 800000000e23089a; asc      #  ;;
  11: len 8; hex 80000000008c8e08; asc         ;;
  12: len 8; hex 8000000045cb8c64; asc     E  d;;
 
 
 *** (2) TRANSACTION:
 TRANSACTION 7600352476, ACTIVE 0 sec inserting
 mysql tables in use 2, locked 2
 LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 75
 MySQL thread id 874775, OS thread handle 139672774735616, query id 3299800787 10.15.0.90 cdrwriter update
    INSERT INTO table_summary_stats
    SET 
        DateAndTime = date_format(from_unixtime(NEW.appEpochMilli/1000), '%Y-%m-%d %H:%i:00'),
        address = NEW.address, 
        group = NEW.group,
        result = NEW.result,
        count = 1
    on duplicate key
    update
        count = count + 1
 
 *** (2) HOLDS THE LOCK(S):
 RECORD LOCKS space id 118597 page no 67 n bits 80 index PRIMARY of table `sms_cdr`.`table_summary_stats` /* Partition `p_2020_11_02` */ trx id 7600352476 lock_mode X locks rec but not gap
 Record lock, heap no 11 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
  0: len 5; hex 99a7c54000; asc    @ ;;
  1: len 4; hex 74657374; asc test;;
  2: len 30; hex 7b0a202022737461747573223a20226572726f72222c0a202022636f6465; asc {   "status": "error",   "code; (total 76 bytes);
  3: len 11; hex 3933373931303130353131; asc 93791010511;;
  4: len 6; hex 0001c5042cdc; asc     , ;;
  5: len 7; hex 02000004ea07ff; asc        ;;
  6: SQL NULL;
  7: len 4; hex 8003095b; asc    [;;
  8: len 8; hex 8000000036a3a0bb; asc     6   ;;
  9: len 8; hex 8000000000785507; asc      xU ;;
  10: len 8; hex 800000000e23089a; asc      #  ;;
  11: len 8; hex 80000000008c8e08; asc         ;;
  12: len 8; hex 8000000045cb8c64; asc     E  d;;
 
 
 *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
 RECORD LOCKS space id 118597 page no 67 n bits 80 index PRIMARY of table `sms_cdr`.`table_summary_stats` /* Partition `p_2020_11_02` */ trx id 7600352476 lock_mode X locks rec but not gap waiting
 Record lock, heap no 10 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
  0: len 5; hex 99a7c53ec0; asc    > ;;
  1: len 4; hex 74657374; asc test;;
  2: len 30; hex 7b0a202022737461747573223a20226572726f72222c0a202022636f6465; asc {   "status": "error",   "code; (total 76 bytes);
  3: len 11; hex 3933373931303130353131; asc 93791010511;;
  4: len 6; hex 0001c5042df9; asc     - ;;
  5: len 7; hex 01000053520238; asc    SR 8;;
  6: SQL NULL;
  7: len 4; hex 80057c22; asc   |";;
  8: len 8; hex 80000000642f4d05; asc     d/M ;;
  9: len 8; hex 8000000000c03473; asc       4s;;
  10: len 8; hex 800000001a7e7aee; asc      ~z ;;
  11: len 8; hex 8000000000f2b5b1; asc         ;;
  12: len 8; hex 800000008060b217; asc      `  ;;
 
 *** WE ROLL BACK TRANSACTION (1)

最佳答案

“插入是分批完成的”——按 4 列 PK 对每个批处理进行排序。这应该消除许多死锁,并将其余的变成“锁等待”。 (也就是说,当出现死锁时,它可以简单地等待另一个连接完成。)

此外,如果可行,请将批处理限制为 100 行。

使用分区键来启动主键几乎总是没有用的。

(我同意您应该尽量避免锁定表。)

说明

经典的死锁是:

I grab row number 1, you grab row 2, then I reach for row 2 (but can't get it) and you reach for row 1 (and can't get it). Neither of us is willing to let go of what we have.

因此,裁判介入并迫使我们中的一个人在他已经回击时进行回击,而让另一个人继续完成。

我(或你)不可能(或不切实际)获取所需的所有行;所以这些行实际上是一次被抓取的。想象一下正在更改数百万行的巨大UPDATE。当我抓取所有这些行时停止一切是不明智的。

这被称为“乐观”——处理假设它将成功并继续前进。 99.999...% 的情况下,典型事务会在任何其他连接与其发生冲突之前完成。

如果我们以相同的“顺序”(例如PRIMARY KEY顺序)抓取行,我们中的一个人就可以完成;另一个可以简单地等待。如果等待时间只有几毫秒,那么延迟是难以察觉的。 (限制批量大小在这里有帮助。)

更好?

摆脱触发器并简单地执行两个批处理语句可能会更好(即更快且不太可能发生死锁) - 一个到原始批处理 INSERT,另一个用于批量更新插入(又名 IODKU)汇总表。

无论如何,捕获事务中的错误并重播整个事务。

更多高速插入讨论:http://mysql.rjweb.org/doc.php/staging_table (虽然不直接适用,但您可能会找到一些相关的提示。)

关于MySQL 触发器导致死锁,通过锁表解决,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64655248/

相关文章:

mysql - InnoDB 是否缓存不存在的键?

sql - Postgresql SQL 状态 : 25P02 Deadlock?

postgresql - 依靠 Postgres 的死锁检测来进行并发控制是否安全?

c# - 只有一个资源和隔离级别可序列化的死锁...?

mysql - 按结果集对分组进行排名

javascript - 使用ajax在数据库中保存多个同名字段

php - 为什么这个 Zend 框架会占用我的 CPU 并加载页面如此之慢

php - 添加功能不适用于数据库

mysql - 为什么在MySQL innodb缓冲池中撤消日志条目(MySQL 5.7)

mysql - 如果我们依赖语句中的索引扫描顺序,两个 InnoDB UPDATE 语句是否可以防止 PK 索引死锁?