做合并功能时,最容易被低估的往往不是算法,而是数据进入合并引擎之后,被表示成了什么。很多团队一开始用简单的文本 diff 顶着,直到字段重命名、列表移动、多端同时编辑这类问题接二连三出现,才意识到:合并的粒度不应该是“行”,也不应该是“整棵树”,而应该是“对象”。这就是 Object Model 在合并工具中如此关键的原因。
Livelymerge 从名字上就能看出它的定位:“Lively”表示活跃、连续、实时,“merge”表示合并。它不是一次性把两段文本拼在一起,而是要持续处理多个来源对同一份数据的修改。这种情况下,合并引擎必须对“这份数据里有哪些对象、对象之间有什么关系、每个对象的属性发生了什么变化”有一套自己的认识。这套认识,就是 Object Model。
本文会从合并场景下的实际痛点出发,讲清楚 Object Model 到底是什么,它和普通数据结构有什么区别,为什么像 Livelymerge 这样的合并引擎离不开它,并结合代码示例演示一个最小可用的对象合并流程。文章不会去翻译某个官方文档,而是尽量把原理、设计和工程落地串起来。如果你正在做数据同步、实时协作编辑、配置合并、多端一致性这类功能,这篇文章应该能帮你少踩几个坑。
1. Object Model 到底是什么:从合并场景说起
Object Model 翻译过来是“对象模型”。在 Java 里有 Java Object Model,在 JavaScript 里有 DOM 这样的文档对象模型,在数据库设计里也有概念模型、逻辑模型的说法。单看名字,它很容易被理解成“一组对象的集合”,或者“用对象表示数据”。但放到 Livelymerge 这类合并引擎的语境里,Object Model 的含义要更具体。
先看一个合并场景。假设有一个配置文件,内容是一段 JSON:
{ "user": { "name": "Alice", "age": 28, "address": { "city": "Beijing", "street": "Zhongguancun" } } }现在两个客户端分别修改了这份数据。客户端 A 把age改成了 29,客户端 B 把address.street改成了 "Wangjing"。合并时,我们希望结果是:age是 29,street是 "Wangjing",name和city保持不变。
如果用文本 diff 来做,遇到任何改动都可能引发整段冲突,因为文本比较无法理解“这两个改动其实修改的是不同属性”。如果用 JSON 树 diff 来做,虽然能按 key 定位,但一旦遇到数组元素的新增、删除、排序,就很容易错位。比如数组里原本有三个对象,两个客户端各删了一个,按索引比较就会误判成“一个被改、一个被删”,而不是“两个不同元素被删除”。
Object Model 要解决的就是这个问题。它把数据看作一个个具有独立身份、类型和属性的对象,合并时只针对对象的属性级变化进行处理。对上面的例子,Object Model 会先识别出user是一个对象,address是user的一个属性且本身也是对象,name、age、city、street是叶子属性。然后合并器比较每次修改涉及的最小范围,只把age的新值和street的新值合并进去,其余原样保留。
可以这样理解三者的区别:
| 合并粒度 | 看待数据的方式 | 典型问题 |
|---|---|---|
| 文本行 | 数据是一堆字符串行 | 无法感知字段语义,小改动引发整块冲突 |
| 树结构 | 数据是一棵 JSON/XML 树 | 数组、重命名、移动操作容易错位 |
| 对象模型 | 数据是一组有身份、有类型、有关联的对象 | 对引擎设计要求更高,但语义最清晰 |
所以,Object Model 不是“把数据变成对象”,而是“让合并引擎用对象视角去理解数据”。这个视角决定了合并引擎能处理多复杂的操作。
2. 为什么 Livelymerge 这类工具必须依赖 Object Model
Livelymerge 强调“Lively”,意味着数据不是静态的,而是持续变化的。典型的场景包括:
- 多人同时编辑同一份在线文档,做实时协同编辑。
- 一个配置中心同时接收多个环境的配置变更,做增量同步。
- 多个服务实例把自己的运行状态上报到同一套监控模型,做状态合并。
- 客户端离线修改本地缓存,重新联网后与服务端数据合并。
这些场景有一个共同点:数据会以多个独立来源、在多个时间点、以增量方式进入系统。如果不先建立 Object Model,合并操作就只能停留在“覆盖”或“拼接”层面。
举个例子。一个团队用同一个数据模型管理用户和权限,运维改了用户备注,产品同时改了角色名称,如果合并时没有对象的身份概念,系统可能认为“用户备注被删了,角色名称被新增了”,从而产生错误的结果。有了 Object Model,每个对象有稳定 ID,属性之间相互独立,合并器才能准确区分“新增”“修改”“删除”三类操作,并针对性地应用。
从实现层面看,Livelymerge 做实时合并时,通常要对数据做以下处理:
- 把输入解析成对象。
- 为对象分配或识别稳定标识。
- 按类型对对象分类,确定属性集合。
- 追踪每个属性的版本或修改时间。
- 在合并时只对发生变化的属性做冲突检测。
这套流程中,任何一步都依赖对象模型的定义。没有 Object Model,解析完数据后得到的只是散乱的 key-value,无法判断哪些 key 属于同一个对象,也无法知道两个 key 是否指向同一个概念。这意味着,Object Model 不是 Livelymerge 的一个附加功能,而是决定其合并能力上限的核心设计。
更直白地说:文本比较看到的是字符差异,树比较看到的是节点差异,对象模型看到的是“业务世界里的实体发生了什么改变”。Livelymerge 之所以叫 Lively,正是因为它要保持这种对象级认知,从而在持续变化的数据流中做语义正确的合并。
3. Object Model 的核心组成部分
一个适合合并引擎使用的对象模型,通常由下面几个部分组成。理解这些部分,比记住某个具体 API 更重要,因为不同工具的命名可能不同,但底层概念是相似的。
| 组成部分 | 解决的问题 | 常见实现方式 |
|---|---|---|
| 身份(Identity) | 区分哪些对象是同一个对象 | 全局唯一 ID、UUID、业务主键 |
| 类型(Type) | 知道对象的属性和行为约束 | 类名、type 字段、schema 定义 |
| 属性(Property) | 描述对象自身状态 | 基本类型字段、嵌套对象、数组 |
| 关系(Relationship) | 描述对象之间的引用和归属 | 外键引用、对象引用、链接 ID |
| 元数据(Metadata) | 记录修改人、版本、时间等信息 | version、updatedAt、author |
| 状态与版本(State & Version) | 判断合并时以哪个版本为基础 | 版本号、向量时钟、Lamport 时间戳 |
身份是整个对象模型里最容易出错的地方。合并时最怕的不是冲突多,而是本来应该是同一个对象的东西,被当成了两个不同对象。比如用户 A 在本地离线创建了一个“项目”对象,没有服务端 ID;用户 B 在同一时间创建了另一个“项目”对象,内容完全不同。服务端合并时,如果只用本地临时 ID 识别,就无法判断这两个对象是否应该合并成一个。
类型也很关键。合并引擎需要知道某个对象是“用户”“订单”还是“配置项”,才能决定它对属性的处理方式。有些人可能会说,JSON 本身有类型信息,string、number、boolean都是类型。但对象模型关心的是更高一层的语义类型。同样是字符串,头像 URL 和用户名在合并时的处理策略显然不同。
关系和属性之间的区别同样不能忽略。属性是对象自身的状态,关系则指向另一个对象。合并文档时,如果把“段落引用了图片”这种关系误当成普通属性,就可能导致引用的对象丢失。Object Model 设计得好的工具,会把属性和关系分开处理,在合并时先保证所有被引用的对象存在,再更新引用关系。
4. 对象模型如何驱动一个完整的合并流程
在 Livelymerge 这类工具中,一次基于对象模型的合并,通常可以拆成六个阶段。理解这个流程,对使用工具、排查问题、甚至自己实现一个合并器都有帮助。
4.1 解析数据
第一步是把输入格式(JSON、XML、YAML、二进制序列化等)转换成内存中的对象图。这一步的输出不是普通字典,而是带有类型和元数据的对象实例。比如一个user对象,解析后应该能通过object.type()得到“User”,通过object.id()得到其唯一 ID。
4.2 归一化
归一化是把不同来源的数据统一成同一套对象模型表示。有的来源可能把userName作为字段名,有的来源可能叫name,归一化后都需要映射成标准的name。归一化还能把嵌套对象打平成带关系的对象集合,方便后续索引。
4.3 建立索引
合并器需要快速找到“本地版本里的对象 X”和“远端版本里的对象 X”的对应关系。这一步通常基于对象 ID 建立哈希索引。如果对象没有稳定 ID,那么索引就无法建立,合并也就无法继续。
4.4 比对变化
比对发生在对象属性层面。合并器比较 base 版本、local 版本、remote 版本三个对象,找出每个属性在 local 和 remote 中分别发生了什么变化。这一步的输出是“属性级差异”,而不是“文档级差异”。
4.5 解决冲突
当 local 和 remote 同时修改了同一个属性,且修改结果不同,就产生冲突。冲突不一定要让用户手动处理。Livelymerge 这类工具通常会根据不同属性类型配置不同的冲突策略:有些字段可以自动合并,有些字段需要进入冲突队列由用户决定。
4.6 应用合并结果
最后把合并后的对象图写回目标存储。实时合并场景下,这一步还需要注意幂等性,防止重复应用同一批变更导致数据不一致。建议在写入前先做一次校验,确认没有循环引用、没有缺失必填字段。
下面用一个具体例子说明。假设 base 版本是:
Person(id="1", name="Alice", age=28, address=Address(city="Beijing"))客户端 local 修改为:
Person(id="1", name="Alice", age=29, address=Address(city="Beijing"))客户端 remote 修改为:
Person(id="1", name="Alice", age=28, address=Address(city="Shanghai"))在对象模型视角下,合并器看到的是:age属性只有一个来源修改了,属于无冲突修改;address属性也只有一个来源修改了,属于无冲突修改。最终合并结果应该是:
Person(id="1", name="Alice", age=29, address=Address(city="Shanghai"))这个结果在文本 diff 中很难自动得到,但在对象模型中非常自然。
5. 一个最小合并引擎示例:用对象模型合并 JSON
为了把概念落到实处,这里用一个最小示例演示“对象模型驱动合并”的基本思路。注意,示例是通用演示,不代表 Livelymerge 的官方 API,但思路是相通的。
5.1 Python 示例:按对象 ID 合并数组元素
# 文件路径:merge_demo.py import copy def is_plain_object(value): return isinstance(value, dict) def is_object_with_id(value): return is_plain_object(value) and "id" in value def merge_node(base, local, remote, path="$"): """ 基于对象模型的最小合并器。 - base: 合并前的基础版本 - local: 本地修改后的版本 - remote: 远端修改后的版本 返回 (merged, conflicts) """ # 如果 local 和 remote 都等于 base,直接返回 base if local == base and remote == base: return local, [] # 叶子值或者类型不一致,无法进一步拆分,直接交给冲突/覆盖逻辑 if not is_plain_object(local) or not is_plain_object(remote): if local == remote: return local, [] return None, [{"path": path, "local": local, "remote": remote}] merged = copy.deepcopy(base) if is_plain_object(base) else {} conflicts = [] # 以 local 和 remote 的键集合为基础,遍历所有可能出现变化的属性 all_keys = set(local.keys()) | set(remote.keys()) for key in all_keys: child_path = f"{path}.{key}" local_value = local.get(key, None) remote_value = remote.get(key, None) base_value = base.get(key, None) if is_plain_object(base) else None # 如果两个来源都没有修改该属性,保留 base if local_value == base_value and remote_value == base_value: if key in base and key not in merged: merged[key] = copy.deepcopy(base_value) continue # 如果两个来源的值相同,直接采用 if local_value == remote_value: merged[key] = copy.deepcopy(local_value) continue # 如果两边都是带 id 的对象,尝试对象级合并,而不是整块覆盖 if isinstance(local_value, dict) and isinstance(remote_value, dict): if is_object_with_id(local_value) and is_object_with_id(remote_value): if local_value.get("id") == remote_value.get("id"): sub_merged, sub_conflicts = merge_node( base_value if isinstance(base_value, dict) else {}, local_value, remote_value, child_path ) if sub_conflicts: conflicts.extend(sub_conflicts) else: merged[key] = sub_merged continue # 数组情况:尝试按元素 id 匹配 if isinstance(local_value, list) and isinstance(remote_value, list) and isinstance(base_value, list): merged_value, array_conflicts = merge_list(base_value, local_value, remote_value, child_path) merged[key] = merged_value conflicts.extend(array_conflicts) continue # 其他情况视为冲突 conflicts.append({ "path": child_path, "base": base_value, "local": local_value, "remote": remote_value }) # 如果 base 中存在但 local 和 remote 都不存在,说明是删除操作 if is_plain_object(base): for key in base.keys(): if key not in local and key not in remote: # 两个来源都删除了该属性,保持删除 merged.pop(key, None) return merged, conflicts def merge_list(base, local, remote, path="$"): """ 对数组进行基于对象 id 的合并。 这里只支持全量重算语义:根据 base/local/remote 推导出合并结果。 """ # 建立 base 的 id 映射 def index_by_id(items): result = {} for item in items: if isinstance(item, dict) and "id" in item: result[item["id"]] = item else: # 没有 id 的元素,用索引占位 result[f"__idx_{len(result)}__"] = item return result base_index = index_by_id(base) local_index = index_by_id(local) remote_index = index_by_id(remote) all_ids = set(local_index.keys()) | set(remote_index.keys()) merged_items = [] conflicts = [] for obj_id in all_ids: local_item = local_index.get(obj_id, None) remote_item = remote_index.get(obj_id, None) base_item = base_index.get(obj_id, None) if local_item == remote_item: if local_item is not None: merged_items.append(copy.deepcopy(local_item)) continue # 两个来源都修改了同一个对象 if isinstance(local_item, dict) and isinstance(remote_item, dict): if local_item.get("id") == remote_item.get("id"): sub_merged, sub_conflicts = merge_node( base_item if isinstance(base_item, dict) else {}, local_item, remote_item, f"{path}[{obj_id}]" ) merged_items.append(sub_merged) conflicts.extend(sub_conflicts) continue # 一个来自 base,一个发生了偏移,视为需要人工解决的冲突 conflicts.append({ "path": f"{path}[{obj_id}]", "base": base_item, "local": local_item, "remote": remote_item }) return merged_items, conflicts if __name__ == "__main__": base_data = { "users": [ {"id": 1, "name": "Alice", "age": 28}, {"id": 2, "name": "Bob", "age": 30} ] } local_data = { "users": [ {"id": 1, "name": "Alice", "age": 29}, {"id": 2, "name": "Bob", "age": 30} ] } remote_data = { "users": [ {"id": 2, "name": "Bob", "age": 31}, {"id": 1, "name": "Alice", "age": 29} ] } merged_data, conflict_list = merge_node(base_data, local_data, remote_data) print("merged:", merged_data) print("conflicts:", conflict_list)运行命令:
python3 merge_demo.py预期输出:
merged: {'users': [{'id': 1, 'name': 'Alice', 'age': 29}, {'id': 2, 'name': 'Bob', 'age': 31}]} conflicts: []这个示例的核心逻辑是:合并时不看数组里元素的先后顺序,而是通过id找到同一个对象,再深入到对象内部做属性级合并。remote_data把数组顺序颠倒了,但合并结果依然正确,因为对象模型已经识别出“order 变化不影响属性合并”。这正是文本 diff 做不到的地方。
5.2 Java 示例:用注解定义合并对象
在 Java 工程里,更常见的是用注解标记对象模型,再在合并工具中读取注解信息。
// 文件路径:src/main/java/com/example/merge/MergeEntity.java package com.example.merge; public class MergeEntity { @MergeId private Long id; @MergeField(strategy = MergeStrategy.LAST_WRITE_WINS) private String name; @MergeField(strategy = MergeStrategy.MANUAL) private String status; // getter 和 setter 省略 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } }对应的注解定义:
// 文件路径:src/main/java/com/example/merge/MergeId.java package com.example.merge; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MergeId { }// 文件路径:src/main/java/com/example/merge/MergeField.java package com.example.merge; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MergeField { MergeStrategy strategy() default MergeStrategy.LAST_WRITE_WINS; }// 文件路径:src/main/java/com/example/merge/MergeStrategy.java package com.example.merge; public enum MergeStrategy { LAST_WRITE_WINS, MANUAL }这样设计的好处是,合并引擎可以在运行时通过反射读取对象的@MergeId和@MergeField注解,判断哪些字段是身份字段、哪些字段可以采用自动策略、哪些字段必须人工确认。这在生产级合并工具中是常见套路。
5.3 对象模型的 schema 描述示例
如果是做配置类合并,可以单独维护一份对象模型 schema,让合并引擎按 schema 处理。
# 文件路径:model/person-model.yaml objectType: Person idField: id fields: - name: id type: string identity: true - name: name type: string mergeStrategy: LAST_WRITE_WINS - name: age type: int mergeStrategy: LAST_WRITE_WINS - name: address type: object refTo: Address mergeStrategy: DEEP_MERGE relationships: - name: roles type: many-to-many target: Role这个 schema 告诉合并引擎五件事:
Person对象用id字段识别身份。name和age是自动合并的叶子字段。address是引用Address对象的嵌套对象,需要深合并。roles是对象之间的关系,不是普通属性,合并时要检查目标对象是否存在。- 每个字段的合并策略可以单独配置。
实际项目中,Livelymerge 这类工具通常也会提供自己的 schema 或模型配置项,核心思想与此一致。使用工具前,先搞清楚它默认的 Object Model 结构,比直接写合并规则更重要,因为规则是建立在模型之上的。
6. 冲突类型与合并策略
对象模型建好了,合并流程中最关键的部分就是对冲突的处理。不同冲突类型适合不同策略,这里列一个相对完整的分类。
| 冲突类型 | 具体表现 | 推荐策略 | 说明 |
|---|---|---|---|
| 修改-修改 | 两个来源修改同一字段且结果不同 | 手动解决或 LAST_WRITE_WINS | 需要版本或时间戳辅助判断写入顺序 |
| 修改-删除 | 一个来源修改字段,另一个来源删除对象 | 默认以修改为准,或进入人工确认队列 | 删除操作影响面大,建议谨慎 |
| 重命名冲突 | 两个来源把同一字段改成了不同名字 | 手动映射 | 自动处理风险高 |
| 数组元素移动 | 两个来源对列表排序不一致 | 以 ID 匹配后忽略顺序差异,或记录顺序版本 | 顺序也是一种状态,单独保存 |
| 类型冲突 | base 中是字符串,local 改成了对象 | 强制类型校验并拒绝合并 | 类型变化通常意味着协议变更 |
| 关系冲突 | 两个来源修改了同一条引用关系 | 手动解决 | 关系变更会影响其他对象的一致性 |
策略的选择原则可以概括成几句话:
- 能自动合并的字段,尽量按属性粒度自动合并,不要动不动进入人工流程。
- 删除类操作永远是最危险的,建议强制走确认队列。
- LAST_WRITE_WINS 必须配合可靠的版本或时间戳,否则会出现乱序覆盖。
- 关系冲突不能简单按“谁后写谁赢”处理,因为关系引用指向的对象可能已经不存在。
Livelymerge 这类实时合并工具,通常会设计一种“先自动合并且记录日志,再把高风险冲突抛给用户”的流程。这样既保证了大多数情况下能自动完成,又不会在关键操作上悄悄埋雷。
7. 常见问题与排查思路
在实际接入对象合并时,遇到的问题往往不是“合并算法不工作”,而是“合并结果不符合预期”。下面整理几个高频问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 合并后字段丢失 | 对象没有稳定 ID,base和local被识别成不同对象 | 检查日志中的对象 ID 映射 | 为每个对象分配稳定的全局 ID |
| 数组顺序反复跳动 | 按索引比较数组元素,而不是按 ID | 查看比对阶段是否打印了元素索引 | 切换到基于 ID 的对象级数组合并 |
| 循环引用导致频繁报错 | 对象模型中直接使用对象引用,没有抽象成 ID 关系 | 检查对象图是否有 A->B->A 路径 | 关系字段只存 ID,合并时再解析 |
| 类型不一致被静默覆盖 | 合并引擎没有做类型校验 | 开启 schema 校验日志 | 在合并前增加类型校验步骤 |
| 重复合并产生重复数据 | 合并操作不是幂等的 | 检查同一批变更是否被重复提交 | 增加幂等控制,按 changeId 去重 |
| 性能差,合并一个大数据集耗时太长 | 对每个对象都做全量深拷贝 | 使用 profiling 查看热点 | 改为增量拷贝,只复制变化路径 |
排查这类问题时,建议第一步先打开合并引擎的差异日志。一个设计良好的对象模型合并器,应该能输出“哪个对象、哪个字段、从什么值改成了什么值”的细粒度日志。如果能输出,定位问题会快很多;如果只能输出“合并成功”或“合并失败”,排查难度会大很多。
8. 最佳实践与工程建议
把 Object Model 落地到真实项目中,有几个工程建议值得记住。
8.1 给对象分配稳定 ID
这一点排在所有建议前面。稳定 ID 是对象模型的基础,也是数组合并、关系合并、增量同步的前提。生产环境应该使用 UUID、Snowflake 或其他全局唯一 ID 方案,不要用内存地址、自增主键、随机数临时冒充 ID。离线编辑场景下,客户端生成的临时 ID 需要能映射到服务端最终 ID,否则合并会反复失败。
8.2 区分业务字段、元数据字段和关系字段
不要把所有东西都塞进同一个对象图。业务字段是真正需要合并的状态,元数据字段如createdAt、version、author用于合并辅助决策,关系字段描述对象之间的引用。三者混在一起,容易在合并时误伤。
建议在 Object Model 设计阶段就明确字段分类,并在 schema 中标注每个字段的类型。上面的 YAML 示例就是一种做法。
8.3 合并不等于覆盖,先 dry-run 再 apply
实时合并系统上线初期,一定要提供 dry-run 能力,也就是只输出合并结果预览,不真正写入。团队可以在测试环境模拟两个客户端同时修改的场景,观察合并结果是否符合预期,再切换到自动应用模式。
这个建议对配置中心、数据同步等场景尤其重要。生产环境直接自动覆盖,一旦出错,回滚成本很高。更稳妥的方式是:合并结果先落到一个待确认状态,通过人工或规则校验后再生效。
8.4 记录完整合并日志,包括操作人和版本
每次合并都应该记录:参与合并的 base 版本、local 版本、remote 版本、合并结果、采用的策略、最终操作人。这样出现问题后可以回溯。日志除了用于排错,还能帮助团队分析哪些字段经常冲突,从而调整合并策略。
8.5 合并前做权限校验,合并后做完整性校验
合并不是纯粹的技术操作,它会影响数据状态。在合并接口中,至少要做两层校验:操作人是否有权修改这些对象;合并结果是否满足对象模型的约束条件,比如必填字段是否存在、类型是否正确、引用关系是否有效。
安全边界设定为最小权限:每个操作人只能修改自己职责范围内的字段。比如普通成员可以合并自己的备注字段,但组织架构调整字段只有管理员能改。
8.6 新字段采用渐进式兼容策略
在对象模型演进过程中,最常见的问题是旧版本客户端不认识新字段。协议设计的经验法则是:新增字段时使用 optional 语义,默认值要与不设置该字段时的行为保持一致;删除字段前先废弃一段时间,给所有客户端足够的升级时间。这样合并引擎才能在多版本共存的场景下保持稳定。
9. 总结与后续学习方向
回到文章开头的问题:为什么合并功能做久了,最终都会绕回 Object Model?因为合并的本质不是把字符拼在一起,而是理解“哪个对象被谁改成了什么”。文本 diff 只知道“这行变了”,树 diff 只知道“这个节点变了”,Object Model 知道的是“这个用户对象的年龄字段被 A 客户端改成了 29,地址字段被 B 客户端改成了上海”。只有这种对象级认知,才能支撑 Livelymerge 这类工具的实时、连续合并能力。
如果你接下来要自己实践,建议按这个顺序推进:
- 用 Python 或 Java 实现本文中的最小合并器,把对象 ID 匹配和属性级合并跑通。
- 加入 schema 定义能力,把字段的合并策略抽离到配置层。
- 加入版本和时间戳,尝试解决乱序问题。
- 最后再考虑分布式场景下的冲突协调,比如 CRDT、向量时钟等更高级的方案。
如果是在项目里引入 Livelymerge,第一步不要急着写合并规则,先看它的 Object Model 文档里如何定义身份、类型和关系。把模型理解透,后面写规则会顺手很多。文章建议收藏备用,遇到合并相关的设计问题时,回来翻一翻这部分小节,应该能省不少排查时间。