Spec-Zone.ru › SQLite

Блокировка файлов и конкурентность в SQLite версии 3

Этот документ был первоначально создан в начале 2004 года, когда SQLite версии 2 ещё широко использовался, и был написан, чтобы представить новые концепции SQLite версии 3 читателям, которые уже были знакомы с SQLite версии 2. Однако в наши дни большинство читателей этого документа, вероятно, никогда не видели SQLite версии 2 и знакомы только с SQLite версии 3. Тем не менее, этот документ продолжает служить авторитетной ссылкой по тому, как работает блокировка файлов базы данных в SQLite версии 3.

В документе описана только блокировка для механизма транзакций более старого режима отката. Блокировка для более нового режима записи вперёд (write-ahead log) или режима WAL описывается отдельно.

1.0 Блокировка файлов и конкурентность в SQLite версии 3

SQLite Версия 3.0.0 ввела новый механизм блокировки и журналирования, предназначенный для повышения конкурентности по сравнению с SQLite версии 2 и для уменьшения проблемы голодания записывающих процессов. Новый механизм также позволяет атомарные фиксации транзакций, включающих несколько файлов базы данных. Этот документ описывает новый механизм блокировки. Целевая аудитория — программисты, желающие понять и/или изменить код пейджера, и рецензенты, работающие над проверкой дизайна SQLite версии 3.

2.0 Обзор

Управление блокировкой и конкурентностью осуществляется модулем пейджера. Модуль пейджера отвечает за обеспечение ACID-свойств SQLite (Атомарность, Согласованность, Изолированность и Долговечность). Модуль пейджера гарантирует, что изменения происходят все сразу, что либо все изменения произошли, либо ни одно из них не произошло, что два или более процесса не пытаются получить доступ к базе данных несовместимыми способами одновременно и что изменения, однажды записанные, сохраняются до явного удаления.

Пейджер не интересуется подробностями B-деревьев, кодировок текста, индексов и т.д. С точки зрения пейджера база данных представляет собой один файл с блоками равного размера. Каждый блок называется "страницей" и обычно имеет размер 1024 байта. Страницы нумеруются, начиная с 1. Таким образом, первые 1024 байта базы данных называются "страницей 1", следующие 1024 байта называются "страницей 2" и так далее. Все остальные детали кодировки обрабатываются верхними уровнями библиотеки. Пейджер взаимодействует с операционной системой, используя один из нескольких модулей (Примеры: os_unix.c, os_win.c), которые обеспечивают унифицированное представление сервисов операционной системы.

Модуль пейджера эффективно контролирует доступ для отдельных потоков, или отдельных процессов, или обоих. На протяжении всего документа, где упоминается слово "процесс", его можно заменить словом "поток", не меняя истинности утверждения.

3.0 Блокировка

С точки зрения одного процесса, файл базы данных может находиться в одном из пяти состояний блокировки:

