Spec-Zone.ru › SQLite

Журнал предварительной записи

Содержание
1. Обзор
2. Как работает WAL
2.1. Создание контрольных точек
2.2. Параллельная работа
2.3. Особенности производительности
3. Включение и настройка режима WAL
3.1. Автоматическая контрольная точка
3.2. Контрольные точки, инициируемые приложением
3.3. Сохранение режима WAL
4. Файл WAL
5. Базы данных только для чтения
6. Как избежать чрезмерного увеличения файлов WAL
7. Реализация общей памяти для индекса WAL
8. Использование WAL без общей памяти
9. Иногда запросы возвращают SQLITE_BUSY в режиме WAL
10. Обратная совместимость
11. Ошибка сброса WAL
11.1. Подробности ошибки
11.2. Низкая вероятность возникновения

1. Обзор

По умолчанию SQLite реализует атомарную фиксацию и откат с помощью журнала отката. Начиная с версии 3.7.0 (2010-07-21) доступен новый вариант — «журнал предварительной записи» (далее — «WAL»).

Использование WAL вместо журнала отката имеет преимущества и недостатки. К преимуществам относятся:

  1. В большинстве сценариев WAL значительно быстрее.
  2. WAL обеспечивает более высокий уровень параллелизма: операции чтения не блокируют запись, а запись не блокирует чтение. Чтение и запись могут выполняться одновременно.
  3. При использовании WAL операции ввода-вывода с диском обычно выполняются более последовательно.
  4. WAL использует гораздо меньше операций fsync() и поэтому менее подвержен проблемам в системах, где системный вызов fsync() работает некорректно.

Однако есть и недостатки:

  1. Все процессы, использующие базу данных, должны находиться на одном компьютере; WAL не работает в сетевой файловой системе. Это связано с тем, что для работы WAL всем процессам необходимо совместно использовать небольшой объем памяти, а процессы на разных компьютерах, разумеется, не могут совместно использовать память.
  2. Транзакции, изменяющие несколько баз данных, подключенных с помощью ATTACH, атомарны для каждой отдельной базы данных, но не атомарны для всех баз данных как единого набора.
  3. После перехода в режим WAL изменить размер страницы невозможно — как для пустой базы данных, так и с помощью VACUUM или восстановления из резервной копии посредством API резервного копирования. Чтобы изменить размер страницы, необходимо использовать режим журнала отката.
  4. Открыть базы данных WAL только для чтения невозможно. Для открывающего процесса необходимы права на запись в файл общей памяти индекса WAL «-shm» wal-index, связанный с базой данных, если этот файл существует; если же файл «-shm» не существует, нужны права на запись в каталог, содержащий файл базы данных. Начиная с версии 3.22.0 (2018-01-22) файл базы данных в режиме WAL можно открыть только для чтения, если файлы -shm и -wal уже существуют или могут быть созданы, либо если база данных является неизменяемой.
  5. В приложениях, где в основном выполняется чтение и запись происходит редко, WAL может быть немного медленнее (возможно, на 1–2 %) традиционного подхода с журналом отката.
  6. С каждой базой данных связан дополнительный квази-постоянный файл «-wal» и файл общей памяти «-shm», что может сделать SQLite менее привлекательной для использования в качестве формата файлов приложения.
  7. Необходимо выполнять дополнительную операцию — создание контрольных точек. Хотя по умолчанию эта операция выполняется автоматически, разработчикам приложений всё же нужно учитывать её особенности.
  8. WAL лучше всего работает с небольшими транзакциями и плохо подходит для очень больших. Для транзакций размером более примерно 100 мегабайт традиционные режимы журнала отката, вероятно, будут быстрее. Для транзакций размером более гигабайта режим WAL может завершиться ошибкой ввода-вывода или нехватки места на диске. Для транзакций размером более нескольких десятков мегабайт рекомендуется использовать один из режимов журнала отката. Начиная с версии 3.11.0 (2016-02-15) режим WAL работает с большими транзакциями так же эффективно, как и режим отката.

2. Как работает WAL

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

