跳到主要内容
约 12 分钟7 张卡

客户端数据同步

客户端数据同步是在网络不稳定、请求可能重复、多个设备都可能修改数据的条件下,让本地状态与服务端状态按业务规则趋于一致。它不只是定时刷新缓存,还要保存用户尚未提交成功的意图,并在进程退出后继续处理。

面试中最有价值的推演通常发生在两个动作之间:本地已经保存,但还没发请求;服务端已经提交,但响应没回来;旧请求返回时,用户又做了新修改。把这些状态和恢复动作讲清楚,才能判断一个同步方案会不会丢数据或重复执行。

1. 面试核心​

先记住这几条,再读正文
  • 缓存、用户修改和同步进度承担不同责任。可以丢弃的响应副本,不能和唯一保存的离线草稿一起清理。卡 · 本地状态的责任
  • 界面读哪个数据源,与谁裁定业务结果是两个问题。界面可以统一观察本地数据库,服务端仍负责权限、共享版本和业务约束。
  • 本地业务变化与待发送记录要原子持久化。否则可能显示已经修改,却没有任何任务能把它同步出去。卡 · 保存业务变化与发送意图
  • 超时表示结果未知,不表示服务端没有执行。同一业务意图重试使用稳定操作 ID,并依靠服务端的幂等协议避免重复副作用。卡 · 服务端提交后,响应可能丢失
  • 同步确认只能确认它对应的那次修改。旧操作成功,不能顺手把用户后来输入的新内容标成已同步。卡 · 旧确认不能清掉新修改
  • 版本冲突需要业务规则处理。条件写入可以发现过期版本,但不会自动决定保留谁的修改;设备时间也不是可靠的全局顺序。卡 · 版本与冲突
  • 增量数据与游标要在一致的本地边界内提交。游标先前进而数据没保存,会在重启后跳过变更。卡 · 增量同步与重新同步
  • 后台调度和网络状态只提供执行机会。同步必须允许中断、重复与恢复,并保留可见的待处理、失败和冲突状态。卡 · 按错误性质恢复

2. 本地状态的责任​

2.1 哪些内容可以重新获取​

缓存保存可以从其他来源重新取得的数据;待同步修改保存尚未被远端确认的用户意图。两者都可能存在数据库里,但丢弃它们的后果不同。

数据示例清理或覆盖前要判断什么
远端内容副本上次拉取的公开文章列表是否能重新获取、是否允许暂时显示旧数据
本地草稿与待提交内容离线编辑的笔记是否还有其他可靠副本,用户是否确认放弃
已确认的远端版本笔记的版本 v7后续修改基于哪一版,需要怎样检测冲突
待发送操作操作 ID、请求内容、重试状态是否已发送、结果是否未知,能否合并或取消
同步进度增量游标、最近成功时间对应数据是否已经持久化,进度是否仍有效

HTTP 响应缓存解决新鲜度与重新验证,不能代替离线操作队列。相关请求语义见 HTTP 缓存。

2.2 界面读本地,不等于本地裁定一切​

一种离线优先设计是让界面统一观察本地数据库:同步器拉取远端结果后先更新数据库,界面随本地状态变化。这样不会一会儿显示网络返回、一会儿又被旧缓存覆盖。

服务端仍负责跨设备共享状态、权限和业务约束。本地显示“已保存”可以只表示已经安全落盘;“已同步”应表示对应修改获得服务端确认。余额扣减、抢库存等业务不能因为支持离线界面,就在客户端承诺最终成功。

读到旧数据后后台刷新是可用策略,但新响应写入前要检查请求所属账号、查询条件和版本。页面已经切换到账号 B 时,账号 A 的晚到响应不应覆盖 B 的数据;取消请求只是减少工作,还应验证响应归属。

3. 离线写入与待发送队列​

3.1 保存业务变化与发送意图​

待发送表(outbox)把要交给远端处理的操作持久化。用户提交修改时,在同一个本地事务中保存业务状态和待发送记录。进程随后被系统结束,重启后仍能找回未完成操作。

