select_for_update 是否看到另一个 select_for_update 事务在解锁后添加的行?

Does select_for_update see rows added by another select_for_update transaction after it unblocks?

我想创建一个 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 中,它说:

Usually, if another transaction has already acquired a lock on one of the selected rows, the query will block until the lock is released.

因此,对于我的示例,我认为这意味着第二个进程将重新运行查询最大 ID,一旦它解锁并获得 11。但我不确定我在解释没错。

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

不,我认为这行不通。

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

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

It is possible for an updating command to see an inconsistent snapshot: it can see the effects of concurrent updating commands on the same rows it is trying to update, but it does not see effects of those commands on other rows in the database.

所以 10 是将要返回的内容。

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

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

原因是为了这次调用:

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

Django 生成的 SQL 和针对数据库的 运行s 实际上是:

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

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

意思是,查询在 运行 两次都扫描了所有行。意思是它第二次 运行s,它将选择新行并相应地 return。

我了解到这种现象称为 "phantom read",这是可能的,因为我的数据库的隔离级别是 REPEATABLE-READ


@KevinChristopherHenry "The issue is that the query is not rerun after the lock is released; the rows have already been selected" 你确定它是这样工作的吗?为什么 READ COMMITTED 暗示 select 在释放锁后没有 运行?我认为隔离级别定义了查询在 运行 时看到的数据快照,而不是 ~when~ 查询是 运行。在我看来, select 发生在锁释放之前还是之后与隔离级别正交。根据定义,阻塞查询在解除阻塞之前不会 select 行吗?

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

P.S。我终于真正阅读了您阅读的 "undesirable results" 文档,我理解了您的观点 - 在该示例中,它似乎忽略了未预先 select 的行,因此得出的结论是我的第二个查询不会选择新行。但我在 shell 中进行了测试,结果确实如此。现在我不知道该怎么做。