Как SQLite предотвращает взаимоблокировки с отложенными транзакциями? - PullRequest
0 голосов
/ 24 апреля 2019

Согласно документации по отложенным операциям:

Поведение транзакции по умолчанию отложено. (...) Первая операция чтения для базы данных создает блокировку SHARED и первая операция записи создает зарезервированную блокировку.

Также в соответствии с документацией на замках:

Любое количество процессов может одновременно удерживать блокировки SHARED (...) Только одна зарезервированная блокировка может быть активна одновременно, хотя несколько Совместно используемые замки могут сосуществовать с одним зарезервированным замком

Это звучит как блокировка нескольких читателей / писателей с произвольным механизмом продвижения читателя к писателю, который, как известно, представляет собой тупиковую опасность:

  • A начинает транзакцию
  • B начинает транзакцию
  • A получает общий ключ и читает что-то
  • B получает общий замок и читает что-то
  • A получает зарезервированный замок и готовится что-то написать. Он не может писать, пока есть другие общие блокировки, поэтому он блокируется.
  • B хочет написать, поэтому пытается зарезервировать блокировку. Уже есть другая ЗАБРОНИРОВАННАЯ блокировка, поэтому она блокируется до тех пор, пока не будет отпущена, все еще удерживая блокированную блокировку.
  • Тупик.

Так как SQLite справляется с этим? Мне на ум приходят два возможных решения, но оба они, похоже, нарушают саму идею транзакции:

  • Потенциальные авторы снимают ОБМЕННЫЕ замки перед приобретением ЗАБРОНИРОВАНО. Это нарушит атомарность между чтением и записью.
  • B не блокируется при попытке зарезервировать блокировку, но из-за ошибок. Это будет означать, что все чтения необходимо будет повторить, что значительно усложнит использование API.

Я что-то упустил? Как SQLite справляется с этим? Почему этот, казалось бы, опасный тип транзакции используется по умолчанию?

1 Ответ

0 голосов
/ 24 апреля 2019

Простым методом проб и ошибок я обнаружил, что они пошли по пути ошибки.

В данном сценарии, когда B пытается взять RESERVED, он сначала будет ждать PRAGMA busy_timeout миллисекунды. Тогда он сообщит Error: database is locked. Транзакция все еще будет активной, поэтому возможна немедленная повторная попытка.

Если A впоследствии попытается COMMIT (или если у него закончится кэш в памяти), он возьмет блокировку PENDING (предотвращая дополнительные блокировки SHARED), а затем дождется EXCLUSIVE. Если некоторые блокировки SHARED остаются через PRAGMA busy_timeout миллисекунд, он выдаст сообщение Ошибка: база данных заблокирована . Транзакция будет по-прежнему активной, поэтому возможна немедленная повторная попытка.

Другими словами, используемый механизм предотвращения тупиковых ситуаций - это тайм-аут. Однако для этого требуется, чтобы пользователи API сотрудничали, откатившись и повторив попытку.

В качестве ориентира:

  • Используйте только BEGIN TRANSACTION (или явно BEGIN DEFERRED TRANSACTION), когда ожидаете только чтения. Возможно, запись может завершиться неудачей, что приведет к откату и повторной попытке всей транзакции.
  • Используйте BEGIN IMMEDIATE TRANSACTION, когда вы ожидаете, возможно, написать в какой-то момент. Это заблокирует всех других писателей и всех других непосредственных, возможно, писателей.
  • BEGIN EXCLUSIVE TRANSACTION немедленно заблокируется, пока не будут сняты все другие блокировки. Я понятия не имею, почему кто-то хотел бы этого. Возможно, чтобы подготовиться к некоторым данным, которые должны быть записаны на диск как можно быстрее после их поступления? РЕДАКТИРОВАТЬ: Кажется, это единственный способ предотвратить тайм-ауты в произвольных точках после начала транзакции.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...