我有两个线程,他们必须更新同一个表,但是第一个线程使用主键锁定单个记录,第二个线程必须使用另一个索引锁定一组记录。锁是用SELECT… FOR UPDATE steatment制成的,我不明白为什么他们会遇到死锁。
这是表:
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)尝试获取锁时发现死锁;尝试重新启动事务
43和39连接是使用连接池的Java应用程序的JDBC连接。
为什么第二个连接没有等待并且它被死锁了?
当您使用二级索引定位和锁定一行时,MySQL将首先锁定二级索引中的条目,然后锁定主键中的相应行。这两个步骤可能会导致您死锁。
让我们假设以下行:
+----+--------+-------+
| id | evento | stato |
+----+--------+-------+
| 1 | 66 | 10 |
+----+--------+-------+
>
您的第一个事务使用主键查找id=1
的行,并在其上放置排他锁。
您的第二个事务使用索引(evento, strato)
查找evento=66
的条目,在该条目上放置排他锁(在辅助索引中),然后尝试在主键中的行中获得排他锁。由于它已经被锁定,它必须等待。
您的第一个事务现在想要更新该行。它有一个排他锁,所以没关系。但是该更新会更改strato
。由于它是索引的,因此必须修改索引(evento, strato)
中的相应条目(这需要排他锁)。不幸的是,第二个事务有排他锁,因此第一个事务必须等待第二个事务。
由于第二个事务已经等待第一个事务,我们有死锁
那么如何预防呢?
对于您的特定情况,您可以使用不同的二级索引来查找您的行,例如KEY evento1(evento)
。如果MySQL使用此索引(并且为了确保,您可以使用SELECT count(*)OF 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)
上获得锁。这一次,它起作用了,因为这现在没有被锁定id=1
的锁,第二个事务可以完成如果这对你的具体情况来说是一个合理的解决方案,那将取决于你的具体情况。例如,添加一个你实际上不想要或不需要的索引来防止一年发生两次的死锁可能是矫枉过正。或者你有更多的情况略有不同,可能需要不同的方法。你可以在例如如何最小化和处理死锁中找到一些额外的一般指南。
请详细说明你的用例。如果你们两个有线程试图保护锁,你可以简单地同时触发更新语句,根据更新返回的行数,你可以相应地构建逻辑。需要更多信息才能评论。