開発者のためのRedisチュートリアル(2)

キャッシュとしてのRedis

今回は前回お話したデータ構造について実際の活用事例を紹介する前に、キャッシュとしてのRedisについて紹介したいと思います。ほとんどのサービスでは、Redisを単なるキャッシュの用途に使用していると思いますが、このときRedisをどのように配置するかによって、システムのパフォーマンスに大きな影響を与える可能性があります。キャッシング戦略は、キャッシュされたデータの種類とそのデータへのアクセスパターンによって異なるため、ここで説明する内容がすべてのサービスに当てはまらないという点をご承知おきください。

Look Aside(= Lazy Loading)

名前からわかるように、この仕組みはキャッシュを横に置き必要なときだけデータをキャッシュにロードするキャッシュ戦略です。キャッシュはデータベースとアプリケーションの間に位置し、単純なキー・バリューの形式を保存します。アプリケーションからデータをインポートするときにRedisへ常にリクエストし、データがキャッシュにあるときはRedisからデータを返します。データがキャッシュに存在しない場合、アプリケーションからデータベースにデータをリクエストし、アプリケーションはこのデータを再びRedisに保存します。下図はこのプロセスを表わしたものです。

上記の構造を使えば、実際に使用されているデータのみをキャッシュすることができ、Redisの障害がアプリケーションに致命的な影響を与えないというメリットがあります。しかし、キャッシュにないデータを照会するときにより長い時間がかかることと、キャッシュが最新のデータを持っていることが保証できないという欠点があります。キャッシュに対応するキーの値が存在しない場合にのみキャッシュの更新が起こるため、データベース内のデータが変更されるときには、その値をキャッシュが把握できないためです。

ライトスルー

ライトスルー(Write-Through)構造は、データベースにデータを作成するたびに、キャッシュにデータを追加したりアップデートを行います。これにより、キャッシュ内のデータを常に最新の状態に保つことができますが、データ入力時に2回のプロセスを経る必要があるため、遅延時間が増加するという欠点があります。また、使用されない可能性があるデータも一旦キャッシュに保存するため無駄なリソースが発生します。これを解決するため、データの入力時にTTLを必ず使用して、使用されていないデータを削除することをお勧めします。

Redis活用事例

「いいね」を処理する

まず記事のコメントに「いいね」を表示する機能について考えてみましょう。

最も重要なことは、ユーザーが1つのコメントに1回だけいいねができるように制限することです。RDBMSでは、ユニーク条件を作成して処理することができます。しかし、多くの入力が発生する環境でRDBMSを利用すると、insertとupdateによるパフォーマンスの低下が必然的に発生します。

RedisのSetを利用すると、この機能を簡単に実装でき、素早く処理することができます。Setは順序がなく重複を許可しない集合です。コメントの番号を使ってキーを作成し、そのコメントに「いいね」をつけたユーザーのIDをアイテムに追加すると、同じID値を保存できないため、1人のユーザーは1つのコメントに1回だけ「いいね」をつけることができます。

Jedis(JavaのRedisライブラリ)からのパイプラインを使って、この機能を実装すると仮定したとき、毎秒約16万件のコマンドを処理することができます。RDBMSと比較すると確実に速い速度です。

ゲームサービスで日別のユニーク訪問者数(Unique Visitor)を集計する

ユニーク訪問者数(UV)は、ユーザーがサービスに何回訪問したとしても、1日に1回だけカウントされる値です。つまり、重複訪問を除いた訪問者の指標と考えることができます。多くのサービスでは、この数値を利用してユーザーの動向を把握し、マーケティング用の資料として活用したりしています。実際のサービスでこれを集計する方法としては、代表的な3つの方法が使用されています。1つ目はアクセスログ(access log)を分析する方法、2つ目は外部サービス(ex. Google Analytics)のサポートを受ける方法、3つ目は、接続情報をログファイルに作成してバッチプログラムで回す方法です。この3つの方法のうちGoogle Analyticsを除いては、情報をリアルタイムで表示することができません。

それでは、Redisのビット演算を活用して、簡単にリアルタイムのユニーク訪問者数を保存し照会する方法を説明します。ゲームのユーザーを1000万人と仮定して、日別の訪問者数を集計します。またこの値は0時を基準に初期化します。

ユーザーIDは、0から順次増加されると仮定して、Stringの各bitを1つのユーザーとして考えます。ユーザーがサービスにアクセスしたとき、ユーザーIDに対応するbitを1に設定します。1つのbitが1人を意味するので、1000万人のユーザは1000万個のbitで表現することができます。これのデータ容量は1.2MB程度の大きさです。RedisのStringの最大の長さは512MBなので、1000万人のユーザーを表すのは十分でしょう。

2020年1月29日にIDが7であるユーザーが訪問した場合、上図のように7番目のインデックスを1に設定します。この日にサービスに訪れた訪問者数の総数を照会するには、この文字列から1に設定されたbit数を求めるBITCOUNT演算を使って簡単に入手することができます。

もし、出席イベントなどを実施するために定められた期間中に毎日訪問したユーザーを集計したい場合なら、RedisのBITOPコマンドを使うと簡単です。Redisサーバーから直接、AND、OR、XOR、NOT演算ができるので、Redisの個々のbitを取得してサーバーで処理する手間を軽減することができます。

2020年1月29日から31日まで毎日アクセスしているユーザーは、IDが7のユーザーと11のユーザーであるということが、BITOPを用いたAND演算から簡単に入手できます。

最近検索したリストを表示する

最近検索された内容を照会することもRedisで簡単に実装できます。Doorayのような協業ツールに最近検索した担当者を照会できる機能を追加すると仮定してみましょう。

この機能をリレーショナルデータベースを使って実装するには、次のようなクエリ文が必要です。

select * from KEYWORD where ID = 123 order by reg_date desc limit 5;


このクエリは、ユーザーが最近検索したテーブルから最新の5つのデータを照会します。しかし、このようにRDBMSのテーブルを利用してデータを保存する場合は、重複除去が必要で、かつメンバー別に保存されたデータの個数を確認して古い用語を削除する作業まで必要になります。

したがって、最初から重複を許容せずに配置し、保存されるRedisのSorted Setを使うと簡単に実装できます。Sorted Setは加重値を基準に昇順でソートされるため、加重値に時間を使用すると、この値が最も大きい、後で入力されたアイテムが最後のインデックスに保存されます。

メンバーID:123が最近検索したユーザーは上図のように配置されて保存されます。このとき、加重は入力時のナノセカンドであり、最初に検索したユーザーのIDは46、最後に検索したユーザーは50です。そして、ID:51を検索すると下図のように最後にデータが追加されます。

常に5人だけを保存する場合は、インデックスが0のアイテムを消去すると保存されます。しかし、アイテム数が6よりも小さいときには、0のインデックスを削除すると保存されないため、毎回アイテム数を先に確認しなければならない煩わしさがあります。このとき、Sorted Setの負のインデックスを使うとより簡単に実装できます。負のインデックスは、下図のようにインデックスの最後から大きい値から小さい値に付けられます。

> ZREMRANGEBYRANK recent:member:123 -6 -6

データにメンバーを追加したあと、常に-6のアイテムを削除すれば、特定の数以上のデータが保存されることを防止できるようになります。インデックスでアイテムを消去するときは、ZREMRANGEBYRANKコマンドを使うと簡単です。このように、RedisのSorted Setを利用すると、多くの工数をかけずに最近検索した担当者を表示できる機能が実装できるでしょう。

Reference

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop