RedisにはSorted Setというデータ型があって、ユーザランキングみたいなものを作るときに便利だったりする。このSorted Setに対する操作はZADD
とかZRANK
のようにZから始まるものとなっている。
Sorted Setから一番スコアが低い(高い)ものを破壊的に取り出すZPOPMIN
(ZPOPMAX
)というコマンドがあり、Spring Data Redisでこれらを使いたかったのだがやり方がわからず、調べてもなかなか出てこないので苦労した。ので、解決までの道のりをメモ。やり方だけ知りたい人は最後の方のサンプルコードを見てください。検証に使っているSpring Data Redisのバージョンは2.5.6で、Javaは11。Redis ConnectorはLettuce。
まず、サポートされているコマンドは以下のように利用できる。
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("ranking", "user1", 100); zSetOperations.add("ranking", "user2", 200); Set elements = zSetOperations.range("ranking", 0L, 1L); System.out.println(elements); // 出力 => [user1, user2]
zSetOperations.add
はZADD
、zSetOperations.range
はZRANGE
コマンドを実行してます。簡単ですね。
ところがZSetOperations
クラスにはpopMin
とかpopMax
みたいなメソッドがありません。こいつは困った。色々調べたけどよくわからない。そうこうしているうちに以下のissueに辿り着いた。
要は次くらいのバージョンで対応してZPOPMIN
, BZPOPMIN
, ZPOPMAX
, BZPOPMAX
, ZMSCORE
が使えるようになるとのこと。へー。でも僕は!今やりたいんです!どうすりゃいいの!と思っていたら興味深いコメントが。
When using Lettuce, you need to provide a CommandOutput to capture the Redis response. For [B]ZPOP[MIN|MAX] that's new ScoredValueOutput<>(ByteArrayCodec.INSTANCE) (via execute(String command, @Nullable CommandOutput commandOutputTypeHint, byte[]... args)).
Spring Data Redis doesn't have yet command output hints for these commands.
なるほど、要はRedisTemplateではできないからLettuceのexecute
コマンドでやれってことね。ふむふむ。というわけでリファレンスを見る。
なるほどわからん…。数時間溶かしてなんとかできたのが以下。各行に何やってるかコメント書いてます。
// ここはさっきとほぼ同じコード ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("ranking", "user1", 100); zSetOperations.add("ranking", "user2", 200); Set elementsBefore = zSetOperations.range("ranking", 0L, 1L); System.out.println("Ranking before ZPOPMIN : " + elementsBefore); // => Ranking before ZPOPMIN : [user1, user2] // LettuceConnectionを取得。キャストするのがミソ。キャストせずRedisConnectionのままだとexecuteが通らない LettuceConnection conn = (LettuceConnection) redisTemplate.getRequiredConnectionFactory().getConnection(); // 実行。第2引数がミソ。戻り値はObject型 Object resultObject = conn.execute("ZPOPMIN", new ScoredValueOutput<>(ByteArrayCodec.INSTANCE), "ranking".getBytes()); // executeの戻り値はScoredValue型にキャストできる。ここの型を変えれば他のコマンドにも対応できそう ScoredValue scoredValue = (ScoredValue) resultObject; // getValueでvalueが取れるが、戻り値はObject型 Object valueObject = scoredValue.getValue(); // Stringを入れてるならStringに変換できる。直接はキャストできないのでbyte配列にしてから String value = new String((byte[]) valueObject); // scoreはdoubleで入ってるのでキャストとかは不要 double score = scoredValue.getScore(); // valueとscoreが取れてめでたしめでたし System.out.println("Popped element : value = " + value + ", score = " + score); // => Popped element : value = user1, score = 100.0 // POPしたのでSorted Setの中身が一つ減っていることを確認 Set elementsAfter = zSetOperations.range("ranking", 0L, 1L); System.out.println("Ranking after ZPOPMIN : " + elementsAfter); // => Ranking after ZPOPMIN : [user2] conn.close();
このサンプルコードがググってさっと見つかればこんなに苦労することもなかったのになあ。。 ということで一応デモアプリをGithubにあげておいた。誰かの役に立てばいいけど。
追記:connectionをcloseしてなかったのでconn.close()
を追加。