Apps Scriptで同時実行すると連番が重なったり、シートの更新が一部消えたりする場合、LockServiceを呼んだかだけでなく、何をどの範囲で保護したかを確認します。古い値を読んだ後でlockを取っても、同じ古い値から上書きする競合は残ります。
この記事は、同じApps Script projectの処理が共有シートをread-modify-writeする場合の設計です。LockServiceはsheet全体を人の編集や別APIから保護するDB transactionではありません。以下の表とコードは検証用の架空例で、Apps ScriptやSheetsで同時実行・書込を試した結果ではありません。
目次
読み取りから書き戻しまでを一つにする
架空のカウンターが40のとき、AとBがともに40を読み、それぞれ41を書けば、二回の更新が一回分になります。必要な保護単位は、lock取得、現在値の読取、検算、変更、書込、反映、解放という範囲です。
表は横にスクロールできます
| 順序の架空例 | A | B | 結果の意味 |
|---|---|---|---|
| lockなし | 40を読む | 40を読む | 同じ旧値を保持 |
| それぞれ書く | 41を書く | 41を書く | 一回分を上書き |
| 読取前に排他 | 40→41を反映して解放 | その後41→42を反映 | 二回分を順に計算する設計 |
これは論理上の例で、実際に42になった実測報告ではありません。GoogleのLock公式資料(新しいタブで開く)も、同時の採番で同じ値を読み重複する例を示しています。すべての更新経路が同じ保護規約を守るかを確認します。
Script・User・Documentのscopeを選ぶ
LockServiceの公式資料(新しいタブで開く)では、Script Lockはscript単位、User Lockはscriptとuser単位、Document Lockはscriptと現在のdocument単位の排他です。複数ユーザーが同じカウンターを更新するのにUser Lockだけを使うと、ユーザーをまたぐ排他にはなりません。
表は横にスクロールできます
| 種類 | 保護の単位 | 判断する条件 |
|---|---|---|
| Script Lock | 同じscriptの実行 | 全userで共有する更新か |
| User Lock | 同じscript・同じuser | userごとに独立した状態か |
| Document Lock | 同じscript・現在document | documentの実行contextがあるか |
standalone scriptやWebアプリなど、包含documentのcontextがない場合、getDocumentLockはnullを返します。いつもbound scriptで動く前提を、別の実行経路へ持ち込みません。Script Lockも別script projectや手作業のシート編集まで止めるものではありません。
取得できた場合だけ更新する
lock objectを取得しただけでは排他は始まりません。tryLockは指定時間内の取得成否をbooleanで返し、waitLockは取得できなければ例外になります。待機時間はミリ秒です。取得失敗を無視して処理を続けないようにします。
次は検証用のbound scriptで、「検証」シートのB2に非負の整数を置いた場合の説明例です。実運用の採番・会計・在庫へそのまま使う完成実装ではありません。5000は例の待機上限で、推奨値や成功保証ではありません。
function incrementTestCounter() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
return { status: 'busy', changed: false };
}
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) throw new Error('Bound spreadsheet required');
const sheet = ss.getSheetByName('検証');
if (!sheet) throw new Error('Test sheet required');
const cell = sheet.getRange('B2');
const value = cell.getValue();
if (!Number.isSafeInteger(value) || value < 0 ||
!Number.isSafeInteger(value + 1)) {
throw new Error('Safe nonnegative integer required');
}
cell.setValue(value + 1);
SpreadsheetApp.flush();
return { status: 'updated' };
} finally {
lock.releaseLock();
}
}
取得に失敗したbusyは更新成功とは別の結果です。例外になった場合も、書込前に止まったのか、書込後の反映で失敗したのかを区別します。flushに失敗したから必ず元に戻ったとは言えず、結果不明のまま同じ更新を無条件に再実行しません。
flushと解放を更新の境界に置く
Googleの公式資料は、spreadsheet操作では保留中の変更を反映するため、lock解放前のSpreadsheetApp.flushを案内しています。finallyで解放すれば、例外経路でも不要な保持を避けられます。ただしflushとlockを組み合わせても、複数cellの更新をDB transactionのように一括rollbackできるわけではありません。
外部APIや長い計算は可能ならlock取得前に済ませ、共有状態に関係する読取と更新だけを短く保ちます。ただしlock前に読んだ共有状態から更新値を決めると競合が戻るため、取得後に必要な現在値を取り直します。
表は横にスクロールできます
| 症状 | 確認する箇所 | 修正の方向 |
|---|---|---|
| lockがあるのに更新が欠ける | 読取がlockより前ではないか | read-modify-write全体を保護 |
| userが違うと重複 | User Lockを使っていないか | 共有状態に合うscope |
| timeoutが増える | 保持時間・外部呼出・多重起動 | 保護範囲と流量を整理 |
| 外部API更新と競合 | 別writerの経路 | 共通の更新責務や保存方式を設計 |
検証は同時実行と失敗経路も含める
検証用シートで、逐次二回、同時二回、lock取得失敗、入力不正、書込結果が不明なケースを分けます。期待値、返った状態、更新後の値、未処理の要求を照合し、実行ログが成功でも一回分が失われていないか確認します。
再試行が必要な業務では、要求IDと処理済み状態を使う重複対策を別に設計します。LockServiceは同時実行を直列にしても、同じ要求が後から再送されたことを自動では識別しません。失敗時は原因を修正して検証用の初期状態へ戻し、実データを一括で巻き戻す手順にはしません。
