game/common/userBindService/userService.go
2025-05-31 23:36:16 +08:00

131 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package userBindService
import (
"context"
"errors"
"fmt"
"game/common/proto/pb"
"game/common/utils"
"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))
}
// 从redis中加载玩家曾经访问过的服务节点名
func (m *UserBindService) LoadFromRedis(userId int64, typeId pb.ServiceTypeId) string {
k := m.makeRedisKey(userId, typeId)
if sName, err := m.rdb.Get(context.Background(), k).Result(); err != nil {
if !errors.Is(err, redis.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
}
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)
}
// 从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 {
node, ok := value.(etcd.ServiceNode)
if ok && node.TypeId == int(typeId) {
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 {
return nil, fmt.Errorf("not found service node.type id:%v. all node:%v", typeId, m.stringAllServiceNode())
}
n := xrand.IntN(len(nodes))
return &nodes[n], nil
}
// 根据服务类型,路由到对应的服务节点
func (m *UserBindService) FindServiceName(userId int64, typeId pb.ServiceTypeId) (string, error) {
// 内存中没有向redis中查询。redis中保留的服务节点不一定是可用的还需要向etcd中验证
if sName := m.LoadFromRedis(userId, typeId); sName != "" && m.serviceIsValid(sName) {
return sName, nil
}
// redis也没有玩家的服务节点信息从etcd中找可用服务节点随机选择一个
node, err := m.RandServiceNode(typeId)
if err != nil {
log.ErrorF("etcd中随机一个服务节点时错误:%v", err)
return "", err
}
//log.DebugF("etcd中随机一个服务节点:%s", node.Name)
m.rdb.Set(context.Background(), m.makeRedisKey(userId, typeId), node.Name, 2*24*time.Hour)
return node.Name, nil
}