node中的緩存機(jī)制
緩存是node開(kāi)發(fā)中一個(gè)很重要的概念缭乘,它應(yīng)用在很多地方,例如瀏覽器有緩存琉用、DNS有緩存堕绩、包括服務(wù)器也有緩存策幼。
一、緩存作用
那緩存是為了做什么呢奴紧?
1.為了提高速度特姐,提高效率。
2.減少數(shù)據(jù)傳輸黍氮,節(jié)省網(wǎng)費(fèi)唐含。
3.減少服務(wù)器的負(fù)擔(dān),提高網(wǎng)站性能沫浆。
4.加快客戶端加載網(wǎng)頁(yè)的速度捷枯。
二、緩存分類
那緩存有幾種策略呢专执?
強(qiáng)制緩存:
1淮捆、概念:
客戶端訪問(wèn)服務(wù)器請(qǐng)求資源,請(qǐng)求成功之后客戶端會(huì)緩存到本地本股,緩存到本地之后争剿,如果以后客戶端再請(qǐng)求該資源此時(shí)不需要請(qǐng)求服務(wù)器了,直接訪問(wèn)本地的就可以痊末。
2、特點(diǎn):
強(qiáng)制緩存不需要與服務(wù)器發(fā)生交互
3哩掺、客戶端訪問(wèn)強(qiáng)制緩存的流程圖解
1)緩存命中
客戶端請(qǐng)求數(shù)據(jù)凿叠,現(xiàn)在本地的緩存數(shù)據(jù)庫(kù)中查找,如果本地緩存數(shù)據(jù)庫(kù)中有該數(shù)據(jù)嚼吞,且該數(shù)據(jù)沒(méi)有失效盒件。則取緩存數(shù)據(jù)庫(kù)中的該數(shù)據(jù)返回給客戶端。
2)緩存未命中
客戶端請(qǐng)求數(shù)據(jù)舱禽,現(xiàn)在本地的緩存數(shù)據(jù)庫(kù)中查找炒刁,如果本地緩存數(shù)據(jù)庫(kù)中有該數(shù)據(jù),且該數(shù)據(jù)失效誊稚。則向服務(wù)器請(qǐng)求該數(shù)據(jù)翔始,此時(shí)服務(wù)器返回該數(shù)據(jù)和該數(shù)據(jù)的緩存規(guī)則返回給客戶端,客戶端收到該數(shù)據(jù)和緩存規(guī)則后里伯,一起放到本地的緩存數(shù)據(jù)庫(kù)中留存城瞎。以備下次使用。
4疾瓮、如何實(shí)現(xiàn)強(qiáng)制緩存脖镀?
1、瀏覽器會(huì)將文件緩存到Cache目錄狼电,第二次請(qǐng)求時(shí)瀏覽器會(huì)先檢查Cache目錄下是否含有該文件蜒灰,如果有弦蹂,并且還沒(méi)到Expires設(shè)置的時(shí)間,即文件還沒(méi)有過(guò)期强窖,那么此時(shí)瀏覽器將直接從Cache目錄中讀取文件凸椿,而不再發(fā)送請(qǐng)求
2、Expires是服務(wù)器響應(yīng)消息頭字段毕骡,在響應(yīng)http請(qǐng)求時(shí)告訴瀏覽器在過(guò)期時(shí)間前瀏覽器可以直接從瀏覽器緩存取數(shù)據(jù)削饵,而無(wú)需再次請(qǐng)求,這是HTTP1.0的內(nèi)容,現(xiàn)在瀏覽器均默認(rèn)使用HTTP1.1,所以基本可以忽略
3未巫、Cache-Control與Expires的作用一致窿撬,都是指明當(dāng)前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數(shù)據(jù)還是重新發(fā)請(qǐng)求到服務(wù)器取數(shù)據(jù),如果同時(shí)設(shè)置的話叙凡,其優(yōu)先級(jí)高于Expires
把資源緩存在客戶端劈伴,如果客戶端再次需要此資源的時(shí)候,先獲取到緩存中的數(shù)據(jù)握爷,看是否過(guò)期跛璧,如果過(guò)期了。再請(qǐng)求服務(wù)器
如果沒(méi)過(guò)期新啼,則根本不需要向服務(wù)器確認(rèn)追城,直接使用本地緩存即可
Cache-Control
private 客戶端可以緩存
public 客戶端和代理服務(wù)器都可以緩存
max-age=60 緩存內(nèi)容將在60秒后失效
no-cache 需要使用對(duì)比緩存驗(yàn)證數(shù)據(jù),強(qiáng)制向源服務(wù)器再次驗(yàn)證. 禁用強(qiáng)制緩存
no-store 所有內(nèi)容都不會(huì)緩存,強(qiáng)制緩存和對(duì)比緩存都不會(huì)觸發(fā)燥撞。兼用強(qiáng)制緩存和對(duì)比緩存?
/**
* 1. 第一次訪問(wèn)服務(wù)器的時(shí)候座柱,服務(wù)器返回資源和緩存的標(biāo)識(shí),客戶端則會(huì)把此資源緩存在本地的緩存數(shù)據(jù)庫(kù)中物舒。
* 2. 第二次客戶端需要此數(shù)據(jù)的時(shí)候色洞,要取得緩存的標(biāo)識(shí),然后去問(wèn)一下服務(wù)器我的資源是否是最新的冠胯。
* 如果是最新的則直接使用緩存數(shù)據(jù)火诸,如果不是最新的則服務(wù)器返回新的資源和緩存規(guī)則,客戶端根據(jù)緩存規(guī)則緩存新的數(shù)據(jù)荠察。
*/
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('crypto');
/**
* 強(qiáng)制緩存
* 把資源緩存在客戶端置蜀,如果客戶端再次需要此資源的時(shí)候,先獲取到緩存中的數(shù)據(jù)悉盆,看是否過(guò)期盾碗,如果過(guò)期了。再請(qǐng)求服務(wù)器
* 如果沒(méi)過(guò)期舀瓢,則根本不需要向服務(wù)器確認(rèn)廷雅,直接使用本地緩存即可
*/
http.createServer(function (req, res) {
let { pathname } = url.parse(req.url, true);
let filepath = path.join(__dirname, pathname);
console.log(filepath);
fs.stat(filepath, (err, stat) => {
if (err) {
return sendError(req, res);
} else {
send(req, res, filepath);
}
});
}).listen(8080);
function sendError(req, res) {
res.end('Not Found');
}
function send(req, res, filepath) {
res.setHeader('Content-Type', mime.getType(filepath));
//expires指定了此緩存的過(guò)期時(shí)間,此響應(yīng)頭是1.0定義的,在1.1里面已經(jīng)不再使用了
res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
res.setHeader('Cache-Control', 'max-age=30');
fs.createReadStream(filepath).pipe(res);
}
對(duì)比緩存:
1航缀、概念:
瀏覽器第一次請(qǐng)求數(shù)據(jù)時(shí)商架,服務(wù)器會(huì)將緩存標(biāo)識(shí)與數(shù)據(jù)一起返回給客戶端,客戶端將二者備份至緩存數(shù)據(jù)庫(kù)中芥玉。
再次請(qǐng)求數(shù)據(jù)時(shí)蛇摸,客戶端將備份的緩存標(biāo)識(shí)發(fā)送給服務(wù)器,服務(wù)器根據(jù)緩存標(biāo)識(shí)進(jìn)行判斷灿巧,判斷成功后赶袄,返回304狀態(tài)碼,通知客戶端比較成功抠藕,可以使用緩存數(shù)據(jù)饿肺。
2、特點(diǎn):需要進(jìn)行比較判斷是否可以使用緩存
3盾似、對(duì)比緩存流程圖解
1)客戶端第一次發(fā)請(qǐng)求
客戶端第一次請(qǐng)求數(shù)據(jù)敬辣,發(fā)現(xiàn)本地緩存中沒(méi)有,就向服務(wù)器發(fā)起請(qǐng)求零院,然后服務(wù)器把請(qǐng)求的數(shù)據(jù)返回給客戶端溉跃,并和客戶端商量你要保存到本地緩存中的規(guī)則,即是否緩存 緩存時(shí)間 有沒(méi)有標(biāo)示 最后修改時(shí)間等信息告抄。
2)客戶端第二次發(fā)請(qǐng)求
客戶端發(fā)起請(qǐng)請(qǐng)求
--->查看本地的緩存數(shù)據(jù)庫(kù)中是否有緩存---> 沒(méi)有---> 向服務(wù)器發(fā)起請(qǐng)求--->服務(wù)器返回200和響應(yīng)內(nèi)容--->顯示
--->查看本地的緩存數(shù)據(jù)庫(kù)中是否有緩存---> 有 ---> 緩存沒(méi)有過(guò)期(本地)---> 緩存中讀取--->顯示
--->查看本地的緩存數(shù)據(jù)庫(kù)中是否有緩存---> 有 ---> 緩存已過(guò)期(本地)---> 本地的緩存中有沒(méi)有Etag和Last-Modified --->有--->發(fā)給服務(wù)器對(duì)應(yīng)的字段 if-none-match 和if-modified-since ---> 服務(wù)器策略撰茎。如果這兩個(gè)字段和服務(wù)器上的這兩個(gè)字段相同 ---> 說(shuō)明數(shù)據(jù)沒(méi)有更新--->返回304--->服務(wù)器從它的緩存庫(kù)中獲取到數(shù)據(jù)給客戶端--->顯示
--->查看本地的緩存數(shù)據(jù)庫(kù)中是否有緩存---> 有 ---> 緩存已過(guò)期(本地)---> 本地的緩存中有沒(méi)有Etag和Last-Modified --->有--->發(fā)給服務(wù)器對(duì)應(yīng)的字段 if-none-match 和if-modified-since ---> 服務(wù)器策略。如果這兩個(gè)字段和服務(wù)器上的這兩個(gè)字段相同 --->說(shuō)明數(shù)據(jù)有更新--->返回200--->重新獲取--->顯示
4打洼、如何實(shí)現(xiàn)對(duì)比緩存乾吻?
/**
- 第一次訪問(wèn)服務(wù)器的時(shí)候,服務(wù)器返回資源和緩存的標(biāo)識(shí)拟蜻,客戶端則會(huì)把此資源緩存在本地的緩存數(shù)據(jù)庫(kù)中。
- 第二次客戶端需要此數(shù)據(jù)的時(shí)候枯饿,要取得緩存的標(biāo)識(shí)酝锅,然后去問(wèn)一下服務(wù)器我的資源是否是最新的。
- 如果是最新的則直接使用緩存數(shù)據(jù)奢方,如果不是最新的則服務(wù)器返回新的資源和緩存規(guī)則搔扁,客戶端根據(jù)緩存規(guī)則緩存新的數(shù)據(jù)。
*/
我們通過(guò)標(biāo)示字段來(lái)判斷緩存中的數(shù)據(jù)是否有效
這個(gè)標(biāo)示有兩種形式:
第一種是最后修改時(shí)間蟋字,Last-Modified
1稿蹲、Last-Modified:響應(yīng)時(shí)告訴客戶端此資源的最后修改時(shí)間
2、If-Modified-Since:當(dāng)資源過(guò)期時(shí)(使用Cache-Control標(biāo)識(shí)的max-age)鹊奖,發(fā)現(xiàn)資源具有Last-Modified聲明苛聘,則再次向服務(wù)器請(qǐng)求時(shí)帶上頭If-Modified-Since。
3、服務(wù)器收到請(qǐng)求后發(fā)現(xiàn)有頭If-Modified-Since則與被請(qǐng)求資源的最后修改時(shí)間進(jìn)行比對(duì)设哗。若最后修改時(shí)間較新唱捣,說(shuō)明資源又被改動(dòng)過(guò),則響應(yīng)最新的資源內(nèi)容并返回200狀態(tài)碼网梢;
4震缭、若最后修改時(shí)間和If-Modified-Since一樣,說(shuō)明資源沒(méi)有修改战虏,則響應(yīng)304表示未更新拣宰,告知瀏覽器繼續(xù)使用所保存的緩存文件。
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
// http://localhost:8080/index.html
http.createServer(function (req, res) {
let {pathname} = url.parse(req.url);
let filepath = path.join(__dirname,pathname);
console.log(filepath);
fs.stat(filepath,function (err, stat) {
if (err) {
return sendError(req,res)
} else {
// 再次請(qǐng)求的時(shí)候會(huì)問(wèn)服務(wù)器自從上次修改之后有沒(méi)有改過(guò)
let ifModifiedSince = req.headers['if-modified-since'];
console.log(req.headers);
let LastModified = stat.ctime.toGMTString();
console.log(LastModified);
if (ifModifiedSince == LastModified) {
res.writeHead('304');
res.end('')
} else {
return send(req,res,filepath,stat)
}
}
})
}).listen(8080)
function send(req,res,filepath,stat) {
res.setHeader('Content-Type', mime.getType(filepath));
// 發(fā)給客戶端之后烦感,客戶端會(huì)把此時(shí)間保存下來(lái)巡社,下次再獲取此資源的時(shí)候會(huì)把這個(gè)時(shí)間再發(fā)給服務(wù)器
res.setHeader('Last-Modified', stat.ctime.toGMTString());
fs.createReadStream(filepath).pipe(res)
}
function sendError(req,res) {
res.end('Not Found')
}
最后修改時(shí)間存在問(wèn)題
1、某些服務(wù)器不能精確得到文件的最后修改時(shí)間啸盏, 這樣就無(wú)法通過(guò)最后修改時(shí)間來(lái)判斷文件是否更新了重贺。
2、某些文件的修改非常頻繁回懦,在秒以下的時(shí)間內(nèi)進(jìn)行修改. Last-Modified只能精確到秒气笙。
3、一些文件的最后修改時(shí)間改變了怯晕,但是內(nèi)容并未改變潜圃。 我們不希望客戶端認(rèn)為這個(gè)文件修改了。
4舟茶、如果同樣的一個(gè)文件位于多個(gè)CDN服務(wù)器上的時(shí)候內(nèi)容雖然一樣谭期,修改時(shí)間不一樣。
第二種是Etag
ETag是實(shí)體標(biāo)簽的縮寫(xiě)吧凉,根據(jù)實(shí)體內(nèi)容生成的一段hash字符串,可以標(biāo)識(shí)資源的狀態(tài)隧出。當(dāng)資源發(fā)生改變時(shí),ETag也隨之發(fā)生變化阀捅。 ETag是Web服務(wù)端產(chǎn)生的胀瞪,然后發(fā)給瀏覽器客戶端。
1饲鄙、客戶端想判斷緩存是否可用可以先獲取緩存中文檔的ETag凄诞,然后通過(guò)If-None-Match發(fā)送請(qǐng)求給Web服務(wù)器詢問(wèn)此緩存是否可用。
2忍级、服務(wù)器收到請(qǐng)求帆谍,將服務(wù)器的中此文件的ETag,跟請(qǐng)求頭中的If-None-Match相比較,如果值是一樣的,說(shuō)明緩存還是最新的,Web服務(wù)器將發(fā)送304 Not Modified響應(yīng)碼給客戶端表示緩存未修改過(guò),可以使用轴咱。
3汛蝙、如果不一樣則Web服務(wù)器將發(fā)送該文檔的最新版本給瀏覽器客戶端
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('let crypto = require(\'mime\');\n');
// http://localhost:8080/index.html
http.createServer(function (req, res) {
let {pathname} = url.parse(req.url);
let filepath = path.join(__dirname,pathname);
console.log(filepath);
fs.stat(filepath,function (err, stat) {
if (err) {
return sendError(req,res)
} else {
let ifNoneMatch = req.headers['if-none-match'];
// 一烈涮、顯然當(dāng)我們的文件非常大的時(shí)候通過(guò)下面的方法就行不通來(lái),這時(shí)候我們可以用流來(lái)解決,可以節(jié)約內(nèi)存
let out = fs.createReadStream(filepath);
let md5 = crypto.createHash('md5');
out.on('data',function (data) {
md5.update(data)
});
out.on('end',function () {
let etag = md5.update(content).digest('hex');
// md5算法的特點(diǎn) 1. 相同的輸入相同的輸出 2.不同的輸入不通的輸出 3.不能根據(jù)輸出反推輸入 4.任意的輸入長(zhǎng)度輸出長(zhǎng)度是相同的
if (ifNoneMatch == etag) {
res.writeHead('304');
res.end('')
} else {
return send(req,res,filepath,stat, etag)
}
});
// 二患雇、再次請(qǐng)求的時(shí)候會(huì)問(wèn)服務(wù)器自從上次修改之后有沒(méi)有改過(guò)
// fs.readFile(filepath,function (err, content) {
// let etag = crypto.createHash('md5').update(content).digest('hex');
// // md5算法的特點(diǎn) 1. 相同的輸入相同的輸出 2.不同的輸入不通的輸出 3.不能根據(jù)輸出反推輸入 4.任意的輸入長(zhǎng)度輸出長(zhǎng)度是相同的
// if (ifNoneMatch == etag) {
// res.writeHead('304');
// res.end('')
// } else {
// return send(req,res,filepath,stat, etag)
// }
// };
// 但是上面的一方案也不是太好跃脊,讀一點(diǎn)緩存一點(diǎn),文件非常大的話需要好長(zhǎng)時(shí)間苛吱,而且我們的node不適合cup密集型酪术,即不適合來(lái)做大量的運(yùn)算,所以說(shuō)還有好多其他的算法
// 三翠储、通過(guò)文件的修改時(shí)間減去文件的大小
// let etag = `${stat.ctime}-${stat.size}`; // 這個(gè)也不是太好
// if (ifNoneMatch == etag) {
// res.writeHead('304');
// res.end('')
// } else {
// return send(req,res,filepath,stat, etag)
// }
}
})
}).listen(8080)
function send(req,res,filepath,stat, etag) {
res.setHeader('Content-Type', mime.getType(filepath));
// 第一次服務(wù)器返回的時(shí)候绘雁,會(huì)把文件的內(nèi)容算出來(lái)一個(gè)標(biāo)示發(fā)送給客戶端
//客戶端看到etag之后,也會(huì)把此標(biāo)識(shí)符保存在客戶端援所,下次再訪問(wèn)服務(wù)器的時(shí)候庐舟,發(fā)給服務(wù)器
res.setHeader('Etag', etag);
fs.createReadStream(filepath).pipe(res)
}
function sendError(req,res) {
res.end('Not Found')
}
存在問(wèn)題
都需要向服務(wù)器端發(fā)請(qǐng)求與服務(wù)器端發(fā)生交互