第4章 本地优先写入策略:写入即确认、乐观锁、本地索引、回滚补偿机制
4.1 边界与定义
本地优先写入(Local-First Write)是离线友好网络层的核心策略,其核心思想是:客户端写入操作首先在本地存储中完成,并立即向用户返回确认,再通过异步通道与远端同步。该策略面向的边界条件如下:
| 维度 | 边界范围 | 排除场景 |
|---|---|---|
| 网络状态 | 离线、弱网、高延迟、间歇性连接 | 实时强一致在线事务(如金融交易) |
| 数据一致性模型 | 最终一致性 + 客户端冲突检测 | 严格线性一致性 |
| 写入响应时间 | 本地写入后立即确认(<10ms) | 等待远端确认后才返回 |
| 索引失效窗口 | 本地索引即时生效,远端索引异步收敛 | 全局索引写入后必须立即可见 |
架构原则:本地优先写入策略不追求“写入即全局可见”,而是追求“写入即本地持久化、用户可感知、异步最终收敛”。这一交换是离线可用性的基石。
4.2 分层架构
策略落地需要四层协作,每层负责独立的职责域:
- 本地存储层(Local Storage Layer)——负责写入即确认、本地索引维护、版本号持久化;
- 冲突检测层(Conflict Detection Layer)——基于乐观锁实现版本比较与冲突标记;
- 同步调度层(Sync Scheduler Layer)——管理同步队列、重试策略、冲突上报;
- 补偿执行层(Compensation Layer)——执行回滚操作与补偿事务,保证本地状态不永久损坏。
层间调用遵循严格单向依赖:本地存储层 → 冲突检测层 → 同步调度层 → 补偿执行层。各层通过标准化事件总线通信,避免循环依赖。
4.3 组件职责
| 组件 | 归属层 | 核心职责 | 关键接口 |
|---|---|---|---|
| LocalWriter | 本地存储层 | 将数据写入本地数据库,持久化版本号,返回确认 | write(key, value, version) → ack |
| VersionKeeper | 冲突检测层 | 维护每个数据项的版本号,比较本地与远端版本 | compare(localVer, remoteVer) → conflict |
| IndexUpdater | 本地存储层 | 更新本地全文/结构索引,保证离线查询可用 | updateIndex(key, meta) → done |
| SyncDispatcher | 同步调度层 | 管理同步优先级、队列持久化、重试与超时 | enqueue(pendingOp) → jobId |
| CompensationManager | 补偿执行层 | 执行回滚、补偿事务、清理脏数据 | rollback(txId) → result |
设计要点:VersionKeeper 不直接参与写入路径,仅在同步阶段被调用,避免在本地写入关键路径中引入额外延迟。
4.4 数据流 / 控制流 / 信任链路
4.4.1 数据流
正常路径:客户端写入 → LocalWriter 持久化数据 + 版本号 → 立即返回 ACK → IndexUpdater 更新本地索引 → 数据进入 SyncDispatcher 队列 → 网络可用时推送远端 → 远端确认后标记已同步。
冲突路径:同步时 VersionKeeper 检测到版本冲突 → 标记冲突数据 → CompensationManager 挂起本地写入 → 执行回滚补偿 → 恢复队列继续同步。
4.4.2 控制流
- 写入阶段:写入请求 → 乐观锁预检(仅在本地)→ 写入并确认 → 索引更新 → 返回成功。控制权始终在客户端线程内,无远程等待。
- 同步阶段:SyncDispatcher 后台轮询 → 取出待同步项 → 调用 VersionKeeper 对比版本 → 若无冲突则推送远端;若冲突则触发 CompensationManager。
4.4.3 信任链路
信任链路分为三层:
- 客户端 → 本地存储:完全信任本地写入成功即有效,版本号由本地单调递增生成。
- 本地存储 → 同步调度:信任版本号未被篡改,同步时携带完整版本链。
- 同步调度 → 远端:远端以本地版本号为参考执行冲突检测,接受或拒绝后返回决策。
设计原则:信任本地版本号的单调性,但远端拥有最终裁决权。远端拒绝时,本地必须执行回滚补偿。
4.5 选型权衡
| 决策点 | 方案 A | 方案 B | 推荐场景 |
|---|---|---|---|
| 版本号生成器 | 本地单调递增整数 | 基于 Lamport 时钟 | 单设备写入选 A;多端交替写入选 B |
| 冲突解决策略 | Last-Write-Wins (LWW) | CRDT 合并 | 简单覆盖场景选 LWW;协作编辑选 CRDT |
| 本地索引引擎 | SQLite FTS5 | LevelDB + 自定义索引 | 结构化数据选 SQLite;KV 型选 LevelDB |
| 补偿方式 | 反向操作补偿 | 状态快照恢复 | 操作可逆时选反向补偿;不可逆时选快照 |
风险提示:使用 LWW 策略时,若本地时钟回拨或 NTP 同步异常,可能导致版本号倒置,触发错误覆盖。建议在 VersionKeeper 中额外引入写入计数器作为二级排序键。
4.6 风险点
- 写后未同步丢失:本地写入确认后,设备在同步前崩溃或数据损坏,导致写入永久丢失。落地对策:本地写入采用 WAL(Write-Ahead Log)保证持久化,同步队列使用独立表存储。
- 乐观锁冲突膨胀:大量冲突数据堆积导致同步阶段处理延迟,影响用户体验。落地对策:限制单次同步批大小,冲突数据优先告警。
- 索引与数据不一致:写入成功但索引更新失败,导致离线查询遗漏。落地对策:索引更新与写入绑定在同一个本地事务中,失败时整体回滚。
- 补偿操作重复执行:回滚补偿逻辑若未设计幂等,可能在重试时产生脏数据。落地对策:每个补偿操作携带全局唯一补偿 ID,由 CompensationManager 去重。
4.7 落地步骤
- 步骤 1:本地存储层搭建
选型本地数据库(SQLite / LevelDB),实现 LocalWriter 组件,支持写入时携带版本号字段,写入后立即返回确认。 - 步骤 2:乐观锁版本管理
实现 VersionKeeper,为每个数据项维护版本号计数器,提供compareAndMark接口。 - 步骤 3:本地索引集成
IndexUpdater 订阅写入事件,在同一个事务中更新索引表。索引表与数据表使用相同分区键。 - 步骤 4:同步调度与队列
SyncDispatcher 基于 FIFO 队列实现异步同步,支持优先级标记(如用户主动触发同步优先于后台同步)。 - 步骤 5:回滚补偿机制
CompensationManager 实现反向操作注册表,每个写入操作预注册补偿函数。同步失败时按注册顺序执行回滚。 - 步骤 6:集成测试与混沌演练
模拟离线 → 写入 → 同步 → 冲突 → 补偿的全链路场景,验证数据最终一致性。
落地检查点:每个步骤完成后,必须通过对应的验收标准(见 4.8 节)方可进入下一步,避免前期设计缺陷向下游扩散。
4.8 验收标准
| 验收项 | 方法 | 通过条件 |
|---|---|---|
| 写入即确认延迟 | 压测工具模拟 1000 次本地写入 | p99 延迟 < 15ms |
| 乐观锁冲突检测 | 构造版本号冲突场景(本地 vs 远端) | 100% 冲突被标记且触发补偿 |
| 本地索引一致性 | 写入后立即查询索引 | 查询结果与写入数据 100% 匹配 |
| 回滚补偿幂等性 | 同一补偿操作执行 3 次 | 最终状态与执行 1 次一致 |
| 同步队列持久化 | 写入后强制 Kill 进程,重启后检查队列 | 未同步项不丢失,恢复后继续同步 |
| 全链路数据收敛 | 离线写入 500 条,恢复网络后等待同步 | 远端数据与本地最终一致,无脏数据 |
验收顺序:建议先通过 1 → 3 → 2 → 5 → 4 → 6 的顺序执行,优先验证本地写入路径,再验证冲突和补偿等异常路径。