一、前置思考
分布式应用的真正考验在于"离线"——用户在地铁上没有网络,手机上的修改如何在到家后自动同步到平板?用户在山里拍照,如何在下山后自动将照片流转到PC?鸿蒙的离线分布式能力让应用在无网络环境下也能正常使用,恢复网络后自动同步。
本文聚焦:
- 离线操作队列(Operation Queue)的设计
- 本地优先原则(Local-First)与Sync-on-Reconnect
- 操作日志(WAL:Write-Ahead Log)的实现
- 上线后批量冲突解决策略
真实痛点场景:
- 离线修改丢失:没网时改了数据,联网后发现全部回到旧状态
- 同步风暴:上线后N个设备的离线修改同时涌来
- 时间戳混乱:离线时设备时间不准,导致同步以错误的时间戳排序
- 巨大操作日志:离线3天的操作日志膨胀到几百MB
二、核心原理
2.1 离线优先架构
┌────────────────────────────────────────┐ │ Local-First 架构 │ │ │ │ 用户操作 ──→ 本地SQLite (WAL模式) │ │ │ │ │ ▼ │ │ 操作日志队列 (OpLog) │ │ ┌──┬──┬──┬──┬──┐ │ │ │op│op│op│op│op│ 待同步 │ │ └──┴──┴──┴──┴──┘ │ │ │ │ │ ┌─────────▼──── (网络恢复) ────┐ │ │ │ 增量同步 PUSH │ │ │ │ 冲突解决 (CRDT/LWW) │ │ │ │ 合并到目标端 │ │ │ └──────────────────────────────┘ │ └────────────────────────────────────────┘2.2 操作队列设计
interfaceOperation{opId:string;// 全局唯一操作IDopType:string;// 'PUT' | 'DELETE' | 'UPDATE'kvKey:string;// 操作的KeykvValue:string|null;// 操作的Value(null=DELETE)timestamp:number;// 操作时间戳 (使用Clock向量)deviceId:string;// 操作设备IDstatus:OpStatus;// PENDING / SYNCED / CONFLICT}enumOpStatus{PENDING=0,// 等待同步SYNCED=1,// 已同步CONFLICT=2,// 冲突待解决DROPPED=3// 已丢弃}classOfflineOperationQueue{privatequeue:Operation[]=[];privatereadonlyMAX_QUEUE_SIZE:number=5000;// 记录操作recordOperation(type:string,key:string,value:string|null):void{constop:Operation={opId:this.generateOpId(),opType:type,kvKey:key,kvValue:value,timestamp:Date.now(),deviceId:this.deviceId,status:OpStatus.PENDING};this.queue.push(op);// 操作日志压缩:相同key只保留最新操作this.compressOps(key);// 限制队列大小if(this.queue.length>this.MAX_QUEUE_SIZE){this.queue.shift();}}// 压缩:同一key只保留最新操作privatecompressOps(key:string):void{letlatestIdx:number=-1;for(leti:number=0;i<this.queue.length;i++){if(this.queue[i].kvKey===key){if(latestIdx>=0){// 删除前一个旧操作this.queue.splice(latestIdx,1);i--;}latestIdx=i;}}}// 获取待同步操作getPendingOps():Operation[]{constpending:Operation[]=[];for(leti:number=0;i<this.queue.length;i++){if(this.queue[i].status===OpStatus.PENDING){pending.push(this.queue[i]);}}returnpending;}// 批量标记已同步markSynced(opIds:string[]):void{for(leti:number=0;i<this.queue.length;i++){constop:Operation=this.queue[i];for(letj:number=0;j<opIds.length;j++){if(op.opId===opIds[j]){op.status=OpStatus.SYNCED;break;}}}}privategenerateOpId():string{returnthis.deviceId+'-'+Date.now()+'-'+Math.random().toString(36).slice(2,6);}privatedeviceId:string='d1';}2.3 Sync-on-Reconnect
classSyncOnReconnectManager{privateopQueue:OfflineOperationQueue;privatekvStore:distributedKVStore.SingleKVStore|null=null;constructor(opQueue:OfflineOperationQueue){this.opQueue=opQueue;}// 网络恢复时的同步asynconNetworkRestored():Promise<SyncResult>{constpendingOps:Operation[]=this.opQueue.getPendingOps();if(pendingOps.length===0){return{syncedCount:0,conflictedCount:0};}console.info('[Sync] 开始同步'+String(pendingOps.length)+'个离线操作');letsyncedCount:number=0;letconflictedCount:number=0;if(this.kvStore!==null){awaitthis.kvStore.startTransaction();for(leti:number=0;i<pendingOps.length;i++){constop:Operation=pendingOps[i];try{awaitthis.applyOperation(op);syncedCount++;}catch(e){op.status=OpStatus.CONFLICT;conflictedCount++;}}awaitthis.kvStore.commitTransaction();}// 标记已同步constsyncedIds:string[]=[];for(leti:number=0;i<pendingOps.length;i++){if(pendingOps[i].status===OpStatus.PENDING){syncedIds.push(pendingOps[i].opId);}}this.opQueue.markSynced(syncedIds);return{syncedCount:syncedCount,conflictedCount:conflictedCount};}privateasyncapplyOperation(op:Operation):Promise<void>{if(this.kvStore===null)return;switch(op.opType){case'PUT':awaitthis.kvStore.put(op.kvKey,op.kvValue??'');break;case'DELETE':awaitthis.kvStore.delete(op.kvKey);break;case'UPDATE':awaitthis.kvStore.put(op.kvKey,op.kvValue??'');break;default:break;}}}interfaceSyncResult{syncedCount:number;conflictedCount:number;}三、避坑速查
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 离线日志膨胀 | 操作队列占用几百MB | 未压缩或限制大小 | 同key只保留最新操作+MAX_QUEUE_SIZE限制 |
| 同步时序错乱 | 旧数据覆盖新数据 | 离线时间戳不准 | 使用逻辑时钟(Lamport Clock)替代物理时间 |
| 重复同步 | 离线操作重复执行 | 幂等性不足 | 每条op分配全局唯一opId,目标端去重 |
| 大量冲突 | 上线后冲突解决卡顿 | 一次性处理所有离线op | 分批同步,每批100条,每批间隔200ms |
四、总结
离线分布式能力的核心:
- 本地WAL操作日志 → 先写本地,再同步
- 操作压缩 → 同key只保留最新
- Sync-on-Reconnect → 网络恢复自动同步
- 分批同步 → 避免同步风暴