cellpadding="20">
РАЗБЛОКИРОВАН Нет блокировок на базе данных. База данных не может быть ни прочитана, ни записана. Любые внутренние кэшированные данные считаются подозрительными и подлежат проверке с файлом базы данных перед использованием. Другие процессы могут читать или писать базу данных в соответствии со своими состояниями блокировки. Это состояние по умолчанию.
ОБЩИЙ База данных может быть прочитана, но не записана. Любое количество процессов может иметь блокировки ОБЩИЕ одновременно, следовательно, может быть много одновременных читателей. Но ни один другой поток или процесс не может записать в файл базы данных, пока активна одна или несколько блокировок ОБЩИХ.
ЗАРЕЗЕРВИРОВАН Блокировка ЗАРЕЗЕРВИРОВАН означает, что процесс планирует записать в файл базы данных в какой-то момент в будущем, но в настоящее время он только читает из файла. Только одна блокировка ЗАРЕЗЕРВИРОВАН может быть активна в один момент времени, хотя несколько блокировок ОБЩИХ могут сосуществовать с одной блокировкой ЗАРЕЗЕРВИРОВАН. ЗАРЕЗЕРВИРОВАН отличается от ОЖИДАЮЩЕЙ тем, что новые блокировки ОБЩИЕ могут быть получены, в то время как блокировка ЗАРЕЗЕРВИРОВАН активна.
ОЖИДАЮЩИЙ Блокировка ОЖИДАЮЩИЙ означает, что процесс, удерживающий блокировку, хочет записать в базу данных как можно скорее и просто ждёт, пока все текущие блокировки ОБЩИЕ не исчезнут, чтобы получить эксклюзивную блокировку. Новые блокировки ОБЩИЕ не допускаются к базе данных, если активна блокировка ОЖИДАЮЩИЙ, хотя существующие блокировки ОБЩИЕ разрешены для продолжения.
ЭКСКЛЮЗИВНЫЙ Для записи в файл базы данных требуется эксклюзивная блокировка. Только одна эксклюзивная блокировка разрешена на файле, и никакие другие блокировки любого вида не допускаются для совместного существования с эксклюзивной блокировкой. Для максимальной конкурентности SQLite стремится минимизировать время удержания эксклюзивных блокировок.

Интерфейс уровня операционной системы понимает и отслеживает все пять состояний блокировки, описанных выше. Модуль пейджера отслеживает только четыре из пяти состояний блокировки. Блокировка ОЖИДАЮЩИЙ всегда является просто временной ступенькой на пути к эксклюзивной блокировке, поэтому модуль пейджера не отслеживает блокировки ОЖИДАЮЩИЙ.

4.0 Журнал отката

Когда процесс хочет изменить файл базы данных (и он не работает в режиме WAL), он сначала записывает исходное неизменённое содержимое базы данных в журнал отката. Журнал отката — это обычный дисковый файл, который всегда расположен в той же директории или папке, что и файл базы данных, и имеет то же имя, что и файл базы данных, с добавлением -journal суффикса. Журнал отката также записывает начальный размер базы данных, чтобы при увеличении размера файла базы данных его можно было обрезать обратно до первоначального размера при откате.

Если SQLite работает с несколькими базами данных одновременно (используя команду ATTACH), то каждая база данных имеет свой собственный журнал отката. Но также существует отдельный агрегированный журнал, называемый супер-журналом. Супер-журнал не содержит данных страниц, используемых для отката изменений. Вместо этого супер-журнал содержит имена отдельных журналов отката каждой из баз данных, присоединённых с помощью ATTACH. Каждый из отдельных журналов отката базы данных также содержит имя супер-журнала. Если нет присоединённых баз данных (или если ни одна из присоединённых баз данных не участвует в текущей транзакции), супер-журнал не создаётся, и обычный журнал отката содержит пустую строку в месте, обычно предназначенном для записи имени супер-журнала.

Журнал отката считается горячим, если он должен быть отменён, чтобы восстановить целостность его базы данных. Горячий журнал создаётся, когда процесс находится посредине обновления базы данных, и аварийное завершение программы или операционной системы, или отключение питания, препятствуют завершению обновления. Горячие журналы — это исключительный случай. Горячие журналы существуют для восстановления после сбоев и отключений питания. Если всё работает правильно (то есть, если нет сбоев или отключений питания), вы никогда не получите горячий журнал.

Если супер-журнал не используется, то журнал считается горячим, если он существует, имеет заголовок, отличный от нуля, и соответствующий файл базы данных не имеет блокировки ЗАРЕЗЕРВИРОВАН. Если в файле журнала указан супер-журнал, то файл журнала считается горячим, если супер-журнал существует и на соответствующем файле базы данных нет блокировки ЗАРЕЗЕРВИРОВАН. Важно понимать, когда журнал является горячим, поэтому предыдущие правила будут повторены в пунктах:

  • Журнал является горячим, если…
    • Он существует, и
    • Его размер больше 512 байтов, и
    • Заголовок журнала не равен нулю и правильно сформирован, и
    • Существует его супер-журнал или имя супер-журнала является пустой строкой, и
    • На соответствующем файле базы данных нет блокировки ЗАРЕЗЕРВИРОВАН.

