为什么我使用 SELECT ... FOR UPDATE 锁从 mysql 获得死锁?

Why I'm getting a deadlock from mysql using SELECT ... FOR UPDATE lock?

我有两个线程,它们必须更新相同的 table 但第一个线程使用主键锁定单个记录,第二个线程必须使用另一个索引锁定一组记录。锁是用 SELECT ... FOR UPDATE 声明创建的,我不明白为什么他们 运行 陷入僵局。

这是table:

CREATE TABLE `ingressi` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `evento` int(10) unsigned NOT NULL,
 `stato` int(10) unsigned NOT NULL,
 ....,
 ....,
 PRIMARY KEY (`id`),
  KEY `evento` (`evento`,`stato`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

这是查询日志(注意连接):

    43 Query    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
    43 Query    set autocommit=0
    43 Query    SELECT stato FROM ingressi WHERE id=1 FOR UPDATE
    39 Query    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
    39 Query    set autocommit=0
    39 Query    SELECT count(*) FROM ingressi WHERE evento=66 FOR UPDATE
    43 Query    UPDATE `ingressi` SET stato=0 WHERE id=1
    43 Query    COMMIT

就在最后一次查询之后,出现了死锁错误。

(conn=39) Deadlock found when trying to get lock; try restarting transaction

43 和 39 连接是使用连接池的 Spring-Java 应用程序的 JDBC 连接。

为什么第二个连接没有等待就死锁了?

请详细说明您的用例。如果你们两个有试图保护锁的线程,您可以简单地同时触发更新语句,并根据更新返回的行数,您可以相应地构建逻辑。需要更多信息才能发表评论。

当您使用二级索引定位并锁定行时,MySQL会先锁定二级索引中的条目,然后再锁定主键中的相应行。这两个步骤可能会导致你的死锁。

让我们假设以下行:

+----+--------+-------+
| id | evento | stato |
+----+--------+-------+
|  1 |     66 |    10 |
+----+--------+-------+
  • 您的第一个事务使用主键找到具有id=1的行,并在其上放置一个独占锁。

  • 您的第二个事务使用索引 (evento, strato) 查找具有 evento=66 的条目,对该条目(在二级索引中)放置排他锁,然后尝试获取主键中行的排他锁。既然已经锁了,那就得等了。

  • 您的第一个事务现在想要更新该行。它有一个独占锁,所以它很好。但是那个更新改变了 strato。既然是索引,那么索引(evento, strato)中对应的条目就得修改(需要排它锁)。不幸的是,第二个事务上有排他锁,所以第一个事务必须等待第二个事务。

  • 由于第二个事务已经在等待第一个事务,我们有一个死锁

那么如何预防呢?

对于您的具体情况,您可以使用不同的二级索引来查找您的行,例如KEY evento1 (evento)。如果 MySQL 使用这个索引(并且为了确保,你可以使用 SELECT count(*) FROM ingressi FORCE INDEX (evento1) WHERE evento=66 FOR UPDATE),这应该可以防止这个特定的死锁:

  • 第一个事务锁定主键id=1
  • 第二个事务在二级索引evento1
  • 中锁定条目(evento1=66,id=1)
  • 第二个事务尝试获取主键 id=1 的锁,主键已锁定,因此等待
  • 第一个事务更新行并需要在二级索引 evento 中获取条目 (evento1=66,stato=10,id=1) 的锁。这一次,它起作用了,因为现在 not 已锁定
  • 第一个事务提交,释放主键上的锁id=1,第二个事务可以完成

对于您的具体情况,这是否是一个合理的解决方案将取决于您的具体情况。它可能例如添加一个您实际上不想要或不需要的索引只是为了防止每年发生两次的死锁,这太过分了。或者,也许您有更多这些情况都略有不同,可能需要不同的方法。您可能会在例如How to Minimize and Handle Deadlocks.