手写 Redis,Storage 临时缓存不是事儿!

562 阅读5分钟

最近自己在做项目时用 node 写了一个获取文件目录的接口,方式:递归。这样就容易造成每次掉接口都会进行递归去获取数据,为了减小服务器压力,我想过用 Redis 去做临时缓存,但因为最近要换服务器又懒得装,而且个人网站要存储的数据量也不是太多,能不能自己去维护一个缓存对象呢?

突然萌生出一个想法:

在起服务 node server.js 时,服务本身是运行在内存中的,那我们可以写一个自己进行管理自己的对象。当然弊端也很明显:1. 性能上肯定比不上 Redis 本身,2. 服务停掉的同时数据缓存也就没了。

RedisV8
实现语言cc++
执行c 直接操作系统将 js 代码编译为机器码执行

那么 c 和 V8(c++) 哪个更快,执行效率更高?咳咳,这个不考,不用画圈。感兴趣的可以搜一搜相关的文章。

Redis 的特点:

  • 存储数据
  • 获取数据
  • 具有缓存时间
  • 过期后删除

自己实现的功能:

  1. 数据缓存,设置过期时间;
  2. 限制最大内存,超出则清理;
  3. 减少程序执行频率,数据防刷。

实现一个简单的缓存对象

定义缓存对象

const length = Symbol('_length');
const data = Symbol('data');

const cache = {
  // 将 data 设置为 Symbol 类型,避免直接查看/添加/删除/修改属性
  [data]: {
    // key: { createTime: 1627001616489, value: 'data', overTime: 1000, count: 0 }
    [length]: 0
  },
  // 获取整个缓存对象的长度
  length() {
    return this[data][length];
  }
}

如果想用在浏览器端,将存放对象 data 换成 Storage,思路是相同的。实现一个具有过期时间的 storage

存储数据格式

key类型说明
createTimeDate.now()数据存放时间
valueany存储对象
overTimenumber过期时间
countnumber记录索引。同一时间内可能存很多数据,记录一个索引,越小则证明存放时间越早

先写一个工具函数:数字生成器

function *createNum() {
  let n = 0
  while (true) {
    yield n;
    n++;
  }
}

const iter = createNum();
iter.next().value;  // --> 0
iter.next().value;  // --> 1
iter.next().value;  // --> 2

存储 & 获取

const cache = {
  [data]: {
    [length]: 0
  },
  // 储存数据
  set(key, value, overTime) {
    if (key === null || key === undefined || key === '') return;
    this[data][length] ++;
    this[data][key] = {
      createTime: Date.now(),  // 创建时间
      value,  // 存放数据
      overTime: overTime,  // 过期时间
      count: iter.next().value,  // 索引,记录数据存放先后顺序
    }
  },
  // 获取数据
  get(key) {
    if (!this[data][key]) return;
    const time = Date.now();
    const obj = this[data][key];
    time - obj.createTime > obj.overTime && this.delete(key);
    return this[data][key]?.value;  // ?. 防止存放 value === undefined 报错 
  }
}

附上完整代码

cache.js
const { 'log': c, warn } = console;
const length = Symbol('_length');
const data = Symbol('data');

function *createNum() {
  let n = 0
  while (true) {
    yield n;
    n++;
  }
}
const iter = createNum();

const cache = {
  // 将 data 设置为 Symbol 类型,避免直接查看/添加/删除/修改属性
  [data]: {
    [length]: 0
  },
  /**
   * 设置缓存数据
   * @param {string | symbol} key 如果不想覆盖此属性,请将 key 设置为 Symbol 类型
   * @param {*} value 
   * @param {number} overTime 过期时间,单位(ms)
   */
  set(key, value, overTime) {
    if (key === null || key === undefined || key === '') return;
    this[data][length] ++;
    this[data][key] = {
      createTime: Date.now(),
      value,
      overTime: overTime,
      count: iter.next().value,  // 同一毫秒内可能存很多数据,记录一个索引,越小则证明存放时间越早
    }
  },
  // 获取数据
  get(key) {
    if (!this[data][key]) return;
    const time = Date.now();
    const obj = this[data][key];
    time - obj.createTime > obj.overTime && this.delete(key);
    return this[data][key]?.value;  // ?. 防止存放 value === undefined 报错
  },
  // 删除数据
  delete(key) {
    if (!this[data][key]) return;
    delete this[data][key];
    this[data][length] --;
  },
  // 清空所有数据
  clear() {
    this[data] = {}
  },
  // data.length
  length() {
    return this[data][length];
  },
  gainAll() {
    return this[data];
  },
  // 获取数据字节大小,粗略计算(没有区分汉字与英文及 symbol 类型的 key.length)
  size() {
    const syms = Object.getOwnPropertySymbols(this[data]);
    let symSize = 0;
    syms.forEach(val => {
      const symValue = this[data][val];
      symSize += JSON.stringify(symValue).length
    })
    const objSize = JSON.stringify(this[data]).length;
    return symSize + objSize;
  }
}

export default cache;
import cache from './cache.js';
const { 'log': c } = console;

