使用 Django 的 get_or_create 在创建时共享锁上死锁 MySQL

Deadlock MySQL with Django's get_or_create on shared lock on creation

提问人:Antonio Cruz 提问时间:11/2/2023 更新时间:11/2/2023 访问量:51

问:

我正在我的数据库上运行一个脚本,该脚本同时运行多个线程。他们正在运行以下代码:

with transaction.atomic():
                (
                    aggregated_report,
                    created,
                ) = db_model.objects.select_for_update().get_or_create(
                    currency=currency,
                    date=today_date,
                    aggregator=aggregator,
                    type=transaction.type,
                    **kwargs,
                    defaults={"value": transaction.value},
                )
                if not created:
                    aggregated_report.value += transaction.value
                    aggregated_report.save()

当我运行脚本时,当 get_or_create() 没有找到对象并且必须创建它时,我遇到了死锁。从应用程序的角度来看,我只是捕获 OperationalError 并重试。

这是我执行 SHOW ENGINE INNODB STATUS 时来自 MySQL 的死锁日志

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-10-30 16:26:39 281472641544064
*** (1) TRANSACTION:
TRANSACTION 2108849, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
SELECT `reports_aggregatedvalues`.`id`, `reports_aggregatedvalues`, `reports_aggregatedvalues`.`currency`, `reports_aggregatedvalues`.`date`, `reports_aggregatedvalues`.`type`, `reports_aggregatedvalues`.`aggregator`, `reports_aggregatedvalues`.`value` FROM `reports_aggregatedvalues` WHERE (`reports_aggregatedvalues`.`aggregator` = '---' AND `reports_aggregatedvalues`.`currency` = 'USD' AND `reports_aggregatedvalues`.`date` = '2023-10-09 00:00:00' AND `reports_aggregatedvalues` AND `reports_aggregatedvalues`.`type` = 'TRANSFER') LIMIT 21 FOR UPDATE
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 5 n bits 384 index Unique Report of table `phoenix`.`reports_aggregatedvalues` trx id 2108849 lock mode S
Record lock, heap no 313 PHYSICAL RECORD: n_fields 6; compact format; info bits 0

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 5 n bits 384 index Unique Report of table `phoenix`.`reports_aggregatedvalues` trx id 2108849 lock_mode X locks rec but not gap waiting
Record lock, heap no 313 PHYSICAL RECORD: n_fields 6; compact format; info bits 0

*** (2) TRANSACTION:
TRANSACTION 2108848, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
SELECT `reports_aggregatedvalues`.`id`, `reports_aggregatedvalues`, `reports_aggregatedvalues`.`currency`, `reports_aggregatedvalues`.`date`, `reports_aggregatedvalues`.`type`, `reports_aggregatedvalues`.`aggregator`, `reports_aggregatedvalues`.`value` FROM `reports_aggregatedvalues` WHERE (`reports_aggregatedvalues`.`aggregator` = '----' AND `reports_aggregatedvalues`.`currency` = 'USD' AND `reports_aggregatedvalues`.`date` = '2023-10-09 00:00:00' AND `reports_aggregatedvalues` AND `reports_aggregatedvalues`.`type` = 'TRANSFER') LIMIT 21 FOR UPDATE
​
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 5 n bits 384 index Unique Report of table `phoenix`.`reports_aggregatedvalues` trx id 2108848 lock mode S
Record lock, heap no 313 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
​
​
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 5 n bits 384 index Unique Report of table `phoenix`.`reports_aggregatedvalues` trx id 2108848 lock_mode X locks rec but not gap waiting
Record lock, heap no 313 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
​
*** WE ROLL BACK TRANSACTION (2)

根据我的理解,发生这种情况是因为在数据库级别,get_or_create首先获取共享锁以进行 SELECT FOR UPDATE,然后尝试升级到独占锁,如果有 2 个线程同时运行并且它们都获得了读取所需的共享锁,它们正在等待彼此释放共享锁,从而导致死锁。

这在我的脑海中是有道理的,但是在运行这个脚本时,我基本上有数千个更新和 5 或 10 个创建,但是,每次我运行它时,死锁只会发生在创建时。

据我了解,通过transaction.atomic块获取的数据库锁只有在我们离开该块后才会释放。但我的问题是:那么,考虑到这也需要从共享锁升级到独占锁,我是否也应该在脚本的这一部分面临死锁?

 aggregated_report.value += transaction.value
 aggregated_report.save()

我是否还有其他方法可以实现此行为以及完全没有死锁?或者只是重试它是否足以满足此用例的需求?

python mysql django 死锁

评论

0赞 nbk 11/2/2023
为什么要进行更新,这将锁定所有选定的行,你真的需要所有行吗
0赞 Antonio Cruz 11/2/2023
我的想法是,当我将事务值添加到报表值时,在get_or_create和保存之间。不会出现并发问题,即保存对象的值并且该线程会覆盖它
0赞 nbk 11/2/2023
您应该在手册中查看锁的手册。锁基本上是至少两个线程尝试访问相同的行,而你的工作就是要避免这种情况
0赞 Antonio Cruz 11/2/2023
发生这种情况时,只需重试交易,我的脚本就可以正常工作。我基本上只是在寻找一个解释,为什么这种死锁只发生在创建时而不是更新时
0赞 nbk 11/2/2023
很难重现连贯访问,基本上你可以先在SQL端启用日志记录,看看在你这边冲浪时会发生什么。还帮助在我的 hpn 端添加日志,以便您可以查看哪一行执行 SQL 查询,以便您可以检查逻辑是否正常

答: 暂无答案