📌

【Express】Redisでクエリ結果をCacheする

2024/02/24に公開

街とその不確かな壁を読み終えました。藤谷です。
データ取得時に下記の流れを作ることでDB負荷を下げるためのコードを解説します。

  1. データ取得のリクエスト実行
  2. repository層でidを元にCacheKeyを作成
  3. CacheKeyでRedisに問い合わせ、KeyとCacheKeyが一致すればそのValueを取得
  4. KeyとCacheKeyが一致しなければ、RDBにクエリ実行
  5. クエリで取得したデータをキャッシュに保存
    同じリソースへのアクセスは次回からキャッシュで処理される

また、本記事では解説しませんが、読む上で押さえておくべきポイントは下記です。

  • CleanArchitectureの構成になっているので、本記事で実行されるRepository層はUseCase層にデータを返す
  • graphqlを採用しているので、apiアクセス時は/graphqlとQueryを使用する

環境

下記のdocker-composeで構築

  • express (ランタイムはbunを使用)
  • postgresql
  • redis
docker-compose.yml
version: "3.9"

services:
  app:
    container_name: bun_app_container
    build:
      context: ./.docker/app
    ports:
      - 8000:8000
    volumes:
      - .:/workspace
      - bun_cache:/bun_dir
    environment:
      - NODE_ENV=developmentstall --frozen-lockfile
    stdin_open: true
    tty: true

  db:
    container_name: bun_db_container
    image: postgres
    ports:
      - 5432:5432
    volumes:
      - bun_db_volume:/var/lib/postgresql/data
    environment:
      POSTGRES_ALLOW_EMPTY_PASSWORD: "yes"
      POSTGRES_ROOT_PASSWORD: ${POSTGRES_ROOT_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DEV_DATABASE: ${POSTGRES_DEV_DATABASE}

  redis:
    container_name: bun_redis_container
    image: redis
    ports:
      - 6379:6379
    volumes:
      - bun_cache:/data

volumes:
  bun_cache:
  bun_db_volume:

Redis初期化

とりあえずRedisの初期化から始めます。

redisClient.ts
import redis from "redis";
import dotenv from "dotenv";
dotenv.config()

export const redisClient = redis.createClient({
    url: process.env.REDIS_URL
  });

(async () => {
  await redisClient.connect();
})();
  
redisClient.on('error', (err) =>
  console.log('Redis Client Error', err)
);

redis.createClientを使用してredisのクライアントを初期化します。
クライアントの初期化だけではアクセスすることができず、redisClient.connect();でコネクションを明示的に通してあげる必要もあります。awaitで待ってあげましょう。

Repository層

repositoryでデータアクセスに関する処理を記述しています。
UseCase層のロジック内で呼び出される前提です。
二つのconsole.logは、redisへのアクセスをわかりやすくする目的です。

find = async(id: number): Promise<Board | null> => {
    const cacheKey = `board:${id}`
    const cacheRecode = await redisClient.get(cacheKey)
    
    if (cacheRecode) {
        console.log('board find cache hit')
        return JSON.parse(cacheRecode)
    }
    
    const board = await prismaContext.board.findUnique({
        where: {
            id: id
        }
    })
    
    if (board) {
        console.log('board find cache add')
        await redisClient.set(cacheKey, JSON.stringify(board))
    }
    
    return board;
}
  1. idで一意のcacheKeyを作成
  2. cachekeyでredisに問い合わせる
    • keyが存在すれば、valueをparseしてUseCase層に返却
    • 存在しなければメソッド内の処理を継続
  3. idでprismaクエリ実行し、返却値をboardに格納
  4. クエリ結果をcacheKeyを使用して、Key-Value形式でredisに格納
  5. board内のデータをUseCase層に返却

一通りのロジックが完成しました。

検証

下記の手順で検証を行います。

  1. 初回アクセス
    • redisに該当keyがないのでクエリ結果を返却
    • redisへのデータ保存が実行され、board find cache addがconsoleで確認できる
  2. 2回目のアクセス
    • redisに該当keyが存在するので、redisからの取得後にparseされたデータを返却
    • board find cache hitがconsoleで確認できる

初回アクセス

query
query GetBoard {
  board(id: 1) {
    id
    content
    user_id
  }
}
response
{
    "data": {
        "board": {
            "id": 1,
            "content": "content01ed",
            "user_id": 1
        }
    }
}
console
Listening on port 8000...
board find cache add

クエリ結果が返却され、redisにデータが保存されています。

2回目のアクセス

Query
query GetBoard {
  board(id: 1) {
    id
    content
    user_id
  }
}
response
{
    "data": {
        "board": {
            "id": 1,
            "content": "content01ed",
            "user_id": 1
        }
    }
}
console
Listening on port 8000...
board find cache hit

redisにcacheKeyの値が確認でき、キャッシュされた値が返却されています。

成功😊


ここまで読んでいただき、ありがとうございました。
Dragonflyとかはredisよりパフォーマンスかなり高いらしいのでどこかで使ってみたいです。
https://www.dragonflydb.io/

Discussion