🐁

Go × Redis で多層キャッシュが手軽に実現できる rueidis を触ってみる

2024/10/06に公開

はじめに

Redis にはクライアント側でもキャッシュができる ( Client-side caching ) 機能があります。

https://redis.io/docs/latest/develop/reference/client-side-caching/

この Client-side caching を Go では redis/rueidis というライブラリを使うことで簡単に実現できます。

https://github.com/redis/rueidis

この rueidis を使えば多層キャッシュが簡単に実装できるということをインメモリキャッシュを使って実装する場合と比較してご紹介します。

多層キャッシュ/クライアントサイドキャッシュとは

イメージとしては以下のようなものになります。

アプリケーション側のキャッシュを取得し、存在しなければ Redis のキャッシュを取得しに行くような実装になります。
Redis の公式ドキュメントにも説明が記載されているので、これを参考に説明していきます。

クライアントサイドキャッシュはハイパフォーマンスなサービスを実現するために使用する技術です。
Redis のキャッシュを利用するのではなく、クライアントサイド(= アプリケーション)のキャッシュを利用します。
クライアントサイドキャッシュを利用することによるメリットは以下の2点があります。

  • 低いレイテンシーでデータを利用できる。
  • Redis への負荷を軽減できる

しかし、クライアントサイドキャッシュを利用する場合、キャッシュをどのように無効化するかという課題があります。(更新された場合に最新の値を取得したい、削除された場合に値が存在しないように振る舞いたい)
単一のサーバーで稼働するアプリケーションではこのような課題は発生しませんが、水平スケールを行うシステムにおいてはこの問題が発生します。
この課題は、クライアントサイドに TTL を設定することで解決できる場合があります。

※ TTL ... Time To Live の略で、有効期限を意味します。

しかし、TTL を設定するだけでは解決できない場合もあります。
キャッシュの有効期限が切れる前にデータが更新された場合、最新の値を取得できません。
そこで Redis では Pub/Sub を利用してクライアントに無効化するメッセージを送信することができます。クライアントはメッセージを受信して、キャッシュを無効化し、不整合な値の取得を防ぐことができます。

rueidis version

今回動作確認に利用する rueidis のバージョンは v1.0.47 です

動作確認環境

キャッシュアクセスを確認するために以下のような構成を docker compose を用いて構成します。

※ APP と BPP は同じ実装です。

単一の API を用意してメソッドによって挙動を変えます。

  • POST によるキーバリューの保存
  • GET によるバリューの取得
  • PUT によるバリューの更新
  • DELETE によるキーバリューの削除

※ 実装を簡素化するためにキーバリューの値はハードコーディングします。

cURL によるシナリオ

cURL を使って以下のようなシナリオを用いてキャッシュアクセスの動作確認を行います。

処理 期待値
1 APP にアクセスしバリューを取得 Not Found
2 BPP にアクセスしバリューを取得 Not Found
3 APP にアクセスしバリューを登録 Created
4 APP にアクセスしバリューを取得 Hit from Redis
5 APP にアクセスしバリューを取得 Hit from InMemory
6 BPP にアクセスしバリューを取得 Hit from Redis
7 BPP にアクセスしバリューを取得 Hit from InMemory
8 BPP にアクセスしバリューを更新 Created
9 APP にアクセスしバリューを取得 Hit From Redis
10 BPP にアクセスしバリューを取得 Hit From Redis
11 APP にアクセスしバリューを削除 No Content
12 APP にアクセスしバリューを取得 Not Found
13 BPP にアクセスしバリューを取得 Not Found
cURL
#!/bin/sh
curl -X GET localhost:8080
curl -X GET localhost:9090
curl -X POST localhost:8080
curl -X GET localhost:8080
curl -X GET localhost:8080
curl -X GET localhost:9090
curl -X GET localhost:9090
curl -X PUT localhost:9090
curl -X GET localhost:9090
curl -X GET localhost:8080
curl -X DELETE localhost:8080
curl -X GET localhost:8080
curl -X GET localhost:9090

インメモリ を使った実装

多層キャッシュ(クライアントキャッシュ)を実現するために redis/go-redisCode-Hex/go-generics-cache を組み合わせた実装を考えます。

