【GASの排他制御】LockServiceでデータの上書きを防止せよ | GASおじさんのブログ
GASのTips

【GASの排他制御】LockServiceでデータの上書きを防止せよ

LockService解説 GASのTips

みなさんこんにちは!GASおじさんです。

今回はGASの排他制御、LockServiceの使い方について解説します。

GASの同時実行によるデータの上書き問題

レコードを追加する3つのボタン

解説のために以下のスプレッドシートを用意しました。

このスプレッドシートには以下のGASが添付されています。

function appendA() {
  appendRecord('A');
}

function appendB() {
  appendRecord('B');
}

function appendC() {
  appendRecord('C');
}

function appendRecord(value) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  for (let i = 1; i <= 10; i++) {
    sheet.appendRow([new Date(), "=ROW()-1", value, i]);
  }
}

スプレッドシート上の青いボタンにはそれぞれ関数を割り当てており、

  • Aボタンを押すとappendA
  • Bボタンを押すとappendB
  • Cボタンを押すとappendC

が実行されるようになっています。

試しにAボタンを押すと、以下のようになります。

10行分のレコードが追加されました。

次にBボタンを押してみます。

Aのレコードに続けて、さらに10行分のBのレコードが追加されました。

最後にCボタンを押してみます。

10行分のCのレコードが追加され、全部で30行分のレコードが登録されました。

ここまでは問題ないでしょう。

3つのボタンを同時に実行してみる

さて、それでは一度すべてのレコードを削除し、再度3つのボタンを実行してみます。

その際、A, B, Cの3つのボタンを同時に押して実行してみます。

動きがわかりやすいように以下に動画を添付します。レコードが追加されていく様子にご注目ください。

ところどころで、データの上書きが起きていることがわかるでしょうか?

そのせいで、本来なら30行分記録されなければならないところを、今回は23行しか記録できませんでした。

つまり、7行分のレコードが虚空の彼方に消えてしまったということになります。

このように、GASを同時多発的に実行するとデータの上書きが発生することがあります。

この問題を解決するのが「排他制御」です。

排他制御とは?

排他制御とは何か。ChatGPTに聞いてみました。

排他制御(Exclusive Control)とは、複数の処理が同時に同じリソース(データやファイルなど)にアクセスした際に、競合や不整合が発生するのを防ぐ仕組みのことです。特にGASでは、スプレッドシートやプロパティサービスなど、共有リソースに対して同時に操作が行われるケースがよくあります。

例えば、複数のユーザーが同時にフォームを送信し、そのデータをスプレッドシートに書き込もうとするとします。このとき、1つのユーザーの操作が他のユーザーの操作に割り込む形でデータを書き換えてしまうことが起こり得ます。これにより、意図しないデータの上書きや消失が発生し、システム全体の信頼性が損なわれる可能性があります。

こうした問題を防ぐために、排他制御を用いて「リソースにアクセスしているのは現在1つの処理だけ」と保証する仕組みが必要です。GASでは、この排他制御を簡単に実現するためにLockServiceが提供されています。このサービスを利用することで、リソースの同時操作によるトラブルを防ぎ、データの一貫性を確保することができます。

排他制御を適切に導入することで、システムの安定性が向上し、信頼性の高いアプリケーションを作ることが可能です。特に、業務システムや複数ユーザーが利用するツールでは必須の技術と言えるでしょう。

ChatGPT

だそうです。要するに同時操作によるデータの上書きを防ぐための仕組みですね。

LockServiceでデータの上書きを防止する

それでは実際に排他制御を実装してみましょう。

GASエディタに以下のスクリプトを追加します。

function appendAwithLock() {
  appendRecordWithLock('A');
}

function appendBwithLock() {
  appendRecordWithLock('B');
}

function appendCwithLock() {
  appendRecordWithLock('C');
}

function appendRecordWithLock(value) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  const lock = LockService.getScriptLock();
  if (lock.tryLock(9000)) {
    for (let i = 1; i <= 10; i++) {
      sheet.appendRow([new Date(), "=ROW()-1", value, i]);
    } 
    lock.releaseLock();
  } else {
    console.log('ロックを取得できませんでした');
  }
}

また、スプレッドシートには新たに3つの黄色ボタンを設置します。

  • 黄色のAボタンを押すとappendAwithLock
  • 黄色のBボタンを押すとappendBwithLock
  • 黄色のCボタンを押すとappendCwithLock

