ShiningDan的博客

博客性能优化之热门数据缓存

Redis 是基于内存来存储数据的,所以 Redis 对数据读取的速度远远大于使用硬盘来读取数据的速度,因此经常被用作缓存。除此之外,Redis 还经常使用在应用排行榜、网站访问统计、数据过期处理、商品秒杀(队列机制)等地方。在本博客中,针对访问量比较大的数据,如首页信息,以及热门的文章,设计了 Redis 缓存方案。

安装 Redis

安装 Redis 的流程还是很简单的,具体的流程可以参考官网介绍 Redis Quick Start。在下载并且编译完成 Redis 以后,在虚拟机上部署 Redis 时,我配置了 redis.conf 中的几个参数,其中有:

1
2
daemonize yes      // 设置 Redis 后台启动
port 7200 // 设置 Redis 的启动端口

当然,除此以外,还可以配置连接 Redis 的用户验证密码,日志的输出记录等。因为 Redis 的数据都是存储在内存中的,所以会比较消耗内存资源,所以在配置文件中,也可以注意以下设置 Redis 可以使用的最大存储内存。具体很多的参数,有需要的同学,可以参考下面文章进行设置:redis.conf配置及说明

在我的环境中,为了方便启动 Redis,还使用 alias 重新定义了 Redis 的启动命令:

1
2
alias startredis="/root/redis/redis-3.2.9/src/redis-server /root/redis/redis-3.2.9/redis.conf"
alias redis-cli="/root/redis/redis-3.2.9/src/redis-cli"

Redis 相关库

Redis 官方提供了多种语言连接的库,其中 JS 的库可以在 NodeRedis 这里找到说明。官方的 redis 库是基于回调函数来进行异步处理的,我们也可以使用 bluebird 来包装该库,来实现基于 Promise 处理的能力。

1
2
3
var redis = require('redis');
bluebird.promisifyAll(redis.RedisClient.prototype);
bluebird.promisifyAll(redis.Multi.prototype);
1
2
3
4
5
6
7
8
9
10
11
// We expect a value 'foo': 'bar' to be present
// So instead of writing client.get('foo', cb); you have to write:
return client.getAsync('foo').then(function(res) {
console.log(res); // => 'bar'
});

// Using multi with promises looks like:

return client.multi().get('foo').execAsync().then(function(res) {
console.log(res); // => 'bar'
});

使用 bluebird 来包装异步调用的方法在 redis GitHub 上也可以找到。

除此以外,还需要注意一点,在使用 Redis 的库的时候,如果有多个查询,可以使用 multi() + execAsync() 函数来进行一次处理多个请求。

同步 Mongodb 数据到 Redis

在我的博客数据库的数据中,需要同步到 Redis 的数据有:

  • series:用来为 /series/ 目录和 /archives/提供数据
  • abstracts:用来为首页提供数据
  • articles:用来为文章提供数据

通过上面需要同步的信息,好像我们已经把数据库中基本所有的数据都同步了,但其实不能这样做。由于 Redis 使用的是内存来缓存数据,所以,如果服务器的内存太小,或者已有的数据太多时,会消耗很大的内存,所以,我们在同步数据的时候要对数据进行选择。

比如说,可以只从 MongoDB 中获取最热门的 20 篇文章,或者只获取 abstracts 中在首页前三页展示的数据等,通过这种方式来选择要同步的数据,来节约服务器的内存使用情况。(在本博客目前还没有进行热门数据的筛选功能,不过预计在接下使用 Google 统计处理这方面的数据,然后再更新本文中热门数据的获取代码。)

所以,目前获取数据的函数只是实现了功能,还没有考虑到重用性,这部分会在接下来使用 Google 统计后,根据获得的数据类型进行设计更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 这个函数实现的功能是从 Mongodb 中获取数据,并且发送给 Redis
*
* @param {any} redisClient
*/
async function fetchFormMongoToRedis(redisClient) {
try {
let [abstracts, series, articles] = await Promise.all([Abstract.find({}), Series.find({}).populate('articles', ['title', 'link', 'meta.createAt']), Article.find({})])

let seriesStrings = series.map((series) => JSON.stringify(series)),
abstractsStrings = abstracts.map((abstract) => JSON.stringify(abstract)),
articlesStrings = articles.map((article) => {
article.md = null;
return JSON.stringify(article);
});

let saveSeries = redisClient.multi().del('series').rpush('series', seriesStrings).execAsync();
let saveAbstracts = redisClient.multi().del('abstracts').rpush('abstracts', abstractsStrings).execAsync();
// let saveArticles = redisClient.multi().del('articles').rpush('articles', articlesStrings).execAsync();
let saveArticlesByLink = redisClient.multi().del('articlesByLink');
for (let i = 0; i < articles.length; i++) {
saveArticlesByLink.hset('articlesByLink', articles[i].link, articlesStrings[i]);
}
saveArticlesByLink.execAsync();
let saveArticlesById = redisClient.multi().del('articlesById');
for (let i = 0; i < articles.length; i++) {
saveArticlesById.hset('articlesById', articles[i]._id.toString(), articlesStrings[i]);
}
saveArticlesById.execAsync();

await Promise.all([ saveSeries, saveAbstracts, saveArticlesByLink, saveArticlesById]).then((value) => console.log('finish fetch data from mongo to redis: series', value[0][1], ', abstracts', value[1][1], ', articles'));
} catch(e) {
console.log(e)
}
}

设置同步周期

博客在设计同步的时候,分为主动同步和被动同步。主动同步是在修改文章和创建新文章的时候,调用上面定义好的 fetchFormMongoToRedis 函数来讲数据同步到 Redis 缓存中,被动同步是每 5 分钟被动调用该函数从数据库中获取一次需要的数据来更新缓存,被动更新的目的是,当使用 Google 统计和 Qisqus 评论系统以后,被动获得最热门的文章以及当前的评论数来进行更新。

本博客系统创建了一个新的 JS 脚本,通过 node-schedule 库来实现每隔 5 分钟获取数据的能力。并且在生产环境中通过 PM2 保障该脚本的运行,相关代码如下:

1
2
3
4
5
6
7
var rule = new schedule.RecurrenceRule();
rule.minute = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
// rule.second = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
schedule.scheduleJob(rule, function(){
console.log('Redis Fetch Schedule start at:' + new Date());
fetchFormMongoToRedis(redisClient);
});

参考