模型文件的多人协作,麻烦之处不在于谁改了什么,而在于每个人的修改都建立在一个即将过期的基准版本上。两个人同时签出一份数据模型,一个把客户表的主键从自增整数改成 UUID,另一个基于旧结构给客户表增加了第三方登录字段。如果他们先后保存,后一个保存操作可能只会保留自己读到的旧结构,把对方的 UUID 改造静默覆盖。解决这个问题的思路大体分成两类:锁定机制在冲突发生前排除并发写入,合并策略则在冲突已经出现后识别差异、整合结果或转人工处理。下面从这两个方向展开。

一、模型锁定机制:以排他权限换取一致性
锁定机制的核心假设是:如果同一时刻只允许一个人修改同一份模型片段,就不会产生并发覆盖。它借鉴了数据库事务中的悲观锁思想。模型管理工具通常会在元数据层增加锁记录,而不是真正把文件设置为只读。锁记录一般包含目标对象、持有者、锁类型、过期时间等信息。例如一个对象级锁可以表示为:
{
"lockType": "object",
"targetId": "entity/Customer",
"owner": "zhang",
"expiresAt": "2024-07-21T10:45:00Z",
"scope": "write"
}
这个锁的作用是阻止其他人对 Customer 实体发起写操作,但不会影响 User、Order 等实体。服务端在每次保存请求时先查询目标对象是否存在未过期锁,如果有且持有者不是当前成员,就拒绝写入并返回锁持有者信息。锁的过期时间很关键,设置太长会导致成员误锁后长时间无法协作,设置太短又可能让长时间编辑的人中途失去锁。多数团队将默认过期时间设在 30 分钟到 2 小时之间,并允许在编辑过程中自动续期。
锁的粒度决定协作效率。文件级锁实现简单,只要某个模型文件被检出,其他人只能查看不能保存。但一个大型模型文件里可能有几十个实体,文件级锁会让无关修改也互相阻塞,比如一个人改客户表,另一个人改订单表,本可以并行处理,却因为同一个文件被锁而被迫排队。对象级锁更细,通常以实体、表、字段为单位,允许不同成员并行编辑不同对象。属性级锁最细,但元数据复杂度上升,锁冲突判断也要更频繁。很多团队选择对象级锁作为默认粒度,再对跨对象的结构调整临时升级到文件级锁。
乐观锁是另一个方向。它不阻止任何人编辑,而是在保存时检查基准版本是否已经变化。常见做法是记录版本号或内容哈希:客户端读取时得到 baseVersion,提交时携带 baseVersion;服务端如果发现当前版本不等于 baseVersion,就说明基准已经过期,必须重新拉取或走合并。这种方式适合冲突概率不高的场景,离线编辑也更友好。下面是一个版本检查的简化逻辑:
function tryCommit(model, baseVersion) {
const current = loadModel(model.id);
if (current.version !== baseVersion) {
throw new ConflictError(
"模型已被其他人修改,当前版本为 " + current.version
);
}
current.version += 1;
current.content = model.content;
save(current);
}
二、合并策略:从文本合并到结构感知的语义合并
当锁定没有拦截住,或者团队采用乐观锁后发生基准过期,就需要合并策略介入。文本文件的三路合并算法把冲突解决抽象成 base、ours、theirs 三个版本之间的差异计算。如果 ours 和 theirs 修改的是不同区域,算法会自动把两者叠加;只有当两者修改同一区域且内容不同时,才产生冲突块提示人工处理。模型文件大多具有结构化特征,直接照搬行级合并很容易得到非法结果。例如两个人分别在 JSON 模型文件的不同位置新增实体,按行合并通常没问题;但如果一个人删除某个实体,另一个人修改该实体内部字段,按行合并可能产生重复、残缺甚至非法 JSON。因此需要把合并粒度提升到模型元素级别。
结构感知合并依据元素 ID 而不是文本行。比如数据模型里的实体、字段、关系都有唯一标识,合并器先加载 base、ours、theirs 三个模型,构建元素映射,再逐个判断元素是新增、删除、修改还是未变。对于新增,如果两方新增同一个 ID 但内容不同则冲突;一方新增一方未变则直接采纳新增。对于删除与修改的组合,需要根据策略决定,通常提示人工确认。下面是一个 JSON 模型三路合并的简化表示,base、ours、theirs 三份内容如下:
{
"base": {
"entities": {
"Customer": {
"fields": { "id": "int" }
}
}
},
"ours": {
"entities": {
"Customer": {
"fields": { "id": "uuid", "name": "string" }
}
}
},
"theirs": {
"entities": {
"Customer": {
"fields": { "id": "int", "age": "int" }
}
}
}
}
结构合并器不会把整个 Customer 对象当作冲突块,而是继续下钻到字段级别。id 字段在 ours 中变成 uuid,在 theirs 中仍为 int,且两边都基于 base 的 int 修改,这就是真正的同一位置不同值,必须标记为冲突;而 name 和 age 分别由两方新增,互不干扰,可以自动并入结果。合并器输出的中间结果类似:
{
"entities": {
"Customer": {
"fields": {
"id": {
"status": "conflict",
"base": "int",
"ours": "uuid",
"theirs": "int"
},
"name": { "status": "added_by_ours", "value": "string" },
"age": { "status": "added_by_theirs", "value": "int" }
}
}
}
}
语义合并进一步提升自动化能力。同样是 id 字段,一边修改类型、另一边修改默认值,行级合并会认为是同一处冲突,但语义合并可以识别这两个修改属于不同的元数据属性,两者不互斥,自动合并成一个同时带有新类型和新默认值的字段。更难的是关系语义:一方删除外键关系,另一方删除被引用表,合并器需要识别删除依赖并阻止产生悬挂引用。通常做法是在合并后运行模型校验器,检查完整性约束、命名唯一性、继承链路是否断开;如果校验失败,则回退到人工合并。这样能保证自动合并结果不会被静默写入一个非法模型。
部分场景还可以配置自定义合并规则,例如字段重命名、实体移动等。简单做法是记录重命名映射,再在合并时应用到另一方的变更上。要求模型编辑器在每次保存时生成结构化 diff,而不是只保存快照。合并策略越早接入模型元数据,后续迁移脚本生成、数据库结构对比就越容易。
三、可落地的团队协作流程与自动化校验
单独有锁或单独有合并都无法覆盖所有情况,实际流程应当把两者串起来。推荐的做法是:编辑前获取对象锁,保存时做版本检查,版本过期则进入三路合并;自动合并完成后运行模型校验器,通过则写入并生成新版本,未通过则保留草稿并通知相关成员。这个流程的关键在于服务端必须同时具备锁记录、版本历史和合并器三个组件,否则编辑器只能退回成手工复制粘贴。
锁状态最好在编辑器的侧边栏和模型树中显示出来,避免成员之间互相等待。对象锁设置过期时间后,还要在释放或过期前把最后修改内容作为检查点保存到服务端。团队也可以借助版本控制的分支策略降低冲突范围:重大结构调整走独立分支,日常字段修改在主干上做小步提交,这样合并频率降低,冲突范围更小。对于需要长时间离线编辑的成员,乐观锁配合结构化合并会比长期持锁更合适。
下面给出一个简化后的模型合并器主流程,用来表达结构合并的完整思路。它先按 ID 构建索引,再比较 base、ours、theirs 三种状态,最后调用 validate 做模型合法性校验:
function mergeModel(base, ours, theirs) {
const baseMap = indexById(base);
const oursMap = indexById(ours);
const theirsMap = indexById(theirs);
const result = { entities: {} };
for (const id of unionKeys(baseMap, oursMap, theirsMap)) {
const b = baseMap[id];
const o = oursMap[id];
const t = theirsMap[id];
if (!b && o && t && !same(o, t)) {
result.entities[id] = conflict(o, t);
} else if (!b && o) {
result.entities[id] = o;
} else if (!b && t) {
result.entities[id] = t;
} else if (b && o && !t) {
result.entities[id] = deleteMarker();
} else if (b && !o && t) {
result.entities[id] = deleteMarker();
} else if (o && t && !same(o, t)) {
result.entities[id] = mergeElement(b, o, t);
} else {
result.entities[id] = o || t || b;
}
}
return validate(result);
}
模型锁定和合并策略不是二选一,而是协作链路里的两个环节。锁降低冲突概率,合并在冲突后兜底;结构感知合并减少误判,校验器阻止非法结果入库。团队应该根据模型规模、成员并行度和编辑工具能力选择悲观锁或乐观锁,并在任何情况下都保留结构化 diff、三路合并和校验器这三道保险。把规则固化到流程中,比每次冲突后手工修复更可靠。