が実行されるようにしました。

それではいざ、黄色の3つのボタンを同時に押して実行してみます。

その様子を動画でご覧ください。

いかがでしょうか?

今回はバッチリ30行分記録してくれましたね!

その際、Aを記録し終えてからBを記録し、Bを記録し終えてからCを記録する、というように、順番に記録されていっていることに気づいたでしょうか。

このように、とあるタスクを実行しているときは、他のタスクは一旦待機させて、前のタスクが終わったら次のタスクを実行する、という制御フローを組むことができるのです。

LockServiceの仕組み

それではあらためてコードを見てみましょう。

function appendRecordWithLock(value) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  const lock = LockService.getScriptLock();
  if (lock.tryLock(9000)) {
    for (let i = 1; i <= 10; i++) {
      sheet.appendRow([new Date(), "=ROW()-1", value, i]);
    } 
    lock.releaseLock();
  } else {
    console.log('ロックを取得できませんでした');
  }
}
  • 4行目でLockオブジェクトを生成
  • 5行目でロックをかける(tryLock)
  • 9行目でロックを解除する(releaseLock)

という流れになっています。

このスクリプトがA, B, Cと3つ同時に実行されるわけですが、その際、カンマ数秒の差で「A」が先に実行されるため、AのtryLockが成功し、BとCは一旦待機状態となります。

このとき最大何秒待機させるかをtryLockの引数にミリ秒単位で指定します。今回は「9000」としているので、最大9秒間待機させることができるということです。

ここの秒数は処理の内容に応じて適切な値を指定してください。あまりに短すぎるとロックをかけるのに失敗して、elseの処理が実行されることになります。

今回はfor文で10行分レコードを追加するという処理を書いており、これがおよそ4〜5秒ほどかかるので、少し余裕を持たせて9秒という設定にしました。実際はもっと余裕を持たせてもいいかと思います。どれくらいの頻度で同時実行が起き得るのかや、実行時間の上限(いわゆる6分の壁)のことなども考えながら、適切な秒数を指定するようにしましょう。

Aのレコード追加が無事終了し、lock.releaseLock()が実行されると、待機していたB(およびC)のスクリプトが再開されます。

BとCは同時に再開されますが、これまたカンマ数秒の差で「B」が先に実行されているため、BのtryLockが成功し、Cは一旦待機状態となります。

あとは同様に、Bのスクリプトでlock.releaseLock()されたら、待機していたCが再開される、という流れですね。

LockServiceの種類

LockServiceには以下の3種類があります。

  • LockService.getScriptLock()
  • LockService.getDocumentLock()
  • LockService.getUserLock()

それぞれ以下のような違いがあります。

ロックの種類適用範囲主な用途使用例
ScriptLockスクリプト全体グローバルな競合防止同時実行が多いスクリプト全体の操作制御
DocumentLock特定のドキュメント単位特定ドキュメント内の操作の競合防止スプレッドシートの操作
UserLock実行中のユーザー単位同一ユーザーの多重実行防止フォームの二重送信防止

コンテナバインドスクリプトを使用していて、特定のドキュメント操作に絞りたい時などはDocumentLockを使う、同一ユーザーによる多重実行を防ぎたい場合などはUserLockを使う、というような使い分けができるようです。

ただ、正直なところ、私はDocumentLockとUserLockを使ったことがなく、この2つの具体的な使いどころはよくわかりません。ScriptLockであれば大抵のケースをカバーできてしまうので、基本はScriptLockを使っておけばいいのかなと考えています。

使用例: 予約システムでダブルブッキング防止

以下の記事で紹介した会議室予約システムでもLockServiceを使っています。

予約システムを作る際はダブルブッキング防止策が非常に重要となりますが、これを実装するうえでLockServiceが大活躍しています。

気になる人はぜひコピーを作成してスクリプトを覗いてみてください。

まとめ

以上、GASの排他制御「LockService」について解説しました。

LockServiceを活用することで、スプレッドシートへのデータの上書きや、Googleカレンダーへのダブルブッキングを防ぐことができます。特に業務で利用するツールや多人数が利用するシステムでは、LockServiceを重宝することになるでしょう。とても便利なのでぜひご活用ください。

それではまた!

コメント

タイトルとURLをコピーしました