NHN Cloud Meetup 編集部
Redis(レディス)とは何か?
2020.02.17
3,077
Redis(レディス)のホームページ(redis.io)では次のように説明しています。
※redis.ioから抜粋
Redisの特徴
– キーバリュー型のインメモリデータストアです。簡単なコマンド(set、get)を使ってデータをすばやく保存できます。
– String、List、Hash、Set、Sorted Set(ソートセット)のような様々なデータ構造をサポートします。
– RDB(メモリのスナップショット)、AOF(set/delなどのアップデートに関するコマンドをファイルに記録)機能を利用して、永続性をサポートします。
– pub/sub機能(Publish/Subscribe)をサポートし、イベントサーバーに活用することができます。
– マスター/スレーブ複製を利用してRedis SentinelによるHAを提供しています。
– シングルスレッドでデータを処理します。
Redisをどこで使えばよいか?
RDBMSのキャッシュ用途に最も多く使用されています。RDBMSでもキャッシュの実装は可能ですが、パフォーマンス低下が懸念されるときにRedisが利用される場合があります。また、キーバリューストレージの特徴を利用して、単一キーの演算処理を完了させたり、キャンセルできる領域に使用されます。
例としては、ユーザープロファイル情報、ウェブサーバーのクラスタ用のセッション情報、ショッピングカート情報、URL短縮情報などがあります。単一キー演算のため、RDBMSより高速処理ができるという利点がありますが、RedisはRDBMSの代替として見做すことはできません。業務システムが複雑で要件が多くなるほど管理すべきキーが多くなるため、複雑度が増しキー設計が難しくなる可能性があります。したがって、RDBMSの代替材ではなく補完材としてRedisを把握し、用途を明確にする必要があります。
Redis活用事例
活用事例1:ページの訪問回数を保存
過去にイベントページが何回呼び出されたかをRDBMSを使って開発したところ、パフォーマンスの問題が発生したため、それをRedisに切り替えて開発した事例があります。
RDBMSは、並行処理のためレコードにロックをかけます。先にロックを獲得するとデータを処理し、ロックを獲得できない場合は待機します。単に待機するのではなく、ロックを獲得できるか確認するため、ループを回しながらCPUリソースを消費するので、CPU使用率が一時に増加します。この事例をRDBMSを利用して実装する場合、イベントテーブルをevent_no、event_name、read_cntで構成し、イベントページが呼び出されるたびに、read_cntを+1する更新クエリを呼び出すことになります。
下図を見てみましょう。BTSのアルバム発売イベントオープン時に10万人のユーザーが接続した場合、イベントテーブル[754 | BTSアルバム発売イベント| 103593] の行にロックを獲得するため、競合が発生し、イベントは失敗するでしょう。これをRedisで実装すると、シングルスレッドで処理されるため、ロックの競合に起因するオーバーヘッドを削除することができます。
要件
1.ユーザーが訪問した回数を照会できる
2.ページあたりの照会と統合されたページへの訪問回数が確認できる
3.イベント終了後も、当該イベントのページ訪問回数が維持される
4.同じユーザーがページを何度訪問しても、すべて訪問回数に含まれる
キー設計
1.イベントページあたりの訪問回数
event:click:<eventNo> ex) event:click:754(BTSアルバム発売イベント), event:click:755(バカンス用品特別イベント)
2.全体の訪問回数
event:click:total
機能の定義
1.イベントページあたりの訪問回数の照会
public String getVisitCount(String eventNo){ return this.redis.get("event:click:" + eventNo); //get("event:click:754") }
2.全体の訪問回数
public String getVisitTotalCount(){ return this.redis.get("event:click:total"); }
3.イベントページの訪問回数を保存
public Long addVisit(String eventNo){ this.redis.incr("event:click:total"); //全体の訪問回数(event:click:total)を1追加する this.redis.incr("event:click:"+eventNo); //特定ページの訪問回数(event:click:754)を1追加する }
活用事例2:カート情報
カートはショッピングモールでよく使用される機能です。特徴は「自分のショッピングカートのリストを照会する」、「自分のショッピングカートにこの商品を追加する」のように単一キー(会員番号)演算が行われ、同時実行の問題はありません。また、頻繁に保存されるため、呼び出し頻度は高いですが、永続性が保証されなくても大きな問題はありません。商品リストを保存するキーと、各商品の情報が保存されるキーが必要なため、カート情報に情報を格納するためのキーの数は、「ショッピングカートに登録された商品個数」+1(商品リスト)です。各商品の情報はJSON形式で保存します。
要件
1.会員が選択した商品をカートに保存
2.商品番号、商品名、商品数を保存
3.保存した時間から3日間有効、以降自動削除
4.すでに存在する商品を再登録するときは、以前に保存した商品を削除して、新しいデータに更新
5.ユーザーのショッピングカートのリストを照会
キー設計
1.カートの商品リスト
<ユーザー番号>:cart:product
2.カートに登録する商品
<ユーザー番号>:cart:productno:<商品番号>
機能の定義
1.カートに商品登録
public String addProduct(String productNo, String productName, int quantity){ JSONArray products = (JSONArray)this.cartInfo.get("products"); // JSONArray形態のproductリストをもってくる。 products.add(productNo); // productリストをproductNoに追加する。 this.redis.set(this.userNo + ":cart:product"), this.cartInfo.toJSONString()); // key:"11111:cart:product" - value:現在のカート情報 // 省略 String productKey = this.userNo + ":cart:product:" + productNo; // key:"11111:cart:product:101" return this.redis.setex(productKey, EXPIRETIME, product.toJSONString()); // key:"11111:cart:product:101" - value:JSON形式のproduct情報、EXPIRETIME以降は自動削除
2.カートの商品削除
public int deleteProducts(String[] productNo){ JSONArray products = (JSONArray)this.cartInfo.get("products"); // JSONArray形態のproductリストをもってくる。 int deleteCount = 0; for (String item : productNo){ products.remove(item); deleteCount += this.redis.del(this.userNo + ":cart:product:" + item; ) } this.redis.set(this.userNo + ":cart:product"), this.cartInfo.toJSONString()); // delete商品を考慮して、現在のカート情報を更新する。 return deleteCount;
3.カートを空にする
public int flushCart(){ JSONArray products = (JSONArray) this.cartInfo.get("products"); for ( int i = 0; i < products.size(); i++){ this redis.del(this.userNo + ":cart:productno:" + products.get(i)); // カート情報の各商品を削除する。"11111:cart:productno:<productno>" } this.redis.set(this.userNo + ":cart:product", ""); // key:"11111"cart:product" 初期化 return products.size(); }
4.カートのリスト照会
public JSONArray getProductList(){ JSONArray products = (JSONArray)this.cartInfo.get("products"); // JSONArray形態のproductリストをもってくる。 JSONArray result = new JSONArray(); for ( int i = 0; i < products.size(); i++){ value = this.redis.get(this.userNo + ":cart:productno:" + products.get(i)); result.add(value); } return result; }
5. 3日が経過した商品を削除
– 商品登録時、setexコマンドを使って有効期限(expiretime)を管理するため別途実装は必要ありません。
活用事例3:いいね処理する
他人の記事に共感を示す「いいね」機能は、RedisのSetDataTypeを使って簡単に実装できます。RDBMSでは一意性制約(Unique Constraint)を利用して処理するため、入力が多くなる環境ではパフォーマンス低下が発生する可能性があります。RedisはSetDataTypeをハッシュを利用して処理するため、時間の複雑度がO(1)で性能面でも有利です。
要件
1.ユーザーは各記事に「いいね」を付けることができる
2.ユーザーは1つの記事に1回だけ「いいね」を付けることができる
3.各記事にユーザーがクリックした「いいね」の回数を出力する
4.ユーザーが特定の記事に「いいね」を付けたか確認する
5.記事を削除するとき、「いいね」の情報も一緒に削除する
キー設計
1.記事の「いいね」数
posting:like:<記事番号>
機能の定義
1.記事に「いいね」表示
– valueでSET(重複を許可しない集合)を定義したので、すでにユーザーが「いいね」を押している場合はfalse、初めて「いいね」を押した場合はtrueを返します。
public boolean like(String postingNo, String userNo){ return this.redis.sadd("posting:like:"+ postingNo, userNo) > 0; // key : "posting:like:1111" - value : userNo }
2.「いいね」表示解除
– srem関数を利用してSETから削除します。
public boolean unLike(String postingNo, String userNo){ return this.redis.srem("posting:like:"+ postingNo, userNo) > 0; // key : "posting:like:1111" - value : userNo }3.ユーザーの「いいね」を表示するか確認
public boolean isLiked(String postingNo, String userNo){ return this.redis.sismember("posting:like:"+ postingNo, userNo); // 当該記事の「いいね」SETにユーザーが含まれるかどうか }
4.「いいね」回数情報を削除
public boolean deleteLikeInfo(String postingNo){ return this.redis.del("posting:like:"+ postingNo) > 0; }
5.記事の「いいね」回数照会
public Long getLikeCount(String postingNo){ return this.redis.scard("posting:like:"+ postingNo); // SETに属する集合の個数をreturn }
まとめ
いくつかの事例を通してRedisをどのように使用すればよいか紹介しました。
事例からも分かるようにRedisはRDBMSの代替材というよりも補完材に近いと言えます。RDBMSでも実装できますが、パフォーマンスが必要な場合に使用すると相互補完され、より良いサービスを顧客に提供することができます。また、キーによる単一演算処理を完了できる場合に使用すると、高いパフォーマンスと開発利便性を実現し、データの永続性を保証するDBMSのキャッシュ用途として使用する場合は最適なサービスが可能です。