一种游戏排行榜的实现方案

Global Rank

本篇博客主要提供了一种游戏全服排行榜的实现方式,主要借助 Redis 的 Sorted Set 数据结构来实现。

将排行榜数据放在 Redis 中可以很好的适配分布式部署的服务

主要思路

  • 当玩家的排行榜数据发送变化的时候(例如:通过新的关卡),直接更新Redis中的数据。

  • 服务节点定期从Redis中拉去最新的排行榜数据,缓存在内存中,当玩家请求排行榜数据的时候,从内存缓存中直接拿出数据

  • 定期清除排行榜中的数据量,只保留需要显示的数量

代码实现

Redis部分

  • 主要是三个接口(Get,Set,Del)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// rank item
type RankItem struct {
PlayerID string
Socre int
Ts int64
}
// Value used to get a float64 to set into Redis
func (i RankItem) Value() float64 {
return float64(i.Score)
}

// interface
Set(item *RankItem) error
Get() ([]RankItem, error)
Del(maxRanksNum int64) (int64, error)
  • 接口的实现
  • 这儿的RedisCache是我自己封装的struct,包含client( github.com/go-redis/redis 库中的 redis.Cmdable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
func (c RedisCache)Set(item *RankItem) error{
if err := c.client.Ping().Err(); err != nil {
// add log ,failed to ping redis
return err
}
// if key $item.PlayerID is already exist, it will replace it with the new value
err := c.client.ZAdd(pveStarRankRedisKey,
redis.Z{Score: item.Value(), Member: item.PlayerID}).Err()
if err != nil {
// todo : add log, failed to set
}
return err
}

func (c RedisCache)Get() ([]RankItem, error) {
if err := c.client.Ping().Err(); err != nil {
// add log, failed to ping redis
return nil, err
}

// get 0-99 in zset(key = pveStarRankRedisKey)
results, err := c.client.ZRevRangeWithScores(pveStarRankRedisKey, 0, 99).Result()
if err != nil {
// add log
return nil, err
}
items := make([]model.RankItem, 0, len(results))
for _, v := range results {
playerID := v.Member.(string)
value := int(v.Score)
items = append(items, model.RankItem{
PlayerID: playerID,
Score: value,
})
}
return items, nil
}

func (c RedisCache)Del(maxRanksNum int64) (int64, error){
if err := c.client.Ping().Err(); err != nil {
// add log
return 0, err
}

num, err := c.client.ZCard(dbKey).Result()
if err != nil {
// add log
return 0, err
}
availDeleteNum := num - maxRanksNum
if availDeleteNum <= 0 {
return 0, nil
}
return c.client.ZRemRangeByRank(dbKey, 0, availDeleteNum-1).Result()
}

Server部分

  • 主要包含定期从Redis中拉去排行榜数据
  • 主要的设计思路就是定时器触发拉取任务,并且存入内存中
  • 内存缓存变量可能会被多线程访问,需要防止静态,存入 atomic.Value 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Rank struct {
rankRedisCache RedisCache
rankData atomic.Value
}

func (p *Rank) loopRanks() {
// get data from redis in 10 second
var ticker = time.NewTicker(10 * time.Second)
defer ticker.Stop()

for range ticker.C {
p.loadRanksFromRedis()
}
}

func (p *Rank) loadRanksFromRedis() {
items, err := p.rankRedisCache.Get()
if err != nil {
// add log
return
}
if len(items) > 0 {
p.rankData.Store(items)
}
}

// get rank data form atomic value
func (p *Rank) GetRankData() []RankItem {
val, _ := p.rankData.Load().([]RankItem)
return val
}