下面用 SQLite 展示“把笔记标题改成新标题,并登记一次操作”的原子边界。先创建表和初始数据,再执行修改事务:

PRAGMA foreign_keys = ON;
CREATE TABLE notes (
account_id TEXT NOT NULL,
note_id TEXT NOT NULL,
title TEXT NOT NULL,
local_revision INTEGER NOT NULL,
server_version TEXT NOT NULL,
sync_state TEXT NOT NULL CHECK (sync_state IN ('synced', 'pending', 'conflict')),
PRIMARY KEY (account_id, note_id)
);
CREATE TABLE outbox (
operation_id TEXT PRIMARY KEY NOT NULL,
account_id TEXT NOT NULL,
note_id TEXT NOT NULL,
local_revision INTEGER NOT NULL,
base_version TEXT NOT NULL,
payload_title TEXT NOT NULL,
FOREIGN KEY (account_id, note_id) REFERENCES notes(account_id, note_id)
);
INSERT INTO notes VALUES ('account-a', 'note-1', '旧标题', 0, 'v7', 'synced');

BEGIN IMMEDIATE;
UPDATE notes SET title = '新标题', local_revision = local_revision + 1,
sync_state = 'pending'
WHERE account_id = 'account-a' AND note_id = 'note-1';
INSERT INTO outbox
SELECT 'op-a1', account_id, note_id, local_revision, server_version, title
FROM notes WHERE account_id = 'account-a' AND note_id = 'note-1';
COMMIT;

提交后,笔记的 local_revision 为 1,状态为 pending,outbox 保存同一修订的标题和基础版本 v7。如果插入因约束失败,调用方必须回滚整个事务,不能捕获错误后继续提交前面的 UPDATE;SQLite 的语句失败不一定自动回滚整个事务。

示例只演示一次编辑,op-a1 是便于阅读的操作 ID。实际应用生成稳定且不冲突的 ID,使用绑定参数,检查 UPDATE 和 INSERT 确实作用于目标行;缺少目标行不能被当成编辑成功。事务与绑定参数的机制见 SQLite。

3.2 排队不等于可以任意重排​

同步器读取待发送记录,按依赖关系发送。对同一笔记先创建再修改,或先上传附件再引用附件,都有顺序约束;多个执行器竞争同一队列时,还需要原子领取或串行协调,不能只在内存里设一个 isSyncing。

为降低复杂度,可以让同一实体一次只有一个已发送但未确认的操作。后续编辑仍保存在本地,等前一操作确定结果、基础版本更新后再形成下一次发送。若采用多操作流水线,则必须额外定义版本链、失败传播和回放规则。

尚未发送的连续草稿更新可能合并,但不能无条件合并计数增加、订单创建等业务动作。已经发送过的操作不能改请求内容后继续使用同一个操作 ID,否则服务端无法判断它到底是哪一次意图。

4. 重试与结果确认​

4.1 服务端提交后,响应可能丢失​

考虑下面的顺序:

步骤客户端知道的状态服务端实际状态
发送操作 op-a1等待响应尚未处理或正在处理
服务端提交修改仍在等待修改已经成功
连接断开,客户端超时结果未知修改仍然成功
用相同 ID 和内容重试请求确认原操作根据幂等记录返回原结果

如果超时后生成一个新 ID,服务端可能把它当成另一笔操作。客户端防重复点击只能阻止一部分入口,不能覆盖重试、重启或多个设备。

幂等协议需要服务端配合:明确账号与接口作用域、同 ID 不同参数的处理、处理中状态、结果保存期限,以及业务提交与幂等记录的一致性。具体实现见 幂等键。不能仅添加一个名为 Idempotency-Key 的请求头,就假设所有服务端都支持相同语义。

4.2 旧确认不能清掉新修改​

假设发送的操作对应 local_revision = 1,等待期间用户又编辑到修订 2。修订 1 成功后,可以移除该操作的待发送记录,并保存它返回的远端版本;但修订 2 仍然待同步,不能把整个笔记无条件设成 synced。

