亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

在Nodejs中實(shí)現(xiàn)一個(gè)緩存系統(tǒng)的方法詳解

 更新時(shí)間:2024年03月26日 08:43:23   作者:Knockkk  
在數(shù)據(jù)庫查詢遇到瓶頸時(shí),我們通??梢圆捎镁彺鎭硖嵘樵兯俣?同時(shí)緩解數(shù)據(jù)庫壓力,在一些簡(jiǎn)單場(chǎng)景中,我們也可以自己實(shí)現(xiàn)一個(gè)緩存系統(tǒng),避免使用額外的緩存中間件,這篇文章將帶你一步步實(shí)現(xiàn)一個(gè)完善的緩存系統(tǒng),需要的朋友可以參考下

在數(shù)據(jù)庫查詢遇到瓶頸時(shí),我們通??梢圆捎镁彺鎭硖嵘樵兯俣?,同時(shí)緩解數(shù)據(jù)庫壓力。常用的緩存數(shù)據(jù)庫有Redis、Memcached等。在一些簡(jiǎn)單場(chǎng)景中,我們也可以自己實(shí)現(xiàn)一個(gè)緩存系統(tǒng),避免使用額外的緩存中間件。這篇文章將帶你一步步實(shí)現(xiàn)一個(gè)完善的緩存系統(tǒng),它將包含過期清除、數(shù)據(jù)克隆、事件、大小限制、多級(jí)緩存等功能。

一個(gè)最簡(jiǎn)單的緩存

class Cache {
  constructor() {
    this.cache = new Map();
  }

  get = (key) => {
    return this.data[key];
  };

  set = (key, value) => {
    this.data[key] = value;
  };

  del = (key) => {
    delete this.data[key];
  };
}

我們使用Map結(jié)構(gòu)來保存數(shù)據(jù),使用方式也很簡(jiǎn)單:

const cache = new Cache();
cache.set("a", "aaa");

cache.get("a") // aaa

添加過期時(shí)間

接下來我們嘗試為緩存設(shè)置一個(gè)過期時(shí)間。在獲取數(shù)據(jù)時(shí),如果數(shù)據(jù)已經(jīng)過期了,則清除它。

class Cache {
  constructor(options) {
    this.cache = new Map();
    this.options = Object.assign(
      {
        stdTTL: 0, // 緩存有效期,單位為s。為0表示永不過期
      },
      options
    );
  }

  get = (key) => {
    const ret = this.cache.get(key);
    if (ret && this._check(key, ret)) {
      return ret.v;
    } else {
      return void 0;
    }
  };

  set = (key, value, ttl) => {
    ttl = ttl ?? this.options.stdTTL;
    // 設(shè)置緩存的過期時(shí)間
    this.cache.set(key, { v: value, t: ttl === 0 ? 0 : Date.now() + ttl * 1000 });
  };

  del = (key) => {
    this.cache.delete(key);
  };

  // 檢查緩存是否過期,過期則刪除
  _check = (key, data) => {
    if (data.t !== 0 && data.t < Date.now()) {
      this.del(key);
      return false;
    }
    return true;
  };
}

module.exports = Cache;

我們寫個(gè)用例來測(cè)試一下:

const cache = new Cache({ stdTTL: 1 }); // 默認(rèn)緩存1s

cache.set("a", "aaa");

console.log(cache.get("a")); // 輸出: aaa

setTimeout(() => {
  console.log(cache.get("a")); // 輸出: undefined
}, 2000);

可見,超過有效期后,再次獲取時(shí)數(shù)據(jù)就不存在了。

私有屬性

前面的代碼中我們用_開頭來標(biāo)明私有屬性,我們也可以通過Symbol來實(shí)現(xiàn),像下面這樣:

const LENGTH = Symbol("length");

class Cache {
  constructor(options) {
    this[LENGTH] = options.length;
  }

  get length() {
    return this[LENGTH];
  }
}

