使用Redis的有序集合来实现排行功能及其性能测量

Redis是一个什么样的东西?

Redis是一种键值型的内存数据库。由于是内存数据库,数据不会持久化,但是可以比MySQL等数据库更快地进行读写操作。在键值对的Value中,不仅可以使用字符串,还可以使用列表、集合等各种数据结构。

“Sorted sets” 是什么意思?

Sorted sets是一种数据结构,其特点是按照某个规则进行排序。
作为值,它保存以下两个信息。

    • Member: 文字列

 

    Score: 数値

保存的信息将根据以下规则进行排序。
对于A和B,

    1. 如果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
}

请参考下面的代码,这里都有提供。

加入排名

总体流程如下:

    1. 将要保存的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
}

获取排名

大致的流程如下。

    1. 使用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参考资料

通过使用快照等功能,可以实现持久化的效果。
广告
将在 10 秒后关闭
bannerAds