如何使用 Redis 在 Node.js 中實現快取

作者選擇 /dev/color 作為 寫作捐贈 計劃的受益者之一。

介紹

大多數應用程序依賴於數據,無論它來自數據庫還是 API。從 API 獲取數據會向 API 服務器發送網絡請求,並將數據作為響應返回。這些往返需要時間,可能會增加應用程序對用戶的響應時間。此外,大多數 API 限制它們在特定時間範圍內為應用程序提供服務的請求次數,這個過程稱為 速率限制

為了解決這些問題,您可以將數據緩存,這樣應用程序只需對 API 發送一次請求,所有後續的數據請求都將從緩存中檢索數據。 Redis 是一個將數據存儲在服務器內存中的內存數據庫工具,是緩存數據的流行工具。您可以使用 node-redis 模塊在 Node.js 中連接到 Redis,該模塊為您提供了在 Redis 中檢索和存儲數據的方法。

在這個教程中,您將建立一個Express應用程序,使用axios模組從一個RESTful API檢索數據。接下來,您將修改應用程序,使用node-redis模組將從API檢索的數據存儲在Redis中。之後,您將實現緩存有效期,以便在一定時間後緩存過期。最後,您將使用Express中間件來緩存數據。

先決條件

要遵循本教程,您需要:

步驟 1 — 設置項目

在這一步驟中,您將安裝此項目所需的依賴項並啟動一個 Express 服務器。在本教程中,您將創建一個包含有關不同類型魚類信息的維基。我們將項目稱為 fish_wiki

首先,使用 mkdir 命令創建項目的目錄:

  1. mkdir fish_wiki

進入該目錄:

  1. cd fish_wiki

使用npm命令初始化package.json文件:

  1. npm init -y

使用-y选项可以自动接受所有默认值。

运行npm init命令后,将在您的目录中创建package.json文件,内容如下:

Output
Wrote to /home/your_username/<^>fish_wiki<^/package.json: { "name": "fish_wiki", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

接下来,您将安装以下软件包:

  • express:用于Node.js的Web服务器框架。
  • axios:Node.js的HTTP客户端,有助于进行API调用。
  • node-redis:允许您在Redis中存储和访问数据的Redis客户端。

要同时安装这三个软件包,请输入以下命令:

  1. npm install express axios redis

安装完软件包后,您将创建一个基本的Express服务器。

使用nano或您选择的文本编辑器,创建并打开server.js文件:

  1. nano server.js

在您的server.js文件中,输入以下代码来创建一个Express服务器:

fish_wiki/server.js
const express = require("express");

const app = express();
const port = process.env.PORT || 3000;


app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

首先,将express导入文件中。在第二行中,将app变量设置为express的一个实例,这样您就可以访问诸如getpostlisten等方法。本教程将重点介绍getlisten方法。

在下一行中,您定义并将port变量分配给您希望服务器监听的端口号。如果环境变量文件中没有可用的端口号,则将使用端口3000作为默认值。

最後,使用app變數,呼叫express模組的listen()方法,在端口3000啟動伺服器。

儲存並關閉檔案。

使用node命令執行server.js檔案以啟動伺服器:

  1. node server.js

控制台會記錄類似以下的訊息:

Output
App listening on port 3000

輸出確認伺服器正在運行並準備好在端口3000上處理任何請求。由於Node.js不會在檔案更改時自動重新加載伺服器,現在您將使用CTRL+C停止伺服器,以便您可以在下一步中更新server.js

安裝完相依性並建立Express伺服器後,您將從一個RESTful API擷取資料。

步驟2 — 沒有快取的RESTful API資料擷取

在這一步驟中,您將建立在前一步中的Express伺服器上,從一個RESTful API擷取資料,而不實作快取,展示當資料未儲存在快取中時會發生什麼。

首先,打開您的文字編輯器中的server.js檔案:

  1. nano server.js

接下來,您將從FishWatch API擷取資料。FishWatch API返回有關魚類的資訊。

在您的server.js文件中,請定義一個函數,該函數使用以下突出顯示的代碼請求API數據:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

在第二行,您導入axios模塊。接下來,您定義一個名為fetchApiData()的異步函數,該函數以species作為參數。為了使函數異步執行,您使用async關鍵字為其添加前綴。

在函數內部,您使用API端點調用axios模塊的get()方法,該方法將從API檢索數據,本例中是FishWatch API。由於get()方法實現了一個promise,您使用await關鍵字為其添加前綴以解決該promise。一旦promise解決並從API返回數據,您將調用console.log()方法。console.log()方法將記錄一條消息,說明已向API發送請求。最後,您從API返回數據。

接下來,您將定義一個接受GET請求的Express路由。請在您的server.js文件中,使用以下代碼定義路由:

fish_wiki/server.js
...

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  ...
});

