Files
DFJ/02_详细设计文档/数据库设计详设.md

210 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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