В WAL-подходе всё устроено наоборот. Исходное содержимое остается в файле базы данных, а изменения добавляются в отдельный файл WAL. COMMIT выполняется при добавлении в WAL специальной записи, указывающей на фиксацию транзакции. Таким образом, COMMIT можно выполнить, ни разу не записывая данные в исходную базу, что позволяет читателям продолжать работу с исходной неизмененной базой, пока изменения одновременно фиксируются в WAL. В конец одного файла WAL можно добавлять несколько транзакций.

2.1. Создание контрольных точек

Разумеется, со временем все транзакции, добавленные в файл WAL, необходимо перенести обратно в исходную базу данных. Перенос транзакций из файла WAL обратно в базу данных называется «созданием контрольной точки».

Разницу между журналом отката и журналом предварительной записи можно представить и так: при использовании журнала отката есть две базовые операции — чтение и запись, а при использовании журнала предварительной записи их три: чтение, запись и создание контрольной точки.

По умолчанию SQLite автоматически создает контрольную точку, когда размер файла WAL достигает порогового значения в 1000 страниц. (Для задания другого значения по умолчанию можно использовать параметр компиляции SQLITE_DEFAULT_WAL_AUTOCHECKPOINT.) Приложениям, использующим WAL, не нужно ничего делать, чтобы запускать создание контрольных точек. Однако при необходимости приложения могут изменить порог автоматического создания контрольных точек. Также можно отключить автоматическое создание контрольных точек и выполнять их в периоды простоя либо в отдельном потоке или процессе.

2.2. Параллельная работа

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

Когда читателю нужна страница данных, он сначала проверяет, есть ли эта страница в WAL, и, если есть, считывает последнюю копию страницы, записанную в WAL до конечной отметки читателя. Если до конечной отметки читателя в WAL нет копии страницы, страница считывается из исходного файла базы данных. Читатели могут работать в отдельных процессах, поэтому, чтобы не заставлять каждого читателя просматривать весь WAL в поисках страниц (в зависимости от частоты создания контрольных точек файл WAL может вырасти до нескольких мегабайт), в общей памяти поддерживается структура данных под названием «wal-index», помогающая читателям быстро находить страницы в WAL с минимальным количеством операций ввода-вывода. Индекс WAL значительно повышает производительность чтения, но использование общей памяти означает, что все читатели должны находиться на одном компьютере. Именно поэтому реализация журнала предварительной записи не работает в сетевой файловой системе.

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

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

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

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

2.3. Особенности производительности

Транзакции записи выполняются очень быстро, поскольку данные записываются только один раз (в отличие от двух раз при использовании журнала отката), а все операции записи выполняются последовательно. Кроме того, синхронизация данных с диском не требуется, если приложение готово пожертвовать сохранностью данных при отключении питания или принудительной перезагрузке. (Если для PRAGMA synchronous задано значение FULL, записывающие процессы синхронизируют WAL при фиксации каждой транзакции, а если задано значение NORMAL — не выполняют эту синхронизацию.)

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

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

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

Обратите внимание: если для PRAGMA synchronous задано значение NORMAL, создание контрольной точки — единственная операция, выполняющая барьер ввода-вывода или синхронизацию (fsync() в Unix или FlushFileBuffers() в Windows). Поэтому, если приложение создает контрольные точки в отдельном потоке или процессе, основной поток или процесс, выполняющий запросы к базе данных и обновляющий ее, никогда не будет заблокирован операцией синхронизации. Это помогает избежать «зависания» приложений, работающих с перегруженным диском. Недостаток такой настройки — транзакции больше не являются надежно сохраненными и могут быть отменены после сбоя питания или аппаратного сброса.

Следует также учитывать компромисс между средней производительностью чтения и средней производительностью записи. Чтобы максимально повысить производительность чтения, нужно по возможности уменьшать размер WAL и, следовательно, часто создавать контрольные точки, возможно, после каждой операции COMMIT. Чтобы максимально повысить производительность записи, нужно распределить затраты на создание каждой контрольной точки на как можно большее количество операций записи; это означает, что контрольные точки следует создавать реже, позволяя WAL вырасти до максимально возможного размера перед каждой контрольной точкой. Поэтому частота создания контрольных точек может различаться в разных приложениях в зависимости от относительных требований к производительности чтения и записи. По умолчанию контрольная точка создается, когда WAL достигает 1000 страниц. Эта стратегия, по-видимому, хорошо работает в тестовых приложениях на рабочих станциях, однако на других платформах или при других нагрузках могут быть эффективнее иные стратегии.

