Skip to content

workflow

Yang Hang edited this page Jul 24, 2016 · 8 revisions

Welcome to the node-project-8 wiki!

欢迎阅读node-project-8的开发&构建流程介绍

在这个项目中,我们将对之前的项目进行一些改进,增添缓存机制。具体功能包括:

  1. 连接redis服务器并进行相关操作。
  2. 缓存图片搜索的结果。
  3. 当缓存命中时直接返回缓存信息。
  4. 新上传图片后清除对应条目的缓存。

##如何运行或测试应用

启动项目后,可以通过Postman来进行模拟请求,相关的Postman设置可以点击下面链接: Run in Postman

知识

本项目假设开发者已经了解以下基础知识,项目开展过程中这些知识不再讲解:

  • NodeJS
  • Express
  • ES 2015,本项目使用到包括但不限于箭头函数、字符串模板、Generator/Promise

云资源

本项目使用到的云资源包括:

  1. 腾讯云 云存储Redis 服务,创建 1 个实例。
  2. (可选)腾讯云注册域名一个,用于部署应用到外网。

开发过程

###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/listAPI,在请求转发并完成后添加缓存处理。

创建/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。

  1. 首先修改/routes/index.js,为转发前或转发后需要额外处理的API添加处理方法,例如/part4/list
'/part4/list': {
	'method': 'get',
	'path': '/list',
	'beforeRequest':require('../handle/list').checkRedis,
	'afterRequest':require('../handle/list').addRedisALL
},
  1. 抽离封装转发函数,方便复用。
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);
  1. 同理在转发前验证并调用对应的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请求处理方法
Clone this wiki locally