ニコニコ生放送に見る Redis 活用ノウハウ

第3回 RedisによるWebアプリケーション開発(1)

この記事を読むのに必要な時間:およそ 4 分

ページネーションの実装

LRANGEコマンドは,リスト内の指定したインデックス範囲の要素を取得できます。

図4 LRANGEコマンドの取得範囲(数値はインデックス)

図4 LRANGEコマンドの取得範囲(数値はインデックス)

次のコードは,ページ番号とページ当たりの要素数を指定して,該当範囲のコンテンツIDを配列で取得しています。

<?php
 
 function contents_list_page($page, $size = 20) {
   if ($page <= 0 || $size <= 0) {
     return array();
   }
   $redis = new Redis();
   $redis->connect('localhost', 6379);
 
   $start = ($page - 1) * $size;
   $end = $start + $size - 1;
   $list = $redis->lRange('contents_list', $start, $end);
   $redis->close();
   return $list;
 }

LRANGEコマンドで取得できる部分リストは,終点として指定したインデックスの要素を⁠含む⁠ことに注意してください。また,始点と終点にはマイナス値を指定できます。-1は末尾を表すので,リスト全体を取得するには次のようにします。

   $list = $redis->lRange('contents_list', 0, -1);

リストのトリミング

Redisはオンメモリデータストアですので,リストに要素を追加し続けるだけではいつか容量の限界に達してしまいます。したがって,何らかの方法でリストのサイズを制限する必要があります。

次のコードは,LTRIMコマンドによってリストから古い要素を削除します。LTRIMコマンドは,リストのサイズを指定した範囲でトリミングします。

<?php
 
 function contents_list_trim($size = 1000) {
   $redis = new Redis();
   $redis->connect('localhost', 6379);
   $redis->lTrim('contents_list', 0, $size - 1);
   $redis->close();
 }

前述したニコニコ生放送の「リアルタイムログ機能」では,直近1,000件のメッセージのみ参照できればよいため,定期的に1,000件でトリミングすることによって,リストが無制限に長くなるのを防いでいます。

リスト全体のアトミックな差し替え

LPUSHやRPUSHを使った更新では,リストの先頭や末尾から1要素づつ追加することになりますが,場合によってはリスト全体をアトミックに更新したい場合もあります。

こんなときに便利なのはRENAMEコマンドです。空のテンポラリリストに更新後の内容をゼロから構築し,リストが完成したら,RENAMEコマンドによってターゲットのリストに「上書き」してしまいます。

<?php
 
 function contents_list_replace($new_idarray) {
   $redis = new Redis();
   $redis->connect('localhost', 6379);
 
   $tmp_key = /* ここでユニークなテンポラリキー名を生成 */;
   foreach ($new_idarray as $id) {
     $redis->lpush($tmp_key, $id);
   }
 
   $redis->rename($tmp_key, 'contents_list');
   $redis->close();
 }

RENAMEコマンドはキー名を変更する機能ですが,変更後のキー名が存在する場合は上書きするので,既存のキーの内容が一瞬で差し替わったように見えるわけです。

前述のユーザー生放送一覧ページでは,この手法を使って,Redis上のアクティブなリストの内容を,バッチ処理で生成した新しいリストの内容でアトミックに差し替える処理を実装しています。

LIST型のソート

SORTコマンドは,他のコマンドと比べると指定可能な修飾子が多く,やや複雑です。

前のコード例と同様に⁠contents_list⁠にコンテンツIDのリストが格納されているとした場合に,単にIDでソートした結果を得るには以下のように指定します。

SORT contents_list

コンテンツの属性情報でソートしたいというケースを考えてみます。各コンテンツIDごとに,そのアクセス回数が次のフォーマットのキー名でRedisに格納されているものとします。

access_count:コンテンツID

このとき,外部キーパターン⁠access_count:*⁠を指定することによって,コンテンツIDのリストをアクセス数でソートした結果を得ることができます。

SORT contents_list BY access_count:*

外部キーパターンを指定すると,リストの各要素の値そのものではなく,対応する外部キーの値(この例ではアクセス数)でソートすることができます。