3. Включение и настройка режима WAL

Для подключения к базе данных SQLite по умолчанию задано значение journal_mode=DELETE. Чтобы переключиться в режим WAL, используйте следующую pragma:

PRAGMA journal_mode=WAL;

Pragma journal_mode возвращает строку с новым режимом журнала. При успешном выполнении pragma возвращает строку «wal». Если переключение в режим WAL не удалось (например, если VFS не поддерживает необходимые примитивы общей памяти), режим ведения журнала не изменится, а возвращенная примитивом строка будет содержать предыдущий режим (например, «delete»).

3.1. Автоматическая контрольная точка

По умолчанию SQLite автоматически создает контрольную точку при выполнении COMMIT, если размер файла WAL при этом достигает 1000 страниц или более, а также при закрытии последнего подключения к файлу базы данных. Настройки по умолчанию рассчитаны на то, чтобы хорошо работать в большинстве приложений. Однако программы, которым требуется больший контроль, могут принудительно запустить создание контрольной точки с помощью pragma wal_checkpoint или вызова интерфейса C sqlite3_wal_checkpoint(). Порог автоматического создания контрольных точек можно изменить, а саму функцию — полностью отключить с помощью pragma wal_autocheckpoint или вызова интерфейса C sqlite3_wal_autocheckpoint(). Программа также может использовать sqlite3_wal_hook(), чтобы зарегистрировать функцию обратного вызова, вызываемую при фиксации любой транзакции в WAL. Эта функция обратного вызова может вызывать sqlite3_wal_checkpoint() или sqlite3_wal_checkpoint_v2() на основании любых критериев, которые считает подходящими. (Механизм автоматического создания контрольных точек реализован как простая оболочка вокруг sqlite3_wal_hook().)

3.2. Контрольные точки, инициируемые приложением

Приложение может инициировать создание контрольной точки, используя любое доступное для записи подключение к базе данных и вызывая sqlite3_wal_checkpoint() или sqlite3_wal_checkpoint_v2(). Существует три типа контрольных точек, различающихся степенью активности: PASSIVE, FULL и RESTART. По умолчанию используется тип PASSIVE: он выполняет столько работы, сколько возможно, не мешая другим подключениям к базе данных, и может не завершиться, если одновременно выполняются операции чтения или записи. Все контрольные точки, инициируемые вызовом sqlite3_wal_checkpoint() и механизмом автоматического создания контрольных точек, имеют тип PASSIVE. Контрольные точки типов FULL и RESTART активнее пытаются завершить операцию и могут быть инициированы только вызовом sqlite3_wal_checkpoint_v2(). Дополнительные сведения о контрольных точках FULL и RESET см. в документации sqlite3_wal_checkpoint_v2().

3.3. Сохранение режима WAL

В отличие от других режимов ведения журнала, режим PRAGMA journal_mode=WAL сохраняется. Если процесс устанавливает режим WAL, а затем закрывает и повторно открывает базу данных, она снова будет открыта в режиме WAL. Напротив, если процесс устанавливает, например, режим PRAGMA journal_mode=TRUNCATE, а затем закрывает и повторно открывает базу данных, она вернется к стандартному режиму отката DELETE, а не к предыдущей настройке TRUNCATE.

Благодаря сохранению режима WAL приложения можно перевести на использование SQLite в режиме WAL без внесения изменений в само приложение. Достаточно выполнить команду «PRAGMA journal_mode=WAL;» для файла или файлов базы данных с помощью оболочки командной строки или другой утилиты, а затем перезапустить приложение.

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

4. Файл WAL

Пока открыто подключение к базе данных в режиме WAL, SQLite поддерживает дополнительный файл журнала, называемый «журналом предварительной записи» или «файлом WAL». Обычно на диске этот файл имеет имя файла базы данных с дополнительным суффиксом «-wal», хотя при компиляции SQLite с параметром SQLITE_ENABLE_8_3_NAMES могут применяться другие правила именования.

Файл WAL существует, пока база данных открыта хотя бы для одного подключения к базе данных. Обычно файл WAL автоматически удаляется при закрытии последнего подключения к базе данных. Однако если последний процесс, в котором была открыта база данных, завершится, не закрыв корректно подключение, или если используется SQLITE_FCNTL_PERSIST_WAL — функция управления файлами, — файл WAL может остаться на диске после закрытия всех подключений к базе данных. Файл WAL является частью постоянного состояния базы данных, поэтому при копировании или перемещении базы данных его необходимо сохранить вместе с ней. Если файл базы данных отделить от файла WAL, транзакции, ранее зафиксированные в базе данных, могут быть потеряны, а файл базы данных может быть поврежден. Единственный безопасный способ удалить файл WAL — открыть файл базы данных с помощью одного из интерфейсов sqlite3_open(), а затем немедленно закрыть базу данных с помощью sqlite3_close().

Формат файла WAL точно определен и является кроссплатформенным.

5. Базы данных только для чтения

В старых версиях SQLite было невозможно читать базу данных в режиме WAL, если она была доступна только для чтения. Иными словами, для чтения базы данных в режиме WAL требовался доступ на запись. Это ограничение было снято начиная с версии SQLite 3.22.0 (2018-01-22).

В новых версиях SQLite базу данных в режиме WAL можно читать с носителя, доступного только для чтения, или при отсутствии прав на запись, если выполняется хотя бы одно из следующих условий:

  1. Файлы -shm и -wal уже существуют и доступны для чтения.
  2. Есть права на запись в каталог, содержащий базу данных, поэтому можно создать файлы -shm и -wal.
  3. Подключение к базе данных открывается с помощью параметра запроса immutable.

Хотя открыть базу данных в режиме WAL только для чтения возможно, перед записью образа базы данных SQLite на носитель, доступный только для чтения, рекомендуется переключить базу данных в режим PRAGMA journal_mode=DELETE.

6. Как избежать чрезмерного увеличения файлов WAL

В обычных случаях новые данные добавляются в файл WAL, пока в нем не накапливается примерно 1000 страниц (то есть около 4 МБ). Затем автоматически создается контрольная точка, а файл WAL используется повторно. Обычно при создании контрольной точки файл WAL не усекается (если не задана pragma journal_size_limit). Вместо этого SQLite просто начинает перезаписывать файл WAL с начала. Это делается потому, что обычно перезаписать существующий файл быстрее, чем дописывать в него данные. При закрытии последнего подключения к базе данных оно создает последнюю контрольную точку, а затем удаляет WAL и связанный с ним файл общей памяти, освобождая место на диске.

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

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

  • Задержка создания контрольной точки. Контрольная точка может завершиться и сбросить файл WAL только в том случае, если другие подключения к базе данных не используют WAL. Если другое подключение выполняет транзакцию чтения, контрольная точка не может сбросить файл WAL, поскольку это может удалить данные, нужные читателю. Контрольная точка выполнит столько работы, сколько возможно, не мешая читателю, но не сможет завершиться. После следующей транзакции записи создание контрольной точки продолжится с того места, на котором оно остановилось. Так будет продолжаться, пока какая-либо контрольная точка не сможет завершиться.

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

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

  • Очень большие транзакции записи. Контрольная точка может завершиться, только когда не выполняются другие транзакции. Это означает, что сбросить файл WAL посреди транзакции записи нельзя. Поэтому большое изменение в большой базе данных может привести к созданию большого файла WAL. Контрольная точка будет создана после завершения транзакции записи (при условии, что другие читатели не препятствуют этому), но до этого момента файл может вырасти до очень большого размера.

    Начиная с версии SQLite 3.11.0 (2016-02-15) размер файла WAL для одной транзакции должен быть пропорционален размеру самой транзакции. Страницы, измененные транзакцией, должны записываться в файл WAL только один раз. Однако в более старых версиях SQLite одна и та же страница могла записываться в WAL несколько раз, если транзакция превышала размер кэша страниц.

