最近自己在做项目时用 node 写了一个获取文件目录的接口,方式:递归。这样就容易造成每次掉接口都会进行递归去获取数据,为了减小服务器压力,我想过用 Redis 去做临时缓存,但因为最近要换服务器又懒得装,而且个人网站要存储的数据量也不是太多,能不能自己去维护一个缓存对象呢?
突然萌生出一个想法:
在起服务 node server.js 时,服务本身是运行在内存中的,那我们可以写一个自己进行管理自己的对象。当然弊端也很明显:1. 性能上肯定比不上 Redis 本身,2. 服务停掉的同时数据缓存也就没了。
| Redis | V8 | |
|---|---|---|
| 实现语言 | c | c++ |
| 执行 | c 直接操作系统 | 将 js 代码编译为机器码执行 |
那么 c 和 V8(c++) 哪个更快,执行效率更高?咳咳,这个不考,不用画圈。感兴趣的可以搜一搜相关的文章。
Redis 的特点:
- 存储数据
- 获取数据
- 具有缓存时间
- 过期后删除
自己实现的功能:
- 数据缓存,设置过期时间;
- 限制最大内存,超出则清理;
- 减少程序执行频率,数据防刷。
实现一个简单的缓存对象
定义缓存对象
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 | 类型 | 说明 |
|---|---|---|
| createTime | Date.now() | 数据存放时间 |
| value | any | 存储对象 |
| overTime | number | 过期时间 |
| count | number | 记录索引。同一时间内可能存很多数据,记录一个索引,越小则证明存放时间越早 |
先写一个工具函数:数字生成器
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)
你以为这样就完事了吗?不,最核心的功能还没有实现。上面代码只有在数据存放时检查了该数据有没有过期,那存放时是不是也应该清理下内存呢!而且要限制内存大小,删除过期数据以及最早的数据。长江水后浪催前浪,尘世上一辈新人换旧人!^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)
在 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();
})