Symbols 在 for...in 迭代中不可枚舉。另外,Object.getOwnPropertyNames() 不會(huì)返回 symbol 對(duì)象的屬性,但是你能使用 Object.getOwnPropertySymbols() 得到它們。

const cache = new Cache({ length: 100 });

Object.keys(cache); // []

Object.getOwnPropertySymbols(cache); // [Symbol(length)]

定期清除過期緩存

之前只會(huì)在get時(shí)判斷緩存是否過期,然而如果不對(duì)某個(gè)key進(jìn)行g(shù)et操作,則過期緩存永遠(yuǎn)不會(huì)被清除,導(dǎo)致無效的緩存堆積。接下來我們要實(shí)現(xiàn)定期自動(dòng)清除過期緩存的功能。

class Cache {
  constructor(options) {
    this.cache = new Map();
    this.options = Object.assign(
      {
        stdTTL: 0, // 緩存有效期,單位為s。為0表示永不過期
        checkperiod: 600, // 定時(shí)檢查過期緩存,單位為s。小于0則不檢查
      },
      options
    );
    this._checkData();
  }

  get = (key) => {
    const ret = this.cache.get(key);
    if (ret && this._check(key, ret)) {
      return ret.v;
    } else {
      return void 0;
    }
  };

  set = (key, value, ttl) => {
    ttl = ttl ?? this.options.stdTTL;
    this.cache.set(key, { v: value, t: Date.now() + ttl * 1000 });
  };

  del = (key) => {
    this.cache.delete(key);
  };

  // 檢查是否過期,過期則刪除
  _check = (key, data) => {
    if (data.t !== 0 && data.t < Date.now()) {
      this.del(key);
      return false;
    }
    return true;
  };

  _checkData = () => {
    for (const [key, value] of this.cache.entries()) {
      this._check(key, value);
    }

    if (this.options.checkperiod > 0) {
      const timeout = setTimeout(
        this._checkData,
        this.options.checkperiod * 1000
      );

      // https://nodejs.org/api/timers.html#timeoutunref
      // 清除事件循環(huán)對(duì)timeout的引用。如果事件循環(huán)中不存在其他活躍事件,則直接退出進(jìn)程
      if (timeout.unref != null) {
        timeout.unref();
      }
    }
  };
}

module.exports = Cache;

我們添加了一個(gè)checkperiod的參數(shù),同時(shí)在初始化時(shí)開啟了定時(shí)檢查過期緩存的邏輯。這里使用了timeout.unref()來清除清除事件循環(huán)對(duì)timeout的引用,這樣如果事件循環(huán)中不存在其他活躍事件了,就可以直接退出。

const timeout = setTimeout(
  this._checkData,
  this.options.checkperiod * 1000
);

// https://nodejs.org/api/timers.html#timeoutunref
// 清除事件循環(huán)對(duì)timeout的引用。如果事件循環(huán)中不存在其他活躍事件,則直接退出進(jìn)程
if (timeout.unref != null) {
  timeout.unref();
}

克隆數(shù)據(jù)

當(dāng)我們嘗試在緩存中存入對(duì)象數(shù)據(jù)時(shí),我們可能會(huì)遇到下面的問題:

const cache = new Cache();

const data = { val: 100 };

cache.set("data", data);

data.val = 101;

cache.get("data") // { val: 101 }

由于緩存中保存的是引用,可能導(dǎo)致緩存內(nèi)容被意外的更改,這就讓人不太放心的。為了用起來沒有顧慮,我們需要支持一下數(shù)據(jù)的克隆,也就是深拷貝。

const cloneDeep = require("lodash.clonedeep");

class Cache {
  constructor(options) {
    this.cache = new Map();
    this.options = Object.assign(
      {
        stdTTL: 0, // 緩存有效期,單位為s
        checkperiod: 600, // 定時(shí)檢查過期緩存,單位為s
        useClones: true, // 是否使用clone
      },
      options
    );
    this._checkData();
  }

  get = (key) => {
    const ret = this.cache.get(key);
    if (ret && this._check(key, ret)) {
      return this._unwrap(ret);
    } else {
      return void 0;
    }
  };