确认逻辑至少要核对账号、实体、操作 ID 和发送时的本地修订。只有当前内容仍对应已确认修订时,才能把它标为已同步;存在后续修改时保留本地内容,再基于新确认的远端状态准备后续操作。服务端如果会规范化标题等内容,也不能用旧操作返回值覆盖更新的用户输入。

本地保存确认结果与删除该操作记录应在同一事务中完成。进程若在收到响应后、保存确认前退出,恢复时仍会重试原操作,因此远端幂等记录的保留期限必须覆盖约定的重试窗口。超出期限需要查询结果或进入人工确认流程,不能继续假设一定安全。

4.3 按错误性质恢复​

短暂连接失败可以按预算退避重试,参数错误通常需要修正,鉴权失效需要恢复身份,版本冲突需要进入冲突流程。不能把所有失败都无限重试,也不能把所有 HTTP 失败都转换成“网络不好”。

退避、随机抖动、最大尝试次数与总时限的设计见 重试。网络已连接只意味着值得尝试,DNS、TLS、服务可用性和账号权限仍可能失败。

5. 版本与冲突​

5.1 条件写入发现过期基础​

基础版本说明这次编辑是基于哪份远端状态产生的。设备 A 和 B 都读到 v7,A 先修改成 v8,B 再提交基于 v7 的完整内容,就可能覆盖 A 的变化。

HTTP 可以使用服务端提供的强 ETag 配合 If-Match。条件不满足时,服务端通常返回 412 Precondition Failed,让客户端处理冲突,而不是直接覆盖。If-Match 使用强比较,弱 ETag 不能充当同样的修改前置条件;ETag 是不透明验证器,不能把其文本大小当作版本先后。

读取时用于重新验证的 If-None-Match,与修改时防止丢失更新的 If-Match 目的不同。客户端也可以使用业务版本号,但比较和写入必须在服务端同一个受保护的边界内完成;先单独读版本、再无条件写入,仍然存在竞争窗口。

5.2 冲突处理由业务决定​

策略可用场景必须接受或解决的代价
服务端版本优先本地副本允许直接刷新不能静默丢掉用户尚未同步的重要修改
最后写入者胜出业务明确允许覆盖的状态必须定义顺序,设备时钟偏差可能误判
字段或操作合并不同字段修改,或有明确合并语义同字段竞争、删除与引用仍需规则
保留冲突供用户选择笔记等不能静默丢失的编辑需要保存双方版本并提供可理解的操作

不能因为两个修改都叫“更新”,就用一个时间戳选择赢家。金额、库存、点赞状态和正文编辑有不同约束;“增加 1”和“设成 1”也不是同一种操作。

解决冲突后,基于确认的最新版本产生新的操作与 ID。原操作仍代表原来的内容,不能把解决后的请求伪装成它的重试。

6. 增量同步与重新同步​

6.1 数据与游标共同提交​

增量游标标识服务端变更流中的读取位置。客户端拿到一批变更和下一游标后,在一个本地事务中应用该批数据并保存游标;全部成功才进入下一批。

如果先保存游标再写数据,崩溃后可能跳过未落盘的变更。若先写数据却没保存游标,下次可能重复收到这批内容,因此应用变更仍需能识别实体版本和重复记录。一次本地事务可以缩小恢复边界,不能让远端传输变成恰好一次。

游标通常是不透明值,不能自行加一或从客户端时间拼出来。分页期间也要遵循服务端定义的快照与排序规则;普通列表分页未必就是可恢复的增量协议。

6.2 删除、过期与全量恢复​

服务端删除某条记录后,如果客户端只拉取“还存在的记录”,就不知道本地那份也应消失。增量协议需要删除事件或保留一定时间的删除标记,并定义离线过久的客户端如何恢复。

游标失效、变更日志过期或本地结构迁移后,可能需要重新获取完整远端状态。全量恢复也不能直接清空尚未同步的草稿和操作:先区分远端副本与本地意图,再决定重放、冲突或由用户处理。

