📌
【Express】Redisでクエリ結果をCacheする
街とその不確かな壁を読み終えました。藤谷です。
データ取得時に下記の流れを作ることでDB負荷を下げるためのコードを解説します。
- データ取得のリクエスト実行
- repository層でidを元にCacheKeyを作成
- CacheKeyでRedisに問い合わせ、KeyとCacheKeyが一致すればそのValueを取得
- KeyとCacheKeyが一致しなければ、RDBにクエリ実行
- クエリで取得したデータをキャッシュに保存
同じリソースへのアクセスは次回からキャッシュで処理される
また、本記事では解説しませんが、読む上で押さえておくべきポイントは下記です。
- 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;
}
- idで一意のcacheKeyを作成
- cachekeyでredisに問い合わせる
- keyが存在すれば、valueをparseしてUseCase層に返却
- 存在しなければメソッド内の処理を継続
- idでprismaクエリ実行し、返却値を
board
に格納 - クエリ結果をcacheKeyを使用して、Key-Value形式でredisに格納
- board内のデータをUseCase層に返却
一通りのロジックが完成しました。
検証
下記の手順で検証を行います。
- 初回アクセス
- redisに該当keyがないのでクエリ結果を返却
- redisへのデータ保存が実行され、
board find cache add
がconsoleで確認できる
- 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よりパフォーマンスかなり高いらしいのでどこかで使ってみたいです。
Discussion