跳到主要內容
← 回到文章列表
N°.002技術筆記

[STELLAR 開發日記] 用 Promise Lock 解掉 Cache Stampede,讀取從 10,000 降到 1,000

發布2026.05.18閱讀1 分鐘

前陣子在看 side project 的 Firestore 的流量圖,發現一個現象:資料量明明不大(當時只有大約 100 筆活動、100-200 筆藝人資料),但讀取次數卻常常逼近每天 50,000 次上限。

往下查後,發現 /coffeeEvent 這個 API 在一小時內就會觸發 100 多次讀取,單小時讀取次數就來到 10,000 次左右。

確認了原因是 Cache Stampede,我有在 side project server 端做 in-memory cache,拿過的資料會暫存起來,避免每次請求都打資料庫。但當快取需要失效時(例如審核通過,需要拿最新資料時),多個請求同時發現快取是空的,每個請求都同時去 Firestore 拿資料,導致 Cache Stampede 進而造成資料庫讀取次數飆高。

要如何解決這樣的問題呢?和 claude 討論的做法是,透過 Promise Lock 的方式,只有第一個請求會去資料庫拿資料,其它的請求等待第一個請求回來解鎖後,取得資料庫的資料。

做法大概像以下這樣:

async getWithLock<T>(key: string, fetchFn: () => Promise<T>, ttlMinutes: number): Promise<T> {

	// 1. 快取有效,直接返回
	const cached = this.get<T>(key);
	if (cached !== null) return cached;
	
	// 2. 有人正在查詢中,等待結果
	const pending = this.pendingRequests.get(key);
	if (pending) {
		return pending as Promise<T>;
	}

	// 3. 沒人在查,執行查詢並設定 Lock
	const promise = fetchFn()
		.then(result => {
			this.set(key, result, ttlMinutes);
			return result;
		})
		.finally(() => {
			this.pendingRequests.delete(key);
		});

	// 快取存起來,讓後來的人拿取
	this.pendingRequests.set(key, promise);
	return promise;
}

用這樣的 JS Promise lock 解決了快取失效時的 Cache Stampede,也確實在快取失效後,讀取次數不會瞬間飆升了!目前專案大概有將近 400 筆活動、300 多筆藝人、團體,審核通過後讀取次數大概僅剩下 800-1000 次,相比改善前每次審核通過後都會是 5,000-10,000 次左右,大幅減少了讀取次數的壓力!

目前大部分時間都是平穩的
END · N°.002

RELATED · 延伸閱讀