我在我的项目中遇到了一些死锁,其中我有多个进程在更改数据库中的相同对象.

我有一个终结点,其要点是获取最近未完成的 playbook ,如果存在则继续更改它,如果不存在,则它应该创建一个.我需要确保,如果我在这个端点收到两个并发请求,其中一个创建对象,另一个阻止执行,直到创建该请求,然后第二个请求将更改创建的对象.

在我的Django应用程序中,我有以下关于Transaction()内部上下文的查询:

# Use list to force evaluation
play = list(Play.objects.select_for_update().filter(
                game=self.game,
                user=self.user,
                discard=False,
                finished=False,
            )
        )

它曾经是.last(),但我读到它在数据库查询中执行的ORDER BY有时会引发死锁问题,所以我try 了这个方法.

根据我的理解(这可能是有缺陷的),MySQL应该在完成的索引上获得独占记录锁(我在我的数据库中有该索引),这将不允许在此事务发生时创建对象,并且任何试图在此事务完成后被阻止的事务都将被阻止.它还应该获取行本身的记录锁,以便任何事务都不能更改其内容

在重负载测试中,我从来没有遇到过并发问题,并且它按预期工作,但是我有时会发现数据库中的死锁,这是我对下面这段特定代码不完全理解的:

 with transaction.atomic():
        Play.objects.select_for_update().get(pk=play.pk)
        <Changes to the Play>
        play.save()

在做SHOW ENGINE INNODB STATUS次时,我得到了以下报告,为了简单起见,我对其进行了编辑:

*** (1) TRANSACTION:
TRANSACTION 901805890, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
LOCK WAIT 116 lock struct(s), heap size 24696, 5 row lock(s)
SELECT `*` FROM `games_play` WHERE (`games_play`.`discard` = 0 AND `games_play`.`finished` = 0 AND `games_play`.`game_id` = 1 AND `games_play`.`user_id` = 28) FOR UPDATE

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 4268 page no 411 n bits 1616 index games_play_finished_71622b41 of table `test_dev`.`games_play` trx id 901805890 lock_mode X locks rec but not gap
Record lock, heap no 18 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 1; hex 80; asc  ;;
 1: len 4; hex 8000425c; asc   B\;;

Record lock, heap no 19 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 1; hex 80; asc  ;;
 1: len 4; hex 8000425d; asc   B];;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4268 page no 969 n bits 80 index PRIMARY of table `test_dev`.`games_play` trx id 901805890 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
 0: len 4; hex 8000425d; asc   B];;
 1: len 6; hex 000035c07741; asc   5 wA;;
 2: len 7; hex 02000001a42d67; asc      -g;;
 3: len 1; hex 81; asc  ;;
 4: len 30; hex 0003006e00190004001d000400210008000029000c2d0004010062657473; asc    n         !    )  -    bets; (total 111 bytes);
 5: len 7; hex 63726173685f37; asc crash_7;;
 6: len 30; hex 00020073001200070019000e00002700004f00696e697469616c6f6e5f6c; asc    s          '  O initialon_l; (total 116 bytes);
 7: len 8; hex 99b07498ef076fa0; asc   t   o ;;
 8: len 8; hex 99b07498f40dce58; asc   t    X;;
 9: len 1; hex 81; asc  ;;
 10: len 4; hex 80000001; asc     ;;
 11: len 4; hex 8000000f; asc     ;;
 12: SQL NULL;


*** (2) TRANSACTION:
TRANSACTION 901805889, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
UPDATE `games_play` SET `finished` = 1, `details` = '{}', `game_token` = '1234', `jwt_token` = NULL, `game_id` = 1, `user_id` = 15, `created` = '2023-06-26', `modified` = '2023-06-26', `discard` = 1 WHERE `games_play`.`id` = 16989

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 4268 page no 969 n bits 80 index PRIMARY of table `test_dev`.`games_play` trx id 901805889 lock_mode X locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
 0: len 4; hex 8000425d; asc   B];;
 1: len 6; hex 000035c07741; asc   5 wA;;
 2: len 7; hex 02000001a42d67; asc      -g;;
 3: len 1; hex 81; asc  ;;
 4: len 30; hex 0003006e00190004001d000400210008000029000c2d0004010062657473; asc    n         !    )  -    bets; (total 111 bytes);
 5: len 7; hex 63726173685f37; asc crash_7;;
 6: len 30; hex 00020073001200070019000e00002700004f00696e697469616c6f6e5f6c; asc    s          '  O initialon_l; (total 116 bytes);
 7: len 8; hex 99b07498ef076fa0; asc   t   o ;;
 8: len 8; hex 99b07498f40dce58; asc   t    X;;
 9: len 1; hex 81; asc  ;;
 10: len 4; hex 80000001; asc     ;;
 11: len 4; hex 8000000f; asc     ;;
 12: SQL NULL;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4268 page no 411 n bits 1616 index games_play_finished_71622b41 of table `test_dev`.`games_play` trx id 901805889 lock_mode X locks rec but not gap waiting
Record lock, heap no 19 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 1; hex 80; asc  ;;
 1: len 4; hex 8000425d; asc   B];;


根据我对此报告的理解,第一个事务获取了已完成索引上的锁以防止插入,并试图自己获取Play It上的锁,但该锁已被另一个事务持有,该事务也希望获取已完成索引上的锁.但这些 playbook 属于不同的用户,在我看来,第一次查询永远不应该获得对该用户的锁定.

我想了解这里发生了什么,以及可以做些什么来防止这种情况发生.

推荐答案

您在finished上似乎只有一个单列索引,例如:

class Play(models.Model):
    finished = models.BooleanField(db_index=True)
    ...

在这种情况下, Select 更新包含.get(pk=play.pk)unfinished Play将首先锁定pkPRIMARY索引,然后try 锁定finished = 0索引记录.

将其删除并在["user_id", "finished"]上添加索引:

class Play(models.Model):
    finished = models.BooleanField()
    ...

    class Meta:
        indexes = [
            models.Index(fields=["user_id", "finished"]),
        ]

在这种情况下,索引锁将用于单独的用户,例如user_id = 15 AND finished = 0.

请记住运行迁移:

python manage.py makemigrations
python manage.py migrate

Python相关问答推荐

GL pygame无法让缓冲区与vertextPointer和colorPointer一起可靠地工作

列表上值总和最多为K(以O(log n))的最大元素数

Pandas 在最近的日期合并,考虑到破产

如何使用pandasDataFrames和scipy高度优化相关性计算

使用miniconda创建环境的问题

如何制作10,000年及以后的日期时间对象?

将9个3x3矩阵按特定顺序排列成9x9矩阵

Pandas—在数据透视表中占总数的百分比

如果满足某些条件,则用另一个数据帧列中的值填充空数据帧或数组

如何使用Pandas DataFrame按日期和项目汇总计数作为列标题

Python逻辑操作作为Pandas中的条件

Python脚本使用蓝牙运行在Windows 11与raspberry pi4

在www.example.com中使用`package_data`包含不包含__init__. py的非Python文件

Geopandas未返回正确的缓冲区(单位:米)

下三角形掩码与seaborn clustermap bug

Python OPCUA,modbus通信代码运行3小时后出现RuntimeError

如果不使用. to_list()[0],我如何从一个pandas DataFrame中获取一个值?

如何在Polars中创建条件增量列?

使用美汤对维基百科表格进行网络刮擦未返回任何内容

如何在Django查询集中生成带有值列表的带注释的字段?