0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Redisキャッシュ入門:Node.jsでAPIの応答速度を改善する実践方法

0
Posted at

Webアプリケーションの利用者が増えると、APIの応答速度やデータベースへの負荷が重要な課題になります。

特に、同じデータを何度も取得するAPIでは、毎回データベースへアクセスすることが必ずしも効率的とは限りません。

このような場合に役立つ技術が Redisを利用したキャッシュ です。

本記事では、Redisの基本的な仕組みから、Node.jsとExpressを使った実装、キャッシュの有効期限、データ更新時の注意点までを紹介します。

1. キャッシュとは?

キャッシュとは、取得や計算に時間がかかるデータを一時的に保存し、次回以降の処理で再利用する仕組みです。

例えば、記事一覧を取得するAPIを考えます。

キャッシュを利用しない場合:

Client
   |
   v
API Server
   |
   v
Database
   |
   v
API Response

毎回データベースへアクセスするため、アクセス数が増えると負荷が高くなる可能性があります。

一方、キャッシュを利用する場合は次のようになります。

Client
   |
   v
API Server
   |
   v
Redis Cache
   |
   +-- Cache Hit --> Response
   |
   +-- Cache Miss --> Database
                         |
                         v
                    Cache Update
                         |
                         v
                      Response

キャッシュにデータが存在する場合は、データベースへのアクセスを省略できます。

ただし、キャッシュの導入が常に高速化につながるとは限りません。データの更新頻度やキャッシュヒット率によって効果は異なります。

2. Redisとは?

Redisは、主にメモリ上でデータを扱うデータストアです。

Web開発では、次のような用途に利用されています。

  • APIレスポンスのキャッシュ
  • セッション情報の保存
  • レート制限
  • 一時的な状態管理
  • ランキングなどのデータ管理

Redisは文字列だけでなく、ハッシュ、リスト、セットなど複数のデータ構造をサポートしています。

今回は、JSONデータを文字列として保存するシンプルなキャッシュを実装します。

3. 開発環境を準備する

今回使用する技術は以下のとおりです。

  • Node.js 20以降
  • Express
  • Redis
  • node-redis

まず、プロジェクトを作成します。

mkdir redis-cache-demo
cd redis-cache-demo
npm init -y
npm install express redis

次に、Dockerを利用してRedisを起動します。

docker run --name redis-demo \
  -p 127.0.0.1:6379:6379 \
  -d redis:7

この例では、Redisのポートをローカルホストに限定しています。

Redisをインターネットへ直接公開しないよう注意しましょう。

4. Node.jsからRedisへ接続する

server.jsを作成します。

const express = require("express");
const { createClient } = require("redis");

const app = express();

const redis = createClient({
  url: process.env.REDIS_URL ||
       "redis://127.0.0.1:6379"
});

redis.on("error", (error) => {
  console.error("Redis error:", error.message);
});

async function startServer() {
  await redis.connect();

  app.get("/health", (req, res) => {
    res.json({ status: "ok" });
  });

  app.listen(3000, () => {
    console.log("Server running on port 3000");
  });
}

startServer().catch(console.error);

実行方法:

node server.js

この時点では、Redisへの接続と基本的なHTTPサーバーの起動を確認できます。

本番環境では、接続失敗時の再試行、タイムアウト、正常終了処理なども考慮する必要があります。

5. Cache-Asideパターンを理解する

キャッシュ設計でよく利用される方式の一つが Cache-Aside です。

基本的な処理は次のとおりです。

  1. Redisにデータがあるか確認する
  2. 存在すればキャッシュから返す
  3. 存在しなければデータベースから取得する
  4. 取得したデータをRedisに保存する
  5. クライアントへ結果を返す

この方式は、既存のAPIへ比較的段階的に導入しやすいことが特徴です。

6. APIレスポンスをキャッシュする

今回は、データベースの代わりにサンプルデータを返す関数を使用します。

以下のコードを、先ほどのstartServer()内のapp.listen()より前に追加します。

async function getArticlesFromDatabase() {
  return [
    {
      id: 1,
      title: "Node.js API Design",
      category: "Backend"
    },
    {
      id: 2,
      title: "Redis Cache Basics",
      category: "Performance"
    },
    {
      id: 3,
      title: "TypeScript Guide",
      category: "Development"
    }
  ];
}

app.get("/api/articles", async (req, res) => {
  const cacheKey = "articles:list:v1";

  try {
    const cached = await redis.get(cacheKey);

    if (cached !== null) {
      return res.json({
        source: "cache",
        data: JSON.parse(cached)
      });
    }

    const articles = await getArticlesFromDatabase();

    await redis.set(
      cacheKey,
      JSON.stringify(articles),
      { EX: 60 }
    );

    return res.json({
      source: "database",
      data: articles
    });
  } catch (error) {
    console.error("Cache error:", error.message);

    return res.status(500).json({
      error: "Unable to load articles"
    });
  }
});

初回アクセスでは、キャッシュが存在しないため、データ取得処理が実行されます。

2回目以降、キャッシュが有効な間はRedisに保存されたデータが返されます。

なお、この例ではRedisの障害時にエラーを返します。実際のサービスでは、要件に応じてデータベースへフォールバックする設計も検討できます。

7. TTLでキャッシュの有効期限を設定する

キャッシュを長期間保存すると、古い情報が表示される可能性があります。

そこで利用するのがTTL(Time To Live)です。

await redis.set(
  "example:key",
  JSON.stringify({ message: "Hello" }),
  { EX: 120 }
);

