愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

More Transactional Redis (1) - Redis is not a transactional storage

オッス!オラ孫悟空!みんな元気にしてっか?

突然だけど、オラ、Redisに保存してるデータに以下のような操作をしてみたくなっちまったんだ!これ擬似コードな!

1: score = ZSCORE {key} {member}
2: if ( ! score ) {
3:   score = defaultScore
4: }
5: ZADD {key} score + diff {member}

具体的な処理は何でもいいんだけど、要は「ZSCOREでsorted setからscoreを取って、その値に応じて分岐した上でZADDで値を更新したい」ってことだな!上の場合はZINCRBYでもいいけど、細けぇこたぁ気にするな!


...

ここまで読んで勘のいい読者ならばお気づきのことでしょうが、上の擬似コードにはいわゆるrace conditionが存在します。実際にはZSCORE, ZADDのコマンドはそれぞれネットワーク経由でRedisに送られ、当然ながらこれらの処理はatomicには行われないため、以下の様なシーケンスが有りえます。

client A: 1でZSCOREを発行
client A: 2のif文でscoreがないと判定
client B: 1でZSCOREを発行
client B: 2のif文でscoreがないと判定
client A: 3でscoreにdefaultScoreを代入してのち、5に進んでZADD
client B: 3でscoreにdefaultScoreを代入してのち、5に進んでZADD

これは望ましい動作ではありません。

MULTI - EXECを使う

「そんなのMULTIでいけるやん」と思ったそこのアナタ!あなたのRedis力は1です。MULTI - EXECは「Redis側にコマンドをキューイングしておいて、まとめて実行する」ための仕組みでしかないため、MULTIの途中でRedisからデータを取り出して中身を見ることは出来ません。そのため、途中でZSCOREした値に応じて処理を分岐することも出来ません。

WATCHを使う

「WATCHならどうだろう」と思ったそこのアナタ!あなたのRedis力は3です。しかし残念ながら、WATCHが使えるのはkey単位での監視のみであり、今回のようにkeyとmemberの両方を使って参照・更新を行うSorted Setについては使えません。厳密には使えることは使えるのですが、同じkey内の別のmemberに対する更新があった場合でもMULTI - EXECの実行全体が失敗してしまうので、使い物になりません。

適当にロックする

「そんなの、何しかのmiddleware (RedisのSETNXでもいい) 使ってロックもどきを実装して、lockが取れなかったら失敗させればいいじゃん」と思ったそこのアナタ!あなたのRedis力は…分かりません。しかし、ロマンスの神様ならぬロックフリーの神様に怒られることは間違いないです。

Lua Scriptingを使う

「そんなの、Lua Scriptingで実装すればいいじゃん」と思ったそこのアナタ!あなたのRedis力は5です。Lua Scripting on Redisはatomicに実行されるため、今回の要件を満たします。ロックを使うよりおそらく実行効率もいいでしょう。


でも、RedisのLua Scriptingなんて機能、使ったことないよー!大丈夫かな?本番投入、できるかな?

この記事は、そんなあなたの疑問にお応えします。(続く)