Стандартный режим и его ограничения
По умолчанию SQLite использует откатный журнал (rollback journal). Когда вы что-то записываете, SQLite копирует исходные страницы в файл -journal, меняет основную базу, а после коммита удаляет журнал. Если процесс упадёт посреди записи, журнал проигрывается в обратном порядке - и незавершённое изменение откатывается.
Просто и надёжно, но есть один болезненный момент: писатели и читатели дерутся за один и тот же файл. Пока писатель держит блокировку базы, ни один читатель не может начать новую транзакцию. Пока активны читатели - ждёт уже писатель. На нагруженном приложении (скажем, веб-сервер с парой одновременных запросов) ошибки SQLITE_BUSY посыплются быстрее, чем хотелось бы.
Режим WAL это меняет.
Что на самом деле делает WAL
Write-ahead logging переворачивает схему с ног на голову. Вместо того чтобы править основной файл базы на месте, писатель дописывает закоммиченные страницы в отдельный файл с суффиксом -wal. Читатели по-прежнему читают основной файл, но дополнительно заглядывают в WAL - вдруг там есть более свежие версии нужных им страниц.
Итог: писатель и сколько угодно читателей могут работать одновременно. Каждый читатель видит согласованный снимок данных на момент начала своей транзакции, а писатель спокойно дописывает в WAL, не трогая то, на что смотрят читатели.
Одной этой директивы достаточно, чтобы переключить базу. Режим прилипает к файлу - он записывается в заголовок, так что все последующие подключения автоматически подхватят WAL. Гонять эту команду на каждом коннекте не нужно: достаточно выполнить её один раз при инициализации базы (или в миграционном скрипте).
PRAGMA возвращает новый режим. Если в ответе wal - всё ок. Если что-то другое, скорее всего, файловая система не поддерживает разделяемую память (об этом ниже).
Как включить WAL и проверить, что он работает
Текущий режим можно посмотреть в любой момент:
Первый вызов включает WAL и возвращает новый режим. Второй (уже без =) просто опрашивает текущее значение. После этого в каталоге, где лежит messages.db, при активности появятся три файла: messages.db, messages.db-wal и messages.db-shm. Последние два возникают и исчезают в зависимости от того, открыты ли соединения.
Файлы -wal и -shm в SQLite
Вместе с режимом WAL появляются два дополнительных файла, и стоит понимать, за что каждый отвечает:
-walхранит зафиксированные транзакции, которые ещё не были перенесены в основную базу. Файл растёт по мере записи и сжимается (или обнуляется) во время checkpoint'а.-shm- это файл разделяемой памяти. По сути, индекс по WAL: благодаря ему все соединения видят согласованную картину того, где какая страница лежит, и не приходится сканировать WAL при каждом запросе.
Практический вывод: никогда не копируйте базу в режиме WAL, забирая только файл .db. Свежие данные лежат в -wal, и без него копия окажется устаревшей или вовсе битой. Либо копируйте все три файла, когда никто не пишет, либо - что гораздо надёжнее - используйте SQLite backup API (об этом в следующей главе).
Параллельная запись в SQLite: один писатель, много читателей
Режим WAL не даёт параллельной записи. SQLite по-прежнему сериализует пишущие транзакции: в любой момент времени блокировку записи держит ровно одна. Изменилось другое - записи больше не блокируют чтение, а чтения не блокируют запись.
Поэтому типичное веб-приложение на WAL ведёт себя так:
- Эндпоинты, активно читающие данные, работают параллельно без конкуренции.
- Эндпоинты с записью выстраиваются в небольшую очередь между собой, но не мешают читателям.
- Долгие читающие запросы (аналитика, экспорт) не заставляют писателей ждать.
Если два соединения попытаются писать одновременно, второе получит SQLITE_BUSY. Лечится это обычно адекватным busy timeout - нужно попросить SQLite подождать немного, прежде чем сдаваться:
busy_timeout=5000 означает: «если база залочена, подожди до 5 секунд, прежде чем выдать ошибку». В паре с WAL этого хватает, чтобы пережить ту конкуренцию, с которой реально сталкивается большинство приложений. А форма BEGIN IMMEDIATE берёт блокировку на запись сразу при старте транзакции, а не при первой операции записи - так мы обходим целый класс дедлоков, когда два соединения одновременно собираются писать.
Checkpoint в SQLite: как WAL сворачивается обратно
Файл WAL не может расти бесконечно. Checkpoint - это процесс, при котором закоммиченные страницы из WAL переносятся в основной файл базы, после чего WAL обнуляется.
SQLite запускает checkpoint автоматически, когда WAL переваливает примерно за 1000 страниц (значение wal_autocheckpoint по умолчанию). В большинстве проектов это можно вообще не трогать. Но если хочется подкрутить параметры или запустить checkpoint вручную:
У wal_checkpoint есть несколько режимов:
PASSIVE- выполняет контрольную точку настолько, насколько это возможно, не мешая ни читателям, ни писателям. Режим по умолчанию.FULL- ждёт, пока активные писатели закончат работу, и затем фиксирует в основной БД всё, что было закоммичено.RESTART- то же, что иFULL, плюс не даёт новым читателям продолжать работать со старым WAL.TRUNCATE- то же, что иRESTART, плюс ужимает файл WAL обратно до нуля байт.
На сервере, как правило, вызывать это вручную не нужно. А вот если вы пишете десктопное приложение и хотите, чтобы при выходе размеры файлов оставались аккуратными, имеет смысл делать TRUNCATE-чекпоинт перед закрытием последнего соединения.
Прагмы, которые хорошо сочетаются с WAL
Сам по себе режим WAL - это уже неплохо. Но в продакшене обычно к нему добавляют ещё пару настроек:
Короткая экскурсия по настройкам:
synchronous=NORMAL- это рекомендованный спутник режима WAL. Он защищает от падений приложения и краха ОС; потерять последние транзакции можно только при отключении питания в крайне неудачный момент, но даже тогда база останется целостной. Значение по умолчаниюFULLбезопаснее, но заметно медленнее.busy_timeoutмы разобрали выше.foreign_keys=ONк WAL отношения не имеет, но включать его стоит на каждом соединении - SQLite по умолчанию не проверяет внешние ключи ради обратной совместимости.
Эти настройки действуют в рамках одного соединения (кроме journal_mode - он сохраняется в файле). Выставляйте их сразу после открытия соединения в коде приложения.
Когда WAL - не лучший выбор
WAL - рекомендация по умолчанию, но в нескольких случаях от него лучше отказаться:
- Сетевые файловые системы. WAL опирается на разделяемую память (
mmap) между процессами, работающими с базой. NFS, SMB и им подобные надёжно это не поддерживают. Если база лежит на сетевой шаре - оставайтесь на rollback journal, а ещё лучше не держите SQLite на сетевом диске вовсе. - Носители только для чтения. WAL должен писать файлы
-walи-shm. База на CD-ROM или аналогичном носителе обязана использовать режим без записи (или открываться только на чтение черезmode=ro). - Пакетные задачи с одним писателем и без параллельных читателей. WAL вреда не нанесёт, но и пользы тоже. Стандартного rollback journal вполне достаточно.
Для 95% приложений - веб-бэкендов, десктопных и мобильных программ, встраиваемых устройств с локальным хранилищем - WAL подходит идеально.
Боевая конфигурация
Вот как обычно выглядит SQLite в проде - набор прагм, который можно прямо взять и запустить:
temp_store=MEMORY держит временные таблицы и индексы в оперативной памяти, а не на диске - небольшой, но бесплатный выигрыш, если памяти не жалко.
Пропишите всё это один раз при создании подключения в коде инициализации БД - и вы покрыли большую часть того, что нужно SQLite-приложению, чтобы нормально жить под параллельной нагрузкой.
Дальше: резервное копирование и восстановление
Теперь рядом с вашей базой лежат файлы -wal и -shm, и просто скопировать основной файл - уже не вариант для надёжного бэкапа. В следующей главе разберём, как правильно бэкапить живую базу SQLite: команду .backup, online backup API и то, как снять консистентный снимок, не останавливая приложение.
Часто задаваемые вопросы
Что такое режим WAL в SQLite?
WAL - это write-ahead logging, упреждающая запись в журнал. Вместо того чтобы сразу писать изменения в основной файл базы и хранить откатный журнал на случай сбоя, SQLite дописывает изменения в отдельный файл -wal, а потом периодически сливает их обратно в основную базу. Главный плюс - параллелизм: читатели и один писатель спокойно работают одновременно и не блокируют друг друга.
Как включить режим WAL в SQLite?
Достаточно один раз выполнить PRAGMA journal_mode=WAL;. Настройка сохраняется в заголовке файла базы, поэтому все последующие подключения автоматически будут работать в WAL - заново выставлять её на каждом соединении не нужно. Если всё прошло успешно, прагма вернёт строку wal.
Поддерживает ли WAL параллельную запись?
Нет, записи по-прежнему сериализуются: блокировку на запись в каждый момент времени держит только один писатель. Но WAL меняет другое - читатели больше не блокируют писателя, а писатель не блокирует читателей. На практике именно это и было главным узким местом в большинстве приложений.
Что за файлы -wal и -shm рядом с базой?
В файле -wal лежат закоммиченные изменения, которые ещё не успели слиться обратно в основной файл базы. -shm - это небольшой индекс в разделяемой памяти, по которому подключения быстро находят нужные страницы внутри WAL. Оба файла создаются автоматически, но если вы копируете базу руками, их нужно копировать вместе с основным файлом - либо использовать backup API.