安卓后台同步:WorkManager 的约束、重试与幂等怎么配合


用户点击同步之后,网络可能中断,应用可能退到后台,进程也可能被回收。把上传放在页面协程里,不能自动保证之后继续执行;把它换成 WorkManager,也不能自动保证每条笔记恰好保存一次。调度系统和业务协议解决的是不同问题。

持久调度与业务数据是两本账

WorkManager 适合可以延后、需要持久调度的工作,不是精确定时器;网络约束满足也不代表整个请求期间都一直有网。工作请求与约束文档给出了请求、约束和退避配置。

调度状态是 ENQUEUED、RUNNING、SUCCEEDED 等工作生命周期;业务状态则可能是待发送、已确认、冲突、需要重新登录。WorkRequest 成功不能被直接解释为“数据库里所有笔记永远都已同步”,尤其是用户还在持续编辑时。

应把待发送数据和稳定操作 ID 放在应用数据库。Worker 的输入只保存小型标识或同步范围,不承担整批正文的持久存储职责。用户强行停止应用、设备节电等系统行为也会影响调度,不能将它描述为一直在线的守护进程。

唯一任务会去重,也可能吞掉一次唤醒

先看一个很小的时序:Worker 查询数据库发现空队列;用户此时新增笔记并 enqueueUniqueWork(KEEP);旧 Worker 还没结束,因此新请求被忽略;旧 Worker 返回成功,新笔记就暂时无人处理。

即使 Worker 循环查询到空,也无法消除“最后一次空查询”与“工作真正结束”之间的窗口。唯一工作只管理 WorkManager 自己的任务关系,不会和应用 Room 数据库自动组成一个事务。唯一工作策略说明解释了 KEEP、REPLACE 与追加链的区别。

对编辑频率不高的笔记应用,可以在持久化编辑后追加一次唤醒,并用 APPEND_OR_REPLACE 避免旧链失败使新工作始终继承取消或失败状态。示例使用 WorkManager 2.8 及以上常见 Kotlin API;需要兼容依赖,未在 Android 工程编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun requestNotesSync(context: Context) {
val request = OneTimeWorkRequestBuilder<SyncNotesWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS
)
.build()

WorkManager.getInstance(context).enqueueUniqueWork(
"notes-sync",
ExistingWorkPolicy.APPEND_OR_REPLACE,
request
)
}

这不是适用于所有场景的唯一方案:高频编辑会制造很多空唤醒,应在应用层合并触发;追加链前面的任务若长时间重试,后继也要等待。使用 KEEP 可以减少任务数量,但必须配套可靠的待办重扫与补偿机制。

仍有一个不可消除的窗口:应用数据提交后、enqueue 之前进程死亡。因此数据库待办是事实来源,应用启动、账号恢复以及适当的周期补偿应重新扫描。周期补偿只提供系统允许时的再次发现机会,不承诺某个固定秒数恢复;若有严格实时要求,需要重新选择架构和产品承诺。

Worker 每次只推进可证明的一批

一次同步可以按有限批次读取未完成操作,逐项发送与确认,并在批次之间检查取消。不要先把全部状态标成“上传中”,然后依赖进程内 finally 改回来:进程被杀时 finally 不一定执行,这些记录可能永久卡住。

如果需要多 Worker 协调,使用数据库原子领取或带到期时间的租约;租约过期允许恢复,操作本身仍必须幂等。单进程 Mutex 只能约束当前进程内的执行,不能替代崩溃恢复协议。

