1. 缓存更新场景分析:网络与本地双数据源、缓存过期策略、Stale‑while-revalidate 模式

1.1 双数据源架构的现实驱动力

在移动端或服务端应用中,纯网络依赖会带来高延迟、弱网不可用与流量浪费。本地数据库(Room、SQLite)作为持久缓存,与远程 API 构成双数据源。数据流管道需要解决:何时读本地、何时读网络、如何保证数据最终一致性。

数据源读取速度一致性保证典型场景
本地(Room/磁盘)微秒~毫秒本地最新,可能落后于远程离线展示、秒开页面
远程(REST/GraphQL)100ms~数秒服务端最新数据刷新、写后读校验

1.2 缓存过期策略

缓存必须设定生命周期,否则会呈现陈旧数据。常见策略如下:

TTL(Time To Live) – 写入后固定时长后失效;适合变化频率可预测的数据。
过期 – 固定时间点(如每天 0 点)全部失效;适合定时更新的大盘数据。
滑动过期 – 每次读取重置计时器,适合频繁访问的热数据。
版本号比对 – 服务端返回数据版本号,本地版本更旧则触发更新;一致性最强,但需要服务端配合。

大多数缓存框架(如 OkHttp Cache、DiskLruCache)默认使用 TTL。但在实时性要求较高的场景下,TTL 过期后直接丢弃缓存会导致白屏或加载态,用户体验差。
于是引入 Stale‑while‑revalidate 模式。

1.3 Stale‑while‑revalidate 模式详解

该模式允许立即返回过期缓存,同时在后台发起网络请求以更新缓存。用户感知不到等待,数据最终收敛到最新状态。流程如下:

  1. 用户请求数据。
  2. 检查本地缓存:只要存在,无论是否过期,直接先返回给 UI。
  3. 如果缓存已过期,异步触发网络请求,等待响应后写入本地,并在下次读取时生效。
  4. 如果缓存不存在,则必须等待网络请求完成后再返回。
适用条件:数据对实时性不极端敏感(如新闻列表、商品信息),且请求功耗/流量可接受。不适合股票行情、实时游戏状态等需要强一致的场景。

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 流程图

下图将上述逻辑以节点和箭头形式呈现:

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() 以感知本地变更。

1.7 本章小结