前提知識
- スレッド — プログラム内で同時に動く処理の単位。複数スレッドがあると、複数の作業を並行して進められる
- mutex(MUTual EXclusion) — 「同時に1人しか入れないトイレの鍵」のようなもの。あるスレッドが鍵を持っている(=ロック中)間、他のスレッドはドアの前で待つ。鍵を返す(=アンロック)と次のスレッドが入れる。これにより共有リソースへの同時アクセスを防ぎ、データの衝突を避ける
- libuv — Node.jsが内部で使っているライブラリ。Node.jsはシングルスレッドだが、DB操作やファイル読み書きなどの重い処理はlibuvが裏側のスレッドプール(デフォルト4スレッド)に投げて並行処理する
スレッディングモード
SQLiteは「複数スレッドからのアクセスをどう扱うか」を3つのモードから選べる。
| モード | フラグ | 特徴 |
|---|---|---|
| Single-thread | — | ロックなし。最速だが1スレッド専用 |
| Multi-thread | OPEN_NOMUTEX | スレッドごとに別の接続なら安全 |
| Serialized | OPEN_FULLMUTEX | 同一接続を複数スレッドで共有しても安全(mutexで直列化) |
なぜNode.jsでFULLMUTEXがデフォルトなのか
db.run("INSERT ...", callback1);
db.run("UPDATE ...", callback2);
db.run("SELECT ...", callback3);
この3つの db.run() は同じ接続(db)を使っているが、実際のSQL実行はlibuvのスレッドプールに渡される。タイミング次第では別々のスレッドが同じ接続に同時アクセスする。
┌──────────────────────────┐
│ Node.js メインスレッド │
│ │
│ db.run("INSERT ...") │
│ db.run("UPDATE ...") │
│ db.run("SELECT ...") │
└────────┬─────────────────┘
│ libuvがスレッドプールに振り分け
▼
┌─────────────────────────────────────────┐
│ libuv スレッドプール │
│ │
│ Thread 1 ──┐ │
│ Thread 2 ──┼──▶ 同じ db 接続に同時アクセス │
│ Thread 3 ──┘ 💥 衝突の危険! │
└─────────────────────────────────────────┘
◆ FULLMUTEXなしの場合(危険)
Thread 1 ─── INSERT ──────▶ ┐
Thread 2 ─── UPDATE ──────▶ ├──▶ 💥 同時に書き込み → クラッシュ/データ破損
Thread 3 ─── SELECT ──────▶ ┘
◆ FULLMUTEXありの場合(安全)
Thread 1 ─── INSERT ──▶ 🔒 実行中...完了 🔓
Thread 2 ─── UPDATE ──▶ (待機)🔒 実行中...完了 🔓
Thread 3 ─── SELECT ──▶ (待機) (待機)🔒 実行中...完了 🔓
├──────────── 時間 ──────────────▶
- FULLMUTEXあり → mutexが接続への同時アクセスを1つずつに制御 → 安全
- FULLMUTEXなし → 同時アクセスが衝突 → クラッシュやデータ破損
そのため sqlite3 ライブラリはデフォルトでFULLMUTEXを有効にしている。
// node-sqlite3/src/database.cc
mode = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;