缓存失效 — 删除或更新缓存中过期数据以确保应用程序接收信息准确性的过程。在移动开发中,缓存失效至关重要:用户期望无需完全重新加载即可获得最新数据。根据 Google Developers, 2025 的数据,正确配置的缓存失效可将网络请求减少 60%,并改善界面响应速度。
要点
缓存失效 — 取消或更新不再与数据源当前状态匹配的缓存记录的过程。与手动清除整个缓存不同,缓存失效是精准操作的:仅针对那些准确性存疑的数据。
缓存存储数据副本以便快速访问。随着时间的推移,数据库或服务器中的原始数据可能会更改 — 例如,用户更新了个人资料或信息流中出现了新帖子。如果缓存未被失效,应用程序将显示过期信息,这在移动应用中会导致交易错误、显示不正确和信任度下降。
任何缓存失效的主要困难 — 众所周知的谚语 “There are only two hard things in Computer Science: cache invalidation and naming things”。复杂性在于,缓存不知道源何时发生了变化,除非明确告知。
根据 Martin Kleppmann(《Designing Data-Intensive Applications》一书作者,O’Reilly, 2017)的数据,正确的缓存失效需要要么集中通知更改,要么在每次读取时检查准确性的机制 — 性能与一致性之间的权衡。
data class CacheEntryT(
val data: T,
val expiresAt: Long,
val version: Int = 0
)
fun CacheT.isValid(key: String): Boolean =
get(key)?.let { it.expiresAt > currentTimeMillis() && it.version == currentVersion(key) } ?: false
此代码展示了一种简单的方法:如果 TTL 未过期且版本与源中的当前版本匹配,则缓存中的记录被视为有效。版本控制机制 — 避免显示过期数据的可靠方法之一。
数据准确性 — 大多数移动应用的关键要求:社交网络、即时通讯、银行服务、电子商务平台。看到错误账户余额或旧消息的用户会失去对应用程序的信任。
除了用户体验之外,缓存失效还解决了节省流量和电池的问题。移动应用无需定期完全重新加载数据,而只需使更改的记录失效并进行精准加载。根据 Meta Engineering (2024) 的数据,在 Facebook Lite 中引入增量缓存失效使流量消耗减少了 35%,而不会损失内容准确性。
另一个重要方面 — 事务一致性。在带有购物车或预订的应用程序中,使用过期缓存可能导致重复扣款或数据冲突。关键操作后的缓存失效保证下一个请求将读取最新数据。
TTL — 最简单的策略,缓存中的每条记录都有一个固定的生存时间。TTL 到期后,数据被视为过期并在下次读取时删除。TTL 适用于按计划更新的数据 — 例如天气或汇率。缺点:数据在 TTL 间隔内可能不准确。
在 Write-Through 策略中,每次数据更改都通过缓存:写入同时执行到缓存和源。这保证了缓存始终包含当前版本。缺点 — 写入延迟增加,因为操作直到从源确认才完成。Write-Through 适用于对一致性要求严格的数据:账户余额、订单状态。
Write-Behind — 异步写入:数据立即进入缓存,稍后由单独进程写入源。这提供了高写入性能,但在同步前发生故障时带来数据丢失风险。在移动应用中,Write-Behind 通常用于分析、日志和非关键用户操作。
Write-Invalidate — 在数据更改时,不是更新缓存,而是简单地删除(失效)相应记录。下一次读取将检测到缓存未命中并从源加载最新数据。此策略易于实现,并且在读取请求明显多于写入请求时效果良好。
| 策略 | 读取性能 | 写入性能 | 一致性 |
|---|---|---|---|
| TTL | 高 | 高 | 弱(可能过期) |
| Write-Through | 高 | 中 | 强 |
| Write-Behind | 高 | 高 | 弱(可能丢失) |
| Write-Invalidate | 中 | 高 | 强(下次读取时) |
策略的选择取决于特定场景的优先级:响应速度、一致性还是资源节约。混合方法 — 例如,收到推送通知时使用 TTL 配合 Write-Invalidate — 提供最佳平衡。
HTTP 缓存 — 客户端的第一级。浏览器或移动应用程序存储带有 Cache-Control 和 ETag 头的服务器响应。当收到 304 Not Modified 响应或 max-age 到期时发生失效。ETag 允许客户端在不加载完整响应的情况下检查资源的准确性。
应用程序缓存 — 第二级,由代码管理:内存缓存(Android 中的 LRU、LruCache)或磁盘缓存(SQLite、Room、Realm)。这里的失效由开发人员控制。根据 Android Developers (2025) 的数据,正确使用 Room 与 Flow 以及通过触发器进行缓存失效可将 UI 重绘次数减少 40%。
服务器缓存 — 第三级:Redis、Memcached、CDN。在此级别,缓存失效通过 TTL、DEL/PURGE 命令或消息代理(RabbitMQ、Kafka)进行。CDN 失效 — 一个独立的任务:由于 CDN 的分布式特性,清除命令可能需要几分钟才能传播。根据 Cloudflare (2024) 的数据,通过 Purge by URL 进行的缓存失效平均需要 5-15 秒才能全球传播。
为了协调所有级别的缓存失效,使用 集中式缓存服务 或事件代理。当数据更改时,源发布一个事件,每个级别收到使特定键失效的命令。这防止了一个级别已更新数据而另一级别继续提供过期版本的情况。
TTL 过长 — 最常见的错误。开发人员 “有保留地” 设置 TTL,导致用户数小时或数天看到过期数据。解决方案:从短 TTL(1-5 分钟)开始,仅在测量实际需求后增加。
单次更改导致整个缓存失效 — 微服务架构中的典型问题。一个用户更新了头像,缓存对所有人生效。当用户数量大时,这会导致 缓存雪崩 效应 — 对源的请求雪崩。解决方案:仅使特定用户的键失效,而不是整个缓存。
写入错误时缺乏缓存失效 — 如果写入源失败而缓存已更新,应用程序将处于不一致状态。解决方案:两阶段失效 — 先清除缓存,然后写入源,出错时回滚失效。
忽略分布式特性 — 在集群环境中,一个节点上的缓存失效并不意味着其他节点收到了命令。如果没有事件代理,部分服务器将继续提供过期数据。Redis Pub/Sub 或 Apache Kafka 通过传播缓存失效事件解决此问题。
确定准确性要求 — 数据 “立即” 新鲜有多关键。对于新闻推送,1-2 分钟的延迟是可以接受的(TTL)。对于账户余额 — 延迟不可接受(Write-Through)。
评估更改频率 — 每天更新一次的数据(产品目录、城市指南)使用 TTL 效果很好。每秒变化数十次的数据(在线状态、汇率)需要通过 WebSocket 或 Firebase Cloud Messaging 进行推送失效。
考虑读取源的成本 — 如果源是涉及 10 个表的昂贵 SQL 查询或带有限制的外部 API,最好使用长 TTL 的积极缓存,但通过推送失效补偿过期数据。如果读取成本低(内存查找),可以使用短 TTL 和 Write-Invalidate。
根据 Google I/O (2025) 的数据,移动应用的典型模式是 Stale-While-Revalidate:用户立即看到缓存数据,应用程序在后台检查其准确性并更新。这结合了响应速度和准确性而无折衷。HTTP 头 Cache-Control 带有 stale-while-revalidate 指令从 Android 10 和 iOS 13 开始得到支持。
常见问题
失效 — 将特定记录标记为过期,之后在下次读取时更新。缓存清除是删除所有记录,这更昂贵且可能暂时降低应用程序性能。
ETag — 是服务器在 HTTP 头中返回的资源的哈希或版本。在重复请求时,客户端发送带有当前 ETag 的 If-None-Match。如果资源未更改,服务器响应 304 Not Modified,缓存保持有效。
Write-Through 配合版本控制 — 最可靠,因为数据始终一致。但写入延迟最大。实践中,更常使用带推送失效的 TTL 以平衡性能和准确性。
使用 概率提前过期 — 每个请求在 TTL 到期前随机检查缓存的准确性。XFetch 算法(Vattani, 2015)根据公式计算重新计算的概率:p = (ttl - age) / (ttl * beta)。
使用 网络调试工具:Charles Proxy、Proxyman 或 Android Studio 和 Xcode 中内置的 Network Inspector。检查数据修改后,下一个请求是否确实加载了新版本,而不是返回缓存版本。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。