7.8 KiB
7.8 KiB
数据库设计详设文档
文档版本: v1.0
撰写人: 数据库架构师
创建日期: 2024年9月11日
1. 技术选型
- 主数据库: MongoDB 6.0+
- 原因: 采用面向文档的存储模型,非常适合存储游戏会话、用户配置等半结构化数据。其灵活的Schema设计能快速迭代,内建的复制和分片功能为高可用和高扩展性提供了有力支持。
- 缓存/消息队列: Redis 7.0+
- 原因: 基于内存的高性能键值存储,用于缓存热点数据(如用户信息、排行榜)、管理WebSocket会话、实现分布式锁及作为多节点部署时的消息总线(Pub/Sub)。
- ORM/ODM: Mongoose 7.x
- 原因: 为MongoDB提供强大的对象数据建模(ODM)能力,支持Schema定义、数据校验、中间件、查询构建等功能,能显著提升开发效率和代码健壮性。
2. MongoDB 数据模型设计
2.1 users 集合
存储用户信息。
-
Schema 定义:
import { Schema, model } from 'mongoose'; const userSchema = new Schema({ _id: { type: String, required: true }, // 使用微信的 openid 作为主键 nickname: { type: String, required: true }, avatarUrl: { type: String, required: true }, stats: { gamesPlayed: { type: Number, default: 0 }, gamesWon: { type: Number, default: 0 }, winRate: { type: Number, default: 0.0 }, totalShots: { type: Number, default: 0 }, totalHits: { type: Number, default: 0 }, accuracy: { type: Number, default: 0.0 }, eloRating: { type: Number, default: 1200 } // Elo积分系统 }, lastLoginAt: { type: Date, default: Date.now }, createdAt: { type: Date, default: Date.now, immutable: true }, updatedAt: { type: Date, default: Date.now } }, { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }); // Mongoose 中间件,用于在胜率和准确率变化时自动更新 userSchema.pre('save', function(next) { if (this.isModified('stats.gamesPlayed') || this.isModified('stats.gamesWon')) { this.stats.winRate = this.stats.gamesPlayed > 0 ? (this.stats.gamesWon / this.stats.gamesPlayed) * 100 : 0; } if (this.isModified('stats.totalShots') || this.isModified('stats.totalHits')) { this.stats.accuracy = this.stats.totalShots > 0 ? (this.stats.totalHits / this.stats.totalShots) * 100 : 0; } next(); }); export const UserModel = model('User', userSchema); -
索引 (Indexes):
_id: (主键) 唯一索引,用于快速查找用户。{ "stats.eloRating": -1 }: 降序索引,用于实现排行榜。
2.2 game_sessions 集合
存储完整的游戏对局信息,用于复盘、数据分析和问题排查。
-
Schema 定义:
import { Schema, model } from 'mongoose'; const positionSchema = new Schema({ x: Number, y: Number }, { _id: false }); const planeSchema = new Schema({ center: positionSchema, direction: String, positions: [positionSchema] }, { _id: false }); const playerStateSchema = new Schema({ userId: { type: String, ref: 'User', required: true }, board: { planes: [planeSchema], attackHistory: [{ position: positionSchema, result: String, // 'miss', 'hit', 'destroy' timestamp: Date }] }, stats: { shots: Number, hits: Number, planesDestroyed: Number } }, { _id: false }); const gameSessionSchema = new Schema({ _id: { type: String, required: true }, // 游戏会话ID roomCode: { type: String, required: true, index: true }, status: { type: String, required: true, enum: ['placing', 'battling', 'finished'], default: 'placing' }, players: [{ type: String, ref: 'User' }], playerStates: [playerStateSchema], winnerId: { type: String, ref: 'User' }, winReason: { type: String, enum: ['ALL_PLANES_DESTROYED', 'SURRENDER', 'TIMEOUT'] }, gameEvents: [{ type: String, playerId: String, data: Schema.Types.Mixed, timestamp: Date }], startedAt: { type: Date, default: Date.now }, finishedAt: { type: Date } }); export const GameSessionModel = model('GameSession', gameSessionSchema); -
索引 (Indexes):
_id: (主键) 唯一索引。{ roomCode: 1 }: 用于通过房间码快速查找游戏。{ "players": 1 }: 多键索引,用于查询某玩家参与的所有对局。{ status: 1, startedAt: -1 }: 复合索引,用于查找特定状态的游戏并按时间排序。
3. Redis 数据结构设计
Redis 用于存储高频访问、易失性或需要原子操作的数据。
3.1 用户会话 (User Session)
- 用途: 存储用户登录状态和WebSocket连接信息。
- 数据结构:
HASH - Key:
session:{userId} - Value:
token:string(JWT)connectionId:string(当前WebSocket连接ID)status:'online' | 'offline' | 'in-game'gameId:string(如果status为in-game)
- TTL: 24小时 (每次访问刷新)
3.2 游戏房间 (Game Rooms)
- 用途: 管理游戏房间列表和房间内玩家状态。
- 数据结构:
HASH - Key:
room:{roomCode} - Value:
name:string(房间名)ownerId:string(房主用户ID)status:'waiting' | 'full' | 'in-game'player1Id:stringplayer2Id:stringplayer1Ready:'0' | '1'player2Ready:'0' | '1'
- TTL: 2小时 (从最后一次活动开始计算)
3.3 游戏状态 (Live Game State)
- 用途: 缓存进行中游戏的核心状态,减少对MongoDB的读写压力。
- 数据结构:
STRING(存储序列化后的GameState对象) - Key:
game:state:{gameId} - Value:
JSON.stringify(GameState) - TTL: 1小时 (游戏结束后删除)
3.4 排行榜 (Leaderboard)
-
用途: 实时更新和查询玩家排名。
-
数据结构:
SORTED SET(ZSET) -
Key:
leaderboard:elo -
Value:
member:userIdscore:eloRating(整数)
-
操作:
- 更新排名:
ZADD leaderboard:elo <eloRating> <userId> - 查询Top 100:
ZREVRANGE leaderboard:elo 0 99 WITHSCORES - 查询玩家排名:
ZREVRANK leaderboard:elo <userId>
- 更新排名:
3.5 分布式锁 (Distributed Lock)
- 用途: 在关键操作(如匹配玩家、创建游戏)中防止并发冲突。
- 数据结构:
STRING - Key:
lock:{resource_name}:{resource_id}(e.g.,lock:room:join:{roomCode}) - Value:
unique_lock_id(e.g., a UUID) - 操作: 使用
SET key value NX PX milliseconds命令实现原子性的加锁操作。NX: 只在键不存在时设置。PX: 设置过期时间(毫秒),防止死锁。
4. 数据一致性策略
-
写操作:
- 关键操作 (如
EXECUTE_ATTACK):- 开启分布式锁。
- 更新Redis中的实时游戏状态 (
game:state:{gameId})。 - 将操作事件异步写入一个队列(如Redis Stream或RabbitMQ)。
- 释放锁。
- 立即向客户端返回成功响应。
- 一个独立的后台Worker消费队列中的事件,批量将游戏会话数据持久化到MongoDB (
game_sessions集合)。
- 关键操作 (如
-
读操作:
- 进行中的游戏: 优先从Redis读取实时状态。
- 历史游戏/玩家统计: 从MongoDB读取。
-
优势:
- 低延迟: 游戏核心逻辑的读写都在内存中完成,响应迅速。
- 高吞吐: 将对DB的写操作异步化和批量化,减轻数据库压力。
- 数据最终一致性: 即使后台Worker暂时失败,数据也保留在队列中,保证最终会持久化到MongoDB。