使用Redis的有序集合来实现排行功能及其性能测量
Redis是一个什么样的东西?
Redis是一种键值型的内存数据库。由于是内存数据库,数据不会持久化,但是可以比MySQL等数据库更快地进行读写操作。在键值对的Value中,不仅可以使用字符串,还可以使用列表、集合等各种数据结构。
“Sorted sets” 是什么意思?
Sorted sets是一种数据结构,其特点是按照某个规则进行排序。
作为值,它保存以下两个信息。
-
- Member: 文字列
- Score: 数値
保存的信息将根据以下规则进行排序。
对于A和B,
-
- 如果A的分数大于B的分数,则A大于B。
- 如果A的分数等于B的分数,那么分别参考A和B的成员,如果A的成员大于B的成员(按字典顺序),则A大于B。
在这里,由于Member是唯一的,按照上述规则,排序顺序将确定唯一。
比如,考虑在某个键上保存以下的值的情况。
{Member: "user1", Score: 100}
{Member: "user2", Score: 90}
{Member: "user3", Score: 100}
在这种情况下,它将被排序并保存如下。
{Member: "user2", Score: 90}
{Member: "user1", Score: 100}
{Member: "user3", Score: 100}
实施排名功能
以下是使用Go语言进行实现的,Go语言的Redis客户端将使用go-redis。考虑将以下三种信息作为排名信息保留。
type User struct {
ID string
Name string
HighScore int
}
请参考下面的代码,这里都有提供。
加入排名
总体流程如下:
-
- 将要保存的Member数据编码为JSON字符串
使用ZADD命令将数据添加到Sorted sets中
将文本编码为JSON字符串
为了保存这次的Member数据,我们需要定义一个包含ID和Name的结构体。
// userIDとuserNameを持った構造体(json文字列にして扱う)
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
}
而且,只需使用encoding/json.Marshal将其转换为JSON字符串即可。
member := &Member{
ID: id,
Name: name,
}
// memberをserializeする
serializedMember, err := json.Marshal(member)
ZADD命令
在go-redis中,定义了以下方法来添加数据到Sorted sets中的ZADD命令。
// Redis `ZADD key score member [score member ...]` command.
func (c cmdable) ZAdd(ctx context.Context, key string, members ...*Z) *IntCmd {
// 略
}
在这里,可以简单地将cmdable视为与Redis的连接。
另外,参数Z指的是具有以下成员和分数的结构体。
type Z struct {
Score float64
Member interface{}
}
此外,IntCmd的返回值是一个结构体,该结构体具有context.Context和error等。
type IntCmd struct {
baseCmd
val int64
}
type baseCmd struct {
ctx context.Context
args []interface{}
err error
keyPos int8
_readTimeout *time.Duration
}
这次,由于需要出现错误,只需在baseCmd的方法中调用Err(),然后进行错误处理即可。
func (cmd *baseCmd) Err() error {
return cmd.err
}
整个代码
根据上述的内容,整个代码如下所示。
package redis
import (
"context"
"encoding/json"
"github.com/arkuchy/redis-ranking/src/db"
goRedis "github.com/go-redis/redis/v8"
)
const (
RedisRanking string = "RedisRanking" // key名
)
// userIDとuserNameを持った構造体(json文字列にして扱う)
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
}
// AddRanking は,ランキングにユーザデータを追加します
func AddRanking(ctx context.Context, id string, name string, score int) error {
conn := db.Conn.GetRedisConn() // Redisとのコネクションを取得(各自作成する必要あり)
member := &Member{
ID: id,
Name: name,
}
// memberをserializeする
serializedMember, err := json.Marshal(member)
if err != nil {
return err
}
if err := conn.ZAdd(ctx, RedisRanking, &goRedis.Z{
Score: float64(score),
Member: serializedMember,
}).Err(); err != nil {
return err
}
return nil
}
获取排名
大致的流程如下。
-
- 使用ZREVRANGE(WITHSCORES)命令从Sorted sets中获取数据
- 解码获取的JSON字符串
ZREVRANGE(WITHSCORES)指令的中文释义如下:逆序范围(包含分数)。
ZREVRANGE命令是指从Sorted sets中按分数降序获取指定数量的成员的命令。另外,如果还想获取分数,只需添加WITHSCORES选项即可。在go-redis中,定义了以下方法。
func (c cmdable) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd {
// 略
}
在这里,start和stop参数表示在按Score降序排列时要获取的范围,注意Redis是从0开始的。
例如,如果要获取排名第1到10位的数据,则将start设为0,stop设为9;如果要获取排名第5到20位的数据,则将start设为4,stop设为19。
另外,返回的ZSliceCmd是一个结构体,除了包含context.Context和error等信息外,还包含一个Z的切片。
type ZSliceCmd struct {
baseCmd
val []Z
}
type baseCmd struct {
ctx context.Context
args []interface{}
err error
keyPos int8
_readTimeout *time.Duration
}
type Z struct {
Score float64
Member interface{}
}
只需一个选择,用中国母语改写如下:
本次讨论,如果你需要[Z]和error,只需调用ZSliceCmd方法中的Result()函数即可。
func (cmd *ZSliceCmd) Result() ([]Z, error) {
return cmd.val, cmd.err
}
解码JSON字符串
可以使用ZREVRANGE来获取数据,在得到的数据是JSON字符串时,需要对其进行解码。
可以使用encoding/json.Unmarshal将其转换为结构体。
member := &Member{
ID: id,
Name: name,
}
err := json.Unmarshal([]byte(serializedMember.(string)), member)
整体的程式碼
根据上述内容,整体代码可以如下所示。
package redis
import (
"context"
"encoding/json"
"github.com/arkuchy/redis-ranking/src/db"
goRedis "github.com/go-redis/redis/v8"
)
const (
RedisRanking string = "RedisRanking"
)
type UserResponse struct {
ID string
Name string
HighScore int
Rank int
}
// userIDとuserNameを持った構造体(json文字列にして扱う)
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
}
// GetRankings は,上位{limit}件のユーザデータを返します
func GetRankings(ctx context.Context, limit int) ([]*UserResponse, error) {
// redisは0始まり
// ex) 1~10 -> start:0, stop:9
start := 0
stop := start + limit - 1
conn := db.Conn.GetRedisConn() // Redisとのコネクションを取得(各自作成する必要あり)
serializedMembersWithScores, err := conn.ZRevRangeWithScores(ctx, RedisRanking, int64(start), int64(stop)).Result()
if err != nil {
return nil, err
}
res := make([]*UserResponse, 0, limit)
member := &Member{}
for i, serializedMemberWithScore := range serializedMembersWithScores {
serializedMember := serializedMemberWithScore.Member // 構造体ZからMemberを取得
score := serializedMemberWithScore.Score // 構造体ZからScoreを取得
if err := json.Unmarshal([]byte(serializedMember.(string)), member); err != nil {
return nil, err
}
u := &UserResponse{
ID: member.ID,
Name: member.Name,
HighScore: int(score),
Rank: i + 1,
}
res = append(res, u)
}
return res, nil
}
性能的测量
我们将使用Go语言的基准测试来比较以下两种获取排名的方法。
-
- RedisのSorted setsを用いた方法
- MySQLのSelect ~ ORDER BYを用いた方法(適切にインデックスを張った状態とする)
在对1000、5000和10000个数据进行保持的情况下,测量获取前100个数据的性能。然而,执行环境如下所示。
CPU: 1.8GHz Intel Core i5
メモリ: 8GB
言語: Golang 1.15.4
此外,请参考此处的MySQL排名功能实现。
结果 [jié guǒ]
(Translation: “Outcome”)
从左侧来看, (Zuǒ cè , )
実行したベンチマーク名 / 実行した回数 / 1回あたりの実行時間(ns/op) / 1回あたりの確保容量(B/op) / 1回あたりのアロケーション回数(allocs/op)
已成为如此。
- 保持データ数が1000の場合
# Redis
BenchmarkGetRankings-4 328 3576331 ns/op 41288 B/op 908 allocs/op
# MySQL
BenchmarkGetRankings-4 63 18586058 ns/op 392967 B/op 18051 allocs/op
- 保持データ数が5000の場合
# Redis
BenchmarkGetRankings-4 348 3679865 ns/op 41952 B/op 908 allocs/op
# MySQL
BenchmarkGetRankings-4 20 58493116 ns/op 2169418 B/op 90068 allocs/op
- 保持データ数が10000の場合
# Redis
BenchmarkGetRankings-4 301 3582936 ns/op 42096 B/op 908 allocs/op
# MySQL
BenchmarkGetRankings-4 12 105611875 ns/op 4443574 B/op 180075 allocs/op
总结
根据测量结果,无论保持数据数是1000、5000还是10000,使用Redis的Sorted Sets进行排名检索的性能优于使用MySQL进行排名检索。另外,随着保持数据量的增加,使用Redis的Sorted Sets相比使用MySQL更具优势,因此在实现大量保持数据的排名功能时,考虑使用Redis可能是一个不错的选择。然而,使用Redis的情况下,存在数据丢失的可能性,因此在添加排名时应考虑在MySQL和Redis中都进行添加,以便应对数据丢失的情况,并采取相应措施。
请查阅参考资料。
本文是根据以下信息撰写的:
· Redis官方文档
· redis-cli命令操作摘要
· go-redis参考资料