一个 Redis 管 100 个游戏:game-bass 的多租户隔离设计
game-bass 是一个给独立游戏团队用的后端模板,卖出去几十份。每份模板跑在同一个服务器上,但数据必须完全隔离——A 游戏的排行榜不能出现在 B 游戏里。解决方案是多租户架构:一个 Redis 实例,用 key 前缀隔离所有游戏项目的数据。
租户 = 游戏项目
每个游戏项目注册后获得一组凭证:
1 | // internal/tenant/tenant.go |
AppID 是公开标识(类似 AWS Access Key ID),AppSecret 是密钥(类似 AWS Secret Access Key)。Secret 只在注册时返回一次,后续不可查询——这是安全设计的核心。
注册流程:IP 限流 + 原子写入
1 | func (m *Manager) Register(ctx context.Context, w http.ResponseWriter, r *http.Request) { |
为什么用 Pipeline 原子写入?如果 Set appID 成功但 Set secret 失败,就会出现”有 appID 但找不到 secret”的脏数据。
Redis Key 设计:前缀隔离
所有租户数据用 key 前缀隔离:
| Key 模式 | 值 | 用途 |
|---|---|---|
baas:tenant:appid:{appID} |
租户 JSON | 正向查找 |
baas:tenant:secret:{secret} |
appID | 反向查找 |
baas:tenant:id_counter |
自增整数 | ID 生成 |
baas:leaderboard:{tenantID}:{board} |
ZSet | 排行榜数据 |
baas:friends:{tenantID}:{playerID} |
Set | 好友关系 |
排行榜、好友、聊天等业务数据都带 {tenantID} 前缀,天然隔离。不需要额外的数据库或表,一个 Redis 实例搞定。
认证中间件:两步查找
1 | func (m *Manager) TenantAuthMiddleware(next core.Handler) core.Handler { |
两步查找(secret → appID → info)而不是直接 secret → info,是因为 LookupBySecret 只返回 appID 字符串,不存完整 JSON,节省 Redis 内存。
IP 限流:内存滑动窗口
1 | const registerRateLimit = 5 // 每 IP 每小时最大注册次数 |
用内存 map 而不是 Redis 做限流,因为注册是低频操作,不需要分布式限流。rateMapMaxSize = 1000 防止内存泄漏——超过 1000 个 IP 时自动清理过期条目。
安全设计要点
| 安全措施 | 实现方式 |
|---|---|
| Secret 不可查询 | 只在注册响应中返回一次,Redis 中只存 secret → appID 映射 |
| 密码学安全随机 | crypto/rand 生成,不用 math/rand |
| IP 限流 | 5 次/小时/IP,防刷注册 |
| Pipeline 原子写入 | 防止 appID/secret 数据不一致 |
| Bearer 前缀兼容 | 支持标准 Authorization: Bearer sk_xxx 格式 |
数据隔离验证
排行榜的 Redis key 是 baas:leaderboard:{tenantID}:{board}。租户 A 的 daily 排行榜和租户 B 的 daily 排行榜完全独立,因为 tenantID 不同。不需要额外的隔离层。
总结
多租户的核心不是”怎么隔离”,是”用最小代价隔离”。一个 Redis 实例 + key 前缀 + 认证中间件,233 行代码覆盖了注册、认证、限流、数据隔离的完整链路。
如果你的游戏后端要服务多个项目,你会选择共享数据库还是 key 前缀隔离?