SQLiteで「database is locked」が出たら、ファイルを削除したり待機時間だけを長くしたりする前に、失敗したSQL、結果コード、接続とtransactionの状態を確認します。別接続との書込競合、同じ接続内の競合、古いsnapshotからの書込昇格では、再試行する単位が違います。
この記事はSQLiteを直接またはdriver経由で使うアプリの診断手順です。以下の接続A/Bや時系列は架空の説明例で、実DBでロックや復旧を再現した結果ではありません。利用driverが返す例外とSQLiteの結果コードの対応は、そのdriverの仕様で確認します。
目次
文字列だけでなく結果コードを確認する
SQLiteの結果コード資料(新しいタブで開く)は、SQLITE_BUSYを主に別接続との競合、SQLITE_LOCKEDを同じ接続内またはshared cacheの競合として区別しています。表示文だけで両者を同一視せず、取得できるならextended result codeも記録します。
表は横にスクロールできます
| 記録 | 調べる内容 | 秘密値を出さない方法 |
|---|---|---|
| 失敗した操作 | BEGIN・読取後のUPDATE・COMMITなど | SQLの種類と処理IDを残す |
| 結果コード | BUSY・LOCKED・extended code | driver例外のコード欄を使う |
| 接続とtransaction | 接続ID、開始/終了、失敗時状態 | parameter値や行本文を記録しない |
| 同時処理 | worker数、migration、別process | 実行主体と時刻を対応付ける |
DB pathも確認します。同じつもりのDBが複数ある、別の管理ツールやmigrationが開いている、といった条件を分けます。ファイルを開いているprocessがあるだけで、必ずそのprocessが現在の書込ロックを保持していると断定しません。
transactionを開いたまま待つ処理を探す
SQLiteのtransaction資料(新しいタブで開く)は、同時に複数の読取transactionを許す一方、書込transactionは一つと説明しています。transactionの中に外部HTTP要求、利用者入力待ち、大きなファイル処理があると、DB以外の待ち時間まで保持期間に含まれる可能性があります。
接続ごとに開始・最初の読取・書込・commitまたはrollbackを並べます。cursorやstatementの終了とtransactionの終了も区別し、例外経路で開いたままにならないか確認します。短くする場合も、まとめて確定すべき業務操作を無条件に分割しません。
表は横にスクロールできます
| 架空の観測 | 原因候補 | 次の確認 |
|---|---|---|
| Aの書込中にBのBEGINが失敗 | 同時writer競合 | Aの保持時間とBの再試行 |
| Aが読取後に待ち、その後UPDATE失敗 | 書込昇格・snapshot競合 | journal modeとextended code |
| 同接続で処理が重なりLOCKED | statement・cursor・shared cache | driverの接続利用契約 |
| COMMITでBUSY | commit時の競合 | transactionが残っているか |
BEGIN IMMEDIATEは書込transactionを早い段階で開始するための選択肢ですが、他writerがいれば開始時にBUSYとなり得ます。どこで競合を検出するかを変える設計判断で、同時書込を無制限に通す設定ではありません。
timeoutと再試行を同じものにしない
busy timeoutの公式説明(新しいタブで開く)は、一定時間待ってもロックを取得できなければBUSYを返す仕組みです。timeoutを増やして一度通っても、保持し続けるtransactionや過剰なwriter数を解決した証拠にはなりません。
待機する時間は、要求全体のtimeoutやジョブの再試行上限と合わせます。driver、接続pool、ジョブ基盤で待機を重ねると、利用者から見た停止時間が長くなります。どの層が待ち、どの層が処理をやり直すかを決めます。
表は横にスクロールできます
| 失敗した状態 | 再試行の判断 | 注意する点 |
|---|---|---|
| transaction開始前のBUSY | 上限付きで開始を再試行 | 同じ処理の同時多重起動を抑える |
| SQLITE_BUSY_SNAPSHOT | transactionを終えて新しい状態からやり直す | 古い読取結果でUPDATEだけ反復しない |
| COMMITのBUSY | driverとtransaction状態を確認 | transaction全体が消えたと決めない |
| 外部への副作用も実行済み | DB状態と外部結果を照合 | メール・課金・通知を一緒に無条件再実行しない |
SQLITE_BUSY_SNAPSHOTは、WALの読取snapshotが他接続の書込で古くなり、書込へ昇格できないケースです。必要なrollbackと新transactionからの再読取をdriverの手順で行います。一方、公式資料はCOMMITがBUSYになってもtransactionが残り、後でcommitを再試行できる場合を説明しています。すべてのBUSYへ同じrollback/retryを当てはめないようにします。
WALはwriter競合の万能な解決ではない
WALの公式資料(新しいタブで開く)は、readerとwriterの並行を改善しても、writerは同時に一つであると説明しています。また、通常のWAL構成は同じhost上のprocessを前提とし、network filesystemでは動作しないとしています。共有ストレージ上のDBへ、診断だけを目的にWALを強制する手順にはしません。
長いread transactionはcheckpointの進行にも影響します。journal modeを変える前に、接続の寿命、transactionの境界、保存場所、backupと復旧方式を確認します。-walや-shmファイルを「残ったロック」と決めて手作業で消すことは避け、DB一式を利用中の方式に沿って扱います。
修正は競合した条件で確かめる
検証用DBでは、まず一つのwriterで基準動作を確認し、実運用に近い数の接続・並列処理へ戻して比較します。一つずつに減らして成功した結果だけを、元の並列構成でも直った証拠にはしません。
完了記録には、保持時間、同時writer数、BUSYの発生位置、再試行上限、最終的な確定結果と二重処理の有無を残します。解消できなければ、書込を一つのqueueへ集約する、transactionを短くする、必要な同時更新量に合う保存方式を検討するなど、観測した原因に対応する設計へ戻ります。
