Redisを使用してNode.jsでキャッシュを実装する方法

著者は、/dev/colorWrite for DOnationsプログラムの寄付先として選びました。

紹介

ほとんどのアプリケーションは、データに依存しています。それがデータベースから来たり、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用のウェブサーバーフレームワーク。
  • axios: APIコールの作成に役立つNode.js HTTPクライアント。
  • 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をインポートします。2行目では、app変数をexpressのインスタンスとして設定し、getpostlistenなどのメソッドにアクセスできます。このチュートリアルではgetメソッドとlistenメソッドに焦点を当てます。

次の行では、port変数を定義してポート番号を割り当てます。環境変数ファイルにポート番号が指定されていない場合、デフォルトとしてポート3000が使用されます。

最後に、app変数を使用して、expressモジュールのlisten()メソッドを呼び出し、ポート3000でサーバーを起動します。

ファイルを保存して閉じます。

nodeコマンドを使用してserver.jsファイルを実行してサーバーを起動します。

  1. node server.js

コンソールには次のようなメッセージが表示されます。

Output
App listening on port 3000

この出力は、サーバーが実行され、ポート3000でのリクエストを処理する準備ができていることを確認します。Node.jsはファイルが変更されたときに自動的にサーバーをリロードしないため、次の手順でserver.jsを更新できるように、CTRL+Cを使用してサーバーを停止します。

依存関係をインストールし、Expressサーバーを作成したら、RESTful APIからデータを取得します。

ステップ2 — キャッシュなしでRESTful APIからデータを取得する

このステップでは、キャッシュを実装せずにRESTful APIからデータを取得するExpressサーバーを前のステップから構築し、データがキャッシュに格納されていない場合に何が起こるかを示します。

まず、テキストエディタでserver.jsファイルを開きます。

  1. nano server.js

