最近我遇到了一个奇怪的mysql死锁,我的表看起来像(为了简单起见,我删除了不相关的列):

CREATE TABLE Node (
    `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT,
    `nodeId` varchar(128) NOT NULL UNIQUE,
    PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE  JobQueue (
    `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT,
    `workerManagementNodeId` varchar(32) DEFAULT NULL,
    CONSTRAINT `fkJbqMgmtNodeId` FOREIGN KEY (`workerManagementNodeId`) REFERENCES `Node` (`nodeId`) ON DELETE SET NULL,
    PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当我的节点关闭时,它将删除节点表中的记录。此时,作业队列可能正在删除job queue表中具有Node.nodeId外键的队列。然后mysql抛出一个异常:
原因:com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException:尝试获取锁时发现死锁;尝试重新启动事务
我检查了数据库,JobQueue被成功删除,但节点没有被删除。我知道外键的顺序可能会导致死锁,但在我的情况下,节点表只有主键没有外键。那么死锁怎么可能发生呢?
顺便说一句:我很确定死锁是由JobQueue引起的,我花了很多时间缩小这个问题的范围,所以在我的测试中只使用这两个表。
更新:
CREATE TABLE  JobQueueEntry (
    `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    `jobQueueId` bigint unsigned NOT NULL,
    `issuerManagementNodeId` varchar(32) DEFAULT NULL,
    PRIMARY KEY  (`id`),
    CONSTRAINT `fkJbqEtryMgmtNodeId` FOREIGN KEY (`issuerManagementNodeId`) REFERENCES `Node` (`nodeId`) ON DELETE SET NULL,
    CONSTRAINT `fkJobQueueId` FOREIGN KEY (`jobQueueId`) REFERENCES `JobQueue` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我终于注意到这仍然是外键顺序引起的问题。实际上还有另一个表JobQueueEntry,它以相反的顺序同时具有Node和JobQueue的外键。所以当删除一个节点时,它试图更新JobQueue和JobQueueEntry。发生死锁是因为JobQueueEntry在节点之前具有JobQueue的外键。
谢谢@ctrl的回答!

最佳答案

首先这应该是一个评论,但我现在没有足够的代表,所以。。。我的“评论”基于我的Oracle经验,但我认为这是一个常见的问题,而且mysql也可以以同样的方式运行。
由于在delete上有一个fk设置为null,当您从节点中删除某个内容时,db引擎必须通过JobQueue来更新它,并且它可能会获取一个表锁来执行此操作(oracle在您的情况下执行此操作)。如果有多个参与者、一些更新/删除作业表和一些更新/删除作业队列表,则可能会导致死锁。
在Oracle中,为了解决这个问题(并获得更好的性能),通常在子表的fk列上创建一个索引,对于workerManagementNodeId。
如果mysql以一种不同的、更聪明的方式来实现这一点,请原谅:)

09-25 19:58