本地优先应用:离线可用与多端同步的架构

主流 App 的模式是"云端中心":数据在云端,本地只是视图——没网就瘫。本地优先(Local-First)相反:数据在本地,云端(如果有)只是同步通道。笔记、收藏、文档这类"个人资产"应用,本地优先是更优解。

一、两种模式的对比

维度 云端中心 本地优先
数据位置 云端为主 本地为主
离线 不可用 完全可用
速度 受网络影响 毫秒级
隐私 数据在别人那 数据在自己手里
同步 天然一致(单源) 需要冲突解决
备份 平台负责 自己负责

适用判断

二、核心架构

S Q / / L C i R t D / e T

三、核心问题 1:本地存储

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- 数据表 + 变更日志表
CREATE TABLE items (
  id TEXT PRIMARY KEY,        -- 全局唯一(UUID,不是自增)
  content TEXT,
  updated_at INTEGER,
  deleted INTEGER DEFAULT 0   -- 软删除(同步需要)
);

CREATE TABLE ops (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  item_id TEXT,
  op_type TEXT,               -- create/update/delete
  payload TEXT,
  ts INTEGER,
  synced INTEGER DEFAULT 0
);

关键

U U I D /

四、核心问题 2:同步

增量同步流程

A B o p s o p s l a s t _ s y n c _ i d
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 同步循环
def sync():
    # 1. 推送本端未同步变更
    unsynced = db.get_ops(synced=False)
    server.push(unsynced)
    # 2. 拉取对端变更
    new_ops = server.pull(last_sync_id)
    for op in new_ops:
        apply_op(op)          # 应用到本地 + 冲突解决
    # 3. 更新游标
    last_sync_id = new_ops[-1].id

冲突解决策略

策略 场景 实现
最后写入胜(LWW) 普通字段 比时间戳,后写覆盖
合并(Merge) 集合/列表 并集合并
CRDT 计数器/文本 数学保证最终一致
保留冲突 重要数据 存两份 + 用户选择

实践

/ L W M W e r g e " "

五、核心问题 3:多端一致性

A B 线 线 2 7 3 5 2 2 0 A 2

关键同步后两边数据必须一致(收敛性)——不然用户发现"这设备和那设备不一样"就失去信任。

六、Fluxio 的实践

F l u x 线 i o O b s i d S i / Q a L n i t e M a r k d o w n v a u l t

七、踩坑记录

  1. 自增 ID 撞车:两设备同时插入 → 用 UUID(gen_random_uuid)
  2. 客户端时间戳不可信:改时区/调时间 → 冲突解决用服务端时间戳
  3. 删除不同步:本地删了,另一设备还留着 → 软删除 + tombstone(墓碑)
  4. 同步循环:A 推到 B,B 又推回 A → 已同步标记 + 幂等应用(op 带 id,重复应用跳过)
  5. 大附件同步:图片全量传 → 增量/分片 + 去重(内容哈希)

总结

本地优先架构的核心认知:

  1. 本地是真身,同步是通道:没网也能用是卖点
  2. 三件套:UUID 主键 + 操作日志 + 软删除(同步的基建)
  3. 冲突解决要有策略:LWW/合并/CRDT/保留,按数据性质选
  4. 收敛性是底线:同步后各端一致,信任才成立

本地优先不是"技术选型",是"数据主权"的立场:你的数据在你手里,云端只是可选的备份与同步。做个人工具,这是最尊重用户的设计。