新聞中心
在 Node.js 中,如何更優(yōu)雅地獲取請(qǐng)求上下文一直是一個(gè)問題,看一下下面的例子。

背景
const http = require('http');
function handler1(req, res) {
console.log(req.url);
}
function handler2(req, res) {
console.log(req.url);
}
http.createServer((req, res) => {
handler1(req, res);
handler2(req, res);
res.end();
}).listen();
上面的例子中,每次收到一個(gè)請(qǐng)求時(shí)都會(huì)執(zhí)行 handler1 和 handler2,為了在不同的地方里都能拿到請(qǐng)求上下文,我們只能逐級(jí)進(jìn)行傳遞,如果業(yè)務(wù)邏輯很復(fù)雜,這個(gè)維護(hù)性是非常差的,下接下來看看如何使用 AsyncLocalStorage 解決這個(gè)問題。
AsyncLocalStorage
AsyncLocalStorage 是基于 Async Hooks 實(shí)現(xiàn)的,它通過上下文傳遞實(shí)現(xiàn)了異步代碼的上下文共享和隔離。下面看一個(gè)例子。
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}
asyncLocalStorage.run(1, () => {
logWithId('start');
setImmediate(() => {
logWithId('finish');
});
});
上面的代碼會(huì)輸出
1: start
1: finish
從中可以看到兩個(gè) logWithId 共享了同一個(gè)上下文,這個(gè)上下文是由 run 函數(shù)設(shè)置的 1,那這種技術(shù)如何解決我們剛開始提出的問題呢?看一下下面的例子。
const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function handler1() {
const { req } = asyncLocalStorage.getStore();
console.log(req.url);
}
function handler2() {
setImmediate(() => {
const { req } = asyncLocalStorage.getStore();
console.log(req.url);
});
}
http.createServer((req, res) => {
asyncLocalStorage.run({ req, res }, () => {
handler1();
handler2();
});
res.end();
}).listen(9999, () => {
http.get({ port: 9999, path: '/test' })
});
執(zhí)行上面代碼輸出如下。
/test
/test
可以看到,我們不需要逐級(jí)地傳遞請(qǐng)求上下文并且可以在任意異步代碼中獲取請(qǐng)求上下文。這讓代碼的編寫和維護(hù)帶來了非常大的好處,不過缺點(diǎn)就是,因?yàn)?AsyncLocalStorage 是基于 Async hooks 的,所以會(huì)帶來一些性能損耗,不同的版本可能不一樣,但是 Node.js 也在不斷地優(yōu)化其性能,我印象中,社區(qū)有人提過使用其他技術(shù)實(shí)現(xiàn) AsyncLocalStorage。
AsyncLocalStorage 原理
知其然知其所以然,只知道怎么使用是不夠的,理解其原理可以幫助我們更好地使用它。下面來分析一下 AsyncLocalStorage 的原理。先看一下創(chuàng)建 AsyncLocalStorage 的邏輯
class AsyncLocalStorage {
constructor() {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
}
}
創(chuàng)建AsyncLocalStorage的時(shí)候很簡單,主要是置狀態(tài)為false,并且設(shè)置kResourceStore的值為Symbol('kResourceStore')。設(shè)置為Symbol('kResourceStore')而不是 'kResourceStore' 很重要,我們后面會(huì)看到。繼續(xù)看一下執(zhí)行AsyncLocalStorage.run的邏輯。
const storageList = [];
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
// 傳遞上下文
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource, type);
}
},
});
run(store, callback, ...args) {
// 把當(dāng)前 AsyncLocalStorage 加入隊(duì)列
ArrayPrototypePush(storageList, this);
// 啟動(dòng) AsyncHooks
storageHook.enable();
// 獲取當(dāng)前的異步資源,比如收到的請(qǐng)求
const resource = executionAsyncResource();
// 記錄舊的上下文
const oldStore = resource[this.kResourceStore];
// 修改當(dāng)前異步資源的上下文
resource[this.kResourceStore] = store;
// 在新的上下文中執(zhí)行傳入的回調(diào)函數(shù)
try {
return ReflectApply(callback, null, args);
} finally {
resource[this.kResourceStore] = oldStore;
}
}
回調(diào)函數(shù)里可以通過 asyncLocalStorage.getStore() 獲得設(shè)置的公共上下文。
getStore() {
const resource = executionAsyncResource();
return resource[this.kResourceStore];
}
getStore的原理很簡單,首先拿到當(dāng)前的異步資源,然后根據(jù)AsyncLocalStorage的kResourceStore的值從resource中拿到公共上下文,如果是同步執(zhí)行g(shù)etStore(比如 handler1 中),那么executionAsyncResource返回的就是我們請(qǐng)求所對(duì)應(yīng)的異步資源,上下文就是在run時(shí)設(shè)置的上下文({req, res}),但是如果是異步getStore那么怎么辦呢?因?yàn)檫@時(shí)候executionAsyncResource返回的不再是請(qǐng)求所對(duì)應(yīng)的異步資源,也就拿不到他掛載的公共上下文。為了解決這個(gè)問題,Node.js對(duì)公共上下文進(jìn)行了傳遞。
const storageList = []; // AsyncLocalStorage對(duì)象數(shù)組
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource);
}
}
});
_propagate(resource, triggerResource) {
const store = triggerResource[this.kResourceStore];
resource[this.kResourceStore] = store;
}
我們看到在每次資源創(chuàng)建的時(shí)候,Node.js會(huì)把當(dāng)前異步資源的上下文掛載到新創(chuàng)建的異步資源中。所以在asyncLocalStorage.getStore() 時(shí)即使不是我們?cè)趫?zhí)行run時(shí)創(chuàng)建的資源對(duì)象,也可以獲得具體asyncLocalStorage對(duì)象所設(shè)置的資源( handler2 中)。關(guān)系圖如下。
這樣就實(shí)現(xiàn)了異步資源上下文的共享和隔離。
總結(jié)
AsyncLocalStorage 有很多用法和用處,我們?cè)?Node.js APM 中也大量用到該技術(shù),通過 AsyncLocalStorage,我們可以無侵入地實(shí)現(xiàn)對(duì) Node.js 應(yīng)用的內(nèi)部進(jìn)行觀測,時(shí)間關(guān)系,本文簡單地介紹了 AsyncLocalStorage 的使用和原理,有興趣的同學(xué)可以自行探索。
當(dāng)前文章:穿針引線之AsyncLocalStorage
網(wǎng)頁路徑:http://fisionsoft.com.cn/article/cdhhiep.html


咨詢
建站咨詢
