分布式服务器房间匹配问题
分布式服务器房间匹配问题
之前接触的业务的玩家加入房间都是自带房间id,不走匹配这个逻辑。但是最近遇到没有房间id,需要在一段匹配时间内的匹配房间,需要实现分布式的匹配房间逻辑,故写此文章记录开发历程。
全局匹配节点
客户端加入房间请求需要携带roomid,但是没有roomid需要前往匹配节点进行匹配获取roomid。匹配节点通常与我们熟知的大厅服务器放在一起。服务端单独开个匹配房间的接口,客户端检测到玩家点击开始游戏后,先调用这个接口向服务端进行房间匹配得到房间id。客户端再拿着房间id向房间进行连接。
匹配逻辑的实现
匹配的需求有这样几个:
- 支持不同房间类型分开匹配。在游戏中体现为不同的段位或者玩家画像进行分开匹配;
- 支持匹配时间,匹配时间内选择相同类型房间的玩家会被匹配到一起
- 支持匹配人数上限。房间人数存在上限,所以匹配期间应该限制匹配人数的上限
实现方案 Redis实现方案
对于需求一:使用redis kv 存储即可实现。key值为能够标识不同房间类型的唯一标识,表示正在匹配中的房间;value为匹配中的房间的房间id,用于请求返回。
对于需求二:给kv设置过期时间即可表示匹配时间
对于需求三:使用redis inrc自增计数即可,在匹配时加以判断
对于匹配错误的情况,就是新建一个全局唯一的房间id缓存进redis
redis作为中间件,这套方案再分布式环境下同样适用。
代码示范
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// 查找可用房间或创建新房间
func findOrCreateRoom(req MatchRequest) (string, error) {
// 1. 构造Redis键名
matchingRoomKey := fmt.Sprintf("matching:room:%s", req.RoomType)
roomPlayersKey := fmt.Sprintf("room:players:%s", req.RoomType)
// 2. 检查是否有正在匹配的同类型房间
roomID, err := rdb.Get(ctx, matchingRoomKey).Result()
if err != nil && err != redis.Nil {
return "", fmt.Errorf("redis error: %v", err)
}
// 3. 如果找到房间,检查人数是否已满
if err != redis.Nil {
// 获取当前房间人数
currentCount, err := rdb.Get(ctx, roomPlayersKey).Int()
if err != nil && err != redis.Nil {
return "", fmt.Errorf("failed to get player count: %v", err)
}
// 如果房间未满,加入该房间
if currentCount < maxRoomPlayers {
// 添加玩家到房间
count, err := rdb.Incr(ctx, roomPlayersKey).Result()
if err != nil {
return "", fmt.Errorf("failed to increment player count: %v", err)
}
// 保存玩家ID到房间成员列表
playerListKey := fmt.Sprintf("room:members:%s", roomID)
rdb.SAdd(ctx, playerListKey, req.PlayerID)
log.Printf("Player %s joined existing room %s (player %d/%d)",
req.PlayerID, roomID, count, maxRoomPlayers)
// 如果达到人数上限,移除匹配中的房间标记
if count >= maxRoomPlayers {
log.Printf("Room %s is now full, removing from matching pool", roomID)
rdb.Del(ctx, matchingRoomKey)
rdb.Del(ctx, roomPlayersKey)
}
return roomID, nil
}
}
// 4. 如果没有合适的房间,创建新房间
return createNewRoom(req)
}
// 创建新房间
func createNewRoom(req MatchRequest) (string, error) {
// 生成唯一房间ID
roomID := uuid.New().String()
// 构造Redis键名
matchingRoomKey := fmt.Sprintf("matching:room:%s", req.RoomType)
roomPlayersKey := fmt.Sprintf("room:players:%s", req.RoomType)
playerListKey := fmt.Sprintf("room:members:%s", roomID)
// 使用管道执行多个命令
pipe := rdb.Pipeline()
// 设置房间为匹配状态,并设置过期时间
pipe.Set(ctx, matchingRoomKey, roomID, time.Second*matchTimeoutSeconds)
// 设置房间当前人数为1
pipe.Set(ctx, roomPlayersKey, 1, time.Second*matchTimeoutSeconds)
// 添加玩家到房间成员列表
pipe.SAdd(ctx, playerListKey, req.PlayerID)
// 设置房间信息的过期时间(略长于匹配时间)
pipe.Expire(ctx, playerListKey, time.Second*matchTimeoutSeconds*2)
// 执行管道
_, err := pipe.Exec(ctx)
if err != nil {
return "", fmt.Errorf("failed to create new room: %v", err)
}
log.Printf("Created new room %s for player %s (type: %s)",
roomID, req.PlayerID, req.RoomType)
return roomID, nil
}
// 获取房间信息
func getRoomInfo(roomID string) (*RoomInfo, error) {
// 检查房间是否存在
playerListKey := fmt.Sprintf("room:members:%s", roomID)
exists, err := rdb.Exists(ctx, playerListKey).Result()
if err != nil {
return nil, fmt.Errorf("redis error: %v", err)
}
if exists == 0 {
return nil, errors.New("room not found")
}
// 获取房间成员
members, err := rdb.SMembers(ctx, playerListKey).Result()
if err != nil {
return nil, fmt.Errorf("failed to get room members: %v", err)
}
return &RoomInfo{
RoomID: roomID,
Players: members,
CreateTime: time.Now(), // 实际应用中可能需要从Redis中获取创建时间
}, nil
}
// 取消匹配
func cancelMatching(playerID string, roomID string) error {
playerListKey := fmt.Sprintf("room:members:%s", roomID)
// 从房间成员列表中移除玩家
removed, err := rdb.SRem(ctx, playerListKey, playerID).Result()
if err != nil {
return fmt.Errorf("failed to remove player from room: %v", err)
}
if removed == 0 {
return errors.New("player not in room")
}
log.Printf("Player %s left room %s", playerID, roomID)
return nil
}
This post is licensed under CC BY 4.0 by the author.