基于 Go + Redis 的分布式游戏匹配系统演示项目,涵盖多实例协调、分片租约、ELO 评分、动态策略引擎与实时监控仪表盘。
| 特性 | 说明 |
|---|---|
| 多实例水平扩展 | 多个 Server 实例共享同一 Redis,通过分片租约协调工作 |
| 分片 + 租约机制 | Shard lease(TTL=10s,Lua CAS 续约),防止重复处理 |
| 防重复匹配 | Lua 原子脚本校验所有玩家均为 queuing 状态后批量设为 matched |
| 动态 MMR 容忍度 | BaseMMR(200) + tickCount × expansion(50),等待越久范围越宽 |
| ELO 评分系统 | 对局结束后异步更新(模拟 10–30s 对局时长,K=32) |
| 策略引擎 | 支持 max_diff / exact / has_tag 三种规则,可混合硬过滤与软惩罚 |
| 玩家属性扩展 | 数值型属性(PlayerAttributes)+ 标签型属性(Tags)双通道 |
| 测试环境隔离 | 独立 Redis 前缀的沙箱环境,API 创建/销毁,支持持久化重启恢复 |
| 异步日志 | Channel-based 非阻塞写入,支持 stdout + 文件双路输出 |
| 实时监控 | SSE 推送 + Chart.js 仪表盘,展示队列深度、匹配速率、等待时间、MMR 分布 |
┌─────────────────────────────────────────────────────────┐
│ HTTP Clients │
└──────────┬──────────────────────────┬───────────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Server :8080│ │ Server :8081│ 多实例
│ (inst-1) │ │ (inst-2) │
└──────┬──────┘ └──────┬──────┘
│ │
└──────────┬───────────────┘
│ Redis (共享状态)
┌───────▼────────┐
│ Redis :6379 │
│ │
│ solo:pool:{N} │ ← 分片匹配池(ZSet,score=MMR)
│ team:pool:{N} │
│ queue:{pid} │ ← 玩家排队信息
│ player:{pid} │ ← 玩家档案
│ match:{mid} │ ← 比赛记录
│ shard:{mode}:{N}:lease │ ← 分片租约
│ metrics:{iid} │ ← 实例指标
└────────────────┘
JoinQueue → SetPlayerState(queuing) → SaveQueueEntry
→ AddToSoloPool / AddToTeamPool (ZSet, score=MMR)
tickLoop (1s) → 遍历分片 → 滑动窗口选候选
→ MMR 范围检查(动态容忍度)
→ 策略硬过滤(GroupCompatible)
→ AtomicSetMatchedState (Lua 原子操作)
→ SaveMatch → asyncELOUpdate (goroutine)
- 每个分片持有
shard:{mode}:{N}:lease(TTL 10s),SETNX 竞争获取 - 每 3s 执行一次 Rebalance:若持有分片数 >
fairShare = (NumShards×2) / numInstances,则主动释放多余分片
match_making_demo/
├── cmd/
│ ├── server/
│ │ ├── main.go # HTTP 服务入口(多实例),embed 嵌入静态资源
│ │ └── web/
│ │ ├── index.html # 仪表盘 HTML shell(侧边栏布局)
│ │ ├── css/
│ │ │ └── style.css # 全局样式
│ │ └── js/
│ │ ├── utils.js # 公共工具函数(apiGet/Post/Delete, spotlight)
│ │ ├── overview.js # Overview 标签(指标卡片、图表、实例表、Redis)
│ │ ├── envs.js # Test Envs 标签 + 仿真引擎
│ │ ├── strategies.js# Strategy 标签(模板库、规则构建器、CRUD)
│ │ ├── help.js # Help 标签(API 文档、参数说明)
│ │ └── app.js # 侧边栏路由、SSE 连接、全局初始化
│ └── simulator/
│ └── main.go # 负载测试模拟器
├── internal/
│ ├── model/
│ │ └── model.go # 数据模型(Player, Match, QueueEntry, PlayerContext…)
│ ├── elo/
│ │ └── elo.go # ELO 算法(K=32)
│ ├── store/
│ │ └── store.go # Redis 操作层(前缀隔离、Lua 脚本、指标存储)
│ ├── matchpool/
│ │ └── matchmaker.go # 核心匹配引擎(分片管理、Tick 循环、队列 API)
│ ├── metrics/
│ │ └── metrics.go # MetricsPayload 定义与 Redis 推送
│ ├── strategy/
│ │ └── strategy.go # 规则引擎(max_diff/exact/has_tag,内置模板)
│ ├── testenv/
│ │ └── manager.go # 隔离测试环境管理(Create/Stop/List/Restore)
│ ├── admin/
│ │ └── handler.go # 管理 HTTP 路由 + SSE 推流 + 静态文件服务
│ ├── api/
│ │ └── handler.go # 玩家 API 路由(注册、加入、取消、状态)
│ └── logger/
│ └── logger.go # Channel-based 异步日志 Writer
├── docker-compose.yml
├── Dockerfile
├── Makefile
└── go.mod
- Go 1.22+
- Redis 7(本地或 Docker)
# 1. 启动 Redis
docker-compose up -d redis
# 2. 编译
go build ./...
# 3. 启动两个服务实例(分开终端)
SOLO_MATCH_SIZE=4 PORT=8080 INSTANCE_ID=inst-1 go run ./cmd/server
SOLO_MATCH_SIZE=4 PORT=8081 INSTANCE_ID=inst-2 go run ./cmd/server
# 4. 启动模拟器(注入 100 个玩家)
go run ./cmd/simulator -players 100 -servers "localhost:8080,localhost:8081"
# 5. 打开仪表盘
open http://localhost:8080/dashboardmake infra-up # 启动 Redis 容器
make run-server1 # 启动实例 1(:8080)
make run-server2 # 启动实例 2(:8081)
make simulate # 运行 100 玩家模拟
make simulate-small # 运行 24 玩家小规模模拟
make flush-redis # 清空 Redis 所有数据
make clean # 清理编译产物# 启动 Redis + 两个 Server 实例
docker-compose up --build
# 仪表盘
open http://localhost:8080/dashboard
open http://localhost:8081/dashboardcompose 文件中已配置:SOLO_MATCH_SIZE=4、TEAM_MATCH_COUNT=2、NUM_SHARDS=4,适合本地演示。
| 变量 | 默认值 | 说明 |
|---|---|---|
REDIS_ADDR |
localhost:6379 |
Redis 地址 |
REDIS_PASSWORD |
"" |
Redis 密码(无密码时留空) |
PORT |
8080 |
HTTP 监听端口 |
INSTANCE_ID |
自动生成 | 实例唯一标识符 |
NUM_SHARDS |
8 |
分片总数(建议生产 16+,演示用 4) |
SOLO_MATCH_SIZE |
12 |
Solo 模式每局人数(演示用 4) |
TEAM_MATCH_COUNT |
4 |
组队模式参赛队伍数(演示用 2) |
TEAM_SIZE |
3 |
每支队伍人数 |
LOG_FILE |
"" |
日志文件路径(空 = 仅 stdout) |
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/match/register |
注册玩家档案 |
POST |
/match/join |
加入匹配队列 |
DELETE |
/match/cancel/:pid |
取消匹配 |
GET |
/match/status/:pid |
查询匹配状态 |
注册玩家
curl -X POST http://localhost:8080/match/register \
-H "Content-Type: application/json" \
-d '{"id":"p1","name":"Alice","mmr":1500,"kd":1.2,"level":30,"idc":"us-east"}'加入队列(支持属性与标签)
curl -X POST http://localhost:8080/match/join \
-H "Content-Type: application/json" \
-d '{
"player_id": "p1",
"mode": "solo",
"attributes": {"ping": 30, "device": 1, "recent_wr": 0.62},
"tags": ["premium", "mobile"]
}'查询状态
curl http://localhost:8080/match/status/p1
# → {"state":"queuing"} 或 {"state":"matched","match":{...}}| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/admin/instances |
所有实例指标快照 |
GET |
/admin/redis |
Redis 内存/连接/OPS 信息 |
GET |
/admin/stream |
SSE 实时推流(metrics + redis 事件) |
GET |
/admin/strategies |
列出所有策略 |
POST |
/admin/strategies |
创建策略 |
PUT |
/admin/strategies/:id |
更新策略 |
DELETE |
/admin/strategies/:id |
删除策略 |
GET |
/admin/strategies/templates |
获取内置策略模板 |
GET |
/admin/envs |
列出测试环境 |
POST |
/admin/envs |
创建隔离测试环境 |
GET |
/admin/envs/:id |
查询环境状态与指标 |
DELETE |
/admin/envs/:id |
停止并删除测试环境 |
POST |
/admin/envs/:id/match/join |
在测试环境中加入队列 |
GET |
/admin/envs/:id/match/status/:pid |
查询测试环境中玩家状态 |
GET |
/dashboard |
监控仪表盘页面 |
策略由一组**规则(Rule)**组成,在每次候选窗口中对玩家两两进行兼容性检查。
| Op | 适用字段 | 语义 |
|---|---|---|
max_diff |
attribute(数值) |
` |
exact |
attribute(数值/枚举) |
a == b,适合设备类型、段位等 |
has_tag |
tag_value(字符串) |
所有玩家必须同时持有或同时不持有该标签 |
# 创建一个"延迟硬过滤 + 胜率软均衡"策略
curl -X POST http://localhost:8080/admin/strategies \
-H "Content-Type: application/json" \
-d '{
"name": "竞技模式",
"rules": [
{"attribute": "ping", "op": "max_diff", "threshold": 60, "hard_filter": true, "weight": 1},
{"attribute": "recent_wr", "op": "max_diff", "threshold": 0.15, "hard_filter": false, "weight": 2}
]
}'通过 GET /admin/strategies/templates 获取,仪表盘 Strategy 页面会以卡片形式展示,点击「Use Template」即可填入编辑器。
| 模板 ID | 名称 | 规则 |
|---|---|---|
tpl_device |
设备类型硬隔离 | device exact hard |
tpl_ping |
延迟范围过滤 | ping max_diff ≤60 hard |
tpl_winrate |
近期胜率均衡 | recent_wr max_diff ≤0.15 soft w=2 |
tpl_premium |
会员专属房 | tag:premium has_tag hard |
cmd/simulator 直接写入 Redis 并并发调用 HTTP API,模拟真实多玩家场景。
go run ./cmd/simulator [flags]
# 参数
-players 100 # 模拟玩家总数
-servers "host:8080,..." # 逗号分隔的服务实例地址
-team-frac 0.33 # 组队玩家比例(会取整为 TeamSize 的倍数)
-team-size 3 # 每队人数
-redis localhost:6379 # Redis 地址(用于直接写入玩家数据)模拟结束后会输出统计报告:
========== SIMULATION RESULTS ==========
Total players: 100
Solo: 67
Team: 33 (in 11 teams)
Matched: 98
Failed/Timeout: 2
Total elapsed: 18.3s
--- Wait Time Distribution (seconds) ---
Min: 1.0
Median: 4.2
P95: 11.8
Max: 15.3
--- MMR Spread Distribution ---
Min: 12.0
Median: 87.5
P95: 198.4
Max: 241.0
=========================================
访问 http://localhost:8080/dashboard,固定侧边栏导航,包含四个页面:
| 页面 | 内容 |
|---|---|
| Overview | 聚合指标卡片(Solo/Team 匹配数、队列深度、平均等待)+ 4 条实时折线图 + 各测试环境队列彩色曲线 + 实例明细表 + Redis 统计 |
| Test Envs | 创建/删除隔离沙箱环境;内置 Poisson 到达 + Gaussian MMR 分布的负载仿真引擎;切换标签时仿真不中断 |
| Strategy | 内置模板库(点击 Use Template 直接填入编辑器)、可视化规则构建器、策略 CRUD |
| Help | API 端点速查、策略引擎说明、仿真器参数文档、架构要点 |
数据通过 GET /admin/stream(SSE)每 2 秒推送一次,无需手动刷新。静态资源通过 //go:embed web 内嵌进二进制,部署无需额外文件。
仪表盘采用模块化 Vanilla JS,通过 Go embed.FS 内嵌,访问路径为 /static/*。
/dashboard → web/index.html(HTML shell)
/static/css/ → web/css/style.css
/static/js/utils.js → 公共 API fetch 封装
/static/js/overview.js → SSE 驱动的聚合指标 + 每 3s 轮询各 env 队列
/static/js/envs.js → 智能渲染(不销毁运行中的仿真卡片)
/static/js/strategies.js→ Template 按钮用 addEventListener 而非 onclick
/static/js/help.js → 静态文档
/static/js/app.js → 侧边栏路由 + SSE 连接
关键设计决策:
- 智能渲染:
_renderedEnvsMap 跟踪已渲染的环境卡片,loadEnvs()只插入新卡片、删除消失的卡片,切换标签不重建 DOM,仿真状态得以保留。 - DOM 按需查询:
simLog/updateSimUI每次调用时通过document.getElementById查找元素,不缓存引用,避免卡片重建后引用失效。 - Template 按钮修复:模板卡片使用
addEventListener('click', () => { ... })闭包传递数据,避免JSON.stringify在 HTML 属性中破坏引号边界。
| 组件 | 版本 | 用途 |
|---|---|---|
| Go | 1.22+ | 服务端语言 |
| gin-gonic/gin | v1.9.1 | HTTP 框架 |
| redis/go-redis | v9.4.0 | Redis 客户端 |
| google/uuid | v1.5.0 | UUID 生成 |
| Redis | 7 | 共享状态存储(匹配池、租约、玩家数据、指标) |
| Chart.js | v4.4.0 CDN | 仪表盘实时图表 |
| CSS / Vanilla JS | — | 仪表盘前端(无框架,//go:embed 内嵌) |
{ "attribute": "ping", // 数值型规则使用(max_diff / exact) "tag_value": "premium", // has_tag 规则使用 "op": "max_diff", // 运算符:max_diff | exact | has_tag "threshold": 60, // max_diff 的容差上限 "weight": 1.0, // 软规则的惩罚权重 "hard_filter": true // true=硬过滤(不满足则不可组队),false=软惩罚 }