【问题标题】:Does select_for_update see rows added by another select_for_update transaction after it unblocks?select_for_update 是否会看到另一个 select_for_update 事务在解除阻塞后添加的行?
【发布时间】:2019-07-28 17:32:00
【问题描述】:

我想创建一个模型,其 ID 等于该模型的当前最大 ID 加一(如自动增量)。我正在考虑使用select_for_update 执行此操作,以确保当前最大 ID 没有竞争条件,如下所示:

with transaction.atomic():
    greatest_id = MyModel.objects.select_for_update().order_by('id').last().id
    MyModel.objects.create(id=greatest_id + 1)

但我想知道,如果两个进程尝试同时运行它,一旦第二个进程解除阻塞,它会看到第一个进程插入的新的最大 ID,还是仍会看到旧的最大 ID?

例如,假设当前最大的 ID 是 10。两个进程去创建一个新模型。第一个锁定 ID 10。然后第二个阻塞,因为 10 被锁定。第一个插入 11 并解锁 10。然后,第二个解除阻塞,现在它会看到第一个插入的 11 是最大的,还是仍然会看到 10,因为那是它阻塞的行?

在 select_for_update docs 中,它说:

通常,如果另一个事务已经在其中一个选定的行上获得了锁,则查询将阻塞,直到锁被释放。

因此,对于我的示例,我认为这意味着第二个进程将在解除阻塞并获得 11 后重新运行最大 ID 的查询。但我不确定我的解释是否正确。

注意:我使用 MySQL 作为数据库。

【问题讨论】:

  • 作为替代方案,考虑使用乐观并发。有关更多详细信息,请参阅my answer here。正如我在那里所说:“如果碰撞很少,这种方法可以很好地工作,如果碰撞频繁,这种方法就很糟糕。”
  • 是的,我不希望经常发生碰撞,所以我实际上打算使用它。非常感谢您的帮助。

标签: mysql django django-models django-queryset django-orm


【解决方案1】:

编辑:我对这个答案的理解是错误的,只是为了文档的缘故留下它,以防我想回到它。

经过一番调查,我相信这会按预期工作。

原因是为了这个调用:

MyModel.objects.select_for_update().order_by('id').last().id

Django 生成并针对 db 运行的 SQL 实际上是:

SELECT ... FROM MyModel ORDER BY id ASC FOR UPDATE;

(对last() 的调用仅在查询集已被评估后发生。)

意思是,查询在两次运行时都会扫描所有行。这意味着它第二次运行时,它将拾取新行并相应地返回。

我了解到这种现象被称为“幻读”,并且是可能的,因为我的数据库的隔离级别是REPEATABLE-READ


@KevinChristopherHenry “问题是在释放锁后查询没有重新运行;行已经被选中”你确定它是这样工作的吗?为什么 READ COMMITTED 意味着释放锁后选择不运行?我认为隔离级别定义了查询在运行时看到的数据快照,而不是~何时~查询运行。在我看来,选择是在释放锁之前还是之后发生与隔离级别正交。而且根据定义,阻塞查询不是在解除阻塞之前不会选择行吗?

对于它的价值,我尝试通过在 shell 中打开两个到我的数据库的单独连接并发出一些查询来测试这一点。首先,我开始了一个事务,并获得了一个锁'select * from MyModel order by id for update'。然后,在第二个中,我做了同样的事情,导致选择被阻止。然后回到第一个,我插入了一个新行,并提交了事务。然后在第二个中,查询解除阻塞,并返回新行。这让我觉得我的假设是正确的。

附:我终于实际上阅读了您阅读的“不良结果”文档,并且我明白了您的观点-在该示例中,它似乎忽略了未预选的行,因此这表明我的第二个查询不会选择向上新行。但是我在shell中进行了测试,它确实做到了。现在我不知道该怎么做。

【讨论】:

  • 您可能没有使用REPEATABLE READ。这是 MySQL 的默认设置,但 Django 将其事务设置为 READ COMMITTED by default。可以在设置中覆盖它,但您当然应该对此保持谨慎:“Django 最适合并默认读取已提交,而不是 MySQL 的默认可重复读取。可重复读取可能会丢失数据。”
  • 我检查了数据库设置并确认它是可重复读取的。我不是这个 Db 的原始所有者,所以我认为其他人一定已经覆盖了它。我会研究改变它是否是一个好的选择。感谢您提供信息。
  • 数据库设置是什么无关紧要,因为这可以由客户端设置。每次 Django 打开一个连接时,它都会将隔离级别设置为设置中指定的级别(或者,默认情况下,READ COMMITTED)。见here
  • 或许不同的是,我的答案是基于PostgreSQL文档,而你的测试是用MySQL。他们的documentation 没有足够的细节让我知道。
  • 哦,在那种情况下,我可能会使用 READ COMMITTED,因为我没有看到设置文件中明确设置了隔离级别。
【解决方案2】:

不,我认为这行不通。

首先,请注意,您绝对应该检查您正在使用的数据库的文档,因为 Django 文档中未包含数据库之间的许多细微差别。

使用PostgreSQL documentation 作为指导,问题在于,在默认的READ COMMITTED 隔离级别下,被阻止的查询将不会重新运行。当第一个事务提交时,被阻塞的事务将能够看到对该行的更改,但无法看到已添加的新行。

更新命令可能会看到不一致的快照:它可以看到并发更新命令对其尝试更新的同一行的影响,但看不到这些命令对数据库中其他行的影响.

所以10 将被返回。

【讨论】:

  • 我认为这是不对的,因为在我的特殊情况下,SQL django 会为 MyModel 中的所有行生成查询,而不仅仅是一个。因此,虽然第二个查询确实会看到一个不一致的快照,但它仍然会扫描所有行,而不仅仅是一个行,并且确实会选择新的行~因为~快照是不一致的。你怎么看?
  • @MichaelHarvey:问题是锁释放后查询没有重新运行;行已被选中。在我链接到的文档中查看“读取提交模式下的不良结果”示例。如果重新运行查询是正确的,那么结果将与描述的不同。
  • 我的回复太长,无法在此处输入,因此我将其添加到我的答案底部。如果您有兴趣继续讨论,请告诉我您的想法。感谢您的意见。
  • 我刚刚通过获取锁并从 shell 插入并尝试执行 select_for_update 然后从 django 应用程序插入来重新测试了这一点。该应用程序失败,因为它没有读取新 ID,并尝试插入旧 ID。你的答案是正确的。
猜你喜欢
  • 2019-02-26
  • 1970-01-01
  • 2014-10-16
  • 2014-08-05
  • 2021-09-30
  • 1970-01-01
  • 1970-01-01
  • 2020-12-21
  • 2021-05-30
相关资源
最近更新 更多