game/common/userBindService/userService.go

128 lines
4.3 KiB
Go
Raw Normal View History

2025-05-27 19:01:21 +08:00
package userBindService
import (
"context"
"fmt"
"game/common/proto/pb"
2025-05-30 23:08:20 +08:00
"game/common/utils"
2025-05-27 19:01:21 +08:00
"github.com/fox/fox/etcd"
"github.com/fox/fox/log"
"github.com/fox/fox/xrand"
"github.com/go-redis/redis/v8"
"time"
)
const (
prefix = "user_bind_service"
)
/*
采用服务器与客户端分担路由到对应服务节点的机制
比如现有两个麻将房(game1,game2)当客户端有指定路由节点game1服务器直接将消息路由到game1节点
客户端没有指定路由节点则服务器从redis查找曾经的绑定节点并验证有效然后转发到对应的节点
如果redis信息已经失效(服务有更新)则从etcd中获取该玩法下所有最新版本的节点(game1,game2)然后随机发送到其中一个节点并在redis中保存绑定关系
如果客户端所有消息都不指定具体的节点名则每次都需要从redis拉取绑定关系会影响路由速度
*/
type UserBindService struct {
rdb *redis.Client
etcdRegistry *etcd.Registry[etcd.ServiceNode]
}
func NewUserBindService(rdb *redis.Client, etcdRegistry *etcd.Registry[etcd.ServiceNode]) *UserBindService {
return &UserBindService{
rdb: rdb,
etcdRegistry: etcdRegistry,
}
}
func (m *UserBindService) makeRedisKey(userId int64, typeId pb.ServiceTypeId) string {
return fmt.Sprintf("%s_%d:%d", prefix, userId, int(typeId))
}
2025-05-27 19:14:43 +08:00
// 从redis中加载玩家曾经访问过的服务节点名
func (m *UserBindService) LoadFromRedis(userId int64, typeId pb.ServiceTypeId) string {
2025-05-27 19:01:21 +08:00
k := m.makeRedisKey(userId, typeId)
if sName, err := m.rdb.Get(context.Background(), k).Result(); err != nil {
log.Error(err.Error())
return ""
} else {
return sName
}
}
// 从redis中解除玩家与节点的绑定关系
func (m *UserBindService) DelUserService(userId int64, typeId pb.ServiceTypeId) {
k := m.makeRedisKey(userId, typeId)
_, _ = m.rdb.Del(context.Background(), k).Result()
}
// 从etcd中检查节点是否有效如果有game1(旧服),game2(新服),都算有效,但是旧服会拒绝新玩家进入,
// 此时旧服不止要拒绝新玩家还要删除redis中的绑定关系。方便客户端重新发消息时路由到新的服务。
func (m *UserBindService) serviceIsValid(serviceName string) bool {
valid := false
m.etcdRegistry.GetNodes().Range(func(k, v interface{}) bool {
if node, ok := v.(etcd.ServiceNode); ok {
if node.Name == serviceName {
valid = true
return false
}
}
return true
})
return valid
}
2025-05-30 23:08:20 +08:00
func (m *UserBindService) stringAllServiceNode() string {
var nodes []any
m.etcdRegistry.GetNodes().Range(func(_, value any) bool {
nodes = append(nodes, value)
return true
})
return utils.JsonMarshal(nodes)
}
2025-05-27 19:01:21 +08:00
// 从etcd中找可用服务节点随机选择一个
func (m *UserBindService) RandServiceNode(typeId pb.ServiceTypeId) (*etcd.ServiceNode, error) {
var nodes []etcd.ServiceNode
var version string
m.etcdRegistry.GetNodes().Range(func(_, value any) bool {
2025-05-30 23:08:20 +08:00
node, ok := value.(etcd.ServiceNode)
if ok && node.TypeId == int(typeId) {
2025-05-27 19:01:21 +08:00
if version < node.Version {
version = node.Version
}
}
return true
})
m.etcdRegistry.GetNodes().Range(func(_, value any) bool {
if node, ok := value.(etcd.ServiceNode); ok && node.TypeId == int(typeId) {
if version == node.Version {
nodes = append(nodes, node)
}
}
return true
})
if len(nodes) == 0 {
2025-05-30 23:08:20 +08:00
return nil, fmt.Errorf("not found service node.type id:%v. all node:%v", typeId, m.stringAllServiceNode())
2025-05-27 19:01:21 +08:00
}
n := xrand.IntN(len(nodes))
return &nodes[n], nil
}
// 根据服务类型,路由到对应的服务节点
func (m *UserBindService) FindServiceName(userId int64, typeId pb.ServiceTypeId) (string, error) {
// 内存中没有向redis中查询。redis中保留的服务节点不一定是可用的还需要向etcd中验证
2025-05-27 19:14:43 +08:00
if sName := m.LoadFromRedis(userId, typeId); sName != "" && m.serviceIsValid(sName) {
2025-05-27 19:01:21 +08:00
return sName, nil
}
// redis也没有玩家的服务节点信息从etcd中找可用服务节点随机选择一个
node, err := m.RandServiceNode(typeId)
if err != nil {
2025-05-30 23:08:20 +08:00
log.ErrorF("etcd中随机一个服务节点时错误:%v", err)
2025-05-27 19:01:21 +08:00
return "", err
}
2025-05-30 23:08:20 +08:00
//log.DebugF("etcd中随机一个服务节点:%s", node.Name)
2025-05-27 19:01:21 +08:00
m.rdb.Set(context.Background(), m.makeRedisKey(userId, typeId), node.Name, 2*24*time.Hour)
return node.Name, nil
}