在上述代碼塊中,您調用express模塊的get()方法,該方法僅侦听GET請求。該方法接受兩個參數:

  • /fish/:species:Express將侦听的端點。此端點使用路由参数:species,捕獲URL中該位置輸入的任何內容。
  • getSpeciesData()(尚未定義):當URL與第一個參數中指定的端點匹配時將調用的回調函數。

現在,路由已定義,請指定getSpeciesData回調函數:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
}
app.get("/fish/:species", getSpeciesData);
...

getSpeciesData函數是傳遞給express模塊的get()方法的第二個參數的異步處理函數。 getSpeciesData()函數接受兩個參數:一個請求對象和一個響應對象。請求對象包含有關客戶端的信息,而響應對象包含將從Express發送給客戶端的信息。

接下來,添加突出顯示的代碼以調用fetchApiData()以從API檢索數據,在getSpeciesData()回調函數中:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  results = await fetchApiData(species);
}
...

在函數中,您從req.params對象中提取從端點捕獲的值,然後將其分配給species變量。在下一行中,您定義results變量並將其設置為undefined

之後,使用species變數作為參數調用fetchApiData()函數。因為fetchApiData()函數返回一個Promise,所以在函數調用前加上await語法。當Promise解析後,它返回數據,然後賦值給results變數。

接下來,添加以下突出顯示的代碼以處理運行時錯誤:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

您定義了try/catch塊來處理運行時錯誤。在try塊中,調用fetchApiData()從API檢索數據。
如果遇到錯誤,catch塊將記錄錯誤並返回404狀態碼和“數據不可用”的響應。

大多數API在沒有特定查詢數據時返回404狀態碼,這會自動觸發catch塊的執行。但是,FishWatch API在沒有特定查詢數據時返回一個帶有空數組的200狀態碼。200狀態碼表示請求成功,因此永遠不會觸發catch()塊。

為了觸發catch()塊,您需要檢查數組是否為空,並在if條件評估為true時拋出錯誤。當if條件評估為false時,您可以向客戶端發送包含數據的響應。

為此,添加以下突出顯示的代碼:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  ...
  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

一旦從 API 返回數據,if 陳述將檢查 results 變量是否為空。 如果條件成立,則使用 throw 陳述將拋出一個帶有消息 API 返回了空數組 的自定義錯誤。運行後,執行將切換到 catch 塊,該塊記錄錯誤消息並返回 404 響應。

相反,如果 results 變量有數據,則 if 陳述條件將不滿足。 因此,程序將跳過 if 塊並執行響應對象的 send 方法,該方法將響應發送給客戶端。

send 方法接受一個具有以下屬性的對象:

  • fromCache: 此屬性接受一個值,幫助您知道數據是來自 Redis 緩存還是 API。您現在指定了一個 false 值,因為數據來自 API。

  • data: 此屬性分配了包含從 API 返回的數據的 results 變量。

此時,您的完整代碼將如下所示:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

現在一切都就緒,保存並退出您的文件。

啟動 express 服務器:

  1. node server.js

Fishwatch API接受許多物種,但我們將僅使用red-snapper魚種作為您在本教程中將要測試的端點的路由參數。

現在在您的本地電腦上啟動您喜歡的網絡瀏覽器。導航到http://localhost:3000/fish/red-snapper URL。

注意:如果您正在遠程服務器上跟隨教程,您可以使用端口轉發在瀏覽器中查看應用程序。

在Node.js服務器仍在運行時,在您的本地計算機上打開另一個終端,然後輸入以下命令:

  1. ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip

連接到服務器後,在您的本地計算機的網絡瀏覽器中導航到http://localhost:3000/fish/red-snapper

頁面加載完成後,您應該會看到fromCache設置為false