cache.set('东皇', '不会玩', 1000);
cache.set('嬴政', '没钱买', 1000);
cache.set('云缨', '贼溜', 1213);
cache.set('百里', '两枪一个', 500);
cache.set('钟无艳', '挨我一锤', 43);

cache.set('云樱', '贼溜', 1213);
cache.set('百里', '两枪一个', 400);
setTimeout(() => {
  cache.get('云樱');
  cache.get('百里');
  c(cache.gainAll());
  c(cache.size());
}, 600)

屏幕截图 2021-07-26 085055.jpg

你以为这样就完事了吗?不,最核心的功能还没有实现。上面代码只有在数据存放时检查了该数据有没有过期,那存放时是不是也应该清理下内存呢!而且要限制内存大小,删除过期数据以及最早的数据。长江水后浪催前浪,尘世上一辈新人换旧人!^v^

限制大小,清理内存

import cache from './cache.js';

export default class Redis {
  constructor(maxCache) {
    this.maxCache = maxCache || 1024 * 1024 * 2;  // 最大缓存数,默认 2M
  }
  
  /**
   * 储存数据,如果已经存在并且没有过期你会直接获取到该数据
   * @param {string} key 设置存放数据的键
   * @param {*} value 建议类型是一个函数(必须返回数据),可以进行数据请求,有缓存时并不会执行
   * @param {date} overTime 过期时间
   * @returns 返回存放的 value
   */
  async deposit(key, value, overTime) {
    const data = cache.get(key)
    if (data) {
      return data;
    } else {
      typeof value === 'function' ? value = await value() : value;
      cache.set(key, value, overTime);  // 先存数据后清内存,防止溢出
      
      const size = cache.size();  // 获取容器已存放数据内存大小
      // 实际缓存的数据比设置缓存数大,进行数据清理
      size > this.maxCache && this.clearCache();

      return value;
    }
  }

  // 清除数据(过期的,早以前的)
  clearCache() {
    this.deleteOverValue();  // 清除已过期数据

    const size = cache.size();
    if (size < this.maxCache) return;  // 再判断一次内存大小,内存超出则做下面的操作

    const obj = cache.gainAll();
    const arr = [];
    for (const prop of Object.entries(obj)) {
      arr.push({key: prop[0], ...prop[1]});
    }
    const newArr = choiceSort(arr);
    this.deleteFristValue(newArr);
  }
  
  // 删除过期的数据
  deleteOverValue() {
    const obj = cache.gainAll();
    const curTime = Date.now();
    for (const prop of Object.entries(obj)) {
      const createTime = prop[1].createTime;
      const overTime = prop[1].overTime;
      if (curTime - createTime > overTime) {
        cache.delete(prop[0]);
      }
    }
  }

  // 删除最早缓存的数据
  deleteFristValue(arr) {
    const key = arr[0].key;
    arr.shift();
    cache.delete(key);

    const size = cache.size();
    if (size <= this.maxCache) return;
    this.deleteFristValue(arr);  // 如果容器内存依然大于设定内存,继续删
  }

}

// 将数组进行排序
function choiceSort(arr) {
  const len = arr.length;
  if (arr == null || len == 0) return [];
  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1; j++) {
      const minValue = arr[j];
      if (minValue.count > arr[j + 1].count) {
        [arr[j], arr[j + 1]] = [arr[j + 1], minValue];  // 数据位置替换:[a, b] --> [b, a]
      }
    }
  }
  return arr;
}

知识点补充:参数替换

var a = 1, b = 2;

// 1. ES6 之前我们是这样做的
var c = b;
b = a;
a = c;
console.log(a, b);  //--> 2 1

// 2. ES6+
[a, b] = [b, a];
console.log(a, b);  //--> 2 1

测试:

import cache from './cache.js';
import Redis from './redis.js';
const redis = new Redis(500);  // 限制内存大小
const { 'log': c } = console;

redis.deposit('东皇', '不会玩', 1000);
redis.deposit('嬴政', '没钱买', 1000);
redis.deposit('明世隐', '来一盘', 2000);
redis.deposit('貂蝉', '不爱吕布', 2000);
redis.deposit('杨戬', () => '放狗咬你', 1000);
redis.deposit('米莱迪', () => ['小兵1', '小兵2', '小兵3'], 600);
redis.deposit(Symbol('阿珂'), () => '看不到我', 1000);
redis.deposit('猴子', async () => {
  return await '棒槌'
}, 600);
redis.deposit('元歌', () => [
  {name: '我可以是任何人' },
], 600);
redis.deposit('鲁班大师', () => '鲁班七号爸爸', 1000);
redis.deposit('云缨', () => '跟我去大理寺玩儿', 300);
setTimeout(async () => {
  redis.deposit('鲁班七号', () => '腿短频率快', 1000);
  c(cache.gainAll());
  c('size:', cache.size());

  const monkey = await redis.deposit('猴子');
  c('monkey:', monkey)
}, 500)

屏幕截图 2021-07-25 095529.jpg

在 node 中的应用,我这里用的是 koa

app.get('/label', async (ctx, next) => {
  const time = 1000 * 60 * 20;
  const data = await redis.deposit('label', async () => {
    return await getFileCatalogue();
  }, time);
  ctx.body = data;
  next();
})