4.1 Работа с горячими журналами

Перед чтением из файла базы данных SQLite всегда проверяет, есть ли у этого файла базы данных горячий журнал. Если файл имеет горячий журнал, то журнал откатается перед чтением файла. Таким образом, мы гарантируем, что файл базы данных находится в согласованном состоянии перед чтением.

Когда процесс хочет прочитать из файла базы данных, он выполняет следующие действия:

  1. Открыть файл базы данных и получить общую блокировку. Если общая блокировка не может быть получена, немедленно завершить выполнение и вернуть SQLITE_BUSY.
  2. Проверить, есть ли у файла базы данных горячий журнал. Если файла нет, мы закончили. Немедленно вернуть результат. Если есть горячий журнал, этот журнал должен быть отменён последующими шагами этого алгоритма.
  3. Получить блокировку ОЖИДАЮЩИЙ, затем эксклюзивную блокировку на файле базы данных. (Примечание: Не получайте блокировку ЗАРЕЗЕРВИРОВАН, потому что это заставит другие процессы думать, что журнал больше не горячий.) Если мы не можем получить эти блокировки, это означает, что другой процесс уже пытается выполнить откат. В этом случае, снять все блокировки, закрыть базу данных и вернуть SQLITE_BUSY.
  4. Прочитать файл журнала и откатить изменения.
  5. Подождать, пока отменённые изменения будут записаны в постоянную память. Это защищает целостность базы данных в случае возникновения другого сбоя или отключения питания.
  6. Удалить файл журнала (или обнулить журнал до нуля байтов, если PRAGMA journal_mode=TRUNCATE установлен, или обнулить заголовок журнала, если PRAGMA journal_mode=PERSIST установлен).
  7. Удалить файл супер-журнала, если это безопасно. Этот шаг является необязательным. Он здесь только для предотвращения появления устаревших супер-журналов на диске. Подробности см. ниже.
  8. Снять эксклюзивные и ожидающие блокировки, но сохранить общую блокировку.

После успешного завершения алгоритма выше, безопасно читать из файла базы данных. После завершения всего чтения общая блокировка снимается.

4.2 Удаление устаревших супер-журналов

Устаревший супер-журнал — это супер-журнал, который больше нигде не используется. Нет требований к удалению устаревших супер-журналов. Единственная причина для этого — освободить дисковое пространство.

Супер-журнал считается устаревшим, если на него не указывают отдельные журналы файлов. Чтобы определить, является ли супер-журнал устаревшим, сначала читаем супер-журнал, чтобы получить имена всех его файлов-журналов. Затем проверяем каждый из этих файлов-журналов. Если какой-либо из файлов-журналов, указанных в супер-журнале, существует и ссылается на супер-журнал, то супер-журнал не устарел. Если все файлы-журналы отсутствуют или ссылаются на другие супер-журналы или вообще не на какой-либо супер-журнал, то проверяемый супер-журнал считается устаревшим и может быть безопасно удалён.

5.0 Запись в файл базы данных

Для записи в базу данных процесс должен сначала получить общую блокировку, как описано выше (возможно, откатив неполные изменения, если есть горячий журнал). После получения общей блокировки должна быть получена блокировка ЗАРЕЗЕРВИРОВАН. Блокировка ЗАРЕЗЕРВИРОВАН сигнализирует о том, что процесс намеревается записать в базу данных в какой-то момент в будущем. Только один процесс за раз может удерживать блокировку ЗАРЕЗЕРВИРОВАН. Но другие процессы могут продолжать читать базу данных, пока блокировка ЗАРЕЗЕРВИРОВАН удерживается.

Если процесс, который хочет записать данные, не может получить блокировку RESERVED, это означает, что другой процесс уже имеет блокировку RESERVED. В этом случае попытка записи завершается неудачей и возвращает SQLITE_BUSY.

