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. BEGINBEGIN IMMEDIATEの違いは何ですか?
    • BEGIN IMMEDIATEはただちに書き込みロックを取り、BEGINは最初の書き込みアクセスを待つ
    • BEGIN IMMEDIATEはロールバックジャーナルを無効にする
    • BEGIN IMMEDIATEは新しいデータベースを作る
    • 振る舞いに違いはない
  3. ファントムリードとは何ですか?
    • 別のトランザクションがまだコミットしていない値を読むこと
    • 同じ行を読み直して異なる値を得ること
    • 範囲クエリを再実行し、その間に挿入された新しい行が現れるのを見ること
    • たった今削除された行を読むこと