  set = (key, value, ttl) => {
    this.cache.set(key, this._wrap(value, ttl));
  };

  del = (key) => {
    this.cache.delete(key);
  };

  // 檢查是否過期,過期則刪除
  _check = (key, data) => {
    if (data.t !== 0 && data.t < Date.now()) {
      this.del(key);
      return false;
    }
    return true;
  };

  _checkData = () => {
    for (const [key, value] of this.cache.entries()) {
      this._check(key, value);
    }

    if (this.options.checkperiod > 0) {
      const timeout = setTimeout(
        this._checkData,
        this.options.checkperiod * 1000
      );

      if (timeout.unref != null) {
        timeout.unref();
      }
    }
  };

  _unwrap = (value) => {
    return this.options.useClones ? cloneDeep(value.v) : value.v;
  };

  _wrap = (value, ttl) => {
    ttl = ttl ?? this.options.stdTTL;
    return {
      t: ttl === 0 ? 0 : Date.now() + ttl * 1000,
      v: this.options.useClones ? cloneDeep(value) : value,
    };
  };
}

我們使用lodash.clonedeep來實(shí)現(xiàn)深拷貝,同時(shí)添加了一個(gè)useClones的參數(shù)來設(shè)置是否需要克隆數(shù)據(jù)。需要注意,在對(duì)象較大時(shí)使用深拷貝是比較消耗時(shí)間的。我們可以根據(jù)實(shí)際情況來決定是否需要使用克隆,或?qū)崿F(xiàn)更高效的拷貝方法。

添加事件

有時(shí)我們需要在緩存數(shù)據(jù)過期時(shí)執(zhí)行某些邏輯,所以我們可以在緩存上添加事件。我們需要使用到EventEmitter類。

const { EventEmitter } = require("node:events");
const cloneDeep = require("lodash.clonedeep");

class Cache extends EventEmitter {
  constructor(options) {
    super();

    this.cache = new Map();
    this.options = Object.assign(
      {
        stdTTL: 0, // 緩存有效期,單位為s
        checkperiod: 600, // 定時(shí)檢查過期緩存,單位為s
        useClones: true, // 是否使用clone
      },
      options
    );
    this._checkData();
  }

  get = (key) => {
    const ret = this.cache.get(key);
    if (ret && this._check(key, ret)) {
      return this._unwrap(ret);
    } else {
      return void 0;
    }
  };

  set = (key, value, ttl) => {
    this.cache.set(key, this._wrap(value, ttl));

    this.emit("set", key, value);
  };

  del = (key) => {
    this.cache.delete(key);

    this.emit("del", key, oldVal.v);
  };

  // 檢查是否過期,過期則刪除
  _check = (key, data) => {
    if (data.t !== 0 && data.t < Date.now()) {
      this.emit("expired", key, data.v);

      this.del(key);
      return false;
    }
    return true;
  };

  _checkData = () => {
    for (const [key, value] of this.cache.entries()) {
      this._check(key, value);
    }

    if (this.options.checkperiod > 0) {
      const timeout = setTimeout(
        this._checkData,
        this.options.checkperiod * 1000
      );

      if (timeout.unref != null) {
        timeout.unref();
      }
    }
  };

  _unwrap = (value) => {
    return this.options.useClones ? cloneDeep(value.v) : value.v;
  };

  _wrap = (value, ttl) => {
    ttl = ttl ?? this.options.stdTTL;
    return {
      t: ttl === 0 ? 0 : Date.now() + ttl * 1000,
      v: this.options.useClones ? cloneDeep(value) : value,
    };
  };
}

module.exports = Cache;

繼承EventEmitter類后,我們只需在判斷數(shù)據(jù)過期時(shí)通過this.emit()觸發(fā)事件即可。如下:

this.emit("expired", key, value);

這樣使用緩存時(shí)就能監(jiān)聽過期事件了。

const cache = new Cache({ stdTTL: 1 });

cache.on("expired", (key ,value) => {
  // ...
})

到這里,我們基本上就實(shí)現(xiàn)了node-cache庫的核心邏輯了。

限制緩存大小??!

稍等,我們似乎忽略了一個(gè)重要的點(diǎn)。在高并發(fā)請(qǐng)求下,如果緩存激增,則內(nèi)存會(huì)有被耗盡的風(fēng)險(xiǎn)。無論如何,緩存只是用來優(yōu)化的,它不能影響主程序的正常運(yùn)行。所以,限制緩存大小至關(guān)重要!

我們需要在緩存超過最大限制時(shí)自動(dòng)清理緩存,一個(gè)常用的清除算法就是LRU,即清除最近最少使用的那部分?jǐn)?shù)據(jù)。這里使用了yallist來實(shí)現(xiàn)LRU隊(duì)列,方案如下:

  • LRU隊(duì)列里的首部保存最近使用的數(shù)據(jù),最近最少使用的數(shù)據(jù)則會(huì)移動(dòng)到隊(duì)尾。在緩存超過最大限制時(shí),優(yōu)先移除隊(duì)列尾部數(shù)據(jù)。
  • 執(zhí)行g(shù)et/set操作時(shí),將此數(shù)據(jù)節(jié)點(diǎn)移動(dòng)/插入到隊(duì)首
  • 緩存超過最大限制時(shí),移除隊(duì)尾數(shù)據(jù)
const { EventEmitter } = require("node:events");
const clone = require("clone");
const Yallist = require("yallist");

class Cache extends EventEmitter {
  constructor(options) {
    super();

    this.options = Object.assign(
      {
        stdTTL: 0, // 緩存有效期,單位為s
        checkperiod: 600, // 定時(shí)檢查過期緩存,單位為s
        useClones: true, // 是否使用clone
        lengthCalculator: () => 1, // 計(jì)算長(zhǎng)度
        maxLength: 1000,
      },
      options
    );
    this._length = 0;
    this._lruList = new Yallist();
    this._cache = new Map();
    this._checkData();
  }

  get length() {
    return this._length;
  }

  get data() {
    return Array.from(this._cache).reduce((obj, [key, node]) => {
      return { ...obj, [key]: node.value.v };
    }, {});
  }

  get = (key) => {
    const node = this._cache.get(key);
    if (node && this._check(node)) {
      this._lruList.unshiftNode(node); // 移動(dòng)到隊(duì)首
      return this._unwrap(node.value);
    } else {
      return void 0;
    }
  };

  set = (key, value, ttl) => {
    const { lengthCalculator, maxLength } = this.options;

    const len = lengthCalculator(value, key);
    // 元素本身超過最大長(zhǎng)度,設(shè)置失敗
    if (len > maxLength) {
      return false;
    }

    if (this._cache.has(key)) {
      const node = this._cache.get(key);
      const item = node.value;

      item.v = value;
      this._length += len - item.l;
      item.l = len;

      this.get(node); // 更新lru
    } else {
      const item = this._wrap(key, value, ttl, len);
      this._lruList.unshift(item); // 插入到隊(duì)首
      this._cache.set(key, this._lruList.head);
      this._length += len;
    }

    this._trim();
    this.emit("set", key, value);
    return true;
  };

  del = (key) => {
    if (!this._cache.has(key)) {
      return false;
    }
    const node = this._cache.get(key);
    this._del(node);
  };

  _del = (node) => {
    const item = node.value;
    this._length -= item.l;
    this._cache.delete(item.k);
    this._lruList.removeNode(node);

    this.emit("del", item.k, item.v);
  };

  // 檢查是否過期,過期則刪除
  _check = (node) => {
    const item = node.value;
    if (item.t !== 0 && item.t < Date.now()) {
      this.emit("expired", item.k, item.v);

      this._del(node);
      return false;
    }

    return true;
  };

