SQLite3 の弱点を克服する Write-Ahead Logging


SQLite3 in production シリーズ第二弾です。

第一回:SQLite3 はなぜ本番環境で使えるようになったのか

第一回では、SQLite3 には以下の弱点があるという話をしました。

  1. 同時アクセスに弱い
  2. 水平スケールが困難

今回は、前者「同時アクセスに弱い」を解消すべく SQLite3 に導入された、「Write-Ahead Logging」について説明します。

SQLite3 のトランザクション戦略

SQLite3 は「トランザクショナルなデータベース」を謳っており、ACID を非常に重要視しています。(参考: SQLite is Transactional ↗

SQLite3 の伝統的なトランザクション実装は 「rollback journal」 ↗ と呼ばれています。簡潔にまとめてみましょう。

  1. 変更を加える前に、本体ファイルのデータをコピーしてバックアップを作成
  2. 本体ファイルに変更を書き込む
  3. コミットするなら、バックアップを破棄。ロールバックするなら、バックアップの内容を本体ファイルに書き戻す

「本体ファイルへの書き込み」は、対象レコードが記録されている領域のビットを直接変更する処理ですから、その領域の「排他ロック」を取る必要があります。 すると、本体ファイルへの書き込み中は、その他全ての読み取り・書き込み処理が待たされます。

この制約ゆえに、SQLite3 は同時アクセスに弱いデータベースでした。

Write-Ahead Logging 登場

「rollback journal」の弱点を克服するために実装されたのが 「Write-Ahead Logging」 ↗ です。以下、WAL と略します。

こちらの手順も簡潔にまとめます。

  1. 変更を書き込む時、ログファイルへ変更ログを追記する
  2. コミットする場合、コミットを示す特別なログを追記

そして、読み取りの際は、本体ファイルとログファイル両方を参照します。

WAL がすごいのは、書き込み命令を、ログファイルへの「追記」と再定義したことです。

追記しかしないなら、書き込み処理が必要とする「排他ロック」の対象範囲は、ログファイルの末尾部分に絞れます。 読み取りの際は、本体とログの両方を参照しますが、ログの末尾に追記が行われている間も、「直前にコミットされた地点」までのログは安全に読めます。 SQLite3 は、この工夫で「読み書きの同時実行」を実現しました。

残る課題

しかし、WAL が実装されたのは2010年の話。ちなみに、Rails 3.0.0 のリリースが 2010/8/29 です。

WAL は確かに SQLite3 の Concurrency を改善しました。しかし、それだけでは「本番環境の Web アプリケーション」で使えるレベルには至らなかったのです。

WAL によって、読み書きを同時に実行できるようになりましたが、「書き込み同士の衝突」は依然としてエラーになってしまうからです。

まとめ

SQLite3 には「rollback journal」と「Write-Ahead Logging」という2つのトランザクション実装があり、WAL によって「読み書きの同時実行」が実現した、というストーリーでした。

残った課題「書き込み衝突によるエラー」を解消するためには、busy_handler というロック競合ハンドラと、Rails の SQLite3 アダプターがどのように busy_handler を活用しているか、を理解する必要があります。 次回をお楽しみに。

フィードバックを送る

  • • お送りいただいた内容は、筆者が全て目を通し、今後の励みとさせていただきます
  • プライバシー: お名前やメールアドレスの収集は行っておりません
  • 返信: 全てへの返信はお約束できかねますが、必要な場合は本文内に連絡先を添えてください
  • 公開の可能性: 個人を特定できない形で記事内で紹介させていただく場合があります