使用锁表解决导致死锁的MySQL触发器

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

我已经和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
现在,我似乎终于设法摆脱了死锁,在执行批插入之前,通过使用“lock tables”语句手动执行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列主键对每批进行排序。这将消除许多死锁,并将其余的变成“锁等待”。(也就是说,当出现死锁时,它可以简单地等待另一个连接完成。)

此外,如果可行,将批次限制为100行

主键
以分区键开始几乎总是无用的

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

解释

经典的死锁是:

我抓第一排,你们抓第二排,然后我抓第二排(但抓不到),你们抓第一排(但抓不到)。我们都不愿意放弃我们所拥有的

因此,一名裁判介入并迫使我们中的一方在他做出回应时做出回应,让另一方继续完成比赛

我(或你)不可能(或不切实际)抓住所有需要的行;所以这些行实际上是一次抓取一行。想想一个巨大的
更新
,它正在改变数百万行。当我抓住所有的行时停止一切是不明智的

这被称为“乐观”——处理过程假设它会成功并向前推进。和99.999…%的时间,一个典型的事务将在任何其他连接冲突之前完成

如果我们以相同的“顺序”(例如
主键
顺序)抓取行,我们中的一个可以完成;另一个可以简单地等待。如果等待时间仅为毫秒,则延迟是不可察觉的。(限制批量大小有助于此。)

更好?

最好(也就是说,更快、更不容易死锁)摆脱触发器,只需执行两个批处理语句——一个用于原始批处理
INSERT
,另一个用于批处理向上插入(也称为IODKU)汇总表

在任何情况下,捕获事务中的错误并重播整个事务


更多关于高速插入的讨论:(虽然不直接适用,但您可能会找到一些相关提示。)

什么是
表格汇总统计数据的结构(
显示创建表格汇总统计数据)
)(编辑要包含的问题)。是否有多个批次更改同一行(或相同主/键的批次)?单个insert语句本身就是一个事务。您可以显示innodb stats的死锁输出吗?唉,该显示无法提供死锁上下文的其余部分——即调用
触发器的指令和transactions.OT中的任何其他语句;设置
DateAndTime
时,不要使用
date\u格式
<代码>日期\格式
创建一个字符串,然后将其转换回日期时间。由于
date\u格式
参数已经是日期时间,请使用它们。也许可以使用
DIV
来消除秒数。您的意思是:“让主键以分区键开头几乎总是无用的。”这背后的逻辑是什么?批插入是否是一个有效的事务,行级锁从开始到结束都被持有?我很难理解为什么会出现僵局。如果正在执行两个批处理插入,即使它们在同一行上,它们也应该相互等待,而不是死锁,对吗?我不明白排序将如何改善这一点。而且这不是一个真正的选项,会给应用程序增加太多的处理负载。@Adriaan-我添加了一个解释。谢谢你的解释。我不理解的是“我们都不愿意放弃我们所拥有的。”-一旦其中一行的锁被其中一个插入的锁取下,就没有任何东西阻止它写入并释放锁,以便另一个插入可以继续,而不管插入的顺序如何。除非出于某种奇怪的原因,它在完成插入后没有释放锁。我的隔离级别是未提交的.@ AdRiaAN——考虑更新计数器,如您的案例中的一部分。如果我撞上一个计数器,那么你撞上了同一个计数器,但是由于任何原因,我得到了
回滚
,这使得计数器的值不正确。