共计 2496 个字符,预计需要花费 7 分钟才能阅读完成。
静态属性设计的现实困境
去年参与一款 MMORPG 技能系统改造时,遇到一个典型场景:战斗策划要求在『火球术』上新增『灼烧层数』属性。由于原系统采用 MySQL 静态表结构(skills表含 20 个固定字段),我们不得不:

- 执行
ALTER TABLE skills ADD COLUMN burn_stack TINYINT - 修改所有相关 API 的 DTO 定义
- 更新前端技能配置面板
整个过程涉及 3 个服务、5 个仓库的代码变更,上线后还因字段默认值问题导致旧技能数据异常。这种强耦合的设计在 3 个月内引发了 4 次紧急版本发布。
技术选型:Schema-less vs 关系型
关系型数据库的痛点
- 新增属性需 DDL 操作,生产环境需停机维护
- 稀疏字段导致存储空间浪费(如只有 5% 技能需要
channel_time字段) - 多表关联查询性能急剧下降
文档数据库的优势
// MongoDB 文档示例
{
"skillId": "fireball",
"name": "火球术",
"attributes": {
"damage": 150,
"burn_stack": 3, // 动态新增字段
"cooldown": {
"base": 5,
"reducible": true // 嵌套结构
}
}
}
通过基准测试,在 1000 万技能数据量下:
| 操作类型 | MySQL(ms) | MongoDB(ms) |
|---|---|---|
| 单条插入 | 12 | 8 |
| 条件查询 | 45 | 22 |
| 新增字段 | 需停机 | 即时生效 |
核心实现方案
动态属性建模
采用 JSON Schema 定义约束规则:
// schema/skill-attribute.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {"damage": { "type": "integer", "minimum": 0},
"cooldown": {
"type": "object",
"properties": {"base": { "type": "number", "minimum": 0},
"reducible": {"type": "boolean"}
}
}
},
"additionalProperties": true // 允许扩展属性
}
运行时校验
使用 Joi 进行动态验证:
import Joi from 'joi';
const attributeSchema = Joi.object().pattern(/^[a-z_]+$/,
Joi.alternatives().try(Joi.number(),
Joi.boolean(),
Joi.object().pattern(Joi.string(), Joi.any())
)
).default({});
function validateSkill(skill) {const { error} = attributeSchema.validate(skill.attributes);
if (error) {throw new Error(`Invalid skill attributes: ${error.message}`);
}
}
GraphQL 动态类型
利用 graphql-type-json 处理动态属性:
import {GraphQLJSON} from 'graphql-type-json';
const typeDefs = gql`
type Skill {
id: ID!
name: String!
attributes: JSON! # 动态类型
}
`;
const resolvers = {
JSON: GraphQLJSON,
Query: {skill: (_, { id}) => {return db.collection('skills').findOne({id});
}
}
};
性能优化实践
MongoDB 索引策略
对高频查询路径建立复合索引:
// 对 damage 和 cooldown.base 建立索引
db.skills.createIndex({
"attributes.damage": 1,
"attributes.cooldown.base": 1
}, {
background: true,
partialFilterExpression: {"attributes.damage": { $exists: true}
}
});
查询优化对比
测试环境(AWS c5.2xlarge):
# 查询 damage>100 且 cooldown.base<3 的技能
# 静态表方案
SELECT * FROM skills
WHERE damage > 100 AND cooldown_base < 3;
# 执行计划: 0.8 秒 (全表扫描)
# 文档方案
db.skills.find({"attributes.damage": { $gt: 100},
"attributes.cooldown.base": {$lt: 3}
}).explain("executionStats");
# 执行计划: 0.12 秒 (索引扫描)
生产环境注意事项
批量更新优化
采用批量有序更新避免锁竞争:
async function batchUpdateSkills(skillUpdates) {
const bulkOps = skillUpdates.map(update => ({
updateOne: {filter: { _id: update.id},
update: {$set: update.fields},
upsert: false
}
}));
await db.collection('skills').bulkWrite(bulkOps, {
ordered: false, // 并行执行
bypassDocumentValidation: false
});
}
Schema 版本控制
使用语义化版本管理 Schema 变更:
// 版本标识存入文档
{
"_schemaVersion": "1.2.0",
"attributes": {/* ... */}
}
// 升级流程
1. 部署新校验逻辑(兼容旧版)2. 后台任务渐进式迁移数据
3. 移除旧版兼容代码
开放性问题
当『眩晕』技能需要跨战斗服同步状态时:
- 如何解决网络分区导致的状态冲突?
- 最终一致性检查周期设置多少合理?
- 是否采用 CRDT 等无冲突数据结构?
欢迎在评论区分享你的分布式系统设计经验。
正文完