go-redis は Redis 公式が管理する Go で Redis を操作するライブラリです。
コードやドキュメントを深く読めていないのですが go-redis を使って Redis の Client-side caching を実装する方法が見つからなかったのでこのライブラリを比較対象として使います。
クライアントキャッシュとして go-generics-cache を選定したのはジェネリクスを使いたいからです。わかりやすさを強調するために TTL は設定せずに実装します。

GET, SET, DEL を使うための実装は以下になります。

https://github.com/otakakot/sample-go-rueidis/blob/main/internal/cache/cache.go#L25-L98

こちらの実装を用いて cURL によるシナリオを実行すると以下のような結果となります。

Not Found
Not Found
Created
value
value
value
value
Created
value # 期待値は 更新後の値
value # 期待値は 更新後の値
(No Content)
Not Found
value # 期待値は NotFound

短期間のアクセスがあった場合は更新と削除においてインメモリのキャッシュにより期待値とはならない結果が返ってきてしまいます。

rueidis を使った実装

クライアントサイドキャッシュを使うためには DoCache() メソッドを利用します。

https://github.com/redis/rueidis/blob/v1.0.47/rueidis.go#L219-L229

DoCache() メソッドは、Redis コマンドの結果をキャッシュします。キャッシュが存在する場合、結果をキャッシュから返します。キャッシュが存在しない場合は、Redis コマンドを実行し、その結果をキャッシュします。
DoCache() メソッドを使うことで、Redis へのリクエストを減らすことができ、結果としてアプリケーションのパフォーマンスが向上します。

GET, SET, DEL を使うための実装は以下になります。

https://github.com/otakakot/sample-go-rueidis/blob/main/internal/cache/cache.go#L102-L167

こちらの実装を用いて cURL によるシナリオを実行すると以下のような結果となります。

Not Found
Not Found
Created
value
value
value
value
Created
9090 # 更新後の値を取得
9090 # 更新後の値を取得
(No Content)
Not Found
Not Found # 削除されているので取得されない

実装もシンプルになり、かつそれぞれの動作において期待する結果となることが確認できました。

おわりに

Redis のクライアントサイドキャッシュの仕様を確認し、rueidis を使って実際に動作確認を行いました。
今回は特に説明をしていませんがクライアントサイドキャッシュを有効にした場合は Redis との接続モードには以下の2種類があります。

  • default mode ... Redis 側でどのクライアントへ無効化メッセージを送信するかを決定する。= Redis 側のCPU負荷が高くなる。
  • broadcasting mode ... Redis 側は無効化メッセージを全てのクライアントへ送信する。 = クライアント側のCPU負荷が高くなる。

今回はdefault modeを利用して動作確認を行っています。
実際に活用する場合には default mode と broadcasting mode のどちらを利用するかを検討する必要があります。

今回実装したコードは以下に置いておきます。バージョン情報などは以下のリポジトリをご確認ください。

https://github.com/otakakot/sample-go-rueidis

おまけ

redis-cli の monitor コマンドを利用することで GET においてキャッシュが本当に有効になっているかどうかを確認することができます。

https://redis.io/commands/monitor/

127.0.0.1:6379> monitor
OK

以下のような処理を行うと

  1. 値を設定
  2. 値を取得
  3. 値をキャッシュし取得
  4. 値をキャッシュから取得

https://github.com/otakakot/sample-go-rueidis/blob/main/main_test.go#L24-L55

monitor には以下が出力されます。

"HELLO" "3"
"CLIENT" "TRACKING" "ON" "OPTIN"
"CLIENT" "SETINFO" "LIB-NAME" "rueidis"
"CLIENT" "SETINFO" "LIB-VER" "1.0.47"
"CLUSTER" "SHARDS"
"PING"
"HELLO" "3"
"CLIENT" "TRACKING" "ON" "OPTIN"
"CLIENT" "SETINFO" "LIB-NAME" "rueidis"
"CLIENT" "SETINFO" "LIB-VER" "1.0.47"
"SET" "key" "value"
"GET" "key"
"HELLO" "3"
"CLIENT" "TRACKING" "ON" "OPTIN"
"CLIENT" "SETINFO" "LIB-NAME" "rueidis"
"CLIENT" "SETINFO" "LIB-VER" "1.0.47"
"CLIENT" "CACHING" "YES"
"MULTI"
"PTTL" "key"
"GET" "key"
"EXEC"

GET コマンドは処理において 3 回実行するように実装していますが monitor には 2 回の履歴した出力されていません。

Discussion