Я хочу создать модель с идентификатором, равным текущему наибольшему идентификатору этой модели плюс один (например, автоинкремент). Я рассматриваю возможность сделать это с помощью select_for_update, чтобы гарантировать отсутствие условий гонки для текущего наибольшего идентификатора, например:
with transaction.atomic():
greatest_id = MyModel.objects.select_for_update().order_by('id').last().id
MyModel.objects.create(id=greatest_id + 1)
Но мне интересно, если два процесса попытаются запустить это одновременно, как только второй разблокируется, увидит ли он новый наибольший идентификатор, вставленный первым процессом, или он все еще увидит старый наибольший идентификатор?
Например, предположим, что текущий наибольший идентификатор равен 10. Два процесса идут на создание новой модели. Первый блокирует ID 10. Затем второй блокирует, потому что 10 заблокирован. Первый вставляет 11 и разблокирует 10. Затем второй разблокирует, и теперь он увидит 11, вставленный первым, как наибольший, или он все еще увидит 10, потому что это строка, на которой он заблокировал?
В select_for_update документы сказано:
Usually, if another transaction has already acquired a lock on one of the selected rows, the query will block until the lock is released.
Так что для моего примера, я думаю, это означает, что второй процесс повторно запустит запрос для наибольшего идентификатора, как только он разблокируется и получит 11. Но я не уверен, что интерпретирую это правильно.
Примечание. Я использую MySQL для базы данных.
Да, я не ожидаю частых столкновений, поэтому я действительно планирую использовать это. Большое вам спасибо за вашу помощь.






Нет, я не думаю, что это сработает.
Во-первых, позвольте мне отметить, что вы должны обязательно проверить документацию по базе данных, которую вы используете, так как между базами данных есть много тонких различий, которые не отражены в документации Django.
Используя Документация по PostgreSQL в качестве ориентира, проблема заключается в том, что на уровне изоляции по умолчанию 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 это то, что будет возвращено.
Я не думаю, что это правильно, потому что в моем конкретном случае SQL django генерирует запросы для всех строк в MyModel, а не только для одной. Таким образом, несмотря на то, что второй запрос увидит несогласованный снимок, он все равно будет сканировать все строки, а не только одну, и действительно выберет новую ~потому что~ снимок несогласован. Как вы думаете?
@MichaelHarvey: проблема в том, что запрос не выполняется повторно после снятия блокировки; строки уже выбраны. Посмотрите в документации, на которую я ссылаюсь, пример «нежелательных результатов в режиме Read Committed». Если бы вы были правы в том, что запрос выполняется повторно, результаты будут отличаться от описанных.
мой ответ слишком длинный, чтобы печатать здесь, поэтому я добавил его в конец своего ответа. Пожалуйста, дайте мне знать, что вы думаете, если вы заинтересованы в продолжении этого обсуждения. Спасибо за ваш вклад.
Я только что перепроверил это, получив блокировку и выполнив вставку из оболочки, и попытался сделать select_for_update, а затем вставить из приложения django. Приложение не удалось, потому что оно не прочитало новый идентификатор и попыталось вставить старый идентификатор. Ваш ответ правильный.
После некоторого расследования я считаю, что это будет работать так, как задумано.
Причина в том, что для этого вызова:
MyModel.objects.select_for_update().order_by('id').last().id
SQL Django генерирует и работает с базой данных на самом деле:
SELECT ... FROM MyModel ORDER BY id ASC FOR UPDATE;
(вызов last() происходит только после того, как набор запросов уже был оценен.)
Это означает, что запрос сканирует все строки при каждом запуске. Это означает, что при втором запуске он подберет новую строку и вернет ее соответствующим образом.
Я узнал, что это явление называется «фантомное чтение» и возможно, потому что уровень изоляции моей БД REPEATABLE-READ.
@KevinChristopherHenry «Проблема в том, что запрос не выполняется повторно после снятия блокировки; строки уже выбраны». Вы уверены, что это так? Почему READ COMMITTED подразумевает, что выбор не выполняется после снятия блокировки? Я думал, что уровень изоляции определяет, какой моментальный снимок данных видит запрос при его выполнении, а не ~когда~ запускается запрос. Мне кажется, что выбор происходит до или после снятия блокировки, ортогонален уровню изоляции. И по определению, заблокированный запрос не выбирает строки до тех пор, пока он не будет разблокирован?
Что бы это ни стоило, я попытался проверить это, открыв два отдельных подключения к моей базе данных в оболочке и выполнив несколько запросов. В первом я начал транзакцию и получил блокировку «выбрать * из заказа MyModel по идентификатору для обновления». Затем, во втором, я сделал то же самое, вызвав блокировку выбора. Затем, вернувшись к первому, я вставил новую строку и зафиксировал транзакцию. Затем во втором запрос разблокировался и вернул новую строку. Это заставляет меня думать, что моя гипотеза верна.
P.S. Я, наконец, действительно прочитал документацию о «нежелательных результатах», которую вы читали, и я понимаю вашу точку зрения - в этом примере похоже, что он игнорирует строки, которые не были предварительно выбраны, так что это указывает на вывод, что мой второй запрос не выберет вверх новый ряд. Но я проверил в оболочке, и это сработало. Теперь я не уверен, что с этим делать.
Вы, вероятно, не используете REPEATABLE READ. Это значение MySQL по умолчанию, но Django устанавливает для своих транзакций значение READ COMMITTEDпо умолчанию. Это можно переопределить в настройках, но вы, безусловно, должны быть осторожны с этим: «Django лучше всего работает с фиксированным чтением по умолчанию, а не с повторяемым чтением MySQL по умолчанию. При повторяемом чтении возможна потеря данных».
Я проверил настройки базы данных и подтвердил, что это ПОВТОРЯЕМОЕ ЧТЕНИЕ. Я не являюсь первоначальным владельцем этого Db, поэтому я предполагаю, что кто-то другой, должно быть, отменил его. Я посмотрю, является ли его изменение хорошим вариантом. Спасибо за информацию.
Не имеет значения, какой параметр базы данных, потому что он может быть установлен клиентом. И каждый раз, когда Django открывает соединение, он устанавливает уровень изоляции, указанный в настройках (или, по умолчанию, READ COMMITTED). См. здесь.
Возможно, разница в том, что мой ответ основан на документации PostgreSQL, а ваш тест — на MySQL. Их документация не содержит достаточно подробностей, чтобы я мог их рассказать.
О, в таком случае я, вероятно, использую READ COMMITTED, потому что я не вижу явно установленного уровня изоляции в файле настроек.
В качестве альтернативы рассмотрите возможность использования оптимистического параллелизма. См. мой ответ здесь для более подробной информации. Как я сказал там: «Такой подход может работать очень хорошо, если столкновения редки, и очень плохо, если они часты».