7. Реализация общей памяти для индекса WAL

Индекс WAL реализован с помощью обычного файла, отображаемого в память посредством mmap для повышения надежности. В ранних (предварительных) реализациях режима WAL индекс WAL хранился в энергозависимой общей памяти, например в файлах, создаваемых в каталоге /dev/shm в Linux или /tmp в других системах Unix. Проблема такого подхода заключается в том, что процессы с разными корневыми каталогами (измененными с помощью chroot) видят разные файлы и, следовательно, используют разные области общей памяти, что приводит к повреждению базы данных. Другие способы создания безымянных блоков общей памяти несовместимы с разными вариантами Unix. Кроме того, нам не удалось найти способ создавать безымянные блоки общей памяти в Windows. Единственный найденный нами способ гарантировать, что все процессы, обращающиеся к одному файлу базы данных, используют одну и ту же общую память, — создать общую память, отобразив файл в память с помощью mmap в том же каталоге, что и сама база данных.

Использование обычного файла на диске для общей памяти имеет недостаток: оно может приводить к ненужным операциям ввода-вывода из-за записи общей памяти на диск. Однако разработчики не считают это серьезной проблемой, поскольку размер индекса WAL редко превышает 32 КиБ, и он никогда не синхронизируется. Кроме того, файл, в котором хранится индекс WAL, удаляется при отключении последнего подключения к базе данных, что часто позволяет избежать любых фактических операций ввода-вывода с диском.

Для специализированных приложений, в которых реализация общей памяти по умолчанию неприемлема, можно реализовать альтернативные методы с помощью пользовательского VFS. Например, если известно, что к определенной базе данных будут обращаться только потоки одного процесса, индекс WAL можно реализовать с помощью динамической памяти, а не настоящей общей памяти.

8. Использование WAL без общей памяти

Начиная с версии SQLite 3.7.4 (2010-12-07) базы данных WAL можно создавать, читать и изменять даже при отсутствии общей памяти, если до первой попытки доступа установить для locking_mode значение EXCLUSIVE. Иными словами, процесс может работать с базой данных WAL без общей памяти, если гарантируется, что это единственный процесс, обращающийся к базе данных. Эта возможность позволяет создавать, читать и изменять базы данных WAL с помощью устаревших реализаций VFS, не поддерживающих методы общей памяти «версии 2» xShmMap, xShmLock, xShmBarrier и xShmUnmap объекта sqlite3_io_methods.

Если до первого обращения к базе данных в режиме WAL установлен режим блокировки EXCLUSIVE, SQLite никогда не пытается вызывать методы общей памяти, поэтому индекс WAL в общей памяти не создается. В этом случае подключение к базе данных остается в режиме EXCLUSIVE, пока режим журнала установлен в WAL; попытки изменить режим блокировки с помощью «PRAGMA locking_mode=NORMAL;» ничего не меняют. Единственный способ выйти из режима блокировки EXCLUSIVE — сначала выйти из режима журнала WAL.

Если при первом обращении к базе данных в режиме WAL действует режим блокировки NORMAL, создается индекс WAL в общей памяти. Это означает, что используемая VFS должна поддерживать общую память «версии 2». Если VFS не поддерживает методы общей памяти, попытка открыть базу данных, уже работающую в режиме WAL, или переключить базу данных в режим WAL завершится ошибкой. Пока индекс WAL в общей памяти используется ровно одним подключением, режим блокировки можно свободно переключать между NORMAL и EXCLUSIVE. Режим блокировки фиксируется в EXCLUSIVE только тогда, когда индекс WAL в общей памяти не используется, то есть если до первого обращения к базе данных в режиме WAL был установлен режим EXCLUSIVE.

9. Иногда запросы возвращают SQLITE_BUSY в режиме WAL

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

Ниже перечислены ситуации, в которых запрос к базе данных в режиме WAL может вернуть SQLITE_BUSY:

  • Если другое подключение к базе данных открыло базу данных в режиме эксклюзивной блокировки, все запросы к базе данных будут возвращать SQLITE_BUSY. Например, Chrome и Firefox открывают файлы своих баз данных в режиме эксклюзивной блокировки, поэтому попытки читать базы данных Chrome или Firefox во время работы приложений приведут к этой проблеме.

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

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

