缓存基础概念

说到离线缓存,很多新手朋友第一反应是“存点数据到本地呗”。没错,但远不止这么简单。我做了这么多年Android,可以负责任地告诉你——缓存设计的好坏,直接决定了你的App能不能在弱网下活下来。

一句话总结:离线缓存不是“存数据”,而是“让数据在无网状态下依然可用且一致”。

为什么需要离线缓存?

你想想看,用户在地铁里刷App,信号断断续续。如果每次都要等网络请求,体验会怎样?我曾经在做一个新闻客户端时,用户在地库加载了5秒还出不来,流失率直接飙升。从那以后,我再也不敢轻视离线缓存。

离线缓存的核心价值有三个:

  • 提升用户体验:数据秒开,不依赖网络
  • 减少流量消耗:避免重复请求相同数据
  • 保障基础可用:无网络时依然能浏览历史内容

说白了,离线缓存就是把“实时数据”和“本地数据”的差异抹平。用户看到的永远是最新可用的内容,而不是白屏或loading转圈。

缓存层级概述

缓存不是孤立的,它分三个层级——内存、磁盘、网络。我个人习惯把它们想象成一个漏斗:最快的在最上面,容量最小的也在最上面。

层级 速度 容量 持久性 典型实现
内存缓存 纳秒级 几MB~几十MB 进程退出即清除 LruCache、HashMap
磁盘缓存 毫秒级 几百MB~几GB App卸载才清除 Room、SharedPreferences、文件
网络缓存 秒级~分钟级 取决于CDN 服务器控制过期 HTTP Cache、Retrofit缓存

这里要注意:内存缓存最快,但最脆弱。我记得有一次,团队把用户配置全放在内存里,一按Home键再回来,数据全丢了,导致页面闪一下空白。嗯,这个坑我踩过。

避坑指南:千万不要把需要持久化的数据只放在内存缓存中。磁盘缓存才是你的“保险柜”。

那么这三个层级怎么配合呢?看下面这张图:

离线缓存请求流程与层级关系 用户请求 内存缓存 (LruCache) 磁盘缓存 (Room) 网络请求 (API) 数据更新 (写回所有缓存) 未命中 未命中 响应数据 写回 写回 命中返回 💡 要点: • 请求优先命中最快的缓存层级(内存→磁盘→网络) • 网络返回数据后,必须写回所有缓存层级,保证下次命中

这张图展示了我最推荐的策略——三级缓存逐级回退。先用内存,没命中用磁盘,再没命中走网络。网络返回的数据,再统一更新到磁盘和内存。这样下次请求就能直接命中。

缓存一致性基本问题

好,现在我们有了三层缓存,问题来了:数据不一致怎么办?

举个真实案例。我参与过一个电商项目,用户点赞操作先写到了本地内存,显示已点赞。但实际网络请求失败了,服务器那边根本没记录。这时候用户看到的是“已点赞”,后台却是“未点赞”,这就是典型的缓存与真实数据不一致。

缓存一致性,说白了就是保证用户在任何地方看到的都是同一份“真相”。主要有三种场景:

场景 问题描述 常见解决方法
写后读不一致 本地修改后,网络写入慢,其他地方读到旧数据 先写本地,异步写网络;或者写时直接过期缓存
多端冲突 手机和服务器数据因同时修改产生冲突 时间戳对比、最后写入覆盖、冲突提示让用户选择
缓存过期 在缓存有效期内,服务器数据已变 设置TTL(过期时间)、主动推送失效通知

我的个人习惯:对于写操作,我倾向于采用“读时校验”策略——读取数据时检查本地时间戳与服务器时间戳是否一致,不一致则丢弃缓存重新拉取。代价是读操作多一次对比,但能有效避免脏数据。

你可能会问:数据不一致能完全避免吗?我的答案是:不能。分布式系统下,一致性只能“尽可能”保证。我们要做的就是让不一致的时间窗口尽量短,让用户基本感知不到。

举个具体的做法:结合Room数据库的版本号机制。每条记录都带一个version字段,每次更新时version加1。读取时对比本地version与服务器返回的version,不一致就覆盖。嗯,这个方法我在好几个项目里用过,效果不错。