Kodokon kodokon.com

Конкурентность: блокировки, уровни изоляции, фантомные чтения

Разберись в модели блокировок SQLite, в его транзакциях и в аномалиях изоляции, которые описывает стандарт SQL.

12 мин · 3 вопросов

Открыть этот урок в Kodokon

SQLite блокирует на уровне всего файла базы данных, а не строки. В режиме по умолчанию (журнал отката) читатели делят блокировку SHARED, но получить блокировку EXCLUSIVE может только один писатель за раз. Следствие: записи сериализуются. Создадим таблицу, чтобы поработать с транзакциями.

SQL
CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  balance INTEGER NOT NULL
);

INSERT INTO accounts (id, balance)
VALUES (1, 100), (2, 50);

-- Read: deferred transaction (default)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
Явная транзакция, обёрнутая в BEGIN/COMMIT.

Блокировка развивается ступенями: UNLOCKED, затем SHARED (чтение), RESERVED (намерение записать), PENDING и, наконец, EXCLUSIVE (запись). Если блокировку на запись уже держит одна транзакция, другая получает ошибку SQLITE_BUSY. Вместо того чтобы падать сразу, задай время ожидания через busy_timeout и включи режим WAL, чтобы читатели и писатель могли продвигаться параллельно.

SQL
-- A single writer, concurrent readers
PRAGMA journal_mode = WAL;

-- Wait up to 5 s if the database is busy
PRAGMA busy_timeout = 5000;
WAL разблокирует параллельные чтения; busy_timeout заставляет подождать.

Стандарт SQL описывает три аномалии: грязное чтение (чтение незафиксированных данных), неповторяющееся чтение (перечитываешь строку и видишь другое значение) и фантомное чтение (повторяешь запрос по диапазону и видишь, как появляются новые строки). Поскольку SQLite сериализует писателей, на практике он ведёт себя как SERIALIZABLE: эти аномалии не появляются (кроме режима общего кэша с read_uncommitted). В других движках уровень выбираешь явно.

SQL
-- Atomic transfer: immediate write lock
BEGIN IMMEDIATE;
UPDATE accounts
SET balance = balance - 10
WHERE id = 1;
UPDATE accounts
SET balance = balance + 10
WHERE id = 2;
COMMIT;
BEGIN IMMEDIATE резервирует блокировку до любой записи.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Какой уровень изоляции SQLite предлагает на практике?
    • READ UNCOMMITTED, грязные чтения возможны по умолчанию
    • READ COMMITTED, как в PostgreSQL
    • SERIALIZABLE, потому что за раз действует только один писатель
    • Никакой, транзакций нет
  2. В чём разница между BEGIN и BEGIN IMMEDIATE?
    • BEGIN IMMEDIATE берёт блокировку на запись сразу; BEGIN ждёт первого обращения на запись
    • BEGIN IMMEDIATE отключает журнал отката
    • BEGIN IMMEDIATE создаёт новую базу данных
    • Разницы в поведении нет
  3. Что такое фантомное чтение?
    • Чтение значения, которое другая транзакция ещё не зафиксировала
    • Повторное чтение той же строки с другим результатом
    • Повторение запроса по диапазону, в котором появляются строки, вставленные тем временем
    • Чтение строки, которую только что удалили