远端新状态覆盖到本地时,应保留待同步修改,或用“远端基线加本地修改”计算显示结果。对同一实体并发执行推送和拉取,还需要协调版本,避免拉取旧快照盖掉刚完成的更新。

7. 调度、账号与验证​

后台任务可能延期、被中断或重复运行。Android 可以按任务性质使用 WorkManager;iOS 需要选择系统允许的后台策略。调度提供机会,同步正确性仍依赖持久队列、检查点和可恢复状态,不能靠“每隔几分钟一定执行一次”保证一致。

账号切换时,同步状态、缓存键、操作归属和凭据都要分清。旧账号未完成的操作不能拿新账号凭据继续发送;退出登录后如何保留或删除草稿,要由明确的产品规则决定。

验证应覆盖这些可以注入的中断点:本地事务失败、提交后进程退出、服务端完成但响应丢失、重复发送、旧确认晚到、版本冲突、游标更新失败和账号切换。检查的是最终数据与待处理状态,不只是请求次数或日志里出现“成功”。

8 · 面试题卡片​

本页题目与参考回答

8.1 离线草稿和 HTTP 缓存为什么不能使用同一种清理策略?​

考察重点:数据责任与丢弃后果。

相关内容:本地状态的责任

参考回答

缓存通常可以重新获取,未同步草稿可能是用户修改的唯一可靠副本。 清理前要区分远端副本、用户意图与同步进度。本地已保存也不等于远端已确认,界面应表达这种差别。

8.2 为什么修改本地数据和写入 outbox 要放在同一事务里?​

考察重点:本地原子性与恢复入口。

相关内容:保存业务变化与发送意图

参考回答

要么业务变化和发送意图一起保存,要么一起失败,避免只留下其中一半。 否则可能显示修改成功却没有任务能同步,或发送了没有对应本地状态的操作。语句失败后还需正确回滚整个事务,不能继续提交前面的变化。

8.3 提交请求超时后,可以换一个操作 ID 再试吗?​

考察重点:结果未知、重复副作用与幂等协议。

相关内容:服务端提交后,响应可能丢失

参考回答

同一业务意图应保留原 ID 和请求内容,按约定查询或重试。 超时不代表服务端未执行,新 ID 可能被当成另一笔操作。安全重试还依赖服务端作用域、参数校验、原子提交和结果保留期限。

8.4 修订 1 同步成功时,本地已经编辑到修订 2,应该怎样处理?​

考察重点:确认归属、版本与后续修改。

相关内容:旧确认不能清掉新修改

参考回答

只确认修订 1 对应的操作,保留修订 2 的内容和待同步状态。 保存返回的远端版本,再按队列策略准备后续操作。不能用旧响应覆盖新输入,也不能无条件把整条记录标成已同步。

8.5 If-Match 能解决多设备编辑的全部冲突吗?​

考察重点:条件写入与业务合并的分工。

相关内容:版本与冲突

参考回答

它可以发现基础版本已经变化,不能自动决定怎样合并内容。 强 ETag 的条件不满足时通常得到 412,客户端再按业务选择保留、合并或让用户处理。解决后的新意图使用新的操作 ID,不能修改原操作内容后复用其 ID。

8.6 增量游标为什么要和这一批数据一起提交?​

考察重点:检查点、崩溃窗口与重复应用。

相关内容:增量同步与重新同步

参考回答

游标表示哪些变更已经安全保存,不能先于对应数据前进。 同一事务能避免进度与数据不一致;仍要支持重复批次和删除事件。游标过期后的全量恢复也要保护未同步修改,不能直接清库。

8.7 只在网络恢复时启动后台同步,是否就能保证最终同步成功?​

考察重点:执行机会、错误分类与持久恢复。

相关内容:按错误性质恢复、调度、账号与验证

参考回答

不能,连网不代表服务可用,后台任务也可能延期或中断。 需要持久队列、可重复执行的协议、退避预算和恢复检查点;鉴权、参数与版本冲突要分别处理。最终成功还依赖服务恢复和业务允许处理该操作,不能无条件保证。

9. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明