- Что такое режим общего кэша SQLite и зачем он появился
- Почему использование общего кэша не рекомендуется
- Модель блокировок общего кэша
- Блокировка на уровне транзакций
- Блокировка на уровне таблиц
- Режим изоляции Read-Uncommitted
- Блокировка на уровне схемы (sqlite_schema)
- Включение режима общего кэша
- Ограничения общего кэша: потоки и виртуальные таблицы
- Общий кэш и базы данных в памяти
- Типичные ошибки при работе с режимом общего кэша
- Часто задаваемые вопросы о режиме общего кэша
Что такое режим общего кэша SQLite и зачем он появился
С версии 3.3.0 (2006-01-11) SQLite предлагает общий кэш (shared-cache), который отключён по умолчанию и предназначен для встроенных серверов. Активировав его, несколько соединений одного потока могут использовать единый кэш данных и схемы — это сокращает потребление памяти и количество операций ввода-вывода
В версии 3.5.0 (2007-09-04) общий кэш был изменён таким образом, что один и тот же кэш может совместно использоваться в рамках всего процесса, а не только внутри одного потока. До этого изменения существовали ограничения на передачу соединений с базой данных между потоками. Эти ограничения были сняты в обновлении 3.5.0. Эта статья описывает режим общего кэша начиная с версии 3.5.0.
Режим общего кэша в ряде случаев изменяет семантику модели блокировок. Для понимания материала потребуется знакомство со стандартной моделью блокировок SQLite (подробности см. в File Locking And Concurrency In SQLite Version 3).
По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы.
По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы.
Почему использование общего кэша не рекомендуется
Режим общего кэша считается устаревшим, и это следует учитывать перед тем, как углубляться в детали его работы. В большинстве ситуаций лучше использовать режим WAL (Write-Ahead Logging).
Режим общего кэша был разработан в ответ на запросы разработчиков Symbian в 2006 году, чтобы устранить блокировки в базе данных контактов телефона при входящих вызовах, когда проводится синхронизация. В 2010 году режим WAL стал более подходящим решением, так как он позволяет одновременно обрабатывать запросы без угрозы для изоляции транзакций.
Приложениям, которые собирают собственную копию SQLite из исходного кода, рекомендуется использовать параметр компиляции -DSQLITE_OMIT_SHARED_CACHE, поскольку результирующий бинарный файл будет одновременно меньше и быстрее.
Хотя интерфейсы общего кэша будут поддерживаться для обратной совместимости, использование этого режима не рекомендуется.
Модель блокировок общего кэша
Внешне, с точки зрения другого процесса или потока, два или более соединения с базой данных, использующих общий кэш, выглядят как единое соединение. Протокол блокировок, используемый для разрешения конфликтов между несколькими общими кэшами или обычными пользователями базы данных, описан в отдельном документе.
На рисунке 1 изображён пример конфигурации времени выполнения, в которой установлены три соединения с базой данных. Соединение 1 является обычным соединением с базой данных SQLite. Соединения 2 и 3 совместно используют кэш. Стандартный протокол блокировок применяется для сериализации доступа к базе данных между соединением 1 и общим кэшем.
Внутренний протокол, используемый для сериализации (или её отсутствия, см. раздел о режиме изоляции Read-Uncommitted ниже) доступа к общему кэшу со стороны соединений 2 и 3, описан в оставшейся части этого раздела.
В модели блокировок общего кэша выделяют три уровня: блокировка на уровне транзакций, таблиц и схемы, которые будут рассмотрены далее.
Блокировка на уровне транзакций
Соединения SQLite могут открывать два типа транзакций: чтения и записи. Транзакция автоматически считается транзакцией чтения, пока не выполняет запись в таблицу, после чего становится транзакцией записи.
Только одно соединение может открывать транзакцию записи в общем кэше в любой момент, и такие транзакции могут сосуществовать с несколькими транзакциями чтения.
Блокировка на уровне таблиц
Когда несколько соединений используют общий кэш, блокировки применяются для сериализации одновременных попыток доступа на уровне отдельных таблиц. Таблицы поддерживают два типа блокировок: «блокировки чтения» и «блокировки записи». Каждое соединение с базой данных имеет либо блокировку чтения, либо запись на каждую таблицу базы данных.
В любой момент времени одна таблица может иметь любое количество активных блокировок чтения или одну активную блокировку записи. Чтобы прочитать данные из таблицы, соединение должно сначала получить блокировку чтения. Чтобы выполнить запись в таблицу, соединение должно получить блокировку записи на эту таблицу. Если требуемую блокировку таблицы получить невозможно, запрос завершается неудачей и вызывающей стороне возвращается SQLITE_LOCKED.
После того как соединение получает блокировку таблицы, она не снимается до завершения текущей транзакции (чтения или записи).
Режим изоляции Read-Uncommitted
Описанное выше поведение может быть незначительно изменено с помощью прагмы read_uncommitted для изменения уровня изоляции с сериализованного (по умолчанию) на read-uncommitted (чтение незафиксированных данных).
Соединение с базой данных в режиме read-uncommitted не пытается получить блокировки чтения перед чтением из таблиц базы данных, как описано выше. Это может привести к несогласованным результатам запросов, если другое соединение с базой данных изменяет таблицу в процессе её чтения, однако это также означает, что транзакция чтения, открытая соединением в режиме read-uncommitted, не может ни блокировать, ни быть заблокированной каким-либо другим соединением.
Режим read-uncommitted не влияет на блокировки, необходимые для записи в таблицы базы данных (то есть соединения в режиме read-uncommitted по-прежнему должны получать блокировки записи, и, следовательно, операции записи в базу данных по-прежнему могут блокировать или быть заблокированными). Кроме того, режим read-uncommitted не влияет на блокировки sqlite_schema, требуемые правилами, перечисленными ниже.
/* Установить значение флага read-uncommitted:
** True -> Перевести соединение в режим read-uncommitted.
** False -> Перевести соединение в сериализованный режим (по умолчанию).
*/
PRAGMA read_uncommitted = <boolean>;
/* Получить текущее значение флага read-uncommitted */
PRAGMA read_uncommitted;
Блокировка на уровне схемы (sqlite_schema)
Таблица sqlite_schema поддерживает блокировки чтения и записи общего кэша так же, как и все остальные таблицы базы данных. Также применяются следующие специальные правила:
- Соединение должно получить блокировку чтения на
sqlite_schemaперед обращением к любым таблицам базы данных или получением любых других блокировок чтения или записи. - Перед выполнением оператора, изменяющего схему базы данных (то есть оператора
CREATEилиDROP TABLE), соединение должно получить блокировку записи наsqlite_schema. - Соединение не может скомпилировать SQL-оператор, если какое-либо другое соединение удерживает блокировку записи на таблицу
sqlite_schemaлюбой присоединённой базы данных (включая базу данных по умолчанию, «main»).
Включение режима общего кэша
Режим общего кэша включается на уровне процесса. Для глобального включения или отключения режима общего кэша через C-интерфейс используется следующий API:
int sqlite3_enable_shared_cache(int);
Каждый вызов sqlite3_enable_shared_cache() влияет на последующие соединения с базой данных, созданные с помощью sqlite3_open(), sqlite3_open16() или sqlite3_open_v2(). Уже существующие соединения с базой данных не затрагиваются. Каждый вызов sqlite3_enable_shared_cache() переопределяет все предыдущие вызовы в рамках одного процесса.
Отдельные соединения с базой данных, созданные с помощью sqlite3_open_v2(), могут выбирать, участвовать или не участвовать в режиме общего кэша, используя флаги SQLITE_OPEN_SHAREDCACHE или SQLITE_OPEN_PRIVATECACHE в качестве третьего параметра. Использование любого из этих флагов переопределяет глобальную настройку режима общего кэша, установленную sqlite3_enable_shared_cache().
Следует использовать не более одного из флагов; если оба флага SQLITE_OPEN_SHAREDCACHE и SQLITE_OPEN_PRIVATECACHE указаны в третьем аргументе sqlite3_open_v2(), поведение не определено.
При использовании URI-имён файлов параметр запроса «cache» может применяться для указания того, будет ли база данных использовать общий кэш. Используйте cache=shared для включения общего кэша и cache=private для его отключения. Возможность использования параметров URI-запроса для указания режима совместного использования кэша позволяет управлять им в операторах ATTACH. Например:
ATTACH 'file:aux.db?cache=shared' AS aux;
Ограничения общего кэша: потоки и виртуальные таблицы
В версиях SQLite с 3.3.0 по 3.4.2 при включённом режиме общего кэша соединение с базой данных могло использоваться только тем потоком, который вызвал sqlite3_open() для его создания. Кроме того, соединение могло совместно использовать кэш только с другим соединением в том же потоке. Эти ограничения были сняты начиная с версии SQLite 3.5.0 (2007-09-04).
В более старых версиях SQLite режим общего кэша нельзя было использовать совместно с виртуальными таблицами. Это ограничение было снято в версии SQLite 3.6.17 (2009-08-10). Если вы работаете с современными версиями SQLite, оба ограничения уже не актуальны, однако знать об их существовании полезно при анализе поведения старых кодовых баз.
Общий кэш и базы данных в памяти
Начиная с версии SQLite 3.7.13 (2012-06-11), общий кэш может использоваться с базами данных в памяти при условии, что база данных создана с использованием URI-имени файла. Для обратной совместимости общий кэш всегда отключён для баз данных в памяти, если для открытия базы данных используется имя :memory: без дополнительных параметров.
До версии 3.7.13 общий кэш всегда был отключён для баз данных в памяти независимо от используемого имени базы данных, текущей системной настройки общего кэша, параметров запроса или флагов.
Включение общего кэша для базы данных в памяти позволяет двум или более соединениям с базой данных в рамках одного процесса иметь доступ к одной и той же базе данных в памяти. База данных в памяти в общем кэше автоматически удаляется и память освобождается при закрытии последнего соединения с этой базой данных.
Типичные ошибки при работе с режимом общего кэша
На практике разработчики нередко допускают несколько характерных ошибок при работе с этим режимом. Я считаю важным перечислить их явно — на нашем опыте именно они чаще всего приводят к трудноотлаживаемым сбоям.
Одновременное указание обоих флагов. Передача и SQLITE_OPEN_SHAREDCACHE, и SQLITE_OPEN_PRIVATECACHE в третьем аргументе sqlite3_open_v2() приводит к неопределённому поведению. Следует использовать строго один из флагов.
Игнорирование ошибки SQLITE_LOCKED. При использовании блокировок на уровне таблиц запрос может завершиться с кодом SQLITE_LOCKED, если требуемую блокировку получить невозможно. Приложение обязано обрабатывать этот код явно, а не рассчитывать на автоматический повтор.
Использование общего кэша с именем :memory:. Открытие базы данных в памяти через имя :memory: без URI-параметров всегда отключает общий кэш, даже если он включён глобально. Для совместного доступа к базе данных в памяти необходимо использовать URI-имя файла.
Применение общего кэша там, где достаточно WAL. Режим WAL решает большинство задач параллельного доступа без дополнительной сложности блокировок на уровне таблиц и схемы. Мой совет — прежде чем включать общий кэш, убедитесь, что WAL действительно не подходит для конкретного сценария.
Компиляция без флага -DSQLITE_OMIT_SHARED_CACHE. Если приложение не использует общий кэш, сборка SQLite с этим флагом даёт меньший и более быстрый бинарный файл без каких-либо потерь функциональности.
Часто задаваемые вопросы о режиме общего кэша
Чем режим общего кэша отличается от режима WAL? Режим WAL обеспечивает параллельный доступ к базе данных без нарушения изоляции транзакций и без дополнительной системы блокировок на уровне таблиц. Общий кэш решает схожую задачу, но через более сложный механизм блокировок, который сложнее отлаживать и который официально считается устаревшим.
Можно ли использовать общий кэш с базой данных в памяти? Да, начиная с версии SQLite 3.7.13, но только если база данных открыта через URI-имя файла. Открытие через имя :memory: всегда отключает общий кэш независимо от глобальных настроек.
Что произойдёт, если запрос не сможет получить блокировку таблицы? Запрос завершится неудачей и вернёт код ошибки SQLITE_LOCKED. Приложение должно явно обрабатывать этот случай.
Влияет ли режим read-uncommitted на операции записи? Нет. Режим read-uncommitted снимает необходимость получать блокировки чтения, но соединение по-прежнему обязано получать блокировки записи. Операции записи могут блокировать и быть заблокированными в любом режиме изоляции.
Как включить общий кэш только для одного соединения, не меняя глобальную настройку? Нужно использовать sqlite3_open_v2() с флагом SQLITE_OPEN_SHAREDCACHE в третьем параметре. Этот флаг переопределяет глобальную настройку, установленную через sqlite3_enable_shared_cache(), для конкретного соединения.



