SQLiteの「database is locked」を切り分ける|接続・トランザクション・同時書き込み

SQLiteのdatabase is lockedを結果コード、接続、transaction状態、同時書込から診断し、待機と再試行の範囲を決めます。

AIものさし編集部

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 codedriver例外のコード欄を使う
接続と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
同接続で処理が重なりLOCKEDstatement・cursor・shared cachedriverの接続利用契約
COMMITでBUSYcommit時の競合transactionが残っているか

BEGIN IMMEDIATEは書込transactionを早い段階で開始するための選択肢ですが、他writerがいれば開始時にBUSYとなり得ます。どこで競合を検出するかを変える設計判断で、同時書込を無制限に通す設定ではありません。

timeoutと再試行を同じものにしない

busy timeoutの公式説明(新しいタブで開く)は、一定時間待ってもロックを取得できなければBUSYを返す仕組みです。timeoutを増やして一度通っても、保持し続けるtransactionや過剰なwriter数を解決した証拠にはなりません。

待機する時間は、要求全体のtimeoutやジョブの再試行上限と合わせます。driver、接続pool、ジョブ基盤で待機を重ねると、利用者から見た停止時間が長くなります。どの層が待ち、どの層が処理をやり直すかを決めます。

表は横にスクロールできます

失敗した状態再試行の判断注意する点
transaction開始前のBUSY上限付きで開始を再試行同じ処理の同時多重起動を抑える
SQLITE_BUSY_SNAPSHOTtransactionを終えて新しい状態からやり直す古い読取結果でUPDATEだけ反復しない
COMMITのBUSYdriverと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を短くする、必要な同時更新量に合う保存方式を検討するなど、観測した原因に対応する設計へ戻ります。

確認した公式資料

質問から自分に合うAIを探す