著者は、/dev/colorをWrite for DOnationsプログラムの寄付先として選びました。
紹介
ほとんどのアプリケーションは、データに依存しています。それがデータベースから来たり、APIから来たりする場合でもです。APIからデータを取得すると、ネットワークリクエストがAPIサーバーに送信され、データが応答として返されます。これらの往復には時間がかかり、アプリケーションの応答時間がユーザーに対して増加する可能性があります。さらに、ほとんどのAPIは、特定の時間枠内にアプリケーションに提供できるリクエストの数を制限します。このプロセスはレート制限として知られています。
これらの問題を回避するために、データをキャッシュすることができます。これにより、アプリケーションはAPIに対して単一のリクエストを行い、その後のすべてのデータリクエストはキャッシュからデータを取得します。Redisは、サーバーのメモリにデータを保存するインメモリデータベースであり、データをキャッシュするための人気のあるツールです。node-redis
モジュールを使用してNode.jsからRedisに接続することができ、これによりRedisでデータを取得および保存するためのメソッドが提供されます。
このチュートリアルでは、Expressアプリケーションを構築し、axios
モジュールを使用してRESTful APIからデータを取得します。次に、node-redis
モジュールを使用してAPIから取得したデータをRedisに格納するアプリを変更します。その後、キャッシュの有効期限を実装して、一定時間が経過した後にキャッシュが期限切れになるようにします。最後に、Expressミドルウェアを使用してデータをキャッシュします。
前提条件
このチュートリアルを実行するには、次のものが必要です:
-
サーバーにNode.js環境がセットアップされていること。Ubuntu 22.04を使用している場合は、Ubuntu 22.04にNode.jsをインストールする方法のオプション3に従って最新バージョンのNode.jsとnpmをインストールします。他のオペレーティングシステムの場合は、Node.jsのインストールとローカル開発環境の作成方法シリーズを参照してください。
-
サーバーにRedisがインストールされています。Ubuntu 22.04をご利用の場合は、Ubuntu 22.04にRedisをインストールしてセキュアにする方法の手順1および2に従ってください。他のオペレーティングシステムをご利用の場合は、Redisのインストールとセキュリティの確保方法をご覧ください。
-
非同期プログラミングの知識。JavaScript におけるイベントループ、コールバック、プロミス、および Async/Await の理解を参照してください。
-
Express ウェブフレームワークの基本的な知識。Node.js と Express の始め方を参照してください。
ステップ 1 — プロジェクトの設定
このステップでは、このプロジェクトに必要な依存関係をインストールし、Express サーバーを開始します。このチュートリアルでは、さまざまな種類の魚に関する情報を含むウィキを作成します。プロジェクト名は fish_wiki
とします。
まず、mkdir
コマンドを使用してプロジェクト用のディレクトリを作成します。
ディレクトリに移動します:
npm
コマンドを使用して、package.json
ファイルを初期化します。
-y
オプションはすべてのデフォルトを自動的に受け入れます。
npm init
コマンドを実行すると、次の内容でpackage.json
ファイルがディレクトリに作成されます。
OutputWrote 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クライアント。
これらのパッケージを一括でインストールするには、次のコマンドを入力します。
パッケージをインストールした後、基本的なExpressサーバーを作成します。
nano
または選択したテキストエディターを使用して、server.js
ファイルを作成して開きます。
server.js
ファイルに、Expressサーバーを作成するための以下のコードを入力します。
まず、ファイルにexpress
をインポートします。2行目では、app
変数をexpress
のインスタンスとして設定し、get
、post
、listen
などのメソッドにアクセスできます。このチュートリアルではget
メソッドとlisten
メソッドに焦点を当てます。
次の行では、port
変数を定義してポート番号を割り当てます。環境変数ファイルにポート番号が指定されていない場合、デフォルトとしてポート3000
が使用されます。
最後に、app
変数を使用して、express
モジュールのlisten()
メソッドを呼び出し、ポート3000
でサーバーを起動します。
ファイルを保存して閉じます。
node
コマンドを使用してserver.js
ファイルを実行してサーバーを起動します。
コンソールには次のようなメッセージが表示されます。
OutputApp listening on port 3000
この出力は、サーバーが実行され、ポート3000
でのリクエストを処理する準備ができていることを確認します。Node.jsはファイルが変更されたときに自動的にサーバーをリロードしないため、次の手順でserver.js
を更新できるように、CTRL+C
を使用してサーバーを停止します。
依存関係をインストールし、Expressサーバーを作成したら、RESTful APIからデータを取得します。
ステップ2 — キャッシュなしでRESTful APIからデータを取得する
このステップでは、キャッシュを実装せずにRESTful APIからデータを取得するExpressサーバーを前のステップから構築し、データがキャッシュに格納されていない場合に何が起こるかを示します。
まず、テキストエディタでserver.js
ファイルを開きます。
次に、FishWatchAPIからデータを取得します。 FishWatch APIは魚の種に関する情報を返します。
server.js
ファイルで、次の強調されたコードを使用してAPIデータをリクエストする関数を定義します:
2行目で、axios
モジュールをインポートします。次に、species
をパラメータとして取る非同期関数fetchApiData()
を定義します。関数を非同期にするために、async
キーワードを接頭辞として付けます。
関数内で、axios
モジュールのget()
メソッドを使用して、データを取得したいAPIエンドポイントを指定します。この例では、FishWatchAPIです。get()
メソッドはpromiseを実装しているため、プロミスを解決するためにawait
キーワードを接頭辞として付けます。プロミスが解決され、APIからデータが返されると、console.log()
メソッドを呼び出します。console.log()
メソッドは、APIにリクエストが送信されたことを示すメッセージをログに記録します。最後に、APIからのデータを返します。
次に、GET
リクエストを受け入れるExpressルートを定義します。 server.js
ファイルで、次のコードでルートを定義します:
前述のコードブロックでは、express
モジュールのget()
メソッドを呼び出し、GET
リクエストのみをリッスンします。このメソッドは2つの引数を取ります:
/fish/:species
: Expressがリスニングするエンドポイントです。このエンドポイントは、URLのその位置に入力されたものをキャプチャするルートパラメータ:species
を取ります。getSpeciesData()
(まだ定義されていません):最初の引数で指定されたエンドポイントにURLが一致したときに呼び出されるコールバック関数です。
今、ルートが定義されたので、getSpeciesData
コールバック関数を指定します:
getSpeciesData
関数は、非同期のハンドラ関数であり、express
モジュールのget()
メソッドに第2引数として渡されます。 getSpeciesData()
関数は2つの引数を取ります:リクエストオブジェクトとレスポンスオブジェクトです。リクエストオブジェクトにはクライアントに関する情報が含まれており、レスポンスオブジェクトにはExpressからクライアントに送信される情報が含まれています。
次に、getSpeciesData()
コールバック関数でfetchApiData()
を呼び出してAPIからデータを取得するためのハイライトされたコードを追加します:
この関数では、req.params
オブジェクトに格納されたエンドポイントからキャプチャされた値を抽出し、species
変数に割り当てます。次の行で、results
変数を定義し、undefined
に設定します。
その後、species
変数を引数としてfetchApiData()
関数を呼び出します。 fetchApiData()
関数の呼び出しはawait
構文で前置されています。なぜなら、それはプロミスを返すからです。 プロミスが解決されると、データが返され、それがresults
変数に割り当てられます。
次に、ランタイムエラーを処理するために、以下のコードを追加します:
ランタイムエラーを処理するためにtry/catch
ブロックを定義します。 try
ブロックでは、APIからデータを取得するためにfetchApiData()
を呼び出します。
エラーが発生した場合、catch
ブロックはエラーをログに記録し、「データが利用できません」という応答とともに404
ステータスコードを返します。
ほとんどのAPIは、特定のクエリに対するデータがない場合に404ステータスコードを返します。これにより、自動的にcatch
ブロックが実行されます。 ただし、FishWatch APIは、特定のクエリに対してデータがない場合に空の配列を含む200ステータスコードを返します。 200ステータスコードは、リクエストが成功したことを意味するため、catch()
ブロックは実行されません。
catch()
ブロックをトリガーするには、配列が空であるかどうかをチェックし、if
条件がtrueの場合にエラーをスローする必要があります。 if
条件がfalseに評価されると、データを含むレスポンスをクライアントに送信できます。
これを行うには、次のコードを追加します:
API からデータが返されると、if
文がresults
変数が空かどうかをチェックします。条件が満たされる場合、throw
文を使用して、メッセージAPI returned an empty array
を持つカスタムエラーをスローします。実行されると、実行はcatch
ブロックに切り替わり、エラーメッセージがログに記録され、404 の応答が返されます。
逆に、results
変数にデータがある場合、if
文の条件は満たされません。その結果、プログラムはif
ブロックをスキップし、応答オブジェクトのsend
メソッドが実行され、クライアントに応答が送信されます。
send
メソッドは、以下のプロパティを持つオブジェクトを取ります:
-
fromCache
: プロパティは、データが Redis キャッシュから来ているか API から来ているかを知らせる値を受け入れます。データが API から来るため、false
の値が割り当てられます。 -
data
: プロパティは、API から返されたデータを含むresults
変数に割り当てられます。
この時点で、完全なコードは次のようになります:
すべてが準備できたら、ファイルを保存して終了します。
Express サーバーを起動します。
Fishwatch APIは多くの種類の魚を受け入れますが、このチュートリアル全体でテストするエンドポイントではred-snapper
魚の種類のみをルートパラメータとして使用します。
今、お気に入りのWebブラウザをローカルコンピュータで起動してください。次に、http://localhost:3000/fish/red-snapper
のURLに移動します。
注意: リモートサーバーでチュートリアルに従っている場合は、ポートフォワーディングを使用してブラウザでアプリを表示できます。
Node.jsサーバーがまだ実行されている場合は、ローカルコンピュータの別のターミナルを開いて、次のコマンドを入力してください:
サーバーに接続したら、ローカルマシンのWebブラウザでhttp://localhost:3000/fish/red-snapper
に移動します。
ページが読み込まれると、fromCache
がfalse
に設定されているのが見えるはずです。
次に、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
ファイルを開きます:
server.js
ファイルで、node-redis
モジュールをインポートします:
同じファイル内で、ハイライトされたコードを追加してnode-redis
モジュールを使用してRedisに接続します:
まず、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
ファイルに、以下のコードを追加および更新してください:
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()
メソッドを使用する必要があります。そのためには、以下の行を追加してください:
else
ブロック内で、データを取得したら、node-redis
モジュールの set()
メソッドを呼び出して、データを Redis に保存します。保存するキーの名前は species
変数の値になります。
set()
メソッドは、2つの引数を取ります。これらはキーと値のペアです:species
と JSON.stringify(results)
です。
最初の引数 species
は、Redis に保存されるデータのキーです。 species
は、定義したエンドポイントに渡された値に設定されます。たとえば、/fish/red-snapper
を訪れると、species
は red-snapper
に設定されます。これが Redis のキーになります。
2番目の引数 JSON.stringify(results)
は、キーの値です。2番目の引数では、results
変数を引数として JSON
の stringify()
メソッドを呼び出しています。これには、API から返されたデータが含まれています。このメソッドは JSON を文字列に変換します。これが、以前に node-redis
モジュールの get()
メソッドを使用してキャッシュからデータを取得する際に、cacheResults
変数を引数として JSON.parse
メソッドを呼び出した理由です。
完全なファイルは以下のようになります:
ファイルを保存して終了し、node
コマンドを使用して server.js
を実行します:
サーバーが起動したら、ブラウザで http://localhost:3000/fish/red-snapper
をリフレッシュしてください。
fromCache
がまだ false
に設定されていることに注目してください:
今度はページを再度更新してください。この時、fromCache
が true
に設定されていることを確認してください:
ページを5回更新し、ターミナルに戻ります。出力は次のようになります:
OutputApp listening on port 3000
Request sent to the API
今回は、複数回のURL更新の後にAPIに送信されたリクエスト
が一度だけログに記録されたことが、前のセクションとは対照的になります。この出力は、サーバーに対して1つのリクエストしか送信されず、その後はデータがRedisから取得されていることを確認します。
データがRedisに格納されていることをさらに確認するために、CTRL+C
を使用してサーバーを停止します。次のコマンドでRedisサーバークライアントに接続します:
キーred-snapper
のデータを取得します:
出力は以下のようになります(簡潔に編集されています):
Output"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"
この出力は、/fish/red-snapper
エンドポイントにアクセスしたときにAPIが返すJSONデータの文字列化バージョンを示しており、APIデータがRedisキャッシュに格納されていることを確認しています。
Redisサーバークライアントから終了します:
APIからデータをキャッシュする方法がわかったので、キャッシュの有効期限も設定できます。
ステップ4 — キャッシュの有効期限の実装
データをキャッシュする際には、データがどのくらい頻繁に変更されるかを把握する必要があります。一部のAPIデータは数分ごとに変更される場合もありますし、数時間、数週間、数ヶ月、または数年ごとに変更される場合もあります。適切なキャッシュ期間を設定することで、アプリケーションがユーザーに最新のデータを提供することができます。
このステップでは、Redisに格納する必要があるAPIデータのキャッシュ有効期限を設定します。キャッシュが期限切れになると、アプリケーションはAPIにリクエストを送信して最新のデータを取得します。
キャッシュの有効期間を設定するためには、APIのドキュメントを参照する必要があります。ほとんどのドキュメントでは、データがどのくらい頻繁に更新されるかが記載されています。ただし、ドキュメントに情報が提供されていない場合もありますので、推測する必要があります。さまざまなAPIエンドポイントのlast_updated
プロパティを確認すると、データがどのくらい頻繁に更新されるかがわかります。
キャッシュの期間を選択したら、それを秒単位に変換する必要があります。このチュートリアルでは、キャッシュの期間を3分、つまり180秒に設定します。このサンプル期間を使用することで、キャッシュの期間機能をテストしやすくなります。
キャッシュの有効期間を実装するには、server.js
ファイルを開きます:
以下のコードを追加します:
node-redis
モジュールのset()
メソッドでは、以下のプロパティを持つオブジェクトを第3引数として渡します:
EX
: キャッシュの期間を秒単位で受け入れます。NX
:true
に設定すると、set()
メソッドはRedisに既に存在しないキーのみを設定します。
ファイルを保存して閉じます。
Redisサーバークライアントに戻り、キャッシュの有効性をテストします:
Redisでred-snapper
キーを削除します:
Redisクライアントを終了します:
次に、node
コマンドで開発サーバーを起動します:
ブラウザに戻り、http://localhost:3000/fish/red-snapper
のURLを更新します。次の3分間、URLを更新すると、ターミナルの出力は次の出力と一致するはずです:
OutputApp listening on port 3000
Request sent to the API
3分が経過したら、ブラウザでURLを更新します。ターミナルでは、「Request sent to the API」というログが2回記録されているはずです:
OutputApp 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
を開きます。
次に、ハイライトされたコードを削除します。
getSpeciesData()
関数では、Redis に保存されているデータを検索するすべてのコードを削除します。また、getSpeciesData()
関数は API からデータを取得し Redis に保存するだけなので、isCached
変数も削除します。
コードが削除されたら、以下のように fromCache
を false
に設定します。getSpeciesData()
関数は次のようになります。
getSpeciesData()
関数は API からデータを取得し、キャッシュに保存してユーザーにレスポンスを返します。
次に、Redis にデータをキャッシュするためのミドルウェア関数を定義する次のコードを追加します。
cacheData()
ミドルウェア関数は3つの引数、req
、res
、およびnext
を受け取ります。 try
ブロックでは、関数はspecies
変数の値がそのキーでRedisに格納されているデータをチェックします。データがRedisに存在する場合、それは返され、cacheResults
変数に設定されます。
次に、if
ステートメントがcacheResults
にデータがあるかどうかをチェックします。データがtrueに評価される場合、results
変数に保存されます。その後、ミドルウェアはsend()
メソッドを使用して、fromCache
がtrue
に設定され、data
がresults
変数に設定されたプロパティを持つオブジェクトを返します。
ただし、if
ステートメントがfalseに評価される場合、実行はelse
ブロックに切り替わります。 else
ブロック内でnext()
を呼び出し、次に実行されるべき次の関数に制御を渡します。
next()
が呼び出されたときにcacheData()
ミドルウェアがgetSpeciesData()
関数に制御を渡すようにするには、express
モジュールのget()
メソッドを更新してください:
get()
メソッドは今、2番目の引数としてcacheData
を取ります。これはRedisでキャッシュされたデータを検索し、見つかった場合に応答を返すミドルウェアです。
今、/fish/:species
エンドポイントを訪れると、cacheData()
が最初に実行されます。データがキャッシュされている場合、レスポンスが返され、リクエスト-レスポンスサイクルはここで終了します。ただし、キャッシュにデータが見つからない場合、getSpeciesData()
が呼び出され、APIからデータを取得し、それをキャッシュに保存し、レスポンスを返します。
完全なファイルは以下のようになります:
ファイルを保存して終了します。
キャッシュを適切にテストするには、Redisのred-snapper
キーを削除できます。これを行うには、Redisクライアントに移動します:
red-snapper
キーを削除します:
Redisクライアントを終了します:
今、server.js
ファイルを実行します:
サーバーが起動したら、ブラウザに戻り、再びhttp://localhost:3000/fish/red-snapper
を訪れます。複数回更新してください。
ターミナルには、APIにリクエストが送信されたことを示すメッセージが記録されます。cacheData()
ミドルウェアは、次の3分間すべてのリクエストを処理します。4分間の間、URLをランダムにリフレッシュした場合、出力は次のようになります:
OutputApp listening on port 3000
Request sent to the API
Request sent to the API
この動作は、前のセクションでアプリケーションが動作していた方法と一致しています。
今や、ミドルウェアを使用してRedisにデータをキャッシュできます。
結論
この記事では、APIからデータを取得し、そのデータをクライアントへの応答として返すアプリケーションを構築しました。その後、アプリを変更して初回の訪問時にAPI応答をRedisにキャッシュし、以降のすべてのリクエストでキャッシュからデータを提供するようにしました。キャッシュの期間を一定の時間が経過した後に期限切れにするように変更し、その後、ミドルウェアを使用してキャッシュデータの取得を処理しました。
次のステップとして、このチュートリアルでカバーされているトピックについてより深く学ぶために、Node Redisのドキュメントを調べることができます。また、node-redis
モジュールの機能について詳しく知るために、AxiosやExpressのドキュメントも参照してください。
Node.jsのスキル構築を続けるには、Node.jsでのコーディングシリーズをご覧ください。
Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis