SQLiteのロックモデル、そのトランザクション、そしてSQL標準が記述する分離の異常を理解しましょう。
このレッスンを Kodokon で開くSQLiteは行ではなく、データベースファイル全体のレベルでロックします。デフォルトのモード(ロールバックジャーナル)では、読み手はSHAREDロックを共有しますが、EXCLUSIVEロックを取得できる書き手は一度に一つだけです。その結果、書き込みは直列化されます。トランザクションを扱うためにテーブルを作りましょう。
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;ロックは段階的に変化します。UNLOCKED、次にSHARED(読み取り)、RESERVED(書き込みの意図)、PENDING、そして最後にEXCLUSIVE(書き込み)です。あるトランザクションがすでに書き込みロックを保持している場合、別のトランザクションはSQLITE_BUSYエラーを受け取ります。すぐに失敗させるのではなく、busy_timeoutで待機の遅延を設定し、読み手と書き手が並行して進めるようにWALモードを有効にしましょう。
-- A single writer, concurrent readers
PRAGMA journal_mode = WAL;
-- Wait up to 5 s if the database is busy
PRAGMA busy_timeout = 5000;SQL標準は三つの異常を記述します。ダーティリード(コミットされていないデータを読むこと)、反復不能読み取り(ある行を読み直して異なる値を見ること)、そしてファントムリード(範囲クエリを再実行して新しい行が現れるのを見ること)です。SQLiteは書き手を直列化するため、実質的にSERIALIZABLEとして振る舞います。これらの異常は現れません(read_uncommittedを伴う共有キャッシュモードを除く)。他のエンジンでは、レベルを明示的に選びます。
-- 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とBEGIN IMMEDIATEの違いは何ですか?