-
Notifications
You must be signed in to change notification settings - Fork 1
workflow
Welcome to the node-project-8 wiki!
欢迎阅读node-project-8的开发&构建流程介绍
在这个项目中,我们将对之前的项目进行一些改进,增添缓存机制。具体功能包括:
- 连接redis服务器并进行相关操作。
- 缓存图片搜索的结果。
- 当缓存命中时直接返回缓存信息。
- 新上传图片后清除对应条目的缓存。
##如何运行或测试应用
启动项目后,可以通过Postman来进行模拟请求,相关的Postman设置可以点击下面链接:
本项目假设开发者已经了解以下基础知识,项目开展过程中这些知识不再讲解:
本项目使用到的云资源包括:
###STEP0.node环境初始化
本步骤源代码可以在 step0 分支上获取
本项目在项目7的基础上进行,修改项目7的/config/proxyConfig.js
,配置正确的代理ip以及端口,以便能对请求进行正常转发。
获取项目后请首先安装依赖项,在项目根目录运行命令安装。
npm install
之后步骤中若切换分支,需要在每个分支下分别安装。
本项目每个文件开头都有个
'use strict'
语句,其作用可以参考 Strict Mode - MDN
修改完成后可以直接在本地启动:
node server.js
此时访问本地地址 http://127.0.0.1:3008 ,应该可以看到页面输出了一句简单的 "Not found:/"
。
###STEP1.制定缓存策略
Redis是一个开源、支持网络、基于内存、键值对存储数据库,使用ANSI C编写。 Redis的外围由一个键、值映射的字典构成。与其他非关系型数据库主要不同在于:Redis中值的类型不仅限于字符串,还支持如下抽象数据类型:
- 字符串列表
- 无序不重复的字符串集合
- 有序不重复的字符串集合
- 键、值都为字符串的哈希表
值的类型决定了值本身支持的操作。Redis支持不同无序、有序的列表,无序、有序的集合间的交集、并集等高级服务器端原子操作。
我们在之前的项目中通过关键字和起始页码,页大小三个参量获得搜索结果,因此我们可以简单的将这3个信息组合在一起作为key,搜索结果作为对应的value存储在redis服务器中,并且为每一个key设置超时时间,超时时间5分钟,超时key会被自动清除。 当请求到来时,我们可以按照同样的方式将上述3个参量组合key在redis中查找,命中后直接返回缓存内容,避免进行硬盘数据库查找,从而减少请求时间。若缓存未命中则在正常的请求后存储key-value数据到redis作为缓存。 当有新的图片上传,图片信息插入到数据库中时,我们可以获取到作为搜索关键字的name和meta中的相应3个字段的值,匹配已缓存的key值,将这些key以及他们对应的value从缓存中删除,从而使得下次请求可以刷新缓存。
一个完备的缓存策略会比较复杂,还应考虑更新带来的缓存刷新以及可能缓存失效风暴和穿透问题。本项目只做简单处理。
###STEP2.连接redis数据库
本步骤源代码可以在 step2 分支上获取
之前我们根据项目制定了相应的缓存策略来缓存搜索结果,以及上传文件后对相应缓存做失效处理。接下来我们根据策略来进行相应的实现。 首先我们需要一台redis服务器,腾讯云 云存储Redis 完全兼容Reids协议,并提供分布式服务。我们首先购买获取Redis实例,创建完成后继续初始化实例密码。
此后我们将使用<实例id:密码>来访问我们的redis实例。
我们使用node-redis来进行连接,首先进行安装。
npm install redis --save
安装后我们创建/lib/redis.js
和/config/redisConfig.js
对连接进行封装。
/config/redisConfig.js
module.exports = {
ip : '实例id',
port : '默认6379',
options: {
auth_pass:'实例id:密码'
}
};
配置项包括redis服务器ip,服务启动端口,以及鉴权id:passoword。 options中可以包含其他配置参数,详细可以查看官方文档。
/lib/redis
'use strict';
var redis = require('redis');
const config = require('../config/redisConfig');
var client = redis.createClient(config.port, config.ip, config.options);
client.on('ready',function(res){
console.log('ready');
});
client.on('error', function (err) {
console.log(err);
return ;
});
module.exports = client;
连接成功,控制台会输出ready,表示连接已经建立。
腾讯云 云存储Redis 不接受外网访问,因此请将项目部署到服务器之后再访问。
###STEP3.为搜索结果增添缓存
本步骤源代码可以在 step3 分支上获取
根据项目7,我们已经做好中间层代理,并为之前的项目设置好符合RESTful思想的路由,并获取了之前API返回的信息,至此我们可以在转发前后进行额外的操作。我们现在为/part4/list
API,在请求转发并完成后添加缓存处理。
创建/handel/list
,根据项目4搜索方式,获取搜索关键参量,组成key进行存储,此处采用了\
作为参量间的分隔符。
module.exports.addRedisALL = function(req,res,data) {
const print = printer(req, res);
const params = req.query;
const keyword = params.search || '';
let pageIndex = +params.pageIndex;
let pageSize = +params.pageSize;
let key = keyword + '\\' + pageIndex + '\\' + pageSize;
if (!isNaN(pageIndex) && !isNaN(pageSize)) {
pageIndex = pageIndex || 0;
pageSize = pageSize || 10;
}
var redisClient = require('../lib/redis');
redisClient.sadd('keys',key,function(err,result) {
console.log('keys');
if(err) {
console.log(err);
return err;
}
else {
redisClient.set(key,data, function(err, result) {
if (err) {
console.log(err);
return err;
}
else {
redisClient.expire(key, 5 * 60);
console.log('expire');
return;
}
});
}
})
};
为了方便管理所有key值,减少搜索时的开销,本项目中使用了一个名为keys的集合来存储所有key,集合中元素唯一。
这里将组合的key值与项目4中搜索返回的对象的字符串作为一组key-value存入redis作为缓存记录。redisClient.expire(key, 5 * 60);
为每一个key-value对设置了5分钟的存在时间,超时该key会被自动删除,从而请求可以得到新的值。
我们在server.js
中,信息返回后调用该函数,将返回信息存入缓存。
req.pipe(request(targetUrl)).on('error', function(err) {
// 处理目标服务器错误
console.log('target server error');
res.status(404).send('Not found:' + req.originalUrl);
return;
}).on('response', function(response) {
// redis缓存处理
var bodyChunks = [];
response.on('data', function(chunk) {
bodyChunks.push(chunk);
}).on('end', function() {
var body = Buffer.concat(bodyChunks);
require('./handle/list').addRedisALL(req,res,body);
console.log('cache');
});
}).pipe(res);
通过postman请求'/part4/list'并携带查询参数,在控制台发现cache输入,查询redis即可发现返回结果已缓存在redis中。
###STEP4.请求搜索API前校验缓存信息
本步骤源代码可以在 step4 分支上获取
我们在上一步已经对搜索结果进行缓存,这里我们将在请求转发前首先验证缓存是否存在,如果存在则结束请求并返回缓存中的结果,否则转发请求。
在/handle/list
中添加函数
module.exports.checkRedis = function(req, res,callback) {
const params = req.query;
const keyword = params.search || '';
let pageIndex = +params.pageIndex;
let pageSize = +params.pageSize;
let start = 0;
const print = printer(req, res);
// see if user has request for a paging
if (!isNaN(pageIndex) && !isNaN(pageSize)) {
pageIndex = pageIndex || 0;
pageSize = pageSize || 10;
start = pageIndex * pageSize;
}
var redisClient = require('../lib/redis');
var key = keyword + '\\' + pageIndex + '\\' + pageSize;
redisClient.get(key, function(err, result) {
if (err) {
console.log(err);
print(err);
return ;
}
else {
if (result) {
console.log('hit')
print(JSON.parse(result));
return ;
}
else {
callback();
}
}
})
};
redisClient.get("key")
用于获取当前缓存中key的值,若存在则命中缓存,若key不存在则证明需要转发请求。
修改server.js
require('./handle/list').checkRedis(req,res,function(){
var pathArr = req.path.split('/');
// 设置转发url
var targetUrl = 'http://' + proxyConfig[pathArr[1]].host + ':' + proxyConfig[pathArr[1]].port + req.originalUrl.replace(/\/part\d/, '');
// 重置请求方式
req.method = routesConfig[req.path].method;
// 不能使用bodyParser,会把req里数据流进行更改,对pipe方法造成影响
req.pipe(request(targetUrl)).on('error', function(err) {
````
}).on('response', function(response) {
````
response.on('data', function(chunk) {
````
}).on('end', function() {
````
});
}).pipe(res);
});
此时再次访问之前访问过的/part4/list
接口,附带相同的参数,即可在控制台中发现hint输出,证明命中缓存,没有在mogodb上进行操作。
###STEP5.修改项目结构,适应所有转发要求。
本步骤源代码可以在 step5 分支上获取
以上步骤以及完成了对搜索结果的缓存,但其影响了项目其他接口的正常使用,换句话说,到此为止我们直接修改项目代码结构使其适应于/part4/list
的API要求但忽略了其他API,现在我们修改项目结构使其适应于所有已有API。
- 首先修改
/routes/index.js
,为转发前或转发后需要额外处理的API添加处理方法,例如/part4/list
。
'/part4/list': {
'method': 'get',
'path': '/list',
'beforeRequest':require('../handle/list').checkRedis,
'afterRequest':require('../handle/list').addRedisALL
},
- 抽离封装转发函数,方便复用。
req.pipe(request(targetUrl)).on('error', function(err) {
// 处理目标服务器错误
res.status(404).send('Not found:' + req.originalUrl);
return;
}).on('response', function(response) {
// redis缓存处理
console.log('cache');
var bodyChunks = [];
response.on('data', function(chunk) {
bodyChunks.push(chunk);
}).on('end', function() {
var body = Buffer.concat(bodyChunks);
var afterRequest = routesConfig[req.path].afterRequest;
if(afterRequest !== undefined){
afterRequest(req,res,body)
}
else {
return ;
}
});
}).pipe(res);
- 同理在转发前验证并调用对应的
beforeRequest()
函数
var beforeRequest = routesConfig[req.path].beforeRequest;
if(beforeRequest !== undefined){
beforeRequest(req,res,function(){
transport(req,res);
})
}
else {
transport(req,res);
}
这样当请求到来时,会先验证其是否有转发前处理函数,如有则执行,没有的话直接进入转发阶段,转发后也同样验证执行转发后处理函数,而函数由配置文件指定,从而使得转发接口可以应用与所有已有API。
###STEP6.上传文件缓存操作
本步骤源代码可以在 step6 分支上获取
当新的文件上传后,可能会影响到已有的缓存结果,使得现有缓存缺失正确信息,因此当新文件上传时,我们需要将对应的缓存清理。
这些操作在文件上传成功后,因此我们添加/handle/upload
var redisClient = require('../lib/redis');
redisClient.smembers('keys',function(err,result) {
if(err){
throw err
}
else if(result.length>0){
var regs = [];
var buffer = [];
result.forEach(function(key){
var keyword = key.match(/^(.*)\\.*\\.*$/)[1];
if(keyword) {
var reg = new RegExp('^.*'+util.escapeRegExp(keyword)+'.*$');
labels.forEach(function(label){
if(reg.test(label)){
buffer.push(key)
}
})
}
});
if(buffer.length){
redisClient.del(buffer);
redisClient.srem('keys',buffer);
}
}
});
redisClient.smembers()
用于获取'keys'集合中的所有成员,也就是缓存的所有key值,便利这些key并从中通过正则/^(.*)\\.*\\.*$/
取出开始到首个\
的部分(也就是搜索时的keyword)与新增图像的name和meta中的3个属性进行模糊匹配,将匹配到的key放入缓存区,所有匹配完成后通过redisClient.del()
和redisClient.srem()
分别从key中和集合中批量删除。
在进行模糊匹配时使用构建正则的方式进行匹配,而由于keyword中往往包含.
等正则中的特殊字符,如果直接采取拼接正则的方式会导致正则错误,因此此处对其进行了转义,转义函数位于/lib/util
.
module.exports.escapeRegExp = function(str){
return str.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$&"); //$&表示匹配到的字符
}
最后修改/routes/index.js
添加路由处理函数的引用。
###STEP7.正式环境部署和后续问题
通过腾讯云 云存储Redis缓存数据的 Node 服务端开发搭建完毕。大家看看从这个案例都学习到了哪些知识。
- Redis的连接配置以及基本用法
- 正则特殊字符转义
- 一般的http请求处理方法