##初めに
普段はインターン先でフロントエンドでの開発を行なっているのですが、この度短期インターンでGoでのバックエンド開発を行うことになりました。
ゲームクライアントがすでに用意されていて、そのゲームのバックエンドを開発するというものです。
その中で、ランキング機能をRedisを用いて実装しようと思い、その時に軽く詰まったことを
備忘録として書いておこうと思います。
##Sorted Sort型について
今回初めてRedisを触り、Redisが持つ各データ型を調べている際に、Sorted Sorts型というまさにランキングに適した型を知りました。
この記事を読む方はすでにご存知かもしれませんが、簡単にRedisのSorted Sorts型について説明したいと思います。
ご存知の方は飛ばしていただけたらと思います。
RedisのSorted Sets型は、key単位で集合を定義することができ、それぞれのデータはMemberとScoreというフィールドを持ちます。
以下の図のイメージですね。
各Memberはそれぞれスコアというフィールドを持ち、そのスコアによって集合内で自動的にランク付けを行なってくれます。
例として、以下の表のように、今回はkeyはranking、MemberにユーザーのID、Scoreにゲーム内でのそのユーザーの最高点数を入れることとします(ディズニーツムツム的なゲームを想像してください)。
AliceのScoreを10000点以上に更新すれば自動でAliceを一番上に配置してくれます。便利ですね。
(※今回はkeyをrankingとしていますが、RedisのDBをranking用に用意して、それぞれのkeyを日付にすることで、日別ランキングやそれらの和集合をとって週別ランキングをxつ作成することも可能だったりします。)
MemberはRedis文字列型、Scoreは浮動小数値で扱われます。
##golangでRedisを扱う
今回、golangでRedisを用いるためのライブラリとして、go-redisを使いました。
そこで、go-redisでSorted Sets型を表す構造体である、*redis.Zの定義を見てみたところ、
型定義は以下になってます。
type Z struct {
Score float64
Member interface{}
}
Memberってinterface型なんや、、、、
これをみた時に、Member型ってもしかして複数の値持てるんじゃね??って思いました。
自分はmember型は1つしか値を持てない、つまりユーザーIDしか入れることができないと思っていましたが、ユーザー名も入れちゃうことができる(複数の値を持つことができる)ポテンシャルがあるのではないかと思いました。
そこで、Member用の構造体を定義して、そのデータをJSONにシリアライズしてそのデータをMemberに保存する持つようにすればいいのではないかと思い、
以下のようにコードを書きました。
func SetUserRankInfo(ctx context.Context, userRankInfo *UserRankInfo) error {
// memberに入れるデータを定義
member := &RankingMember{
UserID: "1",
UserName: "satofumi",
}
// シリアライズする
serializedMember, err := json.Marshal(member)
if err != nil {
log.Printf("failed to marshal json: %v", err)
return err
}
// Sorted Sorts型のデータを用意する
members := &redis.Z{
Score: float64(10000),
Member: serializedMember,
}
_, err = db.RedisClient.ZAdd(ctx, "ranking", members).Result()
if err != nil {
log.Printf("failed in ZAdd: %v", err)
return err
}
return nil
}
redis-cliを使って、データが入ったか確認してみましょう。
127.0.0.1:6379> ZRevRange ranking 0 -1 WITHSCORES
1) "{\"user_id\":\"7d350e11-b09d-4a17-9e5c-f56dc2b8f012\",\"user_name\":\"satofumi\"}"
2) "100000000"
1行目がMember、2行目がScoreですね。
無事JSONとしてデータを入れることが可能になっています。
Redisの場合、Memberの値は一意であり、基本的に1つのMemberは1つのScoreしか持たないようになっています。
今回の場合、同じユーザーIDと名前でデータをJSONでシリアライズすると毎回全く同じ文字列になるので、一意性が担保できています。
今度は逆に取り出してみましょう。
返ってきたデータの値を[]byteにアサートしてJSONにunmarshalすれば取り出せるな!!と思い、
以下のコードを書きました。
(以下のコードは、複数のデータが存在していることを想定して書いています🙏)
type sortedSet struct {
Member: RankingMember
Score: float64
}
// type RankingMember {
userID string
userName string
}
Z, err := db.RedisClient.ZRevRangeWithScores(ctx, "ranking", 0, -1).Result()
if err != nil {
log.Println(err)
return
}
sortedSets := make([]*SortedSet, len(ZList), len(ZList))
for i, Z := range ZList {
var sortedSet SortedSet
// Scoreを取り出す
score := int32(Z.Score)
// Memberを取り出す
member := Z.Member.([]byte)
var rankingMember RankingMember
if err = json.Unmarshal([]byte(member), &rankingMember); err != nil {
log.Println(err)
return
}
sortedSets[i] = &SortedSet{
Member: rankingMember,
Score: score
}
log.Printf("sortedSets[i]=%v", sortedSets[i])
}
これで動かしてみて、うまくいくかと思いきや、エラーが発生↓
http: panic serving [::1]:61321: interface conversion: interface {} is string, not []uint8
member := Z.Member.([]byte)
↑ここでの型アサーションでのエラーですね、、
データを入れる時はstring(member)をしてくれていて、取り出すときはstringで返ってきてくれているようです。
確かにさっきみたように、RedisのMemberにMarshalされた[]byte型で入れたはずなのに実際redisに格納されているデータを見てみると、
stringに変換してくれているんですね便利、、
と言うことで、該当部分を
member := Z.Member.(string)
と直し、もう一度動かしてみると、
log.Printf("userRankInfoList[0]=%v", *userRankInfoList[0])
-> userRankInfoList[0]={{1 satofumi} 10000}
うまく取り出すことができました。
##まとめ
色々書きましたが、この記事の目標はRedisのSorted Sorts型のMemberに複数の値を入れることでした。
memberの構造体を定義してデータをいれ、JSONにMarshalすることで格納することが可能になりました。
初めて記事を書いたので、稚拙な部分等あるかと思いますが暖かい目でみてくださると幸いです。
また、何か誤りやアドバイスがありましたら歓迎しますのでぜひ教えていただきたいです。
##参考記事
http://redis.shibu.jp/datatypes.html
https://redis.io/topics/data-types#sorted-sets
https://siguniang.wordpress.com/2014/09/15/access-ranking-with-redis-sorted-sets/