下面是 CoroutineWorker 中的逻辑片段。UploadOutcome 与 repository 是应用提供的领域抽象,需要通过自定义 WorkerFactory 或依赖注入创建,不能把片段当作完整可运行 Worker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override suspend fun doWork(): Result {
return try {
when (repository.syncPendingBatch()) {
UploadOutcome.Done -> Result.success()
UploadOutcome.MorePending -> Result.retry()
UploadOutcome.TemporaryFailure -> Result.retry()
UploadOutcome.NeedsLogin -> {
repository.markLoginRequired()
Result.failure()
}
UploadOutcome.InvalidData -> {
repository.markNeedsAttention()
Result.failure()
}
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (networkError: IOException) {
Result.retry()
}
}

MorePending 走 retry 是一个简单且保守的时间切片方案,会应用退避,因此吞吐可能较低。对大量数据,可以在一次运行中处理多个受限批次,或设计后续工作;不能为了速度写一个永不结束、完全不检查停止信号的循环。

对无效的单条数据,也可以把它隔离到“需要处理”状态后继续其他数据,而非让一条坏记录堵住整个队列。是否整体失败取决于业务是否允许部分成功,应明确记录已经确认的范围。

重试必须有分类、预算和所有者

断网、部分 5xx 或服务端暂时过载可以重试;错误数据应先修正;认证过期先进行受控刷新,仍失败则落到需要登录状态,由前台展示。不要在 Worker 中启动登录界面。

HTTP 429 需要尊重服务器的 Retry-After 或业务限流协议。WorkManager 退避不是任意服务端时间戳的精确调度器,可以在数据库记录 nextEligibleAt,由下一次工作判断是否已到发送时间,避免每次调度都立刻撞上同一限流。

还应设置业务重试边界:例如操作超过一定年龄进入人工处理,而不是永远重试。runAttemptCount 属于一次工作运行,不适合代替跨多个 WorkRequest 的业务尝试统计。网络库、Worker 和服务端网关若各自重试,会放大流量;通常要选定一层负责主要重试。

响应丢失是幂等设计的关键案例

服务器已经保存笔记,但响应没有到客户端,此时客户端无法从超时判断保存是否发生。非幂等操作不应无条件自动重放;HTTP 语义也区分了安全与幂等。RFC 9110 的重试边界提供了协议依据。

对应用自定义操作 ID,服务端应在同一事务中检查 ID、验证请求摘要、提交业务数据并保存结果。相同 ID 与相同内容重放时返回原结果;相同 ID 与不同内容应拒绝。按租户划分作用域,保留期限要覆盖客户端的最长重试窗口,操作 ID 也不能被其他用户复用。

仅在请求头放一个 Idempotency-Key 没有实现这些承诺。客户端取消了请求,也不代表服务端回滚了已提交事务。恢复后应继续使用原操作 ID 查询或重发,再把明确确认的操作更新为完成。

不要让旧上传确认新编辑

上传期间用户可能再次编辑同一笔记。本地确认必须针对发送时的版本,而不是无条件把整条笔记设成已同步。完整的本地版本、待发送快照与条件确认设计,见本批文章中的 Room 离线优先专题;这里的原则是:调度成功不能扩大业务确认范围。

服务器确认后再提交本地确认事务。如果进程在远端成功与本地确认之间死亡,恢复时允许重复发送,并依靠服务端幂等消除重复效果。用“至少再次尝试 + 可重复确认”来构建恢复,比承诺无法证明的端到端恰好一次更实际。

验证调度窗口,而不只验证正常联网

使用 WorkManager 测试工具控制约束与执行,配合假 Repository 暂停在最后一次空查询。此时提交新操作,观察 KEEP 与追加策略的区别,再验证补偿扫描能找回数据库里的待办。Worker 测试指南提供了相关测试入口。

接着测试远端提交后丢响应、确认前终止进程、认证失效、坏数据夹在有效数据之间、重试中新增操作,以及账号切换。断言不仅包括 Worker 返回值,还包括远端业务记录数、原操作 ID 是否保持、本地未确认版本是否仍然存在。

本文没有执行设备调度实验。其验收标准是:每条未完成操作都能被重新发现,重复执行不产生重复业务效果,无法自动恢复的问题进入可见状态,而不是藏在一个永远显示“同步中”的动画后面。


 上一篇
一台云服务器部署多个网站:域名、端口与 Nginx 如何分工 一台云服务器部署多个网站:域名、端口与 Nginx 如何分工
分清 DNS、连接端口、TLS 名称与 HTTP Host 的职责,就能在一台服务器承载多个网站。本文结合博客和后台应用的路由方案,用分层实验定位站点错配、证书错误与 IPv4、IPv6 路径不一致。
2026-10-16
下一篇 
安卓开发:用 ViewModel 和 StateFlow 管理 Compose 页面状态 安卓开发:用 ViewModel 和 StateFlow 管理 Compose 页面状态
以笔记列表为例,明确首次加载、保留旧内容刷新和失败恢复的状态转移。进一步处理取消不及时导致的旧响应覆盖、生命周期收集、消息消费与进程恢复,让 StateFlow 承载可验证的页面状态。
2026-10-16
  目录