bigo-frontend / blog

👨🏻‍💻👩🏻‍💻 bigo前端技术博客
https://juejin.cn/user/4450420286057022/posts
MIT License
129 stars 9 forks source link

【node实战系列】自行实现应用缓存 #53

Open yeyeye0525 opened 3 years ago

yeyeye0525 commented 3 years ago

file

本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。

【node实战系列】自行实现应用缓存

背景

bigo前端开始推广bff,hello农场作为首个bff落地项目,历经2个月,完成了从0-1的落地实践。

【node实战系列】按照小模块拆分,从开发者的角度讲叙,如何进行bff高可用编码。

本系列文章,基于eggjs框架编码,使用ts语法,为了提升阅读体验,建议大家先了解一下eggjs。

系列文章

欢迎大家关注我们的github blog,持续更新。 https://github.com/bigo-frontend/blog/issues

缓存基本原理

缓存主要用来存放那些读写比很高、很少变化的数据,如字典信息,用户信息,组织机构信息等。应用程序读取数据时,先到缓存中读取,如果读取不到或数据已失效,再访问数据库,并将数据写入缓存。

其实我们经常接触缓存,譬如浏览器的localStorage、vue的computer、keep-alive等等。

在服务端也常用redis作为分布式缓存。

应用缓存

常用的分布式缓存包括Redis、Memcached,因为Redis提供的数据结构比较丰富且简单易用,所以Redis的使用广泛。

但是使用分布式缓存还是有一层外链调用,如果是使用应用缓存,存储在内存里,速度是最快的,我们可以把与用户信息无关的冷数据存储在应用缓存。

合理使用缓存,可以更好的改善系统性能,提高数据读取速度。

自定义应用缓存

nodejs没有提供缓存api,查阅资料发现一个缓存npm包node-cache,但是很久没人维护 了,并且缺少ts定义。

nodejs作为动态语言,对key-value天然支持,不考虑数据淘汰策略,可以考虑自己手撸一个简单应用缓存。

开撸

eggjs支持对application进行扩展,我们可以在app/extend/application.ts定义缓存

1.setCache

import { PlainObject } from "egg";
const CACHE = Symbol('Application#cache');
const CACHECOUNT = Symbol('Application#cachecount');

setCache(key: string, obj: PlainObject, expire: '1m'|'5m'|'10m'|'25m'|'1d'|'7d' = '10m'): boolean {
  try {
    if (!this[CACHE]) { // 不存在
      this[CACHECOUNT] = 0;
      this[CACHE] = {};
    }
    // 优化为计数器
    // const size = JSON.stringify(this[CACHE]).length;
    // if (size > 1024 * 1024 * 20) { // 20M
    if (this[CACHECOUNT] > 50 * 1000) { // 限制5w条数据
      console.log('超出存储限制!');
      this[CACHE] = {}; // 清空数据
      this[CACHECOUNT] = 0;
      return false;
    }
    const timeOV = {
      '1m': 60 * 1000,
      '5m': 5 * 60 * 1000,
      '10m': 10 * 60 * 1000,
      '25m': 25 * 60 * 1000,
      '1d': 24 * 60 * 60 * 1000,
      '7d': 7 * 24 * 60 * 60 * 1000,
    };
    this[CACHE][key] = JSON.stringify({
      expire: Date.now() + timeOV[expire],
      ...obj,
    });
    this[CACHECOUNT]++;
    console.log('设置缓存数据成功');
    return true;
  } catch (error) {
    return false;
  }
}

2.getCache

/**
  * 获取本地缓存
  *
  * @param {*} key 缓存key
  * @returns
  */
getCache(key: string) {
  if (!this[CACHE] || !this[CACHE][key]) { // 不存在
    return null;
  }
  try {
    const data = JSON.parse(this[CACHE][key]);
    if (Date.now() - data.expire > 0) { // 已过期
      this[CACHE][key] = '{}'; // 清空数据
      this[CACHECOUNT]--;
      return null;
    }
    delete data.expire; // 不返回过期时间
    console.log('获取缓存数据成功');
    return data;
  } catch (error) {
    return null;
  }
}

3.cleanCache

cleanCache(key) {
  if (!this[CACHE]) { // 不存在
    return false;
  }
  delete this[CACHE][key];
  this[CACHECOUNT]--;
  return true;
},
cleanCacheAll() {
  if (!this[CACHE]) { // 不存在
    return false;
  }
  this[CACHE] = {};
  this[CACHECOUNT] = 0;
  return true;
}

业务使用示例

const {ctx} = this;
let res: HttpRes<LevelAwardRes>;
const cacheData = ctx.app.getCache(EcacheKey.GETLEVELAWARDINFO); 
if (cacheData) { // 获取缓存
  res = cacheData;
} else {
  res = await this.request();
  if (res.code === 0) { // 设置缓存时间为5分钟
    ctx.app.setCache(EcacheKey.GETLEVELAWARDINFO, res, '5m');
  }
}
const {code, message, data} = res;
if (code !== 0) {
  this.ctx.throwErr(message, code, data);
}

小结

综上,一个简单又实用的缓存功能就实现了。

数据访问通常遵循二八定律,即80%的访问落在20%的数据上,因此利用应用缓存的内存髙速访问特性,将这20%的数据缓存起来,可很好地改善系统性能,提高数据读取速度,降低存储访问压力。希望大家做好高可用实践,给用户带来顺滑体验。

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。

webjohnjiang commented 3 years ago

计数器来限制5w数据量会不会没办法防止某个key的size超大的情况。感觉这个库也能适用这种场景:https://www.npmjs.com/package/lru-cache

yeyeye0525 commented 3 years ago

计数器来限制5w数据量会不会没办法防止某个key的size超大的情况。感觉这个库也能适用这种场景:https://www.npmjs.com/package/lru-cache

平均一条数据1kb,5w条数据不会超过50M,当然如果size很大,就要考虑是否适用缓存,一般业务场景不会存在这种情况。