現在,再刷新URL三次,然後查看您的終端。終端將記錄“每次刷新瀏覽器時發送到API的請求”這句話的次數。

如果您在初始訪問後刷新了URL三次,您的輸出將如下所示:

Output
App listening on port 3000 Request sent to the API Request sent to the API Request sent to the API Request sent to the API

此輸出顯示每次刷新瀏覽器時都會向API服務器發送一個網絡請求。如果您有一個具有1000個用戶且同時訪問相同端點的應用程序,那就是向API發送1000個網絡請求。

當您實現緩存時,對API的請求將只執行一次。所有後續的請求將從緩存中獲取數據,從而提高應用程序的性能。

暫時停止您的Express服務器,請使用CTRL+C

現在您可以從 API 請求數據並將其提供給用戶,您將在 Redis 中緩存從 API 返回的數據。

步驟3 — 使用 Redis 緩存 RESTful API 請求

在這一部分中,您將從 API 緩存數據,以便只有首次訪問您的應用端點時才會從 API 服務器請求數據,所有後續的請求都將從 Redis 緩存中提取數據。

打開server.js文件:

  1. nano server.js

在您的server.js文件中,導入node-redis模塊:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");
...

在同一個文件中,通過添加突出顯示的代碼連接到 Redis 使用node-redis模塊:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  ...
}
...

首先,您將redisClient變量定義為未定義的值。之後,您定義了一個匿名的自我調用的異步函數,這是一個在定義後立即運行的函數。您通過將無名函數定義封裝在括號(async () => {...})中,來定義一個匿名的自我調用的異步函數。為了使其自我調用,您立即在其後添加另一組括號(),最終看起來像(async () => {...})()

在该函数内部,您调用了redis模块的createClient()方法来创建一个redis对象。由于您在调用createClient()方法时没有提供Redis要使用的端口,Redis将使用默认端口6379

您还调用了Node.js的on()方法来在Redis对象上注册事件。on()方法接受两个参数:error和一个回调函数。第一个参数error是当Redis遇到错误时触发的事件。第二个参数是一个回调函数,当error事件被触发时运行。该回调在控制台中记录错误信息。

最后,您调用connect()方法,该方法在默认端口6379上启动与Redis的连接。connect()方法返回一个Promise,因此您使用await语法对其进行解析。

现在您的应用已连接到Redis,您将修改getSpeciesData()回调函数,以在初始访问时将数据存储在Redis中,并在后续的所有请求中从缓存中检索数据。

在您的server.js文件中,添加和更新突出显示的代码:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
     }
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

getSpeciesData 函数中,您使用值为 false 定义了 isCached 变量。在 try 块内,您调用了 node-redis 模块的 get() 方法,并以 species 作为参数。当该方法在 Redis 中找到与 species 变量值匹配的键时,它会返回数据,然后将其赋值给 cacheResults 变量。

接下来,一个 if 语句检查 cacheResults 变量是否有数据。如果条件成立,则将 isCache 变量赋值为 true。在此之后,您调用 JSON 对象的 parse() 方法,并以 cacheResults 作为参数。parse() 方法将 JSON 字符串数据转换为 JavaScript 对象。JSON 解析完成后,您调用 send() 方法,该方法接受一个对象,该对象具有 fromCache 属性,其值设置为 isCached 变量。该方法将响应发送给客户端。

如果 node-redis 模块的 get() 方法在缓存中找不到数据,则将 cacheResults 变量设置为 null。因此,if 语句的结果为 false。当发生这种情况时,执行跳转到 else 块,您在其中调用 fetchApiData() 函数以从 API 获取数据。但是,一旦从 API 返回数据,它就不会保存在 Redis 中。

要将数据存储在 Redis 缓存中,您需要使用 node-redis 模块的 set() 方法将其保存。为此,请添加下面突出显示的行:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

else塊中,一旦數據被獲取,您將調用node-redis模塊的set()方法將數據保存在Redis中,鍵名為species變量的值。

set()方法接受兩個參數,即鍵值對:speciesJSON.stringify(results)

第一個參數species是數據將保存在Redis中的鍵。請記住species被設置為您定義的端點傳遞的值。例如,當您訪問/fish/red-snapper時,species被設置為red-snapper,這將成為Redis中的鍵。

