GASの一時データは CacheService へ

GAS

※本記事はプロモーションを含みます

目次

スプレッドシートのカスタム関数や定期実行トリガーを動かしていると、毎回外部APIを叩きにいったり、シート全体の複雑な集計をゼロからやり直したりして処理が遅くなる現場によく出くわします。Google Apps Script(GAS)には6分という明確な実行時間制限があります。なので、カスタム関数の場合は30秒程度でタイムアウトしてしまうため、実行速度の遅れはそのまま運用の破綻に直結します。そこで候補に挙がるのが一時的なデータの退避場所です。ただし、毎回シートの特定セルに書き込んで読み出したり、PropertiesServiceに放り込んだりしていると、今度はセル読み書きの通信遅延やプロパティストアの容量制限・割当回数に阻まれます。短時間だけ使い回せれば十分な中間データや、数時間変動しないマスター情報の一時退避には、メモリ感覚で扱えるCacheServiceを組み込むのが最もシンプルで効果的です。

プロパティストアやセルを一時置き場にすると発生する処理遅延とクォータ圧迫

GASでデータを一時保存しようと考えたとき、真っ先に思い浮かびやすいのがPropertiesServiceやスプレッドシート上の作業用シートです。ただし、一時キャッシュという用途において、これらは設計上の特性が大きく異なります。スプレッドシートのセルへ書き込む処理はHTTPリクエストを介した高コストな通信です。なので、たとえ数セルの読み書きであっても数十から数百ミリ秒の遅延が積み重なります。これをループ内や頻繁に呼ばれる関数の中で繰り返すと、キャッシュで高速化したいはずが、スプレッドシートへのアクセス自体が最大の障害になって本末転倒な状態に陥ります。

一方のPropertiesServiceは、スクリプトの設定値やユーザーごとの永続トークンなどを保持するための仕組みです。設定値が保存されるという安心感はあります。ただし、プロパティの書き込みや読み込みにも内部的なアクセス制限が存在します。短時間で数百回、数千回と更新を繰り返すと、プロパティ操作の制限上限に抵触してスクリプト全体が停止する危険があります。というのは、値が半永久的に残り続けるため、「1時間だけ有効にしたい」「次のバッチ処理まで持っていればいい」といった消える前提のデータを放り込むと、古い値の消去処理を自分で書かなければならず、キーの管理が複雑化するからです。

CacheServiceは、こうした「一定時間だけ保持されていればよく、消えても再取得できれば問題ないデータ」を扱うために作られています。データは一時保存の領域に近い層で管理されます。なので、プロパティやセル書き込みと比較して圧倒的に返答が速く、GAS全体の実行時間短縮に大きく貢献します。有効期限が切れたデータはシステム側で自動的に破棄されます。なので、ゴミ掃除のスクリプトを自前で用意する必要もありません。

CacheServiceで重いAPIレスポンスを保持して再取得を防ぐ基本構成

CacheServiceを利用する典型的なパターンは、外部の為替レート取得APIや社内システムのREST APIから取得した返答を保存する手順です。この保存によって、同一スクリプト内やトリガー実行間での重複アクセスを排除します。外部通信はネットワークの待ち時間が大きいです。なので、相手先サーバーの負荷状況によっては返答が数秒返ってこないことも珍しくありません。頻繁に呼び出す外部APIの返答をキャッシュして呼び出し回数を激減させることができれば、GAS実行時間の削減だけでなく、外部サービスのAPI呼び出し制限の節約にも直結します。

キャッシュの実装で注意すべき点は、キャッシュが存在しなかった場合の予備処理を必ず同一の流れの中に組み込むことです。キャッシュは一時的なものです。なので、指定した有効期限が切れていなくても、システムのメモリ圧迫やメンテナンス等によって途中で消去される可能性があります。「キャッシュにある前提」でコードを書くと、データが消えていた瞬間にnullが返ります。なので、後続の読み取り処理や計算処理で実行時エラーを引き起こします。まずはキャッシュを読みに行きましょう。ただし、データが存在しなければ重い本処理を実行して、改めてキャッシュに書き込むという条件分岐を徹底しましょう。

function getExchangeRate() {
  const cache = CacheService.getScriptCache();
  let data = cache.get("cached_rates");

  if (!data) {
    // キャッシュが存在しない場合は重いAPI通信や集計を実行
    data = "150.25"; // 本来は UrlFetchApp.fetch() などの取得処理
    // 最大保存時間である最長21600秒(6時間)までメモリ上に保持できる
    cache.put("cached_rates", data, 21600);
  }

  return parseFloat(data);
}

このコードでは、まずScriptCacheからキー名を指定して値を取り出します。初回実行時や前回の書き込みから時間が経過している場合はnullが返ります。なので、ifブロック内の重い取得処理が動きます。取得したデータはcache.put()を使って即座にキャッシュへ保存されます。第3引数には有効期限を秒単位で指定します。なので、最長で21600秒(6時間)までデータを保持できます。2回目以降の呼び出しでは外部通信を行うことがありません。なので、わずか数ミリ秒でキャッシュから値を取り出して返却できるため、スクリプトの実行速度が大幅に向上します。

文字列しか入らない仕様と100KB上限を回避するための前処理

CacheServiceを実務で使い始めるとすぐに直面するのが、「保存できるデータ型は文字列(String)のみ」という厳格なルールです。オブジェクトや配列、数値型をそのままcache.put()の引数に渡すと、予期しない型変換が発生したりエラーになったりします。APIの返答の多くはJSONオブジェクト形式で返ってきます。なので、キャッシュへ格納する際にはJSON.stringify()で文字列化しましょう。ただし、取り出す際にはJSON.parse()で元のオブジェクトへ復元する流れが必須となります。

ここで見落としがちなのが、CacheServiceにおける容量制限です。1つのキーに対して保存できる値の最大サイズは100KB(102,400バイト)に制限されています。スプレッドシート全体から取得した数千行の二次元配列や、構造の深い巨大なJSON返答を安易にJSON.stringify()して突っ込もうとすると、容量オーバーのエラーが発生して書き込みに失敗します。というのは、巨大なデータを一時退避させたい場合、不要な項目をあらかじめ削ぎ落として必要なデータだけに絞り込むか、キーを分割して保存する設計が必要になるからです。

また、CacheService全体で保持できる総容量にも制限があります。なので、無秩序に大量のキーを発行して巨大文字列を保存し続けると、古いキャッシュが予期せず押し出される原因になります。あくまで「頻繁に使う最小限のデータセット」に絞り込んでキャッシュ化することが、安定運用の基本です。たとえば、マスタデータ全体を放り込むのは避けましょう。なので、当日の処理で参照されるキーと値のペアだけを抽出して保存するなどの事前整理が効果を発揮します。

DocumentCache・UserCache・ScriptCacheのスコープ選定でデータが混線する事故

CacheServiceには、アクセスの共有範囲(スコープ)に応じて3つの異なるキャッシュ取得ツールが用意されています。具体的には、コンテナバインドスクリプト専用のgetDocumentCache()、実行ユーザーごとに分離されるgetUserCache()、そしてスクリプト全体で共有されるgetScriptCache()です。このスコープの選択を誤ると、「別ユーザーの作業データが混入する」「バッチ処理でキャッシュが共有されず毎回再計算される」といった致命的な不具合を招きます。

たとえば、社内の複数ユーザーが同一のスプレッドシートを開いてカスタム関数を動かすケースを考えます。為替レートやマスターテーブルのように全員共通で参照してよいデータであれば、getScriptCache()を使ってスクリプト全体で共有するのが最も効率的です。というのは、誰か一人が一度取得してキャッシュに入れてしまえば、他のユーザーが実行した際にもそのキャッシュが利用されるからです。なので、外部APIへのリクエスト数を最小限に抑えられます。

ただし、ユーザー個人のGoogleアカウントに紐づくデータや、ユーザーごとの権限によって取得結果が異なるデータをgetScriptCache()で保存してしまうと問題が起きます。というのは、Aさんの個人データがBさんの画面に表示されるという重大な情報漏洩リスクが生じるからです。個別のユーザーセッションに閉じ込めるべきデータは、必ずgetUserCache()を使用して隔離しましょう。逆に、時間主導型トリガーで動くバックグラウンドバッチ処理では、実行主体がトリガー作成者となります。なので、ユーザーキャッシュを使おうとするとウェブアプリ経由でアクセスした一般ユーザーのキャッシュ空間と完全に分断され、キャッシュが全く機能しない状態に陥ります。処理の実行主体が誰であり、そのデータが誰の視点で共有されるべきなのかを整理した上でスコープを確定させましょう。

キャッシュが消えている前提で組むフォールバック設計と運用の勘所

CacheServiceを使う上で最も根本的な心構えは、「キャッシュはいつでも突然消えうる」という前提で例外処理を設計することです。cache.put()で21600秒(6時間)を指定したとします。ただし、Googleのインフラ側でメモリの再配置やノードの再起動が発生した場合、指定時間より前にキャッシュが消去されるケースは日常的に発生します。というのは、キャッシュは「永続化ストレージ」ではなく、あくまで「消えても業務が止まらない高速化のためのバッファ」だからです。

したがって、業務処理の正確さをキャッシュの存在に依存させてはいけません。キャッシュから値を取得できなかった場合は、必ず正規のデータ参照元(外部API、スプレッドシート本体、BigQuery、Cloud SQLなど)からデータを再取得しましょう。なので、再度キャッシュに詰め直すコードの通り道を準備する必要があります。この予備処理が正しく組まれていれば、キャッシュが何らかの理由で消えても、わずかに実行時間が延びるだけでスクリプト自体は何事もなく処理を完了できます。

また、開発時のテスト工程においてもキャッシュの挙動には注意が必要です。コードを修正してテスト実行している最中、古いキャッシュが残っていることがあります。なので、「バグを修正したはずなのに古いデータが返り続ける」「APIの仕様変更が反映されない」といったトラブルで時間を浪費することがよくあります。開発時やテストの直前には、一時的にキャッシュ取得部分をバイパスしましょう。あるいはcache.remove("key")を実行して確実にキャッシュを破棄する手順を挟む運用を徹底すると、原因特定に迷う時間を激減させることができます。

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