Получив блокировку RESERVED, процесс, который хочет записать данные, создает журнал отката. Заголовок журнала инициализируется исходным размером файла базы данных. В заголовке журнала также зарезервировано место для имени супер-журнала, хотя имя супер-журнала изначально пустое.

Прежде чем внести изменения в любую страницу базы данных, процесс записывает исходное содержимое этой страницы в журнал отката. Изменения страниц сначала хранятся в памяти и не записываются на диск. Исходный файл базы данных остается неизменным, что означает, что другие процессы могут продолжать чтение базы данных.

В конечном итоге процесс записи захочет обновить файл базы данных, либо потому, что его кэш памяти заполнился, либо потому, что он готов зафиксировать свои изменения. Прежде чем это произойдет, записывающий процесс должен убедиться, что ни один другой процесс не читает базу данных, и что данные журнала отката безопасно находятся на поверхности диска, чтобы их можно было использовать для отката незавершенных изменений в случае сбоя электропитания. Этапы следующие:

  1. Убедитесь, что все данные журнала отката фактически были записаны на поверхность диска (а не просто хранятся в кэше операционной системы или контроллера диска), чтобы в случае сбоя электропитания данные всё ещё были доступны после восстановления питания.
  2. Получите блокировку PENDING, а затем блокировку EXCLUSIVE на файле базы данных. Если другие процессы все ещё имеют блокировки SHARED, записывающему процессу может потребоваться подождать, пока эти блокировки SHARED не освободятся, прежде чем он сможет получить блокировку EXCLUSIVE.
  3. Запишите все изменения страниц, которые в настоящее время хранятся в памяти, в исходный файл базы данных на диске.

Если причиной записи в файл базы данных является заполнение кэша памяти, то записывающий процесс не зафиксирует изменения сразу. Вместо этого он может продолжить внесение изменений в другие страницы. Перед записью последующих изменений в файл базы данных журнал отката должен быть снова выведен на диск. Обратите также внимание, что блокировка EXCLUSIVE, полученная записывающим процессом для первоначальной записи в базу данных, должна сохраняться до тех пор, пока все изменения не будут зафиксированы. Это означает, что никакие другие процессы не смогут получить доступ к базе данных с момента выгрузки кэша памяти на диск до фиксации транзакции.

Когда записывающий процесс готов зафиксировать свои изменения, он выполняет следующие шаги:

  1. Получите блокировку EXCLUSIVE на файле базы данных и убедитесь, что все изменения в памяти были записаны в файл базы данных с использованием алгоритма шагов 1-3 выше.
  2. Выведите все изменения файла базы данных на диск. Подождите, пока эти изменения фактически будут записаны на поверхность диска.
  3. Удалите файл журнала. (Или, если PRAGMA journal_mode равен TRUNCATE или PERSIST, соответственно обрежьте файл журнала или обнулите заголовок файла журнала.) Это момент, когда изменения фиксируются. Перед удалением файла журнала, если произойдёт сбой электропитания или аварийное завершение работы, следующий процесс, открывающий базу данных, увидит, что у неё есть горячий журнал, и вернёт изменения назад. После удаления журнала горячий журнал больше не будет, и изменения сохранятся.
  4. Снимите блокировки EXCLUSIVE и PENDING с файла базы данных.

Как только блокировка PENDING будет снята с файла базы данных, другие процессы смогут снова начать чтение базы данных. В текущей реализации блокировка RESERVED также снимается, но это не обязательно для корректной работы.

