nodejs 實踐:express 最佳實踐 express-session 解析
nodejs 發(fā)展很快,從 npm 上面的包托管數(shù)量就可以看出來钧椰。不過從另一方面來看考婴,也是反映了 nodejs 的基礎(chǔ)不穩(wěn)固,需要開發(fā)者創(chuàng)造大量的輪子來解決現(xiàn)實的問題闯估。
知其然睹欲,并知其所以然這是程序員的天性供炼。所以把常用的模塊拿出來看看一屋,看看高手怎么寫的,學(xué)習(xí)其想法袋哼,讓自己的技術(shù)能更近一步冀墨。
引言
最近 ‘雙十一‘ 快到了,領(lǐng)導(dǎo)安排我們給網(wǎng)站做性能優(yōu)化涛贯。其中最要的方向是保證網(wǎng)站的穩(wěn)定性诽嘉。我主要是負(fù)責(zé)用戶登錄入口這一塊的工作。
優(yōu)化的目標(biāo)是:在高峰下弟翘,如果系統(tǒng)服務(wù)器端 session 的存儲(memcached)出現(xiàn)了問題虫腋,用戶還能正常登錄和使用我們的網(wǎng)站。
并已經(jīng)給出了技術(shù)思路:對 session, 進行服務(wù)器端 session(memcached) 和 瀏覽器端 session(cookie) 雙備份衅胀,一但連接發(fā)現(xiàn)服務(wù)器端 session 出現(xiàn)了問題,就啟用瀏覽器端 session, 實現(xiàn)自動降級處理酥筝。
借此機會滚躯,正好比較深入的了解了一下 session 等相關(guān)知識實現(xiàn)。
session
session 是什么
注意這里說的都是網(wǎng)站相關(guān)技術(shù)環(huán)境下嘿歌。
session 是一種標(biāo)識對話的技術(shù)說法掸掏。通過 session ,我們能快速識別用戶的信息宙帝,針對用戶提供不一樣的信息丧凤。
session 的技術(shù)實現(xiàn)上:會對一次對話產(chǎn)生一個唯一的標(biāo)識進行標(biāo)識。
session 生命周期
session 標(biāo)識產(chǎn)生的時機和清除時機:
- 用戶已經(jīng)登錄:這個唯一標(biāo)識會在用戶登錄時產(chǎn)生步脓,用戶點擊退出時或者關(guān)閉瀏覽器時清除愿待。
- 用戶未登錄: 這個唯一標(biāo)識會用用戶進入網(wǎng)站時產(chǎn)生,用戶關(guān)閉所有網(wǎng)站相關(guān)頁面時清除靴患。
session 生命周期: 在生成和清除之間仍侥,在網(wǎng)站內(nèi)的頁面任意跳轉(zhuǎn),session 標(biāo)識不會發(fā)生變化鸳君。
從 session 開始到清除农渊,我們叫一次會話,也就是生成 session或颊。
session 特點
每次對話砸紊, session 的 id 是不一樣的。
session id 需要每次請求都由客戶端帶過來囱挑,用來標(biāo)識本次會話醉顽。這樣就要求客戶端有能用保存的 sesssionId。
session 技術(shù)方案
當(dāng)前業(yè)界通用的方案是:cookie 平挑。當(dāng)然還有無 cookie 的方案徽鼎,對每個鏈接都加上 sessionId 參數(shù)。
session 使用流程
- 用戶登錄后,將 sessionId 存到 cookie 中否淤。
- 用戶在請求的網(wǎng)站別的服務(wù)時悄但,由瀏覽器請求帶上 cookie,發(fā)送到服務(wù)器石抡。
- 服務(wù)器拿到 sessionId 后檐嚣,通過該 Id 找到保存到在服務(wù)器的用戶信息。
- 然后再跟據(jù)用戶信息啰扛,進行相應(yīng)的處理嚎京。
從流程有幾個點要關(guān)注:
- 什么時候根據(jù) sessionId 去拿 session
- 確保 session 可用性
下面就結(jié)合 express-session 來講講具體 session 的實現(xiàn)。
express-session 的分析
主要關(guān)注問題:
- 怎樣產(chǎn)生 session
- 怎樣去拿到 session
- 怎樣去保存 session
- 怎樣去清除 session
express-session 位置
這一一張更詳細(xì)的 session 流程圖隐解,同時也說明了 express-session 的基本的工作模塊鞍帝。
express-session 有四個部分:
- request, response 與 session 的交互的部分
- session 數(shù)據(jù)結(jié)構(gòu)
- session 中數(shù)據(jù)存儲的接口 store
- store 默認(rèn)實現(xiàn) memory(cookie 實現(xiàn)已被廢)
這張是 express-session 的流程圖,從圖中可以看到煞茫, express-session 的工作流程帕涌。
具體的情況只能去看代碼了。
因為我們的網(wǎng)站是 session store 是基于 memcached 的续徽。所以我把 connect-memcached 和 memcached 都看了一遍钦扭。
connect-memcached 是基于 memcached 實現(xiàn) session store 接口客情。
memcached 是基于連接池的應(yīng)用瑞凑,下面是我畫的結(jié)構(gòu)圖:
問題解決方案
上面把 session 和 express-session + connect-memcached 都仔細(xì)看過了惰匙。
回到前面引言中的方案项鬼,我們需要解決以下的問題:
- 基于 memcached 的 session 怎么把數(shù)據(jù)同步到基于 cookie 的 session 中绘盟。
- 怎么把 cookie 的 session 數(shù)據(jù)恢復(fù)到 session 中锡垄。
- 怎樣判斷 memcached 已經(jīng)失去連接货岭。
解決1,2兩個問題,可以讓用戶在一次對話中敦第,在 mecached 和 cookie 中切換垮卓,數(shù)據(jù)還一直存在诬滩,不會丟掉。
第3個問題空镜,就是在 memcached 斷開時,程序能知道 memcached 已斷洼怔,然后數(shù)據(jù)從 cookie 中拿镣隶。
庫選擇
已經(jīng)有 express-session 的方案轻猖,要有一個在客戶端找一個基于 cookie 的 session 方案:cookie-session 和 client-session 這兩個都可以。我選了第二個,主要是加密第二個更好檐束。
方案
我前前后后,考慮了多個方案,方案如下:
首先方案一:主要思路是通過一個基礎(chǔ)的監(jiān)控程序去按時間定時(比如5分鐘)去ping memcached 服務(wù)器,去判斷是否可用办陷,然后把結(jié)果寫入到 zookeeper 中险毁,通過 zookeeper 的變量去控制數(shù)據(jù)從 memcached 的session 中讀取,還是從 cookie session 中讀數(shù)據(jù)。
方案二:在兩個 session 之上嵌戈,再建一個 session覆积,就是對從哪里讀數(shù)據(jù)通過這個 session 來實現(xiàn),也就是代理模式熟呛。
方案三:在 store 層上做一層 common-store , 然后由他負(fù)責(zé)從哪個store 中讀取數(shù)據(jù)宽档,就是 store 的代理。
方案四:不做中間層庵朝,直接使用進行處理吗冤,只用 express-session 進行處理數(shù)據(jù)。
這四個方案都在選擇九府,區(qū)別只是實現(xiàn)上的難度:
- memcached 的不可連接是否可以在框架層感知道
- 業(yè)務(wù)代碼盡量不用調(diào)整
- session 同步方案是否有效
其中第一個問題最重要椎瘟,如果框架層不可感知,那就要有一個外部程序進行處理侄旬,或者寫一個中間件去主動連接一下肺蔚,看看是否可連接。
再一次閱讀 express-session 重點查看 session 中 store 連接這塊儡羔。發(fā)現(xiàn)如果 memcached 不可連接宣羊,req.session 是 undefined 的。
這樣汰蜘,就可以通過判斷 req.session 是否是直來判斷是否可連接仇冯。
第二個問題:因為業(yè)務(wù)代碼中使用都是 req.session 的形式, 從 cookie 中恢復(fù)數(shù)據(jù)的時候族操,就要成初始化成 express-session 的接口苛坚。
這個問題也通過閱讀代碼解決:
req.sessionID = uuid.v4();
req.session = new expresssession.Session(req, data);
req.session.cookie = new memcachedSession.Cookie({
domain: config.cookieDomain,
httpOnly: false
});
通過以上的代碼就可以從數(shù)據(jù)中恢復(fù) session。
第三個問題: 要保證 session 一致坪创,就讓數(shù)據(jù)指向同一個對象
req.session2.sessionBack = req.session;
因此方案1炕婶,方案2姐赡, 方案3 都扔掉莱预,直接方案4。
完整的代碼如下:
const config = global.config;
const session = require('express-session');
/**
* 該中間件主要把 express-session 和 client-session 集中起來處理项滑,如果 memcached 出錯了依沮,使用 cookie session
* @param backSession cookeSession 的鍵名
* @returns {function(*=, *=, *)}
*/
module.exports = (backSession) => {
return (req, res, next) => {
let notUseMemcached = _.get(req.app.locals.pc, 'session.removeMemcached', false);
if (req.session && !notUseMemcached) { // memcached 可用
req.memcachedSessionError = false;
} else { // memcached 不可用
// 重建 session
res.emit('sessionError');
req.memcachedSessionError = true;
req.session = new session.Session(req);
req.session.cookie = new session.Cookie({
domain: config.cookieDomain,
httpOnly: false
});
req.session = Object.assign(req.session, req[backSession].sessionBack);
}
Object.defineProperty(req.session, 'reset', {
configurable: true,
enumerable: false,
value: function() {
req.session.destory();
req[backSession].reset();
},
writable: false
});
// 備份數(shù)據(jù)
req[backSession].sessionBack = req.session;
next();
};
};
這里就不貼 express-session 和 client-session 初始化代碼,需要注意的是:這個中間件要放到初始化的后面枪狂。
app.use(memcachedSession({
// ... options
}));
app.use(cookieSession({
// ... options
}));
app.use(yohoSession({
backSession: 'session2'
}));
總結(jié)
網(wǎng)站穩(wěn)定性一直是一個重要的話題危喉。這次通過 session 的改造,讓我復(fù)習(xí)了很多的知識州疾,學(xué)無止盡辜限。