第二個參數JSON.stringify(results)是鍵的值。在第二個參數中,您調用JSONstringify()方法,並將results變量作為參數,其中包含從API返回的數據。該方法將JSON轉換為字符串;這就是為什麼當您使用node-redis模塊的get()方法從緩存中檢索數據時,您之前使用JSON.parse方法調用cacheResults變量作為參數的原因。

您的完整文件現在看起來像下面這樣:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

保存並退出您的文件,然後使用node命令運行server.js

  1. node server.js

一旦服務器啟動,刷新您瀏覽器中的http://localhost:3000/fish/red-snapper

請注意fromCache仍然設置為false

現在再次刷新頁面,以查看這次fromCache設置為true

刷新頁面五次,然後返回終端。您的輸出將類似於以下內容:

Output
App listening on port 3000 Request sent to the API

現在,發送到API的請求在多次URL刷新後僅被記錄一次,這與上一節中每次刷新都記錄消息的情況形成對比。此輸出確認只發送了一個請求到伺服器,並且隨後的數據是從Redis中獲取的。

為了進一步確認數據已存儲在Redis中,請使用以下命令停止您的伺服器:CTRL+C連接到Redis伺服器客戶端:

  1. redis-cli

檢索鍵red-snapper下的數據:

  1. get red-snapper

您的輸出將類似於以下內容(經過簡短編輯):

Output
"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"

該輸出顯示了當您訪問/fish/red-snapper端點時API返回的JSON數據的字符串版本,這證實了API數據已存儲在Redis快取中。

退出Redis伺服器客戶端:

  1. exit

現在您可以緩存來自API的數據,同時也可以設置緩存的有效性。

第4步 — 實現緩存有效性

在緩存數據時,您需要知道數據更改的頻率。有些 API 數據每幾分鐘就會更改;有些是每幾小時、幾周、幾月或幾年更改一次。設置合適的緩存持續時間確保您的應用程序向用戶提供最新的數據。

在此步驟中,您將為需要存儲在 Redis 中的 API 數據設置緩存有效性。當緩存過期時,您的應用程序將向 API 發送請求以檢索最新數據。

您需要查閱 API 文檔以設置緩存的正確過期時間。大多數文檔都會提及數據更新的頻率。但是,有些情況下文檔未提供相關信息,因此您可能需要猜測。檢查各種 API 端點的 last_updated 屬性可以顯示數據更新的頻率。

一旦選擇了緩存持續時間,您需要將其轉換為秒。在本教程中為了演示,您將把緩存持續時間設置為 3 分鐘或 180 秒。這個示例持續時間將使緩存持續時間功能的測試更加容易。

要實現緩存有效期,打開 server.js 文件:

  1. nano server.js

添加下面的代碼:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  ...
})();

async function fetchApiData(species) {
  ...
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

node-redis 模塊的 set() 方法中,傳遞第三個參數為一個具有以下屬性的對象:

  • EX: 接受以秒為單位的緩存持續時間值。
  • NX: 當設置為 true 時,確保 set() 方法只應該設置一個在 Redis 中不存在的鍵。

保存並退出您的文件。

返回 Redis 服务器客户端以测试缓存的有效性:

  1. redis-cli

在 Redis 中删除 red-snapper 键:

  1. del red-snapper

退出 Redis 客户端:

  1. exit

现在,使用 node 命令启动开发服务器:

  1. node server.js

切换回浏览器并刷新 http://localhost:3000/fish/red-snapper URL。在接下来的三分钟内,如果刷新 URL,终端中的输出应与以下输出一致:

Output
App listening on port 3000 Request sent to the API

三分钟过后,在浏览器中刷新 URL。在终端中,你应该看到“Request sent to the API”已经被记录了两次。

Output
App listening on port 3000 Request sent to the API Request sent to the API

这个输出表明缓存已过期,并且再次向 API 发送了请求。

你可以停止 Express 服务器。

现在,你可以设置缓存的有效性,接下来将使用中间件来缓存数据。

第五步 —— 在中间件中缓存数据

在这一步中,你将使用 Express 中间件来缓存数据。中间件是一个可以访问请求对象、响应对象和在执行后应该运行的回调的函数。在使用中间件时,你可以修改请求和响应对象,或者更早地向用户返回响应。

要在應用程式中使用中介軟體進行快取,您需要修改 getSpeciesData() 處理函式,以從 API 擷取資料並將其存儲在 Redis 中。

當您訪問 /fish/:species 端點時,中介軟體函式將首先運行以在快取中搜尋資料;如果找到,它將返回一個回應,而 getSpeciesData 函式將不運行。但是,如果中介軟體在快取中找不到資料,它將調用 getSpeciesData 函式從 API 擷取資料並將其存儲在 Redis 中。

首先,打開您的 server.js:

  1. nano server.js

接下來,刪除突出顯示的程式碼:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

getSpeciesData() 函式中,您刪除了所有查找存儲在 Redis 中的資料的程式碼。您還刪除了 isCached 變數,因為 getSpeciesData() 函式將只從 API 擷取資料並將其存儲在 Redis 中。

一旦程式碼被刪除,將 fromCache 設置為以下所示的 false,這樣 getSpeciesData() 函式看起來像下面的程式碼:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

getSpeciesData() 函式從 API 擷取資料,將其存儲在快取中,並將回應返回給使用者。

接下來,添加以下程式碼以定義用於在 Redis 中快取資料的中介軟體函式:

fish_wiki/server.js
...
async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}