  _checkData = () => {
    for (const node of this._cache) {
      this._check(node);
    }

    if (this.options.checkperiod > 0) {
      const timeout = setTimeout(
        this._checkData,
        this.options.checkperiod * 1000
      );

      if (timeout.unref != null) {
        timeout.unref();
      }
    }
  };

  _unwrap = (item) => {
    return this.options.useClones ? clone(item.v) : item.v;
  };

  _wrap = (key, value, ttl, length) => {
    ttl = ttl ?? this.options.stdTTL;
    return {
      k: key,
      v: this.options.useClones ? clone(value) : value,
      t: ttl === 0 ? 0 : Date.now() + ttl * 1000,
      l: length,
    };
  };

  _trim = () => {
    const { maxLength } = this.options;

    let walker = this._lruList.tail;
    while (this._length > maxLength && walker !== null) {
      // 刪除隊(duì)尾元素
      const prev = walker.prev;
      this._del(walker);
      walker = prev;
    }
  };
}

代碼中還增加了兩個(gè)額外的配置選項(xiàng):

options = {
  lengthCalculator: () => 1, // 計(jì)算長(zhǎng)度
  maxLength: 1000, // 緩存最大長(zhǎng)度
}

lengthCalculator支持我們自定義數(shù)據(jù)長(zhǎng)度的計(jì)算方式。默認(rèn)情況下maxLength指的就是緩存數(shù)據(jù)的數(shù)量。然而在遇到Buffer類型的數(shù)據(jù)時(shí),我們可能希望限制最大的字節(jié)數(shù),那么就可以像下面這樣定義:

const cache = new Cache({
  maxLength: 500,
  lengthCalculator: (value) => {
    return value.length;
  },
});

const data = Buffer.alloc(100);
cache.set("data", data);

console.log(cache.length); // 100

這一部分的代碼就是參考社區(qū)中的lru-cache實(shí)現(xiàn)的。

多級(jí)緩存

如果應(yīng)用本身已經(jīng)依賴了數(shù)據(jù)庫的話,我們不妨再加一層數(shù)據(jù)庫緩存,來實(shí)現(xiàn)多級(jí)緩存:將內(nèi)存作為一級(jí)緩存(容量小,速度快),將數(shù)據(jù)庫作為二級(jí)緩存(容量大,速度慢) 。有兩個(gè)優(yōu)點(diǎn):

  • 能夠存儲(chǔ)的緩存數(shù)據(jù)大大增加。雖然數(shù)據(jù)庫緩存查詢速度比內(nèi)存慢,但相比原始查詢還是要快得多的。
  • 重啟應(yīng)用時(shí)能夠從數(shù)據(jù)庫恢復(fù)緩存。

通過下面的方法可以實(shí)現(xiàn)一個(gè)多級(jí)緩存:

function multiCaching(caches) {
  return {
    get: async (key) => {
      let value, i;
      for (i = 0; i < caches.length; i++) {
        try {
          value = await caches[i].get(key);
          if (value !== undefined) break;
        } catch (e) {}
      }

      // 如果上層緩存沒查到,下層緩存查到了,需要同時(shí)更新上層緩存
      if (value !== undefined && i > 0) {
        Promise.all(
          caches.slice(0, i).map((cache) => cache.set(key, value))
        ).then();
      }

      return value;
    },
    set: async (key, value) => {
      await Promise.all(caches.map((cache) => cache.set(key, value)));
    },
    del: async (key) => {
      await Promise.all(caches.map((cache) => cache.del(key)));
    },
  };
}

const multiCache = multiCaching([memoryCache, dbCache]);
multiCache.set(key, value)

dbCache對(duì)數(shù)據(jù)量大小不是那么敏感,我們可以在執(zhí)行g(shù)et/set操作時(shí)設(shè)置數(shù)據(jù)的最近使用時(shí)間,并在某個(gè)時(shí)刻清除最近未使用數(shù)據(jù),比如在每天的凌晨自動(dòng)清除超過30天未使用的數(shù)據(jù)。

