```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful

feat(cool): 添加Redis发布功能并实现巅峰赛匹配加入逻辑

新增RedisDo函数用于向Redis频道发布消息,并在巅峰赛场匹配
中添加玩家加入队列的功能。同时修复了socket连接关闭时资源
泄露问题,确保MsgChan正确关闭。

BREAKING CHANGE: 新增的RedisDo函数会直接panic处理错误,
需要调用方注意
This commit is contained in:
昔念
2026-03-04 03:22:43 +08:00
parent 10af34fdad
commit fb78147035
6 changed files with 210 additions and 0 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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() //保存玩家数据

View File

@@ -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
}

View 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
`)

View 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()
}