async function getSpeciesData(req, res) {
...
}
...

cacheData() 中介函數接受三個參數:reqresnext。在 try 區塊中,函數檢查 species 變數中的值是否在 Redis 中存儲了數據。如果數據在 Redis 中,則將其返回並設置到 cacheResults 變數中。

接下來,if 陳述式檢查 cacheResults 是否有數據。如果該值為 true,則將數據保存在 results 變數中。然後,中介使用 send() 方法返回一個具有屬性 fromCache 設置為 truedata 設置為 results 變數的對象。

然而,如果 if 陳述式的評估結果為 false,則執行切換到 else 區塊。在 else 區塊內,調用 next(),將控制傳遞給應在其後執行的下一個函數。

為了在調用 next() 時使 cacheData() 中介函數將控制傳遞給 getSpeciesData() 函數,相應地更新 express 模塊的 get() 方法:

fish_wiki/server.js
...
app.get("/fish/:species", cacheData, getSpeciesData);
...

get() 方法現在將 cacheData 作為其第二個參數,這是一個中介,它在 Redis 中查找已緩存的數據並在找到時返回響應。

現在,當您訪問 /fish/:species 端點時,cacheData() 會首先執行。如果資料已經被快取,它將返回響應,並且請求-響應循環在此結束。但是,如果在快取中找不到資料,將調用 getSpeciesData() 從 API 檢索資料,將其存儲在快取中,並返回響應。

完整的文件現在看起來是這樣的:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", cacheData, getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

保存並退出您的文件。

為了正確測試快取,您可以在 Redis 中刪除 red-snapper 金鑰。要做到這一點,進入 Redis 客戶端:

  1. redis-cli

刪除 red-snapper 金鑰:

  1. del red-snapper

退出 Redis 客戶端:

  1. exit

現在,運行 server.js 文件:

  1. node server.js

一旦服務器啟動,返回瀏覽器並再次訪問 http://localhost:3000/fish/red-snapper。多次刷新它。

終端將記錄發送到 API 的請求消息。 cacheData() 中間件將在接下來的三分鐘內為所有請求提供服務。如果您在四分鐘的時間段內隨機刷新 URL,您的輸出將類似於這樣:

Output
App listening on port 3000 Request sent to the API Request sent to the API

該行為與應用程序在前一節中的工作方式一致。

您現在可以使用中間件在 Redis 中緩存資料。

結論

在這篇文章中,您建立了一個應用程序,從 API 中獲取數據並將數據作為響應返回給客戶端。然後,您修改了應用程序,在初始訪問時將 API 響應緩存到 Redis 中,並在以後的所有請求中從緩存中提供數據。您調整了緩存持續時間,使其在一定時間後過期,然後使用中間件來處理緩存數據檢索。

作為下一步,您可以探索 Node Redis 文檔,以了解 node-redis 模塊中可用的功能。您還可以閱讀 AxiosExpress 文檔,深入了解本教程涉及的主題。

為了繼續建立您的 Node.js 技能,請參閱 如何在 Node.js 中編碼系列

Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis