おそらくはそれさえも平凡な日々

Redisアプリケーションパターン

この記事は、はてなエンジニアアドベントカレンダー2016の12日目の記事です。

先日こういうツイートをしました。

言いたかったのは、Redisはキャッシュのためだけのミドルウェアだと誤解されがちなのですが実際はそうではないということです。実際、公式サイト を見に行くと以下の様なことが書かれています。

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

つまり、Redisは多彩なデータ構造を保持できるインメモリーのデータストアで、様々な活用法があり、キャッシュとして「も」使える、ということです。

Redisとmemcachedの性能比較記事のようなものがネット上には散見されますが、Redisが単なるmemcached代替のように捉えられてしまうのは残念なことです。Redisにもmemcachedにもそれぞれ良さがあり、使い所はかぶるところがあっても異なります。実際、僕は去年のISUCON5では予選ではRedisを使い、決勝ではmemcachedを使いました。

ここでは、Redisの多彩なデータ構造を活かした、アプリケーションでの活用パターンをいくつか解説します。

前提

Redisはとにかく公式のヘルプが充実しており、読み応えが満載です。まず、Redisの各種データ構造に関しては、以下の解説を読むと良いです。

http://redis.io/topics/data-types-intro

各コマンドの解説についても、 http://redis.io/commands を見ると全ての解説がインタラクティブシェルを交えながら理解することができます。このページの良い点として、コマンドの計算量が載っており、パフォーマンスの目安と出来る点があります。

Strings ~ 普通のKV

Stringsはごく普通のKey-Valueです。文字列をセットすることができます。数値として扱うこともできます。

> SET mykey somevalue
OK
> GET mykey
"somevalue"

以下のようにexpireを指定することもできます。

> SET mykey somevalue EX 30
OK

"mykey" という文字列に、"somevalue"という値を30秒間保持します。

SETコマンドを利用した排他制御

ここまでは普通のKey-Valueですが、StringsにはNXという排他制御のためのオプションが存在します。NXは"not exists" の略で、そのキーに値が存在しない時にのみSETが成功します。例えば、以下のようなコマンドを使えば、"lockkey" という文字列で30秒間排他的にロックを取ることが可能になります。

> SET lockkey "aaa" NX EX 30
(integer) 1 // 値のセットに成功すると1が返る
> SET lockkey "bbb" NX EX 30
(integer) 0 // 30秒間は値のセットができないのでセットに失敗して0が返る
> GET lockkey
"aaa"

expireを待たずにロックを解除するにはDELを発行すればよいのですが、ロックを取る時に利用した文字列と一致しているか比較をしてからDELを発行するのが安全です。その場合以下のようなLUAスクリプトをEVALで実行するのが良いでしょう。

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

これは、 https://redis.io/commands/set のページに説明が書いてあります。

また、余談ですが、RedisにはSETNXやSETEXなどのコマンドがありますが、これらは非推奨となっており、上記のように、SETのオプションとして、NXやEXを組み合わせて利用するのが推奨される使い方となっています。上記のsetコマンドのドキュメントに以下のように書かれています。

Since the SET command options can replace SETNX, SETEX, PSETEX, it is possible that in future versions of Redis these three commands will be deprecated and finally removed.

Sorted Set ~ 順序つき集合

Sorted Setは、Redisで扱える強力なデータ構造であり、Redisの便利さの代名詞としてよく取り上げられす。例えば、以下のようにすれば、ハッカーの生誕年順にデータを格納し、取り出すことができます。

> ZADD hackers 1940 "Alan Kay"
(integer) 1
> ZADD hackers 1957 "Sophie Wilson"
(integer) 1
> ZADD hackers 1953 "Richard Stallman"
(integer) 1
> ZADD hackers 1949 "Anita Borg"
(integer) 1
> ZADD hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> ZADD hackers 1914 "Hedy Lamarr"
(integer) 1
> ZADD hackers 1916 "Claude Shannon"
(integer) 1
> ZADD hackers 1969 "Linus Torvalds"
(integer) 1
> ZADD hackers 1912 "Alan Turing"
(integer) 1

Sorted Setとスコアランキング

Sorted Setはスコアランキングなどを格納するのに非常に便利です。上位の一定数のみを残したい場合などは、 ZREMRANGEBYRANK を使って、集合の中の要素の数を削るのがセオリーです。たとえば上記の例で生年が古い順に5名のみを残したい場合は以下のようになります。

> ZREMRANGEBYRANK hackers 5 -1
(integer) 4

Sorted Setを扱う上で留意が必要なのが「同率問題」です。これは、同じスコアの複数要素がある場合、その並び順が取得され、順位が取得されるわけでは無いという問題です。

> ZADD numbers 2 two
(integer) 1
> ZADD numbers 2 ni
(integer) 1
> ZREVRANK numbers ni
(integer) 1
> ZREVRANK numbers two
(integer) 0

同率のスコアがある場合は、キーの辞書順に要素が並べられることが保証されています。なので、RDBMSのように、同一値の場合に並び順が不定にならない点では安心と言えます。

また、同率スコアを考慮した上で正しい順位を取得したい場合は、 ZCOUNT を使いそのスコアより大きい要素の数を数えた上で、それに1をプラスするのがセオリーです。

Sorted Setとタイムライン

Sorted Setを利用したパターンとしてランキング以外によく使われるのは、タイムライン的な機能です。この場合、epochなどをスコアにしてSorted Setに情報を格納することにより新しい順(epochの大きい順)にデータを取得することができるようになります。

Set ~ 集合

RedisというとSorted Setばかり取り上げられますが、個人的にはそれと同じくらい便利だと思っているのがSetです。Setは重複しない集合を扱うことができ、様々な集合演算をおこなうことができます。

直近のログインユーザーの取得

属性ごとに直近の1時間のログインユーザーを取得するようなことをしたい場合、例えば10分毎に別のSetに格納しつつ、SUNIONSTORE で結合することで、ある程度リアルタイムで直近のログインユーザーの集合を属性ごとに作ることができます。

集合からのランダム取得

Setからランダムに値を取り出したい場合に、SRANDMEMBER を利用できます。ランダム取得は、ゲームのマッチング処理などで重宝しますが、RDBMSでは難しい処理なので、Redisならではの威力を発揮できる部分です。ランダムで取得しつつ、集合から削除したい場合には、SPOPが便利です。

List ~ リスト

Listはそのままリストですが、それを扱うための様々なコマンドが揃っています。

Listをキューとして使う

その中でも僕が好きなのが BRPOPLPUSH です。

BRPOPLPUSHは blockingなインターフェースなので先頭に"B"が付いています。具体的には、あるリストの末尾から値を取り出し(RPOP)、その値を別のリストの先頭に追加(LPUSH)します。取り出せる値がない場合には、値が入ってくるまで待ち受けてブロックする(B)というコマンドです。なのでBRPOPLPUSHという名前になっています。Redisの複雑なコマンドの名前は略語を区切り文字無しでつなげることもあり、呪文のようになっていることが多いのですが、BRPOPLPUSHは勢いが感じられてかなり好きです。

BRPOPLPUSHはジョブキューのようなものを作る上で有用です。リストにジョブが投入されるまで待ち受けて、ジョブが投入されたら、ジョブを取得しつつ、別のリストに実行中のジョブとして投入することができるからです。ジョブの実行が終わったら、LREMコマンドを利用して、実行中ジョブのリストから当該ジョブを削除します。

> RPUSH jobs "one"
(integer) 1
> BRPOPLPUSH jobs inprogress 30
"one"
// なんか作業
> LREM inprogress -1 "one"

実際には、実行中ジョブリスト"inprogress"から定期的にタイム会うとしたジョブの削除や"job"への再投入などが必要になりますが、そのための判定として、ジョブ用の文字列に時刻を含んだJSONなどを格納しておくことが良いでしょう。

このあたりの話は、 RPOPLPUSH のページに、"Reliable queue pattern" として書かれているので読むと良いでしょう。

まとめ

ということで、Redisの活用パターンを幾つか紹介しました。Redisにはまだまだ活用方法がありますし、公式のヘルプには読み応えのある有用情報が詰まっているので読んでみると良いでしょう。

created at
last modified at

2016-12-14T10:43:01+0900

comments powered by Disqus