# 数据库设计详设文档 > **文档版本**: 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 定义**: ```typescript 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 定义**: ```typescript 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`: `string` - `player2Id`: `string` - `player1Ready`: `'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`: `userId` - `score`: `eloRating` (整数) - **操作**: - **更新排名**: `ZADD leaderboard:elo ` - **查询Top 100**: `ZREVRANGE leaderboard:elo 0 99 WITHSCORES` - **查询玩家排名**: `ZREVRANK leaderboard:elo ` ### 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. 数据一致性策略 - **写操作**: 1. **关键操作** (如`EXECUTE_ATTACK`): - 开启**分布式锁**。 - 更新Redis中的**实时游戏状态** (`game:state:{gameId}`)。 - 将操作事件**异步写入**一个队列(如Redis Stream或RabbitMQ)。 - **释放锁**。 - 立即向客户端返回成功响应。 2. 一个独立的**后台Worker**消费队列中的事件,批量将游戏会话数据持久化到MongoDB (`game_sessions` 集合)。 - **读操作**: - **进行中的游戏**: 优先从**Redis**读取实时状态。 - **历史游戏/玩家统计**: 从**MongoDB**读取。 - **优势**: - **低延迟**: 游戏核心逻辑的读写都在内存中完成,响应迅速。 - **高吞吐**: 将对DB的写操作异步化和批量化,减轻数据库压力。 - **数据最终一致性**: 即使后台Worker暂时失败,数据也保留在队列中,保证最终会持久化到MongoDB。