feat(cool): 添加Redis发布功能并实现巅峰赛匹配加入逻辑 新增RedisDo函数用于向Redis频道发布消息,并在巅峰赛场匹配 中添加玩家加入队列的功能。同时修复了socket连接关闭时资源 泄露问题,确保MsgChan正确关闭。 BREAKING CHANGE: 新增的RedisDo函数会直接panic处理错误, 需要调用方注意
This commit is contained in:
@@ -95,3 +95,17 @@ func Fail(message string) *BaseRes {
|
|||||||
// }
|
// }
|
||||||
// return nil, nil
|
// return nil, nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func RedisDo(ctx context.Context, funcstring string, a ...any) {
|
||||||
|
|
||||||
|
conn, err := g.Redis("cool").Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close(ctx)
|
||||||
|
_, err = conn.Do(ctx, "publish", funcstring, a)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ func ListenFunc(ctx g.Ctx) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
|
Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
|
||||||
|
_, err = conn.Do(ctx, "subscribe", "sun:join:2458") //加入队列
|
||||||
|
|
||||||
// 4. 循环接收消息
|
// 4. 循环接收消息
|
||||||
connError := false
|
connError := false
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
|||||||
|
|
||||||
//v.LF.Close()
|
//v.LF.Close()
|
||||||
// v.LF.Close()
|
// v.LF.Close()
|
||||||
|
close(v.MsgChan)
|
||||||
if v.Player != nil {
|
if v.Player != nil {
|
||||||
v.Player.Save() //保存玩家数据
|
v.Player.Save() //保存玩家数据
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,30 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/fight"
|
||||||
|
"blazing/logic/service/fight/top/repo"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
goredis "github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 表示"宠物王加入"的入站消息数据
|
||||||
|
type PetTOPLEVELnboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2458" struc:"skip"`
|
||||||
|
Mode uint32 //巅峰赛对战模式 19 = 普通模式单精灵 20 = 普通模式多精灵
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
cool.RedisDo(context.TODO(), "sun:join:2458", data.Head.UserID, data.Mode)
|
||||||
|
client := cool.Redis.Client()
|
||||||
|
|
||||||
|
// 类型断言为 UniversalClient
|
||||||
|
universalClient, _ := client.(goredis.UniversalClient)
|
||||||
|
repo.NewPlayerRepository(universalClient).AddPlayerToPool(context.TODO(), data.Head.UserID, 1)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|||||||
60
logic/service/fight/top/repo/lua.go
Normal file
60
logic/service/fight/top/repo/lua.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import "github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
// AtomicMatchScript 用于在 ZSet 中原子化查询、提取并删除玩家
|
||||||
|
// KEYS[1]: ZSet 的 key
|
||||||
|
// ARGV[1]: 当前玩家的分数
|
||||||
|
// ARGV[2]: 分数差范围 (delta)
|
||||||
|
// ARGV[3]: 最大返回数量
|
||||||
|
// 返回: 匹配的玩家ID列表 (已从ZSet中删除)
|
||||||
|
var AtomicMatchScript = redis.NewScript(`
|
||||||
|
local key = KEYS[1]
|
||||||
|
local score = tonumber(ARGV[1])
|
||||||
|
local delta = tonumber(ARGV[2])
|
||||||
|
local limit = tonumber(ARGV[3])
|
||||||
|
|
||||||
|
local min_score = score - delta
|
||||||
|
local max_score = score + delta
|
||||||
|
|
||||||
|
-- 查询分数范围内的玩家
|
||||||
|
local members = redis.call('ZRANGEBYSCORE', key, min_score, max_score, 'LIMIT', 0, limit)
|
||||||
|
|
||||||
|
if #members == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 原子删除匹配的玩家
|
||||||
|
for i, member in ipairs(members) do
|
||||||
|
redis.call('ZREM', key, member)
|
||||||
|
end
|
||||||
|
|
||||||
|
return members
|
||||||
|
`)
|
||||||
|
|
||||||
|
// CompositeScoreScript 用于将分数和时间戳合并为复合分数
|
||||||
|
// KEYS[1]: ZSet 的 key
|
||||||
|
// ARGV[1]: 玩家ID
|
||||||
|
// ARGV[2]: 玩家分数 (整数部分)
|
||||||
|
// ARGV[3]: 时间戳 (用于小数部分,确保先入队的玩家优先匹配)
|
||||||
|
// 复合分数格式: score.timestamp (分数相同时,时间戳小的优先)
|
||||||
|
var CompositeScoreScript = redis.NewScript(`
|
||||||
|
local key = KEYS[1]
|
||||||
|
local player_id = ARGV[1]
|
||||||
|
local score = tonumber(ARGV[2])
|
||||||
|
local timestamp = tonumber(ARGV[3])
|
||||||
|
|
||||||
|
-- 将时间戳转换为小数部分 (归一化到0-1之间)
|
||||||
|
-- 使用较大的除数确保时间戳差异体现在小数部分
|
||||||
|
-- 时间戳越小,复合分数越小,优先级越高
|
||||||
|
local max_timestamp = 10000000000000 -- 足够大的值来归一化时间戳
|
||||||
|
local decimal_part = timestamp / max_timestamp
|
||||||
|
|
||||||
|
-- 复合分数 = 整数分数 + 时间戳小数部分
|
||||||
|
local composite_score = score + decimal_part
|
||||||
|
|
||||||
|
-- 添加玩家到 ZSet
|
||||||
|
redis.call('ZADD', key, composite_score, player_id)
|
||||||
|
|
||||||
|
return composite_score
|
||||||
|
`)
|
||||||
105
logic/service/fight/top/repo/player.go
Normal file
105
logic/service/fight/top/repo/player.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MatchPoolKey 匹配池的 Redis Key
|
||||||
|
MatchPoolKey = "{match:pool}"
|
||||||
|
// GlobalRankKey 全局排行榜的 Redis Key
|
||||||
|
GlobalRankKey = "{rank:global}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlayerRepository 玩家数据仓库
|
||||||
|
type PlayerRepository struct {
|
||||||
|
client redis.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPlayerRepository 创建 PlayerRepository 实例
|
||||||
|
func NewPlayerRepository(client redis.UniversalClient) *PlayerRepository {
|
||||||
|
return &PlayerRepository{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPlayerToPool 将玩家添加到匹配池
|
||||||
|
// 使用 CompositeScoreScript 计算复合分数,确保先入队的玩家优先匹配
|
||||||
|
func (r *PlayerRepository) AddPlayerToPool(ctx context.Context, playerID uint32, score int64) error {
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
_, err := CompositeScoreScript.Run(ctx, r.client,
|
||||||
|
[]string{MatchPoolKey},
|
||||||
|
playerID,
|
||||||
|
score,
|
||||||
|
timestamp,
|
||||||
|
).Result()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchAndPickPlayers 原子化地从匹配池中查询、提取并删除玩家
|
||||||
|
// 返回匹配到的玩家ID列表
|
||||||
|
func (r *PlayerRepository) SearchAndPickPlayers(ctx context.Context, minScore, maxScore int64, count int) ([]string, error) {
|
||||||
|
// 计算中心分数和差值范围
|
||||||
|
centerScore := (minScore + maxScore) / 2
|
||||||
|
delta := (maxScore - minScore) / 2
|
||||||
|
|
||||||
|
result, err := AtomicMatchScript.Run(ctx, r.client,
|
||||||
|
[]string{MatchPoolKey},
|
||||||
|
centerScore,
|
||||||
|
delta,
|
||||||
|
count,
|
||||||
|
).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将结果转换为字符串切片
|
||||||
|
members, ok := result.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
players := make([]string, 0, len(members))
|
||||||
|
for _, m := range members {
|
||||||
|
if playerID, ok := m.(string); ok {
|
||||||
|
players = append(players, playerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return players, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalRank 获取全局排行榜前 N 名
|
||||||
|
// 返回玩家ID和分数的列表,按分数从高到低排序
|
||||||
|
func (r *PlayerRepository) GetGlobalRank(ctx context.Context, topN int64) ([]redis.Z, error) {
|
||||||
|
return r.client.ZRevRangeWithScores(ctx, GlobalRankKey, 0, topN-1).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePlayerScore 更新玩家在全局排行榜中的分数
|
||||||
|
func (r *PlayerRepository) UpdatePlayerScore(ctx context.Context, playerID string, score float64) error {
|
||||||
|
return r.client.ZAdd(ctx, GlobalRankKey, redis.Z{
|
||||||
|
Score: score,
|
||||||
|
Member: playerID,
|
||||||
|
}).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePlayerFromPool 从匹配池中移除玩家
|
||||||
|
func (r *PlayerRepository) RemovePlayerFromPool(ctx context.Context, playerID string) error {
|
||||||
|
return r.client.ZRem(ctx, MatchPoolKey, playerID).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlayerRank 获取玩家在全局排行榜中的排名 (0-based, 分数从高到低)
|
||||||
|
func (r *PlayerRepository) GetPlayerRank(ctx context.Context, playerID string) (int64, error) {
|
||||||
|
return r.client.ZRevRank(ctx, GlobalRankKey, playerID).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlayerScore 获取玩家在全局排行榜中的分数
|
||||||
|
func (r *PlayerRepository) GetPlayerScore(ctx context.Context, playerID string) (float64, error) {
|
||||||
|
return r.client.ZScore(ctx, GlobalRankKey, playerID).Result()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user