另外我們還需要在初始化時(shí)加載數(shù)據(jù)庫緩存到內(nèi)存中,比如按最近使用時(shí)間倒序返回3000條數(shù)據(jù),并存儲(chǔ)到內(nèi)存緩存中。

參考

以上就是在Nodejs中實(shí)現(xiàn)一個(gè)緩存系統(tǒng)的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Nodejs實(shí)現(xiàn)緩存系統(tǒng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解Node.Js如何處理post數(shù)據(jù)

    詳解Node.Js如何處理post數(shù)據(jù)

    這篇文章給大家介紹了如何利用Node.Js處理post數(shù)據(jù),文中通過實(shí)例和圖文介紹的很詳細(xì),有需要的小伙伴們可以參考借鑒,下面來一起看看吧。
    2016-09-09
  • node.js express cors解決跨域的示例代碼

    node.js express cors解決跨域的示例代碼

    在Web開發(fā)中,當(dāng)一個(gè)網(wǎng)頁的源與另一個(gè)網(wǎng)頁的源不同時(shí),就發(fā)生了跨域,本文就來介紹一下node.js express cors解決跨域的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-12-12
  • 教你用Node.js與Express建立一個(gè)GraphQL服務(wù)器

    教你用Node.js與Express建立一個(gè)GraphQL服務(wù)器

    GraphQL是一種通過強(qiáng)類型查詢語言構(gòu)建api的新方法,下面這篇文章主要給大家介紹了關(guān)于用Node.js與Express建立一個(gè)GraphQL服務(wù)器的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-12-12
  • nodejs簡(jiǎn)單讀寫excel內(nèi)容的方法示例

    nodejs簡(jiǎn)單讀寫excel內(nèi)容的方法示例

    這篇文章主要介紹了nodejs簡(jiǎn)單讀寫excel內(nèi)容的方法,簡(jiǎn)單分析了nodejs常見的讀寫Excel模塊,并結(jié)合實(shí)例形式分析了nodejs讀寫Excel具體操作技巧,需要的朋友可以參考下
    2018-03-03
  • node.js express捕獲全局異常的三種方法實(shí)例分析

    node.js express捕獲全局異常的三種方法實(shí)例分析

    這篇文章主要介紹了node.js express捕獲全局異常的三種方法,結(jié)合實(shí)例形式簡(jiǎn)單分析了node.js express捕獲全局異常的常見操作方法與使用注意事項(xiàng),需要的朋友可以參考下
    2019-12-12
  • nodejs入門教程六:express模塊用法示例

    nodejs入門教程六:express模塊用法示例

    這篇文章主要介紹了nodejs入門教程之express模塊用法,結(jié)合實(shí)例形式分析了express模塊的功能、創(chuàng)建、路由相關(guān)使用技巧,需要的朋友可以參考下
    2017-04-04
  • Node.js Process對(duì)象詳解

    Node.js Process對(duì)象詳解

    本文詳細(xì)講解了Node.js Process對(duì)象,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-08-08
  • express啟用https使用小記

    express啟用https使用小記

    這篇文章主要介紹了express啟用https使用小記,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-05-05
  • nodejs下載指定版本完整圖文步驟

    nodejs下載指定版本完整圖文步驟

    node.js官方版是一款專業(yè)性非常強(qiáng)的瀏覽輔助工具軟件,這款軟件操作十分的簡(jiǎn)單、功能也是非常的強(qiáng)勁,下面這篇文章主要給大家介紹了關(guān)于nodejs下載指定版本的相關(guān)資料,需要的朋友可以參考下
    2023-12-12
  • NodeJS 創(chuàng)建目錄和文件的方法實(shí)例分析

    NodeJS 創(chuàng)建目錄和文件的方法實(shí)例分析

    這篇文章主要介紹了NodeJS 創(chuàng)建目錄和文件的方法,涉及node.js中fs模塊mkdir、writeFile及目錄判斷existsSync等方法的功能與相關(guān)使用技巧,需要的朋友可以參考下
    2023-04-04

最新評(píng)論