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

7.8 KiB
Raw Blame History

数据库设计详设文档

文档版本: 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 (如果statusin-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。