NHN Cloud NHN Cloud Meetup!

Redis(レディス)とは何か?

Redis(レディス)のホームページ(redis.io)では次のように説明しています。

オープンソース(BSDライセンス)のインメモリデータストアで、データベース、キャッシュ、およびメッセージブローカーとして利用されています。String、Hash、List、Set、Sorted Set型のようなデータ構造をサポートし、複製、Luaスクリプト、LRUページの退避、異なるレベルのディスク永続性とトランザクションにも対応しており、Redis Sentinelを通じた高可用性とRedisクラスタを使用した自動パーティショニングを提供しています。
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のキャッシュ用途として使用する場合は最適なサービスが可能です。

NHN Cloud Meetup 編集部

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