オッス!オラ孫悟空!みんな元気にしてっか?
突然だけど、オラ、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力は…分かりません。しかし、ロマンスの神様ならぬロックフリーの神様に怒られることは間違いないです。