なお,外部キーを使って正しくソートするには,それらのキーが同じRedisサーバー上に存在している必要があります。複数のRedisサーバーに分散して格納している場合は注意してください。

次の例は,SQLではおなじみのLIMITやDESCと組み合わせ,アクセス数の多いトップ3を取得しています。

SORT contents_list BY access_count:* LIMIT 0 3 DESC

他にも,ソートした結果を別のキーに格納するSTORE修飾子や,文字列要素を辞書式に並べる為のALPHA修飾子などがあります。詳細な仕様は公式のリファレンスを参照してください。

リストのサイズが非常に大きい場合など,ユーザーのリクエストのたびにソートするのではなく,あらかじめソートした状態で格納しておきたい場合があります。そのときは,LIST型に対してSORTコマンドを使う代わりに,SORTED SET型に格納することを検討してください(実際のところニコニコ生放送では,ソートが必要な箇所の大半はSORTED SETで実装しています⁠⁠。

LIST型を使ったキューの実装

LIST型は連結リストとして実装されているため,キューを構築するのに適しています。GitHubが開発したResqueというバックグラウンド処理ライブラリでは,内部のキューの実装としてRedisが使われているようです。

ニコニコ生放送では,ユーザーのタグ編集情報をタグ検索インデックスサーバーに通知する機構で,LIST型によるキューを実装しています。

ブロッキング形式のキュー処理

LIST型をキューとして利用する場合,通常,キューから要素を取得するためにRPOPコマンド(またはLPOP)を使います。

図5 LIST型によるキューの実装

図5 LIST型によるキューの実装

BRPOPコマンドは,RPOPコマンドのブロッキングバージョンです。リストが空の場合は指定したタイムアウトまで処理がブロックされ,要素が追加されると直ちにその値を返します。このブロッキング形式のポップを使えば,効率のよいキュー処理用のデーモンプログラム等の開発が可能になります。

次のコードは,JavaのRedisクライアントJedisによって,キューからブロッキング形式で要素をポップして処理するプログラムです。

public void run() {
  Jedis jedis = new Jedis("localhost");
  while (true) {
    // BRPOPコマンドでキー"myqueue"から要素を取得する。
    // タイムアウトは5秒に設定。
    List<String> popped = jedis.brpop(5, "myqueue");
    if (popped != null) {
      // 戻り値の最初の要素はキー名
      String key = popped.get(0);
      // 戻り値の2番目の要素は値
      String value = popped.get(1);
      
      doSomething(key, value);
      
    } else {
       System.out.println("timeout!");
    }
  }
}

より堅牢なキュー処理

上記のコードでは,キューからメッセージをポップした直後にプログラムがクラッシュした場合,その内容が失われてしまいます。より堅牢なキュー処理が求められる場合,RPOPLPUSHコマンドが利用できます。このコマンドは,リストの末尾からポップすると⁠同時⁠に別途指定したリストの先頭に同じ要素を追加します。

図6 RPOPLPUSHコマンドの動き

図6 RPOPLPUSHコマンドの動き

元のリストから取り出した要素をバックアップ用のリストに格納しておけば,万が一クラッシュしても,バックアップ用のリストから再処理できるわけです。もちろん,処理に成功した要素はバックアップ用のリストから削除しておきます(LREMコマンドが使えます⁠⁠。

バージョン2.2からはRPOPLPUSHのブロッキングバージョンとしてBRPOPLPUSHが追加され,BLPOPのようなブロッキング形式のコーディングが可能になります。

まとめ

今回は,Redisの代表的なデータ型であるLIST型に注目し,実際のWebアプリケーション開発で頻繁に登場するページネーションやソート,キュー処理などの応用例を見てきました。次回は,SET型やSORTED SET型などについて詳しく紹介する予定です。

著者プロフィール

小野侑一(おのゆういち)

株式会社ドワンゴ ニコニコ生放送システムリーダー。

ニコニコ生放送での開発経験を通して,ウェブをリアルタイム化する技術の重要性に着目。その成果としてリアルタイムタグ検索やRedisを活用したリアルタイム視聴者集計システムを開発。現在は全文検索のリアルタイム化に注目している。

Twitter@synk