Могут ли несколько потоков вызывать повторяющиеся обновления в ограниченном наборе?
В Postgres, если я запускаю следующее утверждение
update table set col = 1 where col = 2
По умолчанию READ COMMITTED
на уровне изоляции от нескольких одновременных сессий я гарантирую, что:
- В случае одного совпадения только 1 поток получит ROWCOUNT, равный 1 (что означает, что только один поток пишет)
- В случае множественного совпадения только 1 поток получит ROWCOUNT > 0 (то есть, только один поток записывает пакет)
1 ответ
Ваши заявленные гарантии применяются в этом простом случае, но не обязательно в чуть более сложных запросах. Смотрите конец ответа для примеров.
Простой случай
Предполагая, что col1 уникален, имеет ровно одно значение "2" или имеет стабильный порядок, поэтому каждый UPDATE
соответствует тем же строкам в том же порядке:
Что произойдет для этого запроса, так это то, что потоки найдут строку с col=2 и все попытаются захватить блокировку записи для этого кортежа. Точно один из них добьется успеха. Другие будут блокировать ожидание транзакции первого потока для фиксации.
Этот первый tx будет писать, фиксировать и возвращать количество строк, равное 1. Коммит снимет блокировку.
Другие тэкс снова попытаются захватить замок. Один за другим они добьются успеха. Каждая транзакция, в свою очередь, проходит следующий процесс:
- Получите блокировку записи на оспариваемом кортеже.
- Перепроверить
WHERE col=2
состояние после получения блокировки. - Повторная проверка покажет, что условие больше не соответствует, поэтому
UPDATE
пропустит этот ряд. -
UPDATE
не имеет других строк, поэтому он сообщит об обновлении нулевых строк. - Фиксация, освобождение блокировки для следующей передачи, пытаясь завладеть ею.
В этом простом случае блокировка на уровне строк и повторная проверка условия эффективно сериализуют обновления. В более сложных случаях не так уж и много.
Вы можете легко продемонстрировать это. Открой, скажем, четыре сессии PSQL. Во-первых, заблокируйте стол с BEGIN; LOCK TABLE test;
* В остальных сессиях запускается идентично UPDATE
s - они заблокируют блокировку уровня таблицы. Теперь снимите блокировку COMMIT
тинг ваш первый сеанс. Смотреть их гонки. Только один сообщит о числе строк, равном 1, остальные сообщат о 0. Это легко автоматизировать и создавать сценарии для повторения и увеличения количества подключений / потоков.
Чтобы узнать больше, прочитайте правила для одновременной записи, стр. 11 проблем параллелизма в PostgreSQL, а затем прочитайте остальную часть этой презентации.
А если col1 не уникален?
Как отметил Кевин в комментариях, если col
не является уникальным, поэтому вы можете сопоставить несколько строк, а затем различные исполнения UPDATE
могли бы получить разные заказы. Это может произойти, если они выбирают разные планы (скажем, один через PREPARE
а также EXECUTE
а другой прямой, или ты балуешься с enable_
GUCs) или если план, который они все используют, использует нестабильный вид равных значений. Если они получают строки в другом порядке, то tx1 заблокирует один кортеж, tx2 заблокирует другой, а затем они попытаются получить блокировки на уже заблокированных кортежах друг друга. PostgreSQL прервет работу одного из них, за исключением тупика. Это еще одна веская причина, по которой весь код вашей базы данных всегда должен быть готов повторить транзакции.
Если вы осторожны, чтобы убедиться, что одновременно UPDATE
s всегда получают одинаковые строки в одном и том же порядке, вы все еще можете положиться на поведение, описанное в первой части ответа.
К сожалению, PostgreSQL не предлагает UPDATE ... ORDER BY
поэтому убедиться, что ваши обновления всегда выбирают одни и те же строки в одном и том же порядке, не так просто, как хотелось бы. SELECT ... FOR UPDATE ... ORDER BY
с последующим отдельным UPDATE
часто самый безопасный.
Более сложные запросы, системы очередей
Если вы выполняете запросы с несколькими этапами, включающими несколько кортежей или другие условия, кроме равенства, вы можете получить удивительные результаты, которые отличаются от результатов последовательного выполнения. В частности, параллельные запуски чего-либо вроде:
UPDATE test SET col = 1 WHERE col = (SELECT t.col FROM test t ORDER BY t.col LIMIT 1);
или другие попытки создать простую систему "очередей" не будут работать так, как вы ожидаете. См. Документацию PostgreSQL по параллелизму и эту презентацию для получения дополнительной информации.
Если вы хотите, чтобы рабочая очередь поддерживалась базой данных, есть хорошо протестированные решения, которые справляются со всеми неожиданно сложными угловыми случаями. Одним из самых популярных является PgQ. По этой теме есть полезная статья на PgCon, а поиск в Google по запросу "postgresql queue" полон полезных результатов.
* Кстати, вместо LOCK TABLE
ты можешь использовать SELECT 1 FROM test WHERE col = 2 FOR UPDATE;
чтобы получить блокировку записи только для этого кортежа. Это блокирует обновления против него, но не блокирует записи в другие кортежи или блокирует любые чтения. Это позволяет вам моделировать различные виды проблем параллелизма.