Node.js AsyncLocalStorage:解決非同步環境下的「追蹤」痛點
在開發 Node.js 後端應用時,我們經常會遇到一個棘手的問題:如何在不逐層傳遞參數 (Prop Drilling) 的情況下,讓深層的函式也能知道目前的 Request 資訊?
例如,你想在各處的 Log 中標記目前的 requestId,或是讓資料庫底層自動帶入目前的 userId。傳統做法是把這些資訊封裝成一個物件,從 Controller 一路傳到 Service 再傳到 Repository,但這會讓程式碼變得極難維護。
AsyncLocalStorage 就是為了解決這個問題而生的原生解決方案。
核心概念:非同步的 Thread Local
在多執行緒的語言(如 Java)中,我們有 ThreadLocal。Node.js 雖然是單執行緒,但透過非同步勾子 (Async Hooks) 機制,AsyncLocalStorage 讓我們能在某個非同步執行的「生命週期」中,共享一個獨立的儲存空間。
實戰開發:建立全鏈路日誌 (Tracing)
下面我們實作一個簡單的機制:在進入 Request 時生成一個 id,並確保後續所有非同步操作都能讀取到它。
1. 建立 Store 模組
// context.js
const { AsyncLocalStorage } = require('node:async_hooks');
// 建立一個存取空間實例
const als = new AsyncLocalStorage();
module.exports = als;
2. 在入口處啟動生命週期
我們可以使用 Express 中間件(或是原生 HTTP Server 的處理函式)來啟動 als.run()。
const http = require('http');
const als = require('./context');
const { v4: uuid } = require('uuid'); // 假設使用 uuid 生成 ID
http
.createServer((req, res) => {
const requestId = uuid();
// 透過 run() 啟動一個隔離的上下文,第一個參數是資料,第二個是後續執行的邏輯
als.run({ requestId }, () => {
handleRequest(req, res);
});
})
.listen(3000);
3. 在任何深層函數中讀取
不管中間經過了多少個 await 或 setTimeout,只要是在該 run() 的回呼路徑內,都能讀到正確的值。
// logger.js
const als = require('./context');
function log(message) {
// 透過 getStore() 獲取當前上下文的資料
const store = als.getStore();
const id = store ? store.requestId : 'system';
console.log(`[${id}] ${message}`);
}
// 某個深層的 Service
async function deepService() {
log('正在處理業務邏輯...');
}
進階應用:資料庫交易管理
AsyncLocalStorage 也常用於自動化資料庫交易。當你進入一個 Transaction 時,將 Transaction 物件存入 ALS,底層的 Repository 就能自動檢查當前是否有進行中的交易,並自動加入,而不需要開發者手動傳遞 Client 物件。
注意事項與效能
- 僅限當前鏈路:ALS 的資料只在
run()的回呼函數及其衍生的非同步操作中有效。如果你主動切斷了非同步鏈路(例如使用process.nextTick或某些不支援非同步追蹤的舊套件),資料可能會遺失。 - 記憶體管理:當
run()指令碼結束後,儲存空間會被自動回收,不需要擔心 Memory Leak。 - 效能考量:Node.js 核心對 ALS 進行了極大的優化,對極大部分的 Web 應用程式來說,效能損耗幾乎可以忽略不計。
總結
AsyncLocalStorage讓你在非同步流程中擁有「全域變數」的便利性。- 最常用於 Request Tracing (RequestId)、User Context 以及 Database Transactions。
- 它是讓 Node.js 程式碼變得簡潔、解耦的核心利器。