HarmonyOS开发中数据缓存策略:网络请求缓存
【摘要】 数据缓存策略:网络请求缓存缓存用得好,流量省一半,体验快一倍 一、背景与动机:为什么需要网络请求缓存?打开一个 App,首页加载了商品列表。用户点进商品详情,又返回首页——商品列表又重新加载了一遍。明明刚才已经加载过了,为什么还要浪费流量和时间?这就是缓存要解决的问题:节省流量:相同请求不重复发送提升速度:直接返回缓存数据,毫秒级响应离线可用:没网也能看缓存数据降低负载:减少服务器压力 缓...
数据缓存策略:网络请求缓存
缓存用得好,流量省一半,体验快一倍
一、背景与动机:为什么需要网络请求缓存?
打开一个 App,首页加载了商品列表。用户点进商品详情,又返回首页——商品列表又重新加载了一遍。
明明刚才已经加载过了,为什么还要浪费流量和时间?
这就是缓存要解决的问题:
- 节省流量:相同请求不重复发送
- 提升速度:直接返回缓存数据,毫秒级响应
- 离线可用:没网也能看缓存数据
- 降低负载:减少服务器压力
缓存策略有哪些?
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache-First | 优先缓存,缓存没有才请求 | 静态资源、不常变数据 |
| Network-First | 优先网络,网络失败才用缓存 | 实时数据、新闻列表 |
| Stale-While-Revalidate | 先返回缓存,后台更新 | 列表数据、用户信息 |
| Network-Only | 只用网络,不缓存 | 敏感数据、实时状态 |
| Cache-Only | 只用缓存 | 离线场景 |
二、核心原理:缓存分层架构
缓存不是"存了就完事",而是分层的体系:
flowchart TB
A[请求发起] --> B{检查内存缓存}
B -->|命中| C[返回内存缓存]
B -->|未命中| D{检查磁盘缓存}
D -->|命中| E[读取磁盘缓存]
E --> F[写入内存缓存]
F --> G[返回数据]
D -->|未命中| H[发起网络请求]
H --> I{请求成功?}
I -->|成功| J[写入磁盘缓存]
J --> K[写入内存缓存]
K --> L[返回数据]
I -->|失败| M{有缓存?}
M -->|有| N[返回过期缓存]
M -->|无| O[返回错误]
classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
classDef info fill:#7ED321,stroke:#5BA318,color:#fff
class A,C,G,L primary
class B,D,I,M info
class E,F,J,K warning
class H,N,O error
三层缓存:
- 内存缓存:Map 结构,最快,但易丢失
- 磁盘缓存:Preferences/RDB,持久化,稍慢
- 网络请求:最慢,但数据最新
三、代码实战:完整缓存系统实现
示例1:缓存配置与类型定义
// network/cache/types.ets
/**
* 缓存策略枚举
*/
export enum CacheStrategy {
/** 优先缓存,缓存没有才请求 */
CACHE_FIRST = 'cache-first',
/** 优先网络,网络失败才用缓存 */
NETWORK_FIRST = 'network-first',
/** 先返回缓存,后台更新 */
STALE_WHILE_REVALIDATE = 'stale-while-revalidate',
/** 只用网络,不缓存 */
NETWORK_ONLY = 'network-only',
/** 只用缓存 */
CACHE_ONLY = 'cache-only'
}
/**
* 缓存项配置
*/
export interface CacheConfig {
/** 缓存键(默认使用请求URL) */
key?: string;
/** 缓存策略 */
strategy?: CacheStrategy;
/** 缓存时长(毫秒),0表示永久 */
maxAge?: number;
/** 是否强制刷新(忽略缓存) */
forceRefresh?: boolean;
/** 网络失败时是否使用过期缓存 */
useExpiredCache?: boolean;
/** 是否缓存到磁盘 */
persist?: boolean;
}
/**
* 缓存项结构
*/
export interface CacheItem<T = any> {
/** 缓存数据 */
data: T;
/** 缓存时间 */
timestamp: number;
/** 过期时间 */
expires: number;
/** 缓存键 */
key: string;
/** 响应头(用于ETag/Last-Modified验证) */
headers?: Record<string, string>;
/** 是否来自缓存 */
fromCache: boolean;
}
/**
* 缓存统计信息
*/
export interface CacheStats {
/** 内存缓存数量 */
memoryCount: number;
/** 磁盘缓存数量 */
diskCount: number;
/** 内存缓存大小(字节) */
memorySize: number;
/** 磁盘缓存大小(字节) */
diskSize: number;
/** 命中次数 */
hitCount: number;
/** 未命中次数 */
missCount: number;
}
示例2:内存缓存实现
// network/cache/MemoryCache.ets
import { CacheItem, CacheStats } from './types';
/**
* 内存缓存
* 使用Map实现,读写速度最快
* 特点:应用重启后丢失
*/
export class MemoryCache {
private cache: Map<string, CacheItem> = new Map();
private maxSize: number; // 最大缓存数量
private hitCount: number = 0;
private missCount: number = 0;
constructor(maxSize: number = 100) {
this.maxSize = maxSize;
}
/**
* 获取缓存
* @param key 缓存键
* @returns 缓存项或null
*/
get<T>(key: string): CacheItem<T> | null {
const item = this.cache.get(key);
if (!item) {
this.missCount++;
return null;
}
// 检查是否过期
if (this.isExpired(item)) {
this.cache.delete(key);
this.missCount++;
return null;
}
this.hitCount++;
return item as CacheItem<T>;
}
/**
* 设置缓存
* @param key 缓存键
* @param data 缓存数据
* @param maxAge 缓存时长(毫秒)
*/
set<T>(key: string, data: T, maxAge: number = 0): void {
// 检查是否超过最大数量
if (this.cache.size >= this.maxSize) {
this.evict(); // 淘汰旧缓存
}
const now = Date.now();
const item: CacheItem<T> = {
data,
timestamp: now,
expires: maxAge > 0 ? now + maxAge : Number.MAX_SAFE_INTEGER,
key,
fromCache: true
};
this.cache.set(key, item);
}
/**
* 删除缓存
*/
delete(key: string): boolean {
return this.cache.delete(key);
}
/**
* 清空所有缓存
*/
clear(): void {
this.cache.clear();
}
/**
* 检查缓存是否存在
*/
has(key: string): boolean {
const item = this.cache.get(key);
if (!item) return false;
if (this.isExpired(item)) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* 获取所有缓存键
*/
keys(): string[] {
return Array.from(this.cache.keys());
}
/**
* 获取缓存统计信息
*/
getStats(): CacheStats {
let memorySize = 0;
this.cache.forEach(item => {
memorySize += this.estimateSize(item.data);
});
return {
memoryCount: this.cache.size,
diskCount: 0,
memorySize,
diskSize: 0,
hitCount: this.hitCount,
missCount: this.missCount
};
}
/**
* 检查是否过期
* @private
*/
private isExpired(item: CacheItem): boolean {
return Date.now() > item.expires;
}
/**
* 淘汰旧缓存(LRU策略)
* @private
*/
private evict(): void {
// 找到最旧的缓存项
let oldestKey: string | null = null;
let oldestTime = Number.MAX_SAFE_INTEGER;
this.cache.forEach((item, key) => {
if (item.timestamp < oldestTime) {
oldestTime = item.timestamp;
oldestKey = key;
}
});
if (oldestKey) {
this.cache.delete(oldestKey);
console.info(`[MemoryCache] 淘汰缓存: ${oldestKey}`);
}
}
/**
* 估算数据大小
* @private
*/
private estimateSize(data: any): number {
try {
return JSON.stringify(data).length * 2; // UTF-16编码
} catch {
return 0;
}
}
}
示例3:磁盘缓存实现
// network/cache/DiskCache.ets
import { preferences } from '@kit.ArkData';
import { CacheItem } from './types';
/**
* 磁盘缓存
* 使用Preferences实现持久化存储
* 特点:应用重启后保留
*/
export class DiskCache {
private store: preferences.Preferences | null = null;
private storeName: string = 'network_cache';
private prefix: string = 'cache_';
/**
* 初始化磁盘缓存
*/
async init(): Promise<void> {
try {
this.store = await preferences.getPreferences(this.storeName);
console.info('[DiskCache] 初始化完成');
} catch (error) {
console.error('[DiskCache] 初始化失败:', error);
}
}
/**
* 获取缓存
*/
async get<T>(key: string): Promise<CacheItem<T> | null> {
if (!this.store) {
await this.init();
}
try {
const json = await this.store!.get(this.prefix + key, '') as string;
if (!json) {
return null;
}
const item: CacheItem<T> = JSON.parse(json);
// 检查是否过期
if (Date.now() > item.expires) {
await this.delete(key);
return null;
}
return item;
} catch (error) {
console.error(`[DiskCache] 读取缓存失败 ${key}:`, error);
return null;
}
}
/**
* 设置缓存
*/
async set<T>(key: string, data: T, maxAge: number = 0): Promise<void> {
if (!this.store) {
await this.init();
}
const now = Date.now();
const item: CacheItem<T> = {
data,
timestamp: now,
expires: maxAge > 0 ? now + maxAge : Number.MAX_SAFE_INTEGER,
key,
fromCache: true
};
try {
const json = JSON.stringify(item);
await this.store!.put(this.prefix + key, json);
await this.store!.flush();
} catch (error) {
console.error(`[DiskCache] 写入缓存失败 ${key}:`, error);
}
}
/**
* 删除缓存
*/
async delete(key: string): Promise<void> {
if (!this.store) {
await this.init();
}
try {
await this.store!.delete(this.prefix + key);
await this.store!.flush();
} catch (error) {
console.error(`[DiskCache] 删除缓存失败 ${key}:`, error);
}
}
/**
* 清空所有缓存
*/
async clear(): Promise<void> {
if (!this.store) {
await this.init();
}
try {
// 获取所有键
const allKeys = await this.store!.getAll();
// 删除缓存相关键
for (const key of Object.keys(allKeys)) {
if (key.startsWith(this.prefix)) {
await this.store!.delete(key);
}
}
await this.store!.flush();
console.info('[DiskCache] 已清空所有缓存');
} catch (error) {
console.error('[DiskCache] 清空缓存失败:', error);
}
}
/**
* 获取所有缓存键
*/
async keys(): Promise<string[]> {
if (!this.store) {
await this.init();
}
const allKeys = await this.store!.getAll();
return Object.keys(allKeys)
.filter(key => key.startsWith(this.prefix))
.map(key => key.substring(this.prefix.length));
}
}
示例4:缓存管理器
// network/cache/CacheManager.ets
import { MemoryCache } from './MemoryCache';
import { DiskCache } from './DiskCache';
import { CacheConfig, CacheStrategy, CacheItem } from './types';
/**
* 缓存管理器
* 统一管理内存缓存和磁盘缓存
*/
export class CacheManager {
private memoryCache: MemoryCache;
private diskCache: DiskCache;
private defaultMaxAge: number = 5 * 60 * 1000; // 默认5分钟
constructor(config?: { maxSize?: number; defaultMaxAge?: number }) {
this.memoryCache = new MemoryCache(config?.maxSize || 100);
this.diskCache = new DiskCache();
if (config?.defaultMaxAge) {
this.defaultMaxAge = config.defaultMaxAge;
}
}
/**
* 初始化缓存管理器
*/
async init(): Promise<void> {
await this.diskCache.init();
console.info('[CacheManager] 初始化完成');
}
/**
* 生成缓存键
* @private
*/
private generateKey(url: string, params?: any): string {
const paramString = params ? JSON.stringify(params) : '';
const raw = `${url}:${paramString}`;
// 使用简单哈希(实际项目可以用MD5)
let hash = 0;
for (let i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash = hash & hash; // 转为32位整数
}
return `req_${Math.abs(hash)}`;
}
/**
* 获取缓存
*/
async get<T>(key: string, config?: CacheConfig): Promise<CacheItem<T> | null> {
// 1. 检查内存缓存
const memoryItem = this.memoryCache.get<T>(key);
if (memoryItem) {
console.info(`[CacheManager] 内存缓存命中: ${key}`);
return memoryItem;
}
// 2. 检查磁盘缓存(如果配置了持久化)
if (config?.persist !== false) {
const diskItem = await this.diskCache.get<T>(key);
if (diskItem) {
console.info(`[CacheManager] 磁盘缓存命中: ${key}`);
// 写入内存缓存
this.memoryCache.set(key, diskItem.data, diskItem.expires - Date.now());
return diskItem;
}
}
return null;
}
/**
* 设置缓存
*/
async set<T>(key: string, data: T, config?: CacheConfig): Promise<void> {
const maxAge = config?.maxAge ?? this.defaultMaxAge;
// 写入内存缓存
this.memoryCache.set(key, data, maxAge);
// 写入磁盘缓存(如果配置了持久化)
if (config?.persist !== false) {
await this.diskCache.set(key, data, maxAge);
}
console.info(`[CacheManager] 缓存已写入: ${key}, 有效期: ${maxAge}ms`);
}
/**
* 删除缓存
*/
async delete(key: string): Promise<void> {
this.memoryCache.delete(key);
await this.diskCache.delete(key);
}
/**
* 清空所有缓存
*/
async clear(): Promise<void> {
this.memoryCache.clear();
await this.diskCache.clear();
console.info('[CacheManager] 所有缓存已清空');
}
/**
* 获取缓存统计信息
*/
getStats() {
return this.memoryCache.getStats();
}
/**
* 执行带缓存的请求
* @param requestFn 实际请求函数
* @param url 请求URL
* @param params 请求参数
* @param config 缓存配置
*/
async executeWithCache<T>(
requestFn: () => Promise<T>,
url: string,
params?: any,
config?: CacheConfig
): Promise<{ data: T; fromCache: boolean }> {
// 生成缓存键
const key = config?.key || this.generateKey(url, params);
// 强制刷新时跳过缓存
if (!config?.forceRefresh) {
// 获取缓存
const cached = await this.get<T>(key, config);
if (cached && config?.strategy !== CacheStrategy.NETWORK_ONLY) {
// 根据策略处理
if (config?.strategy === CacheStrategy.CACHE_FIRST ||
config?.strategy === CacheStrategy.CACHE_ONLY) {
return { data: cached.data, fromCache: true };
}
// Stale-While-Revalidate:先返回缓存,后台更新
if (config?.strategy === CacheStrategy.STALE_WHILE_REVALIDATE) {
// 触发后台更新(不等待)
this.backgroundUpdate(key, requestFn, config);
return { data: cached.data, fromCache: true };
}
}
}
// Network-Only 或 缓存未命中 或 强制刷新
if (config?.strategy === CacheStrategy.CACHE_ONLY) {
throw new Error('No cache available');
}
try {
// 发起网络请求
const data = await requestFn();
// 写入缓存(除非是NETWORK_ONLY)
if (config?.strategy !== CacheStrategy.NETWORK_ONLY) {
await this.set(key, data, config);
}
return { data, fromCache: false };
} catch (error) {
// 网络失败,尝试使用过期缓存
if (config?.useExpiredCache) {
const cached = await this.getExpiredCache<T>(key);
if (cached) {
console.warn(`[CacheManager] 网络失败,使用过期缓存: ${key}`);
return { data: cached.data, fromCache: true };
}
}
throw error;
}
}
/**
* 后台更新缓存
* @private
*/
private async backgroundUpdate<T>(
key: string,
requestFn: () => Promise<T>,
config?: CacheConfig
): Promise<void> {
try {
const data = await requestFn();
await this.set(key, data, config);
console.info(`[CacheManager] 后台更新完成: ${key}`);
} catch (error) {
console.error(`[CacheManager] 后台更新失败: ${key}`, error);
}
}
/**
* 获取过期缓存(用于降级)
* @private
*/
private async getExpiredCache<T>(key: string): Promise<CacheItem<T> | null> {
// 从磁盘获取(不检查过期)
const diskItem = await this.diskCache.get<T>(key);
return diskItem;
}
}
示例5:缓存拦截器
// network/cache/CacheInterceptor.ets
import { Interceptor, Response, RequestConfig } from '../types';
import { CacheManager, CacheConfig, CacheStrategy } from './CacheManager';
/**
* 缓存拦截器
* 在请求前检查缓存,在响应后写入缓存
*/
export class CacheInterceptor implements Interceptor {
private cacheManager: CacheManager;
constructor(cacheManager: CacheManager) {
this.cacheManager = cacheManager;
}
/**
* 请求拦截:检查缓存
*/
async beforeRequest(config: RequestConfig): Promise<RequestConfig> {
// 获取缓存配置
const cacheConfig = (config as any).__cacheConfig as CacheConfig | undefined;
if (!cacheConfig || cacheConfig.strategy === CacheStrategy.NETWORK_ONLY) {
return config;
}
// 标记是否需要检查缓存
(config as any).__checkCache = true;
return config;
}
/**
* 响应拦截:写入缓存
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
const config = response.config;
const cacheConfig = (config as any).__cacheConfig as CacheConfig | undefined;
if (!cacheConfig || cacheConfig.strategy === CacheStrategy.NETWORK_ONLY) {
return response;
}
// 写入缓存
const key = cacheConfig.key || this.generateKey(config.url, config.params);
await this.cacheManager.set(key, response.data, cacheConfig);
return response;
}
/**
* 生成缓存键
* @private
*/
private generateKey(url: string, params?: any): string {
const paramString = params ? JSON.stringify(params) : '';
const raw = `${url}:${paramString}`;
let hash = 0;
for (let i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash = hash & hash;
}
return `req_${Math.abs(hash)}`;
}
}
示例6:实际使用示例
// api/ProductAPI.ets
import { HttpClient } from '../network/HttpClient';
import { CacheManager, CacheStrategy } from '../network/cache';
export class ProductAPI {
private http: HttpClient;
private cacheManager: CacheManager;
constructor(http: HttpClient, cacheManager: CacheManager) {
this.http = http;
this.cacheManager = cacheManager;
}
/**
* 获取商品列表(带缓存)
* 策略:Stale-While-Revalidate
*/
async getProductList(category?: string): Promise<Response<Product[]>> {
return this.cacheManager.executeWithCache(
() => this.http.get<Product[]>('/products', { category }),
'/products',
{ category },
{
strategy: CacheStrategy.STALE_WHILE_REVALIDATE,
maxAge: 5 * 60 * 1000, // 5分钟
persist: true
}
).then(result => ({
data: result.data,
status: 200,
headers: {},
config: {} as any
}));
}
/**
* 获取商品详情(带缓存)
* 策略:Cache-First
*/
async getProductDetail(id: string): Promise<Response<Product>> {
return this.cacheManager.executeWithCache(
() => this.http.get<Product>(`/products/${id}`),
`/products/${id}`,
{},
{
strategy: CacheStrategy.CACHE_FIRST,
maxAge: 10 * 60 * 1000, // 10分钟
persist: true
}
).then(result => ({
data: result.data,
status: 200,
headers: {},
config: {} as any
}));
}
/**
* 搜索商品(不缓存)
*/
async searchProducts(keyword: string): Promise<Response<Product[]>> {
return this.http.get<Product[]>('/products/search', { keyword });
}
/**
* 强制刷新商品列表
*/
async refreshProductList(category?: string): Promise<Response<Product[]>> {
return this.cacheManager.executeWithCache(
() => this.http.get<Product[]>('/products', { category }),
'/products',
{ category },
{
strategy: CacheStrategy.NETWORK_FIRST,
forceRefresh: true,
maxAge: 5 * 60 * 1000
}
).then(result => ({
data: result.data,
status: 200,
headers: {},
config: {} as any
}));
}
}
四、踩坑与注意事项
坑1:缓存键冲突
问题:不同参数的请求生成了相同的缓存键。
// ❌ 错误:只用URL做key
const key = url; // /products?page=1 和 /products?page=2 会冲突
// ✅ 正确:URL + 参数
const key = `${url}:${JSON.stringify(params)}`;
坑2:缓存数据被意外修改
问题:缓存返回的是引用,外部修改会影响缓存。
// ❌ 错误:直接修改缓存数据
const data = await cache.get('key');
data.name = 'modified'; // 缓存也被修改了
// ✅ 正确:返回深拷贝
get<T>(key: string): T | null {
const item = this.cache.get(key);
return item ? JSON.parse(JSON.stringify(item.data)) : null;
}
坑3:缓存占用内存过大
问题:缓存太多数据,导致内存溢出。
解决:设置最大缓存数量,使用 LRU 淘汰:
// 设置最大缓存数量
const cache = new MemoryCache(100); // 最多100条
// 定期清理过期缓存
setInterval(() => {
cache.cleanup();
}, 60 * 1000); // 每分钟清理一次
坑4:敏感数据被缓存
问题:用户信息、token 等敏感数据被缓存到磁盘。
解决:敏感数据标记为不持久化:
await cacheManager.set('user-info', userData, {
persist: false, // 不写入磁盘
maxAge: 60 * 1000 // 1分钟后过期
});
五、HarmonyOS 6 适配要点
1. Preferences API 增强
// HarmonyOS 6 支持更大的存储空间
const store = await preferences.getPreferences('cache', {
// ✨ 新增:指定存储文件路径
filePath: '/data/storage/el2/base/preferences/'
});
// ✨ 新增:批量操作
await store.putBatch([
{ key: 'cache_1', value: 'data1' },
{ key: 'cache_2', value: 'data2' }
]);
2. 缓存大小限制
// HarmonyOS 6 可以获取存储配额
const info = await preferences.getStorageInfo();
console.log('已使用:', info.usedSpace);
console.log('总配额:', info.totalSpace);
3. 缓存加密
// HarmonyOS 6 支持加密存储
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
// 加密缓存数据
async setEncrypted(key: string, data: any): Promise<void> {
const json = JSON.stringify(data);
const encrypted = await this.encrypt(json);
await this.store.put(key, encrypted);
}
// 解密缓存数据
async getDecrypted<T>(key: string): Promise<T | null> {
const encrypted = await this.store.get(key, '');
if (!encrypted) return null;
const json = await this.decrypt(encrypted);
return JSON.parse(json);
}
六、总结
缓存是网络层的"加速器",用好缓存能大幅提升用户体验:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Cache-First | 速度最快 | 数据可能旧 | 静态资源、配置 |
| Network-First | 数据最新 | 速度慢 | 实时数据 |
| Stale-While-Revalidate | 兼顾速度和新鲜度 | 实现复杂 | 列表数据 |
| Network-Only | 数据最新 | 无缓存优势 | 敏感数据 |
| Cache-Only | 离线可用 | 数据可能旧 | 离线场景 |
记住几个原则:
- ✅ 缓存键要唯一,避免冲突
- ✅ 缓存数据要深拷贝,避免污染
- ✅ 缓存大小要限制,避免内存溢出
- ✅ 敏感数据不持久化,避免泄露
- ✅ 过期缓存要清理,避免占用空间
下一篇我们深入离线优先策略,看看如何让应用在无网环境下也能优雅运行。
💡 最佳实践提示:建议根据数据类型设置不同的缓存策略:静态资源用 Cache-First,用户数据用 Network-First,列表数据用 Stale-While-Revalidate。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)