10. Обратная совместимость

Формат файла базы данных в режиме WAL не меняется. Однако файл WAL и индекс WAL — это новые понятия, поэтому старые версии SQLite не смогут восстановить аварийно завершившуюся работу базы данных SQLite, которая на момент сбоя работала в режиме WAL. Чтобы старые версии SQLite (до версии 3.7.0 от 2010-07-22) не пытались восстановить базу данных в режиме WAL, усугубляя ситуацию, номера версий формата файла базы данных (байты 18 и 19 в заголовке базы данных) в режиме WAL увеличиваются с 1 до 2. Таким образом, если старая версия SQLite попытается подключиться к базе данных SQLite, работающей в режиме WAL, она сообщит об ошибке вроде «файл зашифрован или не является базой данных».

Можно явно выйти из режима WAL с помощью такой pragma:

PRAGMA journal_mode=DELETE;

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

11. Ошибка сброса WAL

2026-03-03 один из разработчиков SQLite (Dan) обнаружил и исправил ошибку, которая в редких случаях могла привести к повреждению базы данных. Мы называем её «ошибкой сброса WAL». Основные сведения:

  • Вероятно, эта ошибка присутствует во всех версиях SQLite от 3.7.0 (2010-07-21) до 3.51.2 (2026-01-09). Она исправлена в версии 3.51.3 (2026-03-13) и более поздних версиях. Исправление также доступно для некоторых более ранних выпусков: 3.44.6 и 3.50.7.

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

  • Эта ошибка связана с гонкой данных, возникающей при очень строгих временных условиях. В обычных условиях она вряд ли возникнет. Разработчикам так и не удалось воспроизвести ошибку естественным образом; им пришлось добавить в SQLite специальную логику тестирования, которая намеренно создаёт условия возникновения ошибки, чтобы проверить, что проблема исправлена.

11.1. Подробности ошибки

Происходит следующее:

  1. Одно подключение выполняет контрольную точку. Эта первая контрольная точка должна завершиться полностью. Иными словами, она должна успешно скопировать всё содержимое из файла WAL обратно в базу данных и оставить файл WAL в состоянии, допускающем его сброс.

  2. Вскоре после завершения первой контрольной точки запускается вторая.

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

  4. Из-за гонки данных вторая контрольная точка из шага 2 не обнаруживает, что файл WAL был сброшен при фиксации транзакции на шаге 3. Вторая контрольная точка оставляет поле в заголовке WAL-индекса в неправильном состоянии. Это поле указывает, что часть файла WAL уже прошла контрольную точку, хотя на самом деле это не так.

  5. Фиксируются дополнительные транзакции, чтобы число страниц в файле WAL стало больше, чем их было во время первой контрольной точки из шага 1.

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

11.2. Низкая вероятность возникновения

Чтобы эта ошибка возникла, множество условий должно совпасть в нужный момент. Настолько много, что разработчикам SQLite не удалось воспроизвести её естественным образом в лаборатории. Единственный способ вызвать сбой, который удалось найти разработчикам, заключался в изменении исходного кода SQLite для вызова обратного вызова, управляемого функцией sqlite3_test_control(), что позволяет тестовой программе или скрипту запустить транзакцию записи из шага 3 в нужный момент во время второй контрольной точки. Без этого изменения кода проблема ни разу не наблюдалась при разработке и тестировании.

Хотя эта ошибка встречается редко, последствия у неё серьёзные, поэтому разработчикам приложений следует перейти на версию SQLite, в которой она исправлена. Однако это не экстренная ситуация. Судя по имеющимся телеметрическим данным, вероятность возникновения этой проблемы в реальных условиях, по-видимому, не превышает ожидаемую вероятность сбоев SSD и/или попадания космических лучей. Поэтому, даже если вы используете версию SQLite без исправления и не делаете ничего необычного, вы вряд ли когда-либо столкнётесь с этой проблемой.

Последнее обновление этой страницы: 2026-03-20 12:37:00Z

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

Spec-Zone.ru

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