次に、FishWatchAPIからデータを取得します。 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}`);
});

2行目で、axiosモジュールをインポートします。次に、speciesをパラメータとして取る非同期関数fetchApiData()を定義します。関数を非同期にするために、asyncキーワードを接頭辞として付けます。

関数内で、axiosモジュールのget()メソッドを使用して、データを取得したいAPIエンドポイントを指定します。この例では、FishWatchAPIです。get()メソッドはpromiseを実装しているため、プロミスを解決するためにawaitキーワードを接頭辞として付けます。プロミスが解決され、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リクエストのみをリッスンします。このメソッドは2つの引数を取ります:

  • /fish/:species: Expressがリスニングするエンドポイントです。このエンドポイントは、URLのその位置に入力されたものをキャプチャするルートパラメータ:speciesを取ります。
  • getSpeciesData()(まだ定義されていません):最初の引数で指定されたエンドポイントにURLが一致したときに呼び出されるコールバック関数です。

今、ルートが定義されたので、getSpeciesDataコールバック関数を指定します:

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

getSpeciesData関数は、非同期のハンドラ関数であり、expressモジュールのget()メソッドに第2引数として渡されます。 getSpeciesData()関数は2つの引数を取ります:リクエストオブジェクトとレスポンスオブジェクトです。リクエストオブジェクトにはクライアントに関する情報が含まれており、レスポンスオブジェクトにはExpressからクライアントに送信される情報が含まれています。

次に、getSpeciesData()コールバック関数でfetchApiData()を呼び出してAPIからデータを取得するためのハイライトされたコードを追加します:

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()関数の呼び出しはawait構文で前置されています。なぜなら、それはプロミスを返すからです。 プロミスが解決されると、データが返され、それが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ブロックでは、APIからデータを取得するためにfetchApiData()を呼び出します。
エラーが発生した場合、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 returned an empty arrayを持つカスタムエラーをスローします。実行されると、実行はcatch ブロックに切り替わり、エラーメッセージがログに記録され、404 の応答が返されます。

逆に、results 変数にデータがある場合、if 文の条件は満たされません。その結果、プログラムはif ブロックをスキップし、応答オブジェクトのsend メソッドが実行され、クライアントに応答が送信されます。

send メソッドは、以下のプロパティを持つオブジェクトを取ります:

  • fromCache: プロパティは、データが Redis キャッシュから来ているか API から来ているかを知らせる値を受け入れます。データが API から来るため、false の値が割り当てられます。

  • 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魚の種類のみをルートパラメータとして使用します。

今、お気に入りのWebブラウザをローカルコンピュータで起動してください。次に、http://localhost:3000/fish/red-snapperのURLに移動します。

注意: リモートサーバーでチュートリアルに従っている場合は、ポートフォワーディングを使用してブラウザでアプリを表示できます。

Node.jsサーバーがまだ実行されている場合は、ローカルコンピュータの別のターミナルを開いて、次のコマンドを入力してください:

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

サーバーに接続したら、ローカルマシンのWebブラウザでhttp://localhost:3000/fish/red-snapperに移動します。

ページが読み込まれると、fromCachefalseに設定されているのが見えるはずです。

次に、URLをさらに3回更新し、ターミナルを見てください。ターミナルには、ブラウザを更新した回数だけ「APIに送信されたリクエスト」というメッセージが記録されます。

初回の訪問後にURLを3回更新した場合、出力は次のようになります:

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へのリクエストは1回だけ行われます。その後のすべてのリクエストはキャッシュからデータを取得し、アプリケーションのパフォーマンスが向上します。

現時点では、ExpressサーバーをCTRL+Cで停止してください。

今、APIからデータを要求し、ユーザーに提供できるようになったので、APIから返されたデータをRedisにキャッシュします。

ステップ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");
...

同じファイル内で、ハイライトされたコードを追加してnode-redisモジュールを使用して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 () => {...})()のようになります。

関数内で、redisモジュールのcreateClient()メソッドを呼び出し、redisオブジェクトを作成します。 createClient()メソッドを呼び出す際にRedisが使用するポートを指定しなかったため、Redisはデフォルトポートである6379ポートを使用します。

また、Node.jsのon()メソッドを呼び出して、Redisオブジェクトにイベントを登録します。 on()メソッドは2つの引数を取ります:errorとコールバック関数です。最初の引数errorは、Redisがエラーに遭遇した時にトリガーされるイベントです。2番目の引数は、errorイベントが発生した時に実行されるコールバック関数です。このコールバック関数はエラーをコンソールにログします。

最後に、デフォルトポート6379でRedisとの接続を開始するconnect()メソッドを呼び出します。 connect()メソッドはプロミスを返すため、それを解決するためにawait構文を付けます。

これでアプリケーションがRedisに接続されましたので、初回の訪問時にはRedisにデータを保存し、その後のすべてのリクエストでキャッシュからデータを取得するようにgetSpeciesData()コールバックを変更します。

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関数では、isCached変数をfalseの値で定義します。tryブロック内で、node-redisモジュールのget()メソッドをspeciesを引数として呼び出します。メソッドがRedis内でspecies変数の値に一致するキーを見つけた場合、それはデータを返し、それがcacheResults変数に割り当てられます。

次に、if文がcacheResults変数にデータがあるかどうかをチェックします。条件が満たされると、isCache変数にtrueが割り当てられます。その後、parse()メソッドをJSONオブジェクトの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() メソッドは、2つの引数を取ります。これらはキーと値のペアです:speciesJSON.stringify(results) です。

最初の引数 species は、Redis に保存されるデータのキーです。 species は、定義したエンドポイントに渡された値に設定されます。たとえば、/fish/red-snapper を訪れると、speciesred-snapper に設定されます。これが Redis のキーになります。

2番目の引数 JSON.stringify(results) は、キーの値です。2番目の引数では、results 変数を引数として JSONstringify() メソッドを呼び出しています。これには、API から返されたデータが含まれています。このメソッドは JSON を文字列に変換します。これが、以前に node-redis モジュールの get() メソッドを使用してキャッシュからデータを取得する際に、cacheResults 変数を引数として JSON.parse メソッドを呼び出した理由です。

完全なファイルは以下のようになります:

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 に設定されていることに注目してください:

今度はページを再度更新してください。この時、fromCachetrue に設定されていることを確認してください:

ページを5回更新し、ターミナルに戻ります。出力は次のようになります:

Output
App listening on port 3000 Request sent to the API

今回は、複数回のURL更新の後にAPIに送信されたリクエストが一度だけログに記録されたことが、前のセクションとは対照的になります。この出力は、サーバーに対して1つのリクエストしか送信されず、その後はデータが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()メソッドでは、以下のプロパティを持つオブジェクトを第3引数として渡します:

  • 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を更新します。次の3分間、URLを更新すると、ターミナルの出力は次の出力と一致するはずです:

Output
App listening on port 3000 Request sent to the API

3分が経過したら、ブラウザでURLを更新します。ターミナルでは、「Request sent to the API」というログが2回記録されているはずです:

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

この出力は、キャッシュの有効期限が切れ、APIへのリクエストが再度行われたことを示しています。

Expressサーバーを停止できます。

これでキャッシュの有効期限を設定できるようになったので、次はミドルウェアを使用してデータをキャッシュします。

ステップ5 — ミドルウェアでデータをキャッシュする

このステップでは、Expressミドルウェアを使用してデータをキャッシュします。ミドルウェアは、リクエストオブジェクト、レスポンスオブジェクト、および実行後に実行するコールバックにアクセスできる関数です。ミドルウェアの後に実行される関数もリクエストおよびレスポンスオブジェクトにアクセスできます。ミドルウェアを使用すると、リクエストおよびレスポンスオブジェクトを変更したり、ユーザーに対して早くレスポンスを返したりすることができます。

アプリケーションでミドルウェアを使用してキャッシュするには、getSpeciesData() ハンドラ関数を変更して、API からデータを取得し Redis に保存します。Redis でデータを検索するすべてのコードを cacheData ミドルウェア関数に移動します。

/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 に保存されているデータを検索するすべてのコードを削除します。また、getSpeciesData() 関数は API からデータを取得し Redis に保存するだけなので、isCached 変数も削除します。

コードが削除されたら、以下のように fromCachefalse に設定します。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()ミドルウェア関数は3つの引数、reqres、およびnextを受け取ります。 tryブロックでは、関数はspecies変数の値がそのキーでRedisに格納されているデータをチェックします。データがRedisに存在する場合、それは返され、cacheResults変数に設定されます。

次に、ifステートメントがcacheResultsにデータがあるかどうかをチェックします。データがtrueに評価される場合、results変数に保存されます。その後、ミドルウェアはsend()メソッドを使用して、fromCachetrueに設定され、dataresults変数に設定されたプロパティを持つオブジェクトを返します。

ただし、ifステートメントがfalseに評価される場合、実行はelseブロックに切り替わります。 elseブロック内でnext()を呼び出し、次に実行されるべき次の関数に制御を渡します。

next()が呼び出されたときにcacheData()ミドルウェアがgetSpeciesData()関数に制御を渡すようにするには、expressモジュールのget()メソッドを更新してください:

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

get()メソッドは今、2番目の引数として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()ミドルウェアは、次の3分間すべてのリクエストを処理します。4分間の間、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