副標:用三層快取(記憶體 / CacheService / PropertiesService)大幅降低 API 次數與延遲
問題背景與需求
- 目標:在 Google Apps Script 中實作快取層,減少重複 API 呼叫與資料庫查詢
- 限制: CacheService 單筆上限 100 KB 最長有效期 21600 秒(6 小時) ScriptCache / UserCache / DocumentCache 範圍不同
- 輸出:可複用的 getOrSet() 工具、命名空間化 key、多層快取示範
💡 為什麼我會做這個? 專案中 API 呼叫量逼近上限,且相同請求反覆出現。先用快取減少呼叫與延遲,再談下游優化。
架構與流程
Caller
└─▶ Memory Cache(單次執行內暫存)
├─ 命中 → 回傳
└─ 未命中 → CacheService(Script/User/Document)
├─ 命中 → 回傳 + 回灌 Memory
└─ 未命中 → 呼叫 API/DB
└─ 成功 → 寫入 CacheService + Memory (+ PropertiesService 備援)
環境與版本
- OS:不限
- Runtime:Google Apps Script(V8)
- 套件:無
- API:CacheService
步驟實作
- 檔案與路徑:/gas/cache/cache_util.gs
var __MEM_CACHE__ = {};
function getCacheByScope(scope) {
switch (scope) {
case 'user': return CacheService.getUserCache();
case 'document': return CacheService.getDocumentCache();
default: return CacheService.getScriptCache();
}
}
function makeCacheKey(ns, key) {
return ns + '::' + key;
}
function getOrSet(opts) {
var ns = opts.ns || 'default';
var key = makeCacheKey(ns, opts.key);
var ttl = opts.ttl || 300;
var scope = opts.scope || 'script';
var provider = opts.provider;
var useProps = !!opts.fallbackToProps;
if (__MEM_CACHE__.hasOwnProperty(key)) return __MEM_CACHE__[key];
var cache = getCacheByScope(scope);
var raw = cache.get(key);
if (raw) {
var val = JSON.parse(raw);
__MEM_CACHE__[key] = val;
return val;
}
if (useProps) {
var props = PropertiesService.getScriptProperties();
var pRaw = props.getProperty(key);
if (pRaw) {
try {
var pVal = JSON.parse(pRaw);
cache.put(key, JSON.stringify(pVal), Math.min(ttl, 300));
__MEM_CACHE__[key] = pVal;
return pVal;
} catch (e) {}
}
}
var data = provider();
var json = JSON.stringify(data);
if (json.length <= 100 * 1024) {
cache.put(key, json, Math.min(ttl, 21600));
}
if (useProps && json.length <= 500 * 1024) {
PropertiesService.getScriptProperties().setProperty(key, json);
}
__MEM_CACHE__[key] = data;
return data;
}
function invalidateCache(ns, businessKey, scope) {
var key = makeCacheKey(ns, businessKey);
delete __MEM_CACHE__[key];
(getCacheByScope(scope || 'script')).remove(key);
}
- 檔案與路徑:/gas/api/user_api.gs
function fetchUserProfileFromApi(userId) {
var url = 'https://api.example.com/data?uid=' + encodeURIComponent(userId);
var res = UrlFetchApp.fetch(url, { 'muteHttpExceptions': true, 'timeout': 10000 });
if (res.getResponseCode() !== 200) throw new Error('API_ERROR');
return JSON.parse(res.getContentText());
}
function getUserProfile(userId) {
return getOrSet({
ns: 'api:v1:user',
key: 'uid=' + userId,
ttl: 900,
scope: 'script',
fallbackToProps: true,
provider: function () {
return fetchUserProfileFromApi(userId);
}
});
}
- 檔案與路徑:/gas/cache/scope_examples.gs
function getUserPrefs(userId) {
return getOrSet({
ns: 'prefs:v1',
key: userId,
ttl: 3600,
scope: 'user',
provider: function () {
return { theme: 'dark', locale: 'zh-TW' };
}
});
}
function getDocMeta() {
return getOrSet({
ns: 'docmeta:v1',
key: 'current',
ttl: 600,
scope: 'document',
provider: function () {
return { title: DocumentApp.getActiveDocument().getName() };
}
});
}
驗證與結果
首次呼叫:820 ms(API 實際請求)
第二次呼叫:12 ms(Memory 命中)
冷啟呼叫:55 ms(CacheService 命中)
常見錯誤與排錯表
訊息/現象 可能原因 解法 Exceeded maximum cache size 單筆超過 100KB 精簡資料、拆分多 key Service invoked too many times 太頻繁存取 CacheService 提高 TTL、加強 Memory 命中 更新後資料沒變 忘記失效快取 呼叫 invalidateCache() Key 打架 無命名空間 使用 ns::key 並加版本號延伸與 TODO
- 列表/分頁資料分片快取
- API 搭配 ETag/Last-Modified 減少資料量
- 實作批次失效(prefix-based invalidate)
版本資訊(Changelog)
- 2025-08-14:初稿
工具小筆記|
- CacheService 三種範圍:Script(專案共享)、User(個人)、Document(綁文件)
- TTL 建議:易變 1–5 分鐘;一般 10–30 分鐘;低頻資料 1–6 小時
- Key 建議加版本號(如 api:v1)避免破壞性更新時資料不一致












