在之前的文章NodeJs:腾讯新闻构建高性能的 react 同构直出方案里,我们简单介绍了下缓存的使用,不过讲解的不深,这里我再着重讲解下。 缓存有很多种方式: 因这种缓存通过只在单个设备(cookie,localstorage)或者访问周期内(mobx)有效,一般只是缓存一些跟用户相关的个性化数据。 通常会缓存一些个性化的数据,例如用户今日是否已点击过某个按钮,引导性弹窗是否今天是否已弹出过,已请求到的用户相关的数据缓存到 redux, mobx 等中(避免重复请求接口)。 网上已经有很多讲解浏览器缓存的文章了,这里我们只是简单的介绍下 Cache-Control 和 etag 的使用。 Cache-Control 属于强缓存类型。 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache 或 from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。 设置 etag 属于协商缓存。 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况。 Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 知会客户端直接使用本地缓存即可。 强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。 Cache-Control 通常用于静态资源的缓存,而且一般缓存时间都比较长,比如有 24 小时的,有 30 天的。而 Etag 的协商缓存通常用来缓存可变化的页面,例如展示数据的页面,这些页面的缓存根据内容的变化来进行缓存。 我博客的页面则是使用了强缓存与协商缓存并存的策略,因为博客文章在生成之后,除非有错误修改,一般也不会再进行变动了。 这里主要是 node 的进程缓存和远端的 redis 或 memcached 等缓存。 把数据缓存到进程中,无需额外的 I/O 开销,读写速度快;但缺点是数据容易失效,一旦程序出现异常时缓存直接丢失,同时内存缓存无法达到进程之间的共享。 我们的 node 服务在启动时采用的是 cluster 模式,即按照当前机器中的 CPU 数量来启动进程。若我们把数据存储到本地进程后,读取数据也是非常的快。在这里我使用的是 lru-cache 模块,即采用的最近最少使用策略淘汰多余的多余的缓存。 进程缓存之间的数据是无法共享的,因此可能会出现多个进程之间的数据产生不同步的现象。针对这个问题,我们可以把进程缓存的时间的设置的短一些,然后再读取 redis 缓存保持同步。若 redis 缓存也失效的时候,则通过 node 服务更新最新的内容。 我们可以把接口数据,页面整个 html 都缓存到内存缓存中,当遇到缓存后,则直接返回,否则进行后续的操作。 注意: 开发过程中,想用 因此,要么考虑这两个参数,要么考虑使用整个 url 来作为 key 进行缓存。同时,我们的抢金达人是在新闻客户端和新闻客户端极速版中,同时访问的。而且使用的是同一个 url,当时考虑的是 url+ua 来作为 key 进行缓存。可是用整个 url 来缓存的时候,又会因为扫描等带有随机参数的措施,产生 总结一下产生的问题是: 最终的方案是: 在服务文件 server.js 中,则读取/设置缓存数据: 这里我们定义了几个规则,可以很方便的控制每个接口的缓存粒度: 对一些不常更新的组件,例如 我们在上面讲解了缓存的手段和缓存的粒度。其实从最开始设想的直接缓存整个页面编译出来的 html,简直不要太粗暴,后面我们采用了接口粒度和组件粒度的缓存方式,能够更加精细化的控制。 同时,多种缓存方式的结合,也能为我们提供更好的效果,当接口已经缓存后,那么在相应的一段时间内,浏览器缓存也是奏效的(Etag 一直保持不变),可以直接使用浏览器缓存,当接口数据更新后,浏览器缓存也会相应的更新。
1. 前端缓存 #
2. 浏览器缓存 #
2.1 Cache-Control #
Cache-Control: max-age=300
后,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。2.2 etag #
2.3 总结 #
3. 内存缓存 #
3.1 进程缓存 #
const LruCache = require('lru-cache');
const lru = new LruCache(50); // 最多缓存50条数据
lru.set('testKey', '123test', 1000 * 10); // 缓存10秒钟
const data = lru.get('testKey');
console.log(data);
3.2 redis 缓存 #
const LruCache = require('lru-cache');
const { getRedisCache, setRedisCache } = require('./redis');
const nodeLogger = require('./node_logger'); // 本地日志
const nodeAtta = require('./node_atta'); // atta上报
const lru = new LruCache(50); // https://www.npmjs.com/package/lru-cache
const cache = {
open:
process.env.NEXT_APP_ENV === 'production' ||
process.env.NEXT_APP_ENV === 'pre', // 整个缓存的开关
openRedis:
process.env.NEXT_APP_ENV === 'production' ||
process.env.NEXT_APP_ENV === 'pre', // redis缓存的开关
lruTimeout: 20,
redisTimeout: 60,
async get(key, lruTimeout) {
if (this.open) {
if (lru.has(key)) {
return Promise.resolve({
value: lru.get(key),
from: 'lru', // 缓存来自lru
});
}
// 穿透lru缓存时,进行上报,统计lru缓存穿透率
nodeAtta({
level: 'info',
file: '[shell][cache]',
fn: 'cache.get',
msg: `penetration lru cache, key: ${key}`,
});
try {
if (this.openRedis) {
const value = await getRedisCache(key);
if (value) {
// 不能直接调用this.set,会造成key无限不过期的
lru.set(
key,
value,
(lruTimeout || this.lruTimeout) * 1000
);
return Promise.resolve({
value,
from: 'redis', // 缓存来自redis
});
}
// 穿透redis缓存时,进行上报,统计redis缓存穿透率
nodeAtta({
level: 'info',
file: '[shell][cache]',
fn: 'cache.get',
msg: `penetration redis cache, key: ${key}`,
});
}
} catch (error) {
nodeLogger.error('getRedisCache error', error);
nodeAtta({
file: '[shell][cache]',
fn: 'cache.get.redis',
msg: `${key}, ${error.message}`,
});
}
}
return Promise.resolve({ value: null, from: null }); // 没有命中缓存
},
// 设置缓存
set(key, value, timeout) {
if (this.open) {
const lruTimeout =
timeout && timeout.lruTimeout
? timeout.lruTimeout
: this.lruTimeout;
const redisTimeout =
timeout && timeout.redisTimeout
? timeout.redisTimeout
: this.redisTimeout;
lru.set(key, value, lruTimeout * 1000);
if (this.openRedis) {
try {
setRedisCache(key, value, redisTimeout);
} catch (error) {
// 设置Redis缓存失败
nodeLogger.error('setRedisCache error', error);
nodeAtta({
file: '[shell][cache]',
fn: 'cache.set.catch',
msg: `${key}, ${error.message}`,
});
}
}
}
},
del(key) {
lru.del(key);
},
};
module.exports = cache;
3.3 接口缓存 #
req.path
作为 key 来存储整个页面的 html,但后来发现一个问题,是页面会根据传入参数的不同,展示不同的页面,例如若 url 中有个参数noticetab
,则首页中展示对应的大图。那么这时就要把参数考虑进去。同时,端外携带某个 noticetab 带入到端内后,也要展示这个大图,但参数会变成_addparams
。缓存穿透
,导致缓存失效,会把正常的缓存内容给挤掉。而且缓存整个页面时,粒度太粗,无法精细化控制。
接口缓存
。按照接口进行精细化的缓存管理,可以设置每个接口的缓存时间,同时不受页面 url 的影响,而且也会根据 ua 的不同,请求不同的后端的接口。// request.ts
const request = async (
url: string,
options: RequestProps = { params: {} }
): Promise
// server.js
const apiHandler = async (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('x-cache-from', 'server-proxy');
const { path, query } = req;
const { mock, env } = query; // 这里参数cache的名称跟缓存控制模块的名字冲突
if (!query.cache) {
// 若没有设置缓存,则直接请求接口并返回
return req.pipe(requestApi(getApiOrigin(env) + req.url)).pipe(res);
}
const { url } = req; // 接口的url地址
const { value } = await cache.get(url);
if (value) {
return res.send(JSON.parse(value));
}
const reqUrl = getApiOrigin(env) + url;
try {
const result = await requestApi(reqUrl); // 请求接口
if (result && result.code === 0) {
// 当接口返回正常,且code===0时才缓存数据
cache.set(url, JSON.stringify(result), query.cache);
}
// 其他情况交给接入层处理
res.send(result);
} catch (error) {
nodeLogger.error('server.js[apihandle]', reqUrl, error);
}
};
3.4 组件缓存 #
head.jsx
或foot.jsx
等,我们可以将编译后产生的 html 进行缓存,这样在下次访问时,可以直接使用已编译好的内容。const getComponentCache = (Component) => {
const { name, cache } = Component.type;
// 若已经有缓存的内容,则直接返回
if (name && cache.has(name)) {
return cache.get(name);
}
const html = renderToStaticMarkup(Component);
// 若需要缓存,则缓存起来
if (cache && name) {
cache.set(name, html, cache);
}
return html;
};
4. 总结 #
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论