1. 缓存更新场景分析:网络与本地双数据源、缓存过期策略、Stale‑while-revalidate 模式
1.1 双数据源架构的现实驱动力
在移动端或服务端应用中,纯网络依赖会带来高延迟、弱网不可用与流量浪费。本地数据库(Room、SQLite)作为持久缓存,与远程 API 构成双数据源。数据流管道需要解决:何时读本地、何时读网络、如何保证数据最终一致性。
| 数据源 | 读取速度 | 一致性保证 | 典型场景 |
|---|---|---|---|
| 本地(Room/磁盘) | 微秒~毫秒 | 本地最新,可能落后于远程 | 离线展示、秒开页面 |
| 远程(REST/GraphQL) | 100ms~数秒 | 服务端最新 | 数据刷新、写后读校验 |
1.2 缓存过期策略
缓存必须设定生命周期,否则会呈现陈旧数据。常见策略如下:
TTL(Time To Live) – 写入后固定时长后失效;适合变化频率可预测的数据。
过期 – 固定时间点(如每天 0 点)全部失效;适合定时更新的大盘数据。
滑动过期 – 每次读取重置计时器,适合频繁访问的热数据。
版本号比对 – 服务端返回数据版本号,本地版本更旧则触发更新;一致性最强,但需要服务端配合。
过期 – 固定时间点(如每天 0 点)全部失效;适合定时更新的大盘数据。
滑动过期 – 每次读取重置计时器,适合频繁访问的热数据。
版本号比对 – 服务端返回数据版本号,本地版本更旧则触发更新;一致性最强,但需要服务端配合。
大多数缓存框架(如 OkHttp Cache、DiskLruCache)默认使用 TTL。但在实时性要求较高的场景下,TTL 过期后直接丢弃缓存会导致白屏或加载态,用户体验差。
于是引入 Stale‑while‑revalidate 模式。
1.3 Stale‑while‑revalidate 模式详解
该模式允许立即返回过期缓存,同时在后台发起网络请求以更新缓存。用户感知不到等待,数据最终收敛到最新状态。流程如下:
- 用户请求数据。
- 检查本地缓存:只要存在,无论是否过期,直接先返回给 UI。
- 如果缓存已过期,异步触发网络请求,等待响应后写入本地,并在下次读取时生效。
- 如果缓存不存在,则必须等待网络请求完成后再返回。
适用条件:数据对实时性不极端敏感(如新闻列表、商品信息),且请求功耗/流量可接受。不适合股票行情、实时游戏状态等需要强一致的场景。
1.4 Kotlin Flow 实现骨架
以下代码演示了使用 NetworkBoundResource 模式将双数据源与 Stale‑while‑revalidate 结合:
fun <T> flowCacheStrategy(
localSource: Flow<T>,
remoteSource: suspend () -> T,
isCacheExpired: (T) -> Boolean
): Flow<Resource<T>> = flow {
// 1. 先发射本地数据(可能过期)
localSource.collect { localData ->
emit(Resource.Success(localData))
if (isCacheExpired(localData)) {
// 2. 过期时后台刷新
try {
val fresh = remoteSource()
// 3. 更新本地缓存(假设通过 DAO 写入)
updateLocalCache(fresh)
// 4. 再次发射新数据
emit(Resource.Success(fresh))
} catch (e: Exception) {
// 刷新失败仍保留旧数据,用户可见
emit(Resource.Error(e, localData))
}
}
}
}
该方案的关键在于 Flow 的冷流特性:collect 时会挂起,后台刷新通过 try/catch 保证不打断主数据流。
1.5 Stale‑while‑revalidate 流程图
下图将上述逻辑以节点和箭头形式呈现:
1.6 常见陷阱与调试建议
- 重复刷新:多个消费者同时观察同一个 Flow 时可能导致并发网络请求。解决方案:使用
shareIn或stateIn加上WhileSubscribed(5000)防抖。 - 内存泄漏:在 ViewModel 中使用
Flow.flowOn(Dispatchers.IO)并配合viewModelScope取消作用域。 - 缓存与网络顺序:若网络请求比本地缓存还快,可能出现“先新后旧”的闪烁。可在数据库层添加时间戳排序,丢弃早于当前缓存的数据。
性能提示:Stale‑while‑revalidate 模式中后台网络请求不应阻塞 UI 发射;务必在
flowOn(Dispatchers.IO) 上下文中执行远程调用。同时请为 Room 查询添加 asFlow() 以感知本地变更。