第 36 章
游戏数据库设计
游戏数据库设计实战
游戏数据库有独特的读写模式:极高的并发读写(百万级在线玩家)、大量实时更新(玩家位置/状态/道具)、超高频的排行榜查询、虚拟货币的精确性要求。MySQL 在游戏行业广泛使用,通常与 Redis 形成双剑合璧的架构。
游戏数据库的特点
| 特征 | 具体表现 | 应对策略 |
|---|---|---|
| 高频写入 | 玩家每次移动、攻击都触发数据更新 | Redis 缓存 + 异步落库 |
| 热点数据 | 热门区域/副本的玩家数据集中访问 | 分区、分服 |
| 排行榜查询 | 实时全服/区排名,百万级排序 | Redis Sorted Set |
| 道具属性灵活 | 武器有攻击力,衣服有防御,宠物有技能 | JSON 列 + 属性表 |
| 日志海量 | 每次战斗、充值、登录都要记录 | 单独日志库 + 分区 |
| 开服/合服操作 | 新服定期开,老服定期合并 | 分库 + 迁移工具 |
玩家账户体系
-- 账号表(全局唯一,跨服)
CREATE TABLE accounts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
account_name VARCHAR(50) NOT NULL COMMENT '登录账号',
password_hash VARCHAR(128) NOT NULL,
email VARCHAR(200),
mobile VARCHAR(20),
platform TINYINT NOT NULL DEFAULT 1 COMMENT '1=自建 2=微信 3=QQ 4=Apple',
platform_uid VARCHAR(100) COMMENT '第三方平台 UID',
register_ip VARCHAR(45),
last_login_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_account (account_name),
UNIQUE KEY uk_platform (platform, platform_uid),
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号表(全局)';
-- 角色表(每个账号可以有多个角色/分服角色)
CREATE TABLE players (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
account_id BIGINT UNSIGNED NOT NULL,
server_id SMALLINT NOT NULL COMMENT '大区/服务器 ID',
name VARCHAR(32) NOT NULL COMMENT '角色名(服务器内唯一)',
level SMALLINT NOT NULL DEFAULT 1,
exp BIGINT NOT NULL DEFAULT 0,
vip_level TINYINT NOT NULL DEFAULT 0,
-- 战斗力等核心属性
power INT NOT NULL DEFAULT 0,
hp INT NOT NULL DEFAULT 100,
mp INT NOT NULL DEFAULT 50,
-- 位置信息(实时更新走 Redis)
map_id INT COMMENT '最后所在地图',
pos_x FLOAT,
pos_y FLOAT,
-- 时间
last_online DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_server_name (server_id, name),
INDEX idx_account (account_id),
INDEX idx_server_level (server_id, level DESC),
INDEX idx_server_power (server_id, power DESC) -- 战力排行榜基础
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'
PARTITION BY LIST (server_id) (
PARTITION p_s1_100 VALUES IN (1,2,3,...,100),
PARTITION p_s101_200 VALUES IN (101,...,200),
PARTITION p_others VALUES IN (DEFAULT)
);
道具背包系统
-- 道具模板表(静态数据,通常从配置文件加载到 Redis)
CREATE TABLE item_templates (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
type TINYINT NOT NULL COMMENT '1=武器 2=防具 3=消耗品 4=材料 5=宠物',
quality TINYINT NOT NULL COMMENT '1=白 2=绿 3=蓝 4=紫 5=橙 6=红',
max_stack SMALLINT NOT NULL DEFAULT 1 COMMENT '最大堆叠数',
base_attrs JSON NOT NULL COMMENT '{"atk":100,"def":50,"spd":30}',
icon VARCHAR(200),
PRIMARY KEY (id),
INDEX idx_type_quality (type, quality)
) ENGINE=InnoDB;
-- 玩家背包(运行时数据)
CREATE TABLE player_items (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
player_id BIGINT UNSIGNED NOT NULL,
server_id SMALLINT NOT NULL,
item_id INT UNSIGNED NOT NULL COMMENT '道具模板 ID',
bag_slot SMALLINT COMMENT '背包格子位置(NULL=仓库)',
quantity INT NOT NULL DEFAULT 1,
level TINYINT NOT NULL DEFAULT 0 COMMENT '强化等级',
attrs JSON COMMENT '个体属性(随机词缀、强化加成)',
expire_at DATETIME COMMENT '限时道具过期时间',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_player (player_id, server_id),
INDEX idx_expire (expire_at) -- 定时清理过期道具
) ENGINE=InnoDB
PARTITION BY HASH (player_id) PARTITIONS 32; -- 按玩家 ID 分区,均匀分布
道具转移(原子操作)
-- 玩家 A 将道具转移给玩家 B
BEGIN;
-- 检查并扣除 A 的道具
UPDATE player_items
SET quantity = quantity - ?
WHERE id = ? AND player_id = ? AND quantity >= ?;
IF ROW_COUNT() = 0 THEN ROLLBACK; END IF;
-- 增加 B 的道具(若已有则叠加)
INSERT INTO player_items (player_id, server_id, item_id, quantity)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity);
COMMIT;
排行榜设计
实时排行榜是游戏最高频的查询场景之一。纯 MySQL 方案在百万级用户时性能差,生产环境几乎全部使用 Redis Sorted Set。
-- MySQL 持久化排行榜数据(定期快照)
CREATE TABLE leaderboard_snapshots (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
server_id SMALLINT NOT NULL,
season SMALLINT NOT NULL COMMENT '赛季',
rank_type TINYINT NOT NULL COMMENT '1=等级 2=战力 3=PVP积分',
player_id BIGINT UNSIGNED NOT NULL,
score BIGINT NOT NULL,
rank INT NOT NULL,
snapshot_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_snapshot (server_id, season, rank_type, player_id),
INDEX idx_rank (server_id, season, rank_type, rank)
) ENGINE=InnoDB
PARTITION BY RANGE (season) (
PARTITION p_s1 VALUES LESS THAN (2),
PARTITION p_s2 VALUES LESS THAN (3),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
// Redis Sorted Set 实现实时排行榜
key := fmt.Sprintf("lb:pvp:server:%d:season:%d", serverID, season)
// 更新分数
rdb.ZAdd(ctx, key, &redis.Z{Score: float64(newScore), Member: playerID})
// 查询排名(0-based,+1 变成 1-based)
rank, _ := rdb.ZRevRank(ctx, key, playerID).Result()
fmt.Println("排名:", rank+1)
// 查询 Top 100
top100, _ := rdb.ZRevRangeWithScores(ctx, key, 0, 99).Result()
// 查询玩家周围排名(上下 5 名)
rank, _ = rdb.ZRevRank(ctx, key, playerID).Result()
nearby, _ := rdb.ZRevRangeWithScores(ctx, key,
max(0, rank-5), rank+5).Result()
// 定期(每小时)将 Redis 排行榜快照到 MySQL
// 赛季结束时做最终快照,赛季奖励以此为准
虚拟货币体系
游戏虚拟货币(钻石、金币、点券)的设计原则与金融账户相同:必须精确、有流水、不可直接修改余额。
CREATE TABLE player_wallets (
player_id BIGINT UNSIGNED NOT NULL,
server_id SMALLINT NOT NULL,
-- 充值型货币
diamonds INT NOT NULL DEFAULT 0 COMMENT '钻石(充值)',
-- 游戏内产出货币
gold BIGINT NOT NULL DEFAULT 0 COMMENT '金币',
honor INT NOT NULL DEFAULT 0 COMMENT '荣誉点',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁',
PRIMARY KEY (player_id, server_id),
CONSTRAINT chk_diamonds CHECK (diamonds >= 0),
CONSTRAINT chk_gold CHECK (gold >= 0)
) ENGINE=InnoDB;
CREATE TABLE currency_logs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
player_id BIGINT UNSIGNED NOT NULL,
server_id SMALLINT NOT NULL,
currency TINYINT NOT NULL COMMENT '1=钻石 2=金币 3=荣誉',
change_type TINYINT NOT NULL COMMENT '1=充值 2=消费 3=任务奖励 4=退款',
amount INT NOT NULL COMMENT '变动量(正为收入,负为支出)',
balance INT NOT NULL COMMENT '变动后余额',
source_id VARCHAR(64) COMMENT '关联订单/任务 ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_player_time (player_id, server_id, created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p_2024_q1 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p_2024_q2 VALUES LESS THAN (TO_DAYS('2024-07-01')),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
匹配与战绩
-- 对战记录
CREATE TABLE battle_records (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
server_id SMALLINT NOT NULL,
battle_type TINYINT NOT NULL COMMENT '1=PVP 2=PVE 3=公会战',
started_at DATETIME(3) NOT NULL,
ended_at DATETIME(3),
duration_ms INT COMMENT '战斗时长(毫秒)',
winner TINYINT COMMENT '1=队伍1 2=队伍2 0=平局',
detail JSON COMMENT '详细战报(伤害/技能使用等)',
PRIMARY KEY (id),
INDEX idx_server_type_time (server_id, battle_type, started_at)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(started_at)) (
PARTITION p_30d VALUES LESS THAN (TO_DAYS(DATE_SUB(NOW(), INTERVAL 30 DAY))),
PARTITION p_cur VALUES LESS THAN MAXVALUE
);
-- 玩家战绩(每场战斗的参战玩家记录)
CREATE TABLE battle_players (
battle_id BIGINT UNSIGNED NOT NULL,
player_id BIGINT UNSIGNED NOT NULL,
team TINYINT NOT NULL,
damage_dealt BIGINT NOT NULL DEFAULT 0,
damage_taken BIGINT NOT NULL DEFAULT 0,
kills SMALLINT NOT NULL DEFAULT 0,
deaths SMALLINT NOT NULL DEFAULT 0,
score_delta INT NOT NULL DEFAULT 0 COMMENT 'PVP 分数变动',
PRIMARY KEY (battle_id, player_id),
INDEX idx_player (player_id)
) ENGINE=InnoDB;
行为日志(埋点)
-- 玩家行为日志(写多读少,单独数据库)
CREATE TABLE player_event_logs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
player_id BIGINT UNSIGNED NOT NULL,
server_id SMALLINT NOT NULL,
event_type VARCHAR(50) NOT NULL COMMENT 'login/logout/level_up/purchase/...',
event_data JSON NOT NULL,
client_ip VARCHAR(45),
device_id VARCHAR(100),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_player_event (player_id, event_type, created_at),
INDEX idx_date (DATE(created_at))
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p_d1 VALUES LESS THAN (TO_DAYS('2024-02-01')),
-- 按月分区,超过 6 个月的数据归档到 ClickHouse
PARTITION p_max VALUES LESS THAN MAXVALUE
) COMMENT='行为日志(建议 6 个月后归档到 ClickHouse/Doris)';
热更新配置数据
-- 游戏配置表(道具属性、关卡数据等,通常热更新)
CREATE TABLE game_configs (
config_key VARCHAR(100) NOT NULL,
config_val JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (config_key)
) ENGINE=InnoDB COMMENT='游戏配置(MySQL 持久化,Redis 缓存)';
-- 版本控制
CREATE TABLE game_config_history (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
config_key VARCHAR(100) NOT NULL,
config_val JSON NOT NULL,
version INT NOT NULL,
operator VARCHAR(50),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_key_version (config_key, version)
) ENGINE=InnoDB;
游戏数据库分服架构:国内手游通常采用"分服"架构,每个区服(server_id)在同一个 MySQL 实例上有独立的 Database,服务器合并时用数据迁移工具将两个 Database 的数据合并。这种架构天然隔离玩家群体,但跨服活动(排行榜、竞技场)需要聚合多个服的数据,通常通过专用的跨服 MySQL 实例实现。