Если транзакция включает несколько баз данных, используется более сложная последовательность фиксации, как показано ниже:

  1. Убедитесь, что все отдельные файлы баз данных имеют блокировку EXCLUSIVE и действительный журнал.
  2. Создайте супер-журнал. Имя супер-журнала произвольное. (В текущей реализации к имени основного файла базы данных добавляются случайные суффиксы, пока не будет найдено имя, которое не существует ранее.) Заполните супер-журнал именами всех отдельных журналов и выведите его содержимое на диск.
  3. Запишите имя супер-журнала во все отдельные журналы (в отведенном для этого месте в заголовках отдельных журналов) и выведите содержимое отдельных журналов на диск, и подождите, пока эти изменения достигнут поверхности диска.
  4. Выведите все изменения файла базы данных на диск. Подождите, пока эти изменения фактически будут записаны на поверхность диска.
  5. Удалите файл супер-журнала. Это момент, когда изменения фиксируются. Перед удалением файла супер-журнала, если произойдёт сбой электропитания или аварийное завершение работы, отдельные файловые журналы будут считаться горячими и будут отменены следующим процессом, который попытается их прочитать. После удаления супер-журнала файловые журналы больше не будут считаться горячими, и изменения сохранятся.
  6. Удалите все отдельные файлы журналов.
  7. Снимите блокировки EXCLUSIVE и PENDING со всех файлов баз данных.

5.1 Голод записывающего процесса

В SQLite версии 2, если много процессов читают данные из базы данных, может быть так, что никогда не наступает момент, когда нет активных читателей. И если на базе данных всегда есть хотя бы одна блокировка чтения, ни один процесс не сможет внести изменения в базу данных, потому что невозможно получить блокировку записи. Такая ситуация называется «голодом записывающего процесса».

SQLite версии 3 стремится избежать голода записывающего процесса с помощью блокировки PENDING. Блокировка PENDING позволяет существующим читателям продолжать работу, но предотвращает подключение новых читателей к базе данных. Таким образом, когда процесс хочет записать в занятую базу данных, он может установить блокировку PENDING, что предотвратит подключение новых читателей. Предполагая, что существующие читатели в конечном итоге завершат работу, все блокировки SHARED в конечном итоге будут сняты, и записывающему процессу будет предоставлена возможность внести свои изменения.

6.0 Как испортить файлы вашей базы данных

Модуль страниц очень надёжен, но его можно обойти. В этом разделе пытаются определить и объяснить риски. (См. также раздел «Возможные проблемы» в статье о Атомной фиксации в статье о Атомной фиксации).

Очевидно, что аппаратный или операционный сбой, который вносит некорректные данные в середину файла базы данных или журнала, вызовет проблемы. Точно так же, если вредоносный процесс откроет файл базы данных или журнал и запишет некорректные данные в середину его, база данных будет повреждена. В отношении таких проблем мало что можно сделать, поэтому они не рассматриваются подробно.

SQLite использует консультативные блокировки POSIX для реализации блокировки в Unix. В Windows он использует системные вызовы LockFile(), LockFileEx() и UnlockFile(). SQLite предполагает, что все эти системные вызовы работают как заявлено. Если это не так, то база данных может быть повреждена. Следует отметить, что консультативные блокировки POSIX известны своими ошибками или даже отсутствием реализации во многих реализациях NFS (включая недавние версии Mac OS X), а также есть сообщения о проблемах блокировки для сетевых файловых систем в Windows. Лучшая защита — не использовать SQLite для файлов на сетевой файловой системе.

SQLite использует системный вызов fsync() для вывода данных на диск в Unix и использует FlushFileBuffers() для того же в Windows. Опять же, SQLite предполагает, что эти службы операционной системы работают как заявлено. Но сообщалось, что fsync() и FlushFileBuffers() не всегда работают корректно, особенно с некоторыми сетевыми файловыми системами или недорогими дисками IDE. По-видимому, некоторые производители дисков IDE имеют контроллеры, которые сообщают, что данные достигли поверхности диска, когда на самом деле данные всё ещё находятся в кэше памяти диска.