EX: 120は、有効期限を120秒に設定することを意味します。

TTLは、データの更新頻度や求められる鮮度に応じて決定します。

データの種類 TTLの考え方
更新頻度が低い記事情報 比較的長く設定できる
頻繁に変わる一覧データ 短めの設定を検討する
個人情報や権限情報 キャッシュの必要性から慎重に判断する
正確な最新状態が必要なデータ キャッシュを利用しない選択肢もある

TTLに唯一の正解はありません。

実際の更新頻度とユーザー要件を確認しながら調整することが重要です。

8. データ更新時にキャッシュを削除する

キャッシュを利用する場合、データ更新後に古い情報が残る問題があります。

例えば、記事を更新した後にキャッシュを削除する方法があります。

async function invalidateArticlesCache() {
  await redis.del("articles:list:v1");
}

データ更新処理が成功した後に、この関数を実行します。

async function updateArticle(article) {
  await saveArticleToDatabase(article);

  await invalidateArticlesCache();
}

saveArticleToDatabase()は、実際のデータベース更新処理を表す関数です。

ただし、データベース更新とキャッシュ削除は別々の操作です。

更新後にキャッシュ削除が失敗する場合や、同時リクエストによって古いデータが再び保存される場合もあるため、厳密な整合性が必要な用途では追加の設計が必要になります。

9. キャッシュキーの設計

キャッシュキーは、どのデータを保存しているか分かる形式にすると管理しやすくなります。

例えば:

articles:list:v1
articles:detail:123
categories:list:v1
public:settings:v1

キーを設計する際には、次の点を意識します。

  • データの種類を明確にする
  • 必要に応じてIDを含める
  • バージョンを管理する
  • キーの衝突を避ける
  • ユーザーごとのデータを適切に分離する

特に、ユーザー固有のデータをキャッシュする場合は、異なるユーザー間で情報が共有されないよう注意が必要です。

10. キャッシュスタンピードへの対策

人気のあるデータのキャッシュが期限切れになると、多数のリクエストが同時にデータベースへアクセスすることがあります。

この現象は、Cache Stampedeと呼ばれます。

対策として、以下の方法が考えられます。

  • TTLにランダムな変動を加える
  • ロックを利用して再取得処理を制御する
  • 古いキャッシュを一時的に返す
  • 人気データを事前に更新する

例えば、TTLにランダムな変動を加える方法は次のとおりです。

const baseTTL = 60;
const jitter = Math.floor(Math.random() * 20);

await redis.set(
  cacheKey,
  JSON.stringify(data),
  { EX: baseTTL + jitter }
);

複数のキャッシュキーが同時に期限切れになる可能性を減らすために利用できます。

ただし、単一の人気キーに対する同時アクセスを完全に防ぐものではありません。

11. デジタルプラットフォームへの応用

記事一覧、公開コンテンツ、カテゴリ情報などを提供するWebサービスでは、キャッシュによってデータ取得処理の負荷を軽減できる場合があります。

例えば、WOW88 のようなデジタルプラットフォームを技術的な設計例として考える場合、頻繁に参照される公開コンテンツ情報をRedisへ一時保存する構成が考えられます。

ただし、キャッシュ対象は慎重に選択する必要があります。

公開情報とユーザー固有の情報を区別し、データの鮮度、アクセス権限、更新頻度を考慮することが重要です。

また、キャッシュ導入による効果は実測データで評価する必要があります。

12. キャッシュの効果を測定する

キャッシュを導入した後は、次の指標を確認します。

指標 確認する内容
Cache Hit Rate キャッシュから取得できた割合
API Latency APIの応答時間
Database Queries データベースへの問い合わせ数
Redis Memory Usage Redisのメモリ使用量
Error Rate エラーの発生割合

例えば、キャッシュヒット率は次のように計算できます。

Cache Hit Rate =
Cache Hits / (Cache Hits + Cache Misses)

キャッシュヒット率が高くても、API全体の応答時間が改善していない場合は、別の処理がボトルネックになっている可能性があります。

そのため、複数の指標を組み合わせて評価することが重要です。

13. 本番環境への導入チェックリスト

Redisキャッシュを本番環境へ導入する際には、次の項目を確認しましょう。

□ キャッシュ対象のデータを明確にしている
□ 適切なTTLを設定している
□ キャッシュキーの命名規則を統一している
□ データ更新時の無効化処理を設計している
□ Redisへのアクセスを適切に制限している
□ ユーザー固有データの分離を確認している
□ Redis障害時の動作を設計している
□ キャッシュヒット率を監視している
□ API応答時間を測定している
□ キャッシュスタンピードを考慮している

まとめ

Redisキャッシュは、Webアプリケーションのパフォーマンス改善に役立つ技術の一つです。

今回紹介した重要なポイントは次のとおりです。

  • キャッシュの基本的な仕組みを理解する
  • RedisとNode.jsを接続する
  • Cache-Asideパターンを実装する
  • TTLでデータの有効期限を管理する
  • データ更新時のキャッシュ無効化を設計する
  • キャッシュスタンピードへの対策を検討する
  • 応答時間やヒット率を継続的に測定する

キャッシュは単にデータを保存する仕組みではなく、データの鮮度、整合性、障害時の動作を含めて設計する必要があります。

まずは更新頻度の低い公開データから導入し、実測結果を確認しながら適用範囲を広げることが、安定したWebアプリケーションを構築するための現実的な方法です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?