寫入與並發:appendRow 與 LockService
Sheet 的寫入層要處理的核心問題是:當多個 beacon 幾乎同時打進來、各自觸發一個 doPost 執行實例、都要往同一張表寫一列時,這些並發寫入會不會互相干擾。單筆寫入很單純,並發才是這一層真正要判斷的地方。先講單筆怎麼寫,再講並發何時需要保護。
appendRow:單筆寫入
appendRow 在表格最後一列的後面新增一列,是 log 型資料最自然的寫法——每個事件就是一列、append 到尾巴。它一次接一個陣列、對應各欄:
1sheet.appendRow([new Date(), data.path, data.dev]);appendRow 的好處是它自己找「最後一列的下一列」,不必你算行號。對「一次寫一筆瀏覽」這種場景,它是最直接的選擇。要注意它是「整列 append」——如果需要一次寫很多列(例如批次彙總),逐筆呼叫 appendRow 會慢,那時改用 getRange(...).setValues(二維陣列) 一次寫一整塊,效能差很多。單筆用 appendRow、批次用 setValues,是 Sheets 寫入的基本分工。
並發:兩個 doPost 同時 append 會怎樣
Apps Script 允許多個執行實例同時跑(個人帳號上限 30 個併發)。當兩個 beacon 幾乎同時到達,平台會起兩個 doPost 實例並行執行,兩者都呼叫 appendRow。多數情況下 Google 會把兩次 append 排到不同列、相安無事;但在高並發下,「兩個實例同時判斷『最後一列是第 100 列』、都想寫第 101 列」的競態是可能發生的,結果是一筆覆蓋另一筆、少記一筆。
這個風險要不要處理,取決於你的瞬間併發有多高。個人 blog 的流量分散在整天,任何一刻同時到達的 beacon 通常是個位數甚至零,競態機率極低、就算偶爾少記一筆對「哪篇有人看」的判斷也無影響——這種情境不必加保護,保持 appendRow 的簡單。真正需要處理的是「短時間尖峰」:某篇文章被大量分享、一分鐘湧入幾百次瀏覽,這時並發拉高、競態才變得值得防。
LockService:需要時才序列化
要防競態,用 LockService 把寫入序列化——讓同一時間只有一個執行實例能進入寫入區段,其他的排隊等它做完:
1function doPost(e) {
2 var lock = LockService.getScriptLock();
3 lock.waitLock(5000); // 最多等 5 秒拿鎖
4 try {
5 var data = JSON.parse(e.postData.contents);
6 SpreadsheetApp.getActiveSpreadsheet().getSheetByName("工作表1")
7 .appendRow([new Date(), data.path || "", data.dev || ""]);
8 } finally {
9 lock.releaseLock(); // 一定要放,否則後面全部卡住
10 }
11 return ContentService.createTextOutput(JSON.stringify({ ok: true }))
12 .setMimeType(ContentService.MimeType.JSON);
13}getScriptLock 取的是整個腳本共用的鎖,保證所有 doPost 實例排隊寫入。waitLock(5000) 是「最多等 5 秒」——等不到就丟例外(併發高到 5 秒還排不進來,這筆放棄,總比無限等好)。releaseLock 必須放在 finally,確保就算中間出錯也會釋放,否則鎖沒放、後面的請求全部卡死。
加鎖的代價是寫入從並行變序列,尖峰時每筆要多等前面的做完,吞吐下降。所以它是「有並發競態風險時才加」的保護,不是預設就要有的東西。判斷訊號回到前面那句:瞬間併發會不會高到讓競態實際發生——不會就別加,會就加上。這也是為什麼流量統計的基本版 doPost(模組二)刻意不含 lock:先讓簡單版跑起來,等真的遇到尖峰漏記,再加這一層。
下一步
寫入處理好了,資料表本身怎麼設計、以及 Sheets 累積到多大會撐不住,見資料模型與容量邊界。
#automation #google-sheets #appendrow #lockservice #concurrency