Если произойдёт сбой или потеря электропитания и в результате останется горячий журнал, но этот журнал будет удалён, следующий процесс, открывающий базу данных, не будет знать, что она содержит изменения, которые нужно отменить. Отмена не произойдёт, и база данных останется в несогласованном состоянии. Журналы отката могут быть удалены по целому ряду причин:

  • Администратор может очищать данные после сбоя ОС или потери электропитания, видит файл журнала, считает его ненужным и удаляет его.
  • Кто-то (или какой-то процесс) может переименовать файл базы данных, но не сможет переименовать связанный с ним журнал.
  • Если файл базы данных имеет псевдонимы (жёсткие или мягкие ссылки) и файл открывается другим псевдонимом, отличным от того, который использовался для создания журнала, то журнал не будет найден. Чтобы избежать этой проблемы, не следует создавать ссылки на файлы баз данных SQLite.
  • Повреждение файловой системы после потери электропитания может привести к переименованию или удалению журнала.

Последний (четвертый) пункт заслуживает дополнительных комментариев. Когда SQLite создает файл журнала в Unix, он открывает директорию, содержащую этот файл, и вызывает fsync() для директории, пытаясь вывести информацию о директории на диск. Но предположим, что какой-то другой процесс добавляет или удаляет несвязанные файлы в директорию, содержащую базу данных и журнал в момент потери электропитания. Предположительно несвязанные действия этого другого процесса могут привести к тому, что файл журнала будет удалён из директории и перемещен в "lost+found". Это маловероятный сценарий, но он может произойти. Лучшие меры предосторожности — использовать файловую систему с журналированием или разместить базу данных и журнал в отдельной директории.

Если коммит включает несколько баз данных и супержурнал, и различные базы данных находятся на разных томах диска, а во время коммита происходит отключение электропитания, то при включении компьютера диски могут быть смонтированы с разными именами. Или некоторые диски могут вообще не быть смонтированы. В этом случае отдельные файловые журналы и супержурнал могут не найти друг друга. Худший сценарий в этом случае — коммит перестаёт быть атомарным. Некоторые базы данных могут быть откатаны, а другие — нет. Все базы данных будут продолжать оставаться самосогласованными. Чтобы защититься от этой проблемы, сохраняйте все базы данных на одном томе диска и/или монтируйте диски с использованием точно тех же имён после отключения электропитания.

7.0 Управление транзакциями на уровне SQL

Изменения в блокировке и управлении параллельностью в SQLite версии 3 также вносят некоторые тонкие изменения в работу транзакций на уровне языка SQL. По умолчанию SQLite версии 3 работает в режиме автокоммита. В режиме автокоммита все изменения в базе данных комми тятся, как только завершатся все операции, связанные с текущим соединением с базой данных.

Команда SQL «BEGIN TRANSACTION» (ключевое слово TRANSACTION необязательно) используется для перевода SQLite из режима автокоммита. Обратите внимание, что команда BEGIN не приобретает никаких блокировок на базе данных. После команды BEGIN при выполнении первого оператора SELECT будет получена блокировка SHARED. Блокировка RESERVED будет получена при выполнении первого оператора INSERT, UPDATE или DELETE. Эксклюзивная блокировка (EXCLUSIVE) не приобретается до тех пор, пока кэш памяти не заполнится и не потребуется его выгрузка на диск или пока транзакция не будет завершена. Таким образом, система откладывает блокировку доступа для чтения к файлу до последнего возможного момента.

Команда SQL «COMMIT» фактически не коммитит изменения на диск. Она просто включает режим автокоммита. Затем, по завершении команды, срабатывает обычная логика автокоммита, вызывающая фактический коммит на диск. Команда SQL «ROLLBACK» также работает путём включения автокоммита, но также устанавливает флаг, который сообщает логике автокоммита откатить, а не коммитить.

Если команда SQL COMMIT включает автокоммит, а логика автокоммита затем пытается зафиксировать изменения, но терпит неудачу, поскольку другой процесс удерживает блокировку SHARED, то автокоммит автоматически отключается. Это позволяет пользователю повторно выполнить команду COMMIT в более позднее время, когда блокировка SHARED будет иметь возможность освободиться.

SQLite is in the Public Domain.
https://sqlite.org/lockingv3.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API