- Обзор механизма блокировок
- Пять состояний блокировки
- Журнал отката
- Работа с горячими журналами
- Удаление устаревших супер-журналов
- Запись в файл базы данных
- Голодание процессов записи и как SQLite его решает
- Как повредить файлы базы данных
- Управление транзакциями на уровне SQL
- Типичные ошибки при работе с блокировками
- Часто задаваемые вопросы о блокировках SQLite
Обзор механизма блокировок
SQLite версии 3.0.0 представил новый механизм блокировки и журналирования, который значительно повышает параллельный доступ, решая проблему записи, подверженной голоданию. Этот механизм также поддерживает атомарную фиксацию транзакций, охватывающих несколько файлов баз данных. Документ ориентирован на программистов, стремящихся понять или изменить код модуля pager (диспетчера страниц), а также рецензентов, анализирующих конструкцию SQLite версии 3.

Блокировка и управление параллельным доступом обрабатываются модулем pager, который отвечает за обеспечение свойств ACID в SQLite (атомарность, согласованность, изолированность, долговечность). Модуль pager обеспечивает одновременное применение изменений, гарантируя, что либо все изменения применяются, либо не применяется ничего в случае конфликта.
Он также следит за тем, чтобы несколько процессов не взаимодействовали с базой данных с конфликтами доступа и чтобы изменения сохранялись до явного удаления. Модуль pager также предоставляет кэш в памяти для части содержимого дискового файла.
Модуль pager не занимается деталями B-деревьев, кодировок текста, индексов и тому подобного. С точки зрения модуля pager база данных состоит из единственного файла с блоками одинакового размера. Каждый блок называется «страницей» и обычно имеет размер 1024 байта. Страницы нумеруются начиная с 1: первые 1024 байта базы данных называются «страницей 1», вторые 1024 байта — «страницей 2» и так далее.
Все прочие детали кодирования обрабатываются вышележащими слоями библиотеки. Модуль pager взаимодействует с операционной системой посредством одного из нескольких модулей (например: os_unix.c, os_win.c), которые предоставляют унифицированную абстракцию для сервисов операционной системы.
Модуль pager управляет доступом как для потоков, так и для процессов, обеспечивая гибкость использования. На протяжении текста под словом «процесс» подразумевается также и «поток», что не меняет сути.
По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы.
По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы.
Пять состояний блокировки
С точки зрения отдельного процесса файл базы данных может находиться в одном из пяти состояний блокировки.
UNLOCKED — на базе данных не удерживается никаких блокировок. База данных не может быть ни прочитана, ни записана. Любые внутренне кэшированные данные считаются ненадёжными и подлежат проверке по файлу базы данных перед использованием. Другие процессы могут читать или записывать базу данных в соответствии со своими собственными состояниями блокировки. Это состояние по умолчанию.
SHARED — база данных может быть прочитана, но не записана. Любое количество процессов может одновременно удерживать блокировки SHARED, поэтому возможно множество одновременных читателей. Однако ни один другой поток или процесс не может выполнять запись в файл базы данных, пока активна одна или более блокировок SHARED.
RESERVED — блокировка RESERVED означает, что процесс планирует выполнить запись в файл базы данных в какой-то момент в будущем, но в настоящее время только читает из файла. В один момент времени может быть активна только одна блокировка RESERVED, хотя несколько блокировок SHARED могут сосуществовать с одной блокировкой RESERVED. RESERVED отличается от PENDING тем, что новые блокировки SHARED могут быть получены при наличии блокировки RESERVED.
PENDING — блокировка PENDING означает, что процесс, удерживающий блокировку, хочет выполнить запись в базу данных как можно скорее и лишь ожидает снятия всех текущих блокировок SHARED, чтобы получить блокировку EXCLUSIVE. Никакие новые блокировки SHARED не допускаются для базы данных, если активна блокировка PENDING, хотя существующие блокировки SHARED могут продолжать действовать.
EXCLUSIVE — блокировка EXCLUSIVE необходима для выполнения записи в файл базы данных. На файле допускается только одна блокировка EXCLUSIVE, и никакие другие блокировки любого вида не могут сосуществовать с блокировкой EXCLUSIVE. Для максимизации параллельного доступа SQLite стремится минимизировать время удержания блокировок EXCLUSIVE.
Уровень интерфейса с операционной системой понимает и отслеживает все пять описанных выше состояний блокировки. Модуль pager отслеживает только четыре из пяти состояний блокировки. Блокировка PENDING всегда является лишь временным промежуточным шагом на пути к блокировке EXCLUSIVE, поэтому модуль pager не отслеживает блокировки PENDING.
Журнал отката
Когда процесс может изменить файл базы данных (если не используется режим WAL — Write-Ahead Logging), оригинальное содержимое базы данных сначала записывается в журнал отката. Этот журнал представляет собой обычный файл на диске, размещенный в том же каталоге, что и база данных, и имеет то же имя, но с добавлением суффикса -journal.
Журнал фиксирует первоначальный размер базы данных, чтобы в случае увеличения можно было вернуть его к исходному значению при откате.
Если SQLite работает с несколькими базами данных (с помощью команды ATTACH), каждая из них имеет свой собственный журнал отката. Однако также существует агрегированный журнал, названный супер-журналом. Супер-журнал не хранит данные страниц для отката; вместо этого он содержит имена отдельных журналов отката для каждой подключённой базы. Каждый из журналов также указывает на имя супер-журнала.
Если подключённых баз данных нет, супер-журнал не создаётся, и обычный журнал отката остаётся пустым.
Журнал отката называется горячим, если его необходимо откатить для восстановления целостности базы данных. Горячий журнал создаётся, когда процесс находится в середине обновления базы данных, а сбой программы, операционной системы или отключение питания не позволяют завершить обновление. Горячие журналы — это исключительная ситуация: они существуют для восстановления после сбоев и отключений питания. Если всё работает корректно, горячий журнал никогда не возникнет.
Если супер-журнал не задействован, то журнал является горячим, если он существует, имеет ненулевой заголовок, а соответствующий файл базы данных не имеет блокировки RESERVED. Если в файловом журнале указан супер-журнал, то файловый журнал является горячим, если его супер-журнал существует и на соответствующем файле базы данных нет блокировки RESERVED. Журнал является горячим, если выполняются все следующие условия:
- он существует,
- его размер превышает 512 байт,
- заголовок журнала ненулевой и корректно сформирован,
- его супер-журнал существует, или имя супер-журнала является пустой строкой,
- на соответствующем файле базы данных нет блокировки
RESERVED.
Работа с горячими журналами
Перед чтением из файла базы данных SQLite всегда проверяет, есть ли у этого файла горячий журнал. Если у файла есть горячий журнал, то журнал откатывается до того, как файл будет прочитан. Таким образом обеспечивается согласованное состояние файла базы данных перед его чтением.
Когда процесс хочет прочитать данные из файла базы данных, он выполняет следующую последовательность шагов:
- Открыть файл базы данных и получить блокировку
SHARED. Если блокировкуSHAREDполучить невозможно, немедленно завершить работу с ошибкой и вернутьSQLITE_BUSY. - Проверить, есть ли у файла базы данных горячий журнал. Если горячего журнала нет, работа завершена. Если горячий журнал есть, он должен быть откатан последующими шагами этого алгоритма.
- Получить блокировку
PENDING, а затем блокировкуEXCLUSIVEна файл базы данных. (Не следует получать блокировкуRESERVED, так как это заставит другие процессы думать, что журнал больше не является горячим.) Если получить эти блокировки не удаётся, это означает, что другой процесс уже пытается выполнить откат. В этом случае снять все блокировки, закрыть базу данных и вернутьSQLITE_BUSY. - Прочитать файл журнала и откатить изменения.
- Дождаться записи откатанных изменений на постоянное хранилище. Это защищает целостность базы данных на случай ещё одного отключения питания или сбоя.
- Удалить файл журнала (или усечь журнал до нулевой длины, если установлен
PRAGMA journal_mode=TRUNCATE, или обнулить заголовок журнала, если установленPRAGMA journal_mode=PERSIST). - Удалить файл супер-журнала, если это безопасно. Этот шаг необязателен и нужен только для того, чтобы устаревшие супер-журналы не засоряли дисковое пространство.
- Снять блокировки
EXCLUSIVEиPENDING, но сохранить блокировкуSHARED.
После успешного завершения приведённого выше алгоритма чтение из файла базы данных становится безопасным. После завершения всего чтения блокировка SHARED снимается.
Удаление устаревших супер-журналов
Устаревший супер-журнал — это супер-журнал, который больше не используется ни для каких целей. Удалять устаревшие супер-журналы не обязательно. Единственная причина для этого — освобождение дискового пространства.
Супер-журнал является устаревшим, если ни один отдельный файловый журнал не ссылается на него. Чтобы определить, является ли супер-журнал устаревшим, сначала читается супер-журнал для получения имён всех его файловых журналов. Затем каждый из этих файловых журналов проверяется.
Если хотя бы один из файловых журналов, перечисленных в супер-журнале, существует и ссылается обратно на супер-журнал, то супер-журнал не является устаревшим. Если все файловые журналы либо отсутствуют, либо ссылаются на другие супер-журналы или вообще не ссылаются ни на какой супер-журнал, то проверяемый супер-журнал является устаревшим и может быть безопасно удалён.
Запись в файл базы данных
Чтобы выполнить запись в базу данных, процесс должен сначала получить блокировку SHARED, как описано выше (при необходимости откатив незавершённые изменения при наличии горячего журнала). После получения блокировки SHARED необходимо получить блокировку RESERVED. Блокировка RESERVED сигнализирует о том, что процесс намерен выполнить запись в базу данных в какой-то момент в будущем.
Только один процесс одновременно может удерживать блокировку RESERVED, при этом другие процессы могут продолжать читать базу данных.
Если процесс, желающий выполнить запись, не может получить блокировку RESERVED, это означает, что другой процесс уже держит блокировку RESERVED. В этом случае попытка записи завершается неудачей и возвращается SQLITE_BUSY.
После получения блокировки RESERVED процесс, желающий выполнить запись, создаёт журнал отката. Заголовок журнала инициализируется исходным размером файла базы данных. В заголовке журнала также резервируется место для имени супер-журнала, хотя изначально имя супер-журнала пустое.
Перед внесением изменений в любую страницу базы данных процесс записывает исходное содержимое этой страницы в журнал отката. Изменения страниц сначала хранятся в памяти и не записываются на диск. Исходный файл базы данных остаётся неизменным, что означает, что другие процессы могут продолжать читать базу данных.
В конечном счёте процесс записи захочет обновить файл базы данных — либо потому, что его кэш памяти заполнился, либо потому, что он готов зафиксировать изменения. Прежде чем это произойдёт, процесс записи должен убедиться, что ни один другой процесс не читает базу данных, и что данные журнала отката надёжно записаны на поверхность диска. Шаги следующие:
- Убедиться, что все данные журнала отката действительно записаны на поверхность диска (а не просто хранятся в кэше операционной системы или контроллера диска), чтобы в случае сбоя питания данные сохранились после восстановления питания.
- Получить блокировку PENDING, а затем блокировку EXCLUSIVE на файл базы данных. Если другие процессы всё ещё удерживают блокировки SHARED, процессу записи, возможно, придётся подождать, пока эти блокировки SHARED не будут сняты.
- Записать все изменения страниц, хранящиеся в памяти, в исходный файл базы данных на диске.
Если причиной записи в файл базы данных является заполнение кэша памяти, то процесс записи не будет выполнять фиксацию немедленно. Вместо этого он может продолжить вносить изменения в другие страницы. Перед записью последующих изменений в файл базы данных журнал отката должен быть снова сброшен на диск.
Следует также отметить, что блокировка EXCLUSIVE, полученная процессом записи для первоначальной записи в базу данных, должна удерживаться до тех пор, пока все изменения не будут зафиксированы. Это означает, что ни один другой процесс не сможет получить доступ к базе данных с момента первого переполнения кэша памяти на диск до момента фиксации транзакции.
Когда процесс записи готов зафиксировать изменения, он выполняет следующие шаги:
- Получить блокировку EXCLUSIVE на файл базы данных и убедиться, что все изменения в памяти записаны в файл базы данных.
- Сбросить все изменения файла базы данных на диск. Дождаться, пока эти изменения действительно будут записаны на поверхность диска.
- Удалить файл журнала. (Или, если
PRAGMA journal_modeимеет значениеTRUNCATEилиPERSIST, усечь файл журнала или обнулить заголовок файла журнала соответственно.) Это момент фиксации изменений. До удаления файла журнала, если произойдёт сбой питания или сбой системы, следующий процесс, открывший базу данных, увидит, что у неё есть горячий журнал, и откатит изменения. После удаления журнала горячего журнала больше не будет, и изменения сохранятся. - Снять блокировки EXCLUSIVE и PENDING с файла базы данных.
Как только блокировка PENDING снята с файла базы данных, другие процессы могут снова начать читать базу данных. В текущей реализации блокировка RESERVED также снимается, однако это не является обязательным условием для корректной работы.
Если транзакция затрагивает несколько баз данных, используется более сложная последовательность фиксации:
- Убедиться, что все отдельные файлы баз данных имеют блокировку EXCLUSIVE и действительный журнал.
- Создать супер-журнал. Имя супер-журнала произвольно. (В текущей реализации к имени основного файла базы данных добавляются случайные суффиксы до тех пор, пока не будет найдено имя, которое ранее не существовало.)
- Заполнить супер-журнал именами всех отдельных журналов и сбросить его содержимое на диск.
- Записать имя супер-журнала во все отдельные журналы (в пространство, зарезервированное для этой цели в заголовках отдельных журналов), сбросить содержимое отдельных журналов на диск и дождаться, пока эти изменения достигнут поверхности диска.
- Сбросить все изменения файлов баз данных на диск. Дождаться, пока эти изменения действительно будут записаны на поверхность диска.
- Удалить файл супер-журнала. Это момент фиксации изменений. До удаления файла супер-журнала, если произойдёт сбой питания или сбой системы, отдельные файловые журналы будут считаться горячими и будут откатаны следующим процессом, который попытается их прочитать. После удаления супер-журнала файловые журналы больше не будут считаться горячими, и изменения сохранятся.
- Удалить все отдельные файлы журналов.
- Снять блокировки EXCLUSIVE и PENDING со всех файлов баз данных.
Голодание процессов записи и как SQLite его решает
В SQLite версии 2, если множество процессов читают из базы данных, может сложиться ситуация, при которой никогда не наступает момент, когда нет активных читателей. И если на базе данных всегда есть хотя бы одна блокировка чтения, ни один процесс никогда не сможет вносить изменения в базу данных, поскольку получить блокировку записи будет невозможно. Эта ситуация называется голоданием процессов записи.
SQLite версии 3 стремится избежать голодания процессов записи посредством использования блокировки PENDING. Блокировка PENDING позволяет существующим читателям продолжать работу, но предотвращает подключение новых читателей к базе данных. Таким образом, когда процесс хочет выполнить запись в занятую базу данных, он может установить блокировку PENDING, которая предотвратит появление новых читателей.
При условии, что существующие читатели в конечном счёте завершат работу, все блокировки SHARED в итоге будут сняты, и процессу записи будет предоставлена возможность внести свои изменения.
Как повредить файлы базы данных
Модуль pager весьма надёжен, но его можно обойти. В этом разделе я постараюсь выявить и объяснить соответствующие риски. (См. также раздел «Что может пойти не так» в статье об атомарной фиксации транзакций.)
Очевидно, что аппаратный сбой или сбой операционной системы, вносящий некорректные данные в середину файла базы данных или журнала, вызовет проблемы. Аналогично, если некий процесс-нарушитель откроет файл базы данных или журнала и запишет в него искажённые данные, база данных окажется повреждённой. С подобными проблемами мало что можно поделать, поэтому они не рассматриваются далее.
SQLite использует консультативные блокировки POSIX (advisory locks) для реализации блокировок в Unix. В Windows для этого применяются системные вызовы LockFile(), LockFileEx() и UnlockFile(). SQLite предполагает, что все эти системные вызовы работают так, как заявлено. Если это не так, результатом может стать повреждение базы данных.
Следует отметить, что консультативные блокировки POSIX, как известно, содержат ошибки или вовсе не реализованы во многих реализациях NFS (включая последние версии Mac OS X), а также поступают сообщения о проблемах с блокировками для сетевых файловых систем в Windows. Лучшая защита — не использовать SQLite для файлов, расположенных на сетевой файловой системе.
SQLite использует системный вызов fsync() для сброса данных на диск в Unix и FlushFileBuffers() для той же цели в Windows. Здесь также SQLite предполагает, что эти службы операционной системы функционируют так, как заявлено. Однако поступали сообщения о том, что fsync() и FlushFileBuffers() не всегда работают корректно, особенно с некоторыми сетевыми файловыми системами или недорогими IDE-дисками.
По всей видимости, некоторые производители IDE-дисков используют контроллеры, которые сообщают о том, что данные достигли поверхности диска, тогда как в действительности данные всё ещё находятся в энергозависимой кэш-памяти электроники дискового накопителя. Также поступают сообщения о том, что Windows иногда игнорирует FlushFileBuffers() по неустановленным причинам. Я не могу подтвердить ни одно из этих сообщений.
Но если они соответствуют действительности, это означает, что повреждение базы данных возможно после неожиданного отключения питания. Это аппаратные и/или программные ошибки операционной системы, от которых SQLite не в состоянии защититься.
Если файловая система Linux ext3 смонтирована без параметра barrier=1 в /etc/fstab и кэш записи дискового накопителя включён, то после отключения питания или сбоя ОС может произойти повреждение файловой системы.
Вероятность повреждения зависит от особенностей аппаратного обеспечения контроллера диска; повреждение более вероятно при использовании недорогих потребительских дисков и менее вероятно для корпоративных устройств хранения данных с расширенными функциями, такими как энергонезависимые кэши записи. Различные эксперты по ext3 подтверждают такое поведение.
Нам сообщают, что большинство дистрибутивов Linux не используют barrier=1 и не отключают кэш записи, поэтому большинство дистрибутивов Linux уязвимы к этой проблеме. Обратите внимание, что это проблема операционной системы и аппаратного обеспечения, и SQLite ничего не может сделать для её обхода. Другие СУБД также сталкивались с этой же проблемой.
Если произошёл сбой или отключение питания, в результате которого возник горячий журнал, но этот журнал был удалён, следующий процесс, открывающий базу данных, не будет знать, что она содержит изменения, требующие отката. Откат не произойдёт, и база данных останется в несогласованном состоянии. Журналы отката могут быть удалены по ряду причин:
- Администратор может выполнять очистку после сбоя ОС или отключения питания, обнаружить файл журнала, принять его за мусор и удалить.
- Кто-то (или какой-либо процесс) может переименовать файл базы данных, не переименовав при этом связанный с ним журнал.
- Если файл базы данных имеет псевдонимы (жёсткие или символические ссылки) и файл открывается по псевдониму, отличному от того, который использовался при создании журнала, журнал не будет найден. Чтобы избежать этой проблемы, не следует создавать ссылки на файлы базы данных SQLite.
- Повреждение файловой системы после отключения питания может привести к переименованию или удалению журнала.
Последний пункт заслуживает дополнительного комментария. Когда SQLite создаёт файл журнала в Unix, он открывает каталог, содержащий этот файл, и вызывает fsync() для каталога, стремясь записать информацию о каталоге на диск. Но предположим, что в момент отключения питания какой-либо другой процесс добавляет или удаляет не связанные с базой данных файлы в каталоге, содержащем базу данных и журнал.
Эти, казалось бы, не связанные действия другого процесса могут привести к тому, что файл журнала будет исключён из каталога и перемещён в lost+found. Это маловероятный сценарий, но он возможен. Лучшая защита — использовать журналируемую файловую систему или хранить базу данных и журнал в отдельном каталоге.
При фиксации транзакции, затрагивающей несколько баз данных и использующей супер-журнал, если различные базы данных находились на разных томах диска и в процессе фиксации произошло отключение питания, то после перезапуска машины диски могут быть смонтированы под другими именами. Или некоторые диски могут вообще не быть смонтированы. В этом случае отдельные файловые журналы и супер-журнал могут не найти друг друга.
Наихудший исход этого сценария состоит в том, что фиксация перестаёт быть атомарной. Некоторые базы данных могут быть откатаны, а другие — нет. Все базы данных сохранят внутреннюю согласованность. Для защиты от этой проблемы следует хранить все базы данных на одном томе диска и/или монтировать диски после отключения питания строго под теми же именами.
Управление транзакциями на уровне SQL
Изменения в механизме блокировок и управлении параллельным доступом в SQLite версии 3 также вносят некоторые тонкие изменения в то, как транзакции работают на уровне языка SQL. По умолчанию SQLite версии 3 работает в режиме автофиксации (autocommit). В режиме автофиксации все изменения в базе данных фиксируются сразу после завершения всех операций, связанных с текущим подключением к базе данных.
SQL-команда BEGIN TRANSACTION (ключевое слово TRANSACTION является необязательным) используется для вывода SQLite из режима автофиксации. Обратите внимание, что команда BEGIN не устанавливает никаких блокировок на базу данных. После команды BEGIN блокировка SHARED будет установлена при выполнении первого оператора SELECT.
Блокировка RESERVED будет установлена при выполнении первого оператора INSERT, UPDATE или DELETE. Блокировка EXCLUSIVE не устанавливается до тех пор, пока либо кэш памяти не заполнится и не потребует сброса на диск, либо до момента фиксации транзакции. Таким образом, система откладывает блокировку доступа к файлу на чтение до последнего возможного момента.
SQL-команда COMMIT фактически не фиксирует изменения на диске. Она лишь снова включает режим автофиксации. Затем, по завершении команды, стандартная логика автофиксации берёт управление на себя и выполняет фактическую запись на диск. SQL-команда ROLLBACK также работает путём повторного включения режима автофиксации, но при этом устанавливает флаг, который указывает логике автофиксации выполнить откат, а не фиксацию.
Если SQL-команда COMMIT включает автофиксацию, и логика автофиксации затем пытается зафиксировать изменения, но терпит неудачу, поскольку какой-либо другой процесс сохраняет блокировку SHARED, то автофиксация автоматически отключается снова. Это позволяет пользователю повторить попытку выполнения COMMIT позднее, после того как блокировка SHARED будет снята.
Типичные ошибки при работе с блокировками
Понимание механизма блокировок помогает избежать ряда распространённых ошибок, с которыми я сталкивался на практике.
Хранение базы данных на сетевой файловой системе. Консультативные блокировки POSIX ненадёжно работают на NFS, а блокировки Windows некорректно функционируют на сетевых дисках. Результатом может стать одновременная запись двух процессов без взаимного исключения и последующее повреждение данных. Единственная надёжная защита — держать файл базы данных на локальном диске.
Удаление или переименование файла базы данных без журнала. Если администратор переименовывает файл .db, не переименовав файл -journal, SQLite при следующем открытии не найдёт журнал и не выполнит откат незавершённой транзакции. База данных окажется в несогласованном состоянии без каких-либо явных признаков ошибки.
Игнорирование кода возврата SQLITE_BUSY. Когда процесс не может получить нужную блокировку, SQLite возвращает SQLITE_BUSY, а не блокирует вызывающий поток бесконечно. Приложение обязано обрабатывать этот код и повторять попытку или сообщать пользователю об ошибке. Игнорирование SQLITE_BUSY приводит к молчаливой потере данных.
Использование жёстких или символических ссылок на файл базы данных. Если файл базы данных открывается по псевдониму, отличному от того, который использовался при создании журнала, SQLite не найдёт журнал отката. Создавать ссылки на файлы базы данных SQLite не рекомендуется.
Отключение barrier=1 на ext3/ext4. Большинство дистрибутивов Linux не монтируют файловые системы с параметром barrier=1 по умолчанию. При включённом кэше записи дискового накопителя это создаёт риск повреждения файловой системы после отключения питания — и SQLite здесь ничего не может сделать, поскольку проблема находится на уровне ОС и железа.
Часто задаваемые вопросы о блокировках SQLite
Что происходит, если два процесса одновременно пытаются записать в одну базу данных SQLite?
Первый процесс, получивший блокировку RESERVED, продолжает работу. Второй процесс получает SQLITE_BUSY и должен повторить попытку позже. Одновременная запись двух процессов невозможна: блокировка EXCLUSIVE допускается только в единственном экземпляре.
Чем блокировка PENDING отличается от блокировки RESERVED?
Блокировка RESERVED сигнализирует о намерении записи, но не блокирует новых читателей. Блокировка PENDING запрещает новым читателям получать блокировку SHARED, позволяя при этом уже существующим читателям завершить работу. Именно PENDING решает проблему голодания процессов записи.
Почему SQLite не блокирует вызывающий поток при занятой базе данных, а возвращает ошибку?
Это намеренное архитектурное решение. SQLite возвращает SQLITE_BUSY, чтобы приложение само управляло стратегией повторных попыток. Можно настроить тайм-аут ожидания через sqlite3_busy_timeout() — тогда SQLite будет автоматически повторять попытку в течение указанного времени перед возвратом ошибки.
Что такое горячий журнал и когда он возникает?
Горячий журнал — это журнал отката, который остался на диске после аварийного завершения процесса записи (сбой программы, ОС или отключение питания). При следующем открытии базы данных SQLite обнаруживает горячий журнал и автоматически откатывает незавершённую транзакцию перед любым чтением.
Безопасно ли использовать SQLite на сетевых файловых системах?
Нет. Консультативные блокировки POSIX ненадёжно реализованы во многих версиях NFS, включая некоторые версии Mac OS X. Аналогичные проблемы существуют для сетевых дисков в Windows. Для файлов на сетевых файловых системах рекомендуется использовать клиент-серверную СУБД вместо SQLite.



