HarmonyOS开发中缓存策略设计:内存缓存与磁盘缓存

一、小知识

你有没有遇到过这种情况——应用启动时加载一堆数据,用户切换个页面又重新请求一遍,网络稍微慢点就白屏转圈圈?说白了,这就是缓存没做好。

缓存这东西,听着简单,做起来全是坑。光有内存缓存吧,应用一杀数据就没了;光有磁盘缓存吧,每次读取还得IO,性能上不去。真正靠谱的做法是多级缓存——内存做一级,磁盘做二级,两层配合才能既快又持久。

更麻烦的是缓存淘汰。内存不是无限大的,你总不能让缓存把应用撑爆吧?这时候就得用LRU(Least Recently Used)算法,把那些"八百年没人用"的数据清理掉,给新数据腾地方。

HarmonyOS里做缓存,你既可以用Preferences做轻量级存储,也能用RDB存结构化数据,还能直接操作文件系统。选哪种方案,得看你的数据特点和性能要求——咱们这篇就把这些方案掰开揉碎讲清楚。

二、核心原理

2.1 多级缓存架构

多级缓存的核心思想是读写分离、逐级降级。读取时先查内存,命中就返回;没命中再查磁盘,磁盘有就回填内存并返回;都没有才走网络请求。写入时则要同步更新所有层级。

读取请求

内存缓存
命中?

返回内存数据

磁盘缓存
命中?

回填内存缓存

网络请求

写入磁盘缓存

写入内存缓存

2.2 LRU淘汰算法

LRU的核心是维护一个访问顺序链表。每次访问数据时,把它移到链表头部;缓存满了要淘汰时,直接删掉链表尾部的数据——因为尾部肯定是"最久未使用"的。

实现上有两种常见方案:

  • LinkedHashMap:利用其accessOrder特性自动维护顺序
  • 双向链表+HashMap:O(1)时间复杂度的经典实现

HarmonyOS的ArkTS里没有现成的LinkedHashMap,咱们得自己实现一个双向链表版本。

LRU缓存结构

HashMap
Key→Node

双向链表
维护访问顺序

访问KeyA

NodeA移至链表头部

缓存已满

删除链表尾部Node

从HashMap移除对应Key

2.3 缓存策略模式

实际业务中,不同数据对缓存的要求差异很大:

  • 用户头像:可以缓存很久,淘汰优先级低
  • 新闻列表:需要定时刷新,过期时间短
  • 临时数据:用完就删,不需要持久化

所以咱们要设计一个灵活的策略模式,让每种数据都能配置自己的缓存行为。

三、代码实战

3.1 LRU内存缓存实现

先实现一个通用的LRU缓存类,这是整个缓存体系的核心:

/**
 * LRU缓存节点 - 双向链表结构
 */
class LRUNode<K, V> {
  key: K;
  value: V;
  prev: LRUNode<K, V> | null = null;
  next: LRUNode<K, V> | null = null;

  constructor(key: K, value: V) {
    this.key = key;
    this.value = value;
  }
}

/**
 * LRU内存缓存实现
 * 基于双向链表+HashMap,O(1)时间复杂度
 */
export class LRUCache<K, V> {
  private capacity: number;           // 最大容量
  private cache: Map<K, LRUNode<K, V>>;  // HashMap存储
  private head: LRUNode<K, V> | null;    // 链表头(最近使用)
  private tail: LRUNode<K, V> | null;    // 链表尾(最久未使用)
  private size: number = 0;              // 当前大小

  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();
    this.head = null;
    this.tail = null;
  }

  /**
   * 获取缓存值,并移动到链表头部
   */
  get(key: K): V | undefined {
    const node = this.cache.get(key);
    if (!node) {
      return undefined;
    }
    
    // 移动到头部表示最近访问
    this.moveToHead(node);
    return node.value;
  }

  /**
   * 设置缓存值
   */
  set(key: K, value: V): void {
    const existingNode = this.cache.get(key);
    
    if (existingNode) {
      // 已存在,更新值并移到头部
      existingNode.value = value;
      this.moveToHead(existingNode);
    } else {
      // 新节点
      const newNode = new LRUNode(key, value);
      this.cache.set(key, newNode);
      this.addToHead(newNode);
      this.size++;

      // 超过容量,淘汰尾部
      if (this.size > this.capacity) {
        this.removeTail();
      }
    }
  }

  /**
   * 删除缓存
   */
  delete(key: K): boolean {
    const node = this.cache.get(key);
    if (!node) {
      return false;
    }
    
    this.removeNode(node);
    this.cache.delete(key);
    this.size--;
    return true;
  }

  /**
   * 清空缓存
   */
  clear(): void {
    this.cache.clear();
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  /**
   * 获取当前缓存大小
   */
  getSize(): number {
    return this.size;
  }

  /**
   * 将节点移到链表头部
   */
  private moveToHead(node: LRUNode<K, V>): void {
    this.removeNode(node);
    this.addToHead(node);
  }

  /**
   * 添加节点到头部
   */
  private addToHead(node: LRUNode<K, V>): void {
    node.prev = null;
    node.next = this.head;

    if (this.head) {
      this.head.prev = node;
    }
    this.head = node;

    if (!this.tail) {
      this.tail = node;
    }
  }

  /**
   * 从链表中移除节点
   */
  private removeNode(node: LRUNode<K, V>): void {
    if (node.prev) {
      node.prev.next = node.next;
    } else {
      this.head = node.next;
    }

    if (node.next) {
      node.next.prev = node.prev;
    } else {
      this.tail = node.prev;
    }
  }

  /**
   * 淘汰尾部节点(最久未使用)
   */
  private removeTail(): void {
    if (!this.tail) {
      return;
    }

    this.cache.delete(this.tail.key);
    this.removeNode(this.tail);
    this.size--;
  }
}

3.2 磁盘缓存实现

磁盘缓存咱们用RDB数据库来存,比Preferences更适合存大量结构化数据:

import relationalStore from '@ohos.data.relationalStore';
import { LRUCache } from './LRUCache';

/**
 * 磁盘缓存配置
 */
export interface DiskCacheConfig {
  dbName: string;          // 数据库名
  tableName: string;       // 表名
  maxAge: number;          // 默认过期时间(毫秒)
}

/**
 * 缓存数据实体
 */
interface CacheEntity {
  key: string;
  value: string;           // JSON序列化后的值
  timestamp: number;       // 写入时间戳
  expireTime: number;      // 过期时间
}

/**
 * 磁盘缓存 - 基于RDB实现
 */
export class DiskCache {
  private rdbStore: relationalStore.RdbStore | null = null;
  private config: DiskCacheConfig;
  private context: Context;

  constructor(context: Context, config: DiskCacheConfig) {
    this.context = context;
    this.config = config;
  }

  /**
   * 初始化数据库
   */
  async init(): Promise<void> {
    const config: relationalStore.StoreConfig = {
      name: this.config.dbName,
      securityLevel: relationalStore.SecurityLevel.S1
    };

    this.rdbStore = await relationalStore.getRdbStore(this.context, config);
    
    // 创建缓存表
    const createSql = `
      CREATE TABLE IF NOT EXISTS ${this.config.tableName} (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL,
        timestamp INTEGER NOT NULL,
        expireTime INTEGER NOT NULL
      )
    `;
    await this.rdbStore.executeSql(createSql);
  }

  /**
   * 写入缓存
   */
  async set<T>(key: string, value: T, maxAge?: number): Promise<void> {
    if (!this.rdbStore) {
      throw new Error('DiskCache not initialized');
    }

    const now = Date.now();
    const expireTime = now + (maxAge ?? this.config.maxAge);
    
    const entity: CacheEntity = {
      key,
      value: JSON.stringify(value),
      timestamp: now,
      expireTime
    };

    // 使用replace语义,存在则更新
    const valueBucket: relationalStore.ValuesBucket = {
      key: entity.key,
      value: entity.value,
      timestamp: entity.timestamp,
      expireTime: entity.expireTime
    };

    await this.rdbStore.insert(this.config.tableName, valueBucket, 
      relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
  }

  /**
   * 读取缓存
   */
  async get<T>(key: string): Promise<T | null> {
    if (!this.rdbStore) {
      throw new Error('DiskCache not initialized');
    }

    const predicates = new relationalStore.RdbPredicates(this.config.tableName);
    predicates.equalTo('key', key);
    
    const resultSet = await this.rdbStore.query(predicates);
    
    try {
      if (resultSet.goToFirstRow()) {
        const expireTime = resultSet.getLong(resultSet.getColumnIndex('expireTime'));
        
        // 检查是否过期
        if (Date.now() > expireTime) {
          // 异步删除过期数据
          this.delete(key);
          return null;
        }

        const valueStr = resultSet.getString(resultSet.getColumnIndex('value'));
        return JSON.parse(valueStr) as T;
      }
      return null;
    } finally {
      resultSet.close();
    }
  }

  /**
   * 删除缓存
   */
  async delete(key: string): Promise<void> {
    if (!this.rdbStore) {
      return;
    }

    const predicates = new relationalStore.RdbPredicates(this.config.tableName);
    predicates.equalTo('key', key);
    await this.rdbStore.delete(predicates);
  }

  /**
   * 清理所有过期缓存
   */
  async clearExpired(): Promise<number> {
    if (!this.rdbStore) {
      return 0;
    }

    const predicates = new relationalStore.RdbPredicates(this.config.tableName);
    predicates.lessThan('expireTime', Date.now());
    
    return await this.rdbStore.delete(predicates);
  }

  /**
   * 清空所有缓存
   */
  async clear(): Promise<void> {
    if (!this.rdbStore) {
      return;
    }

    await this.rdbStore.executeSql(`DELETE FROM ${this.config.tableName}`);
  }
}

3.3 多级缓存管理器

把内存缓存和磁盘缓存组合起来,形成完整的多级缓存体系:

import { LRUCache } from './LRUCache';
import { DiskCache } from './DiskCache';

/**
 * 缓存策略配置
 */
export interface CachePolicy {
  memoryCache: boolean;    // 是否启用内存缓存
  diskCache: boolean;      // 是否启用磁盘缓存
  memoryMaxSize: number;   // 内存缓存最大条目数
  diskMaxAge: number;      // 磁盘缓存过期时间(毫秒)
}

/**
 * 默认缓存策略
 */
const DEFAULT_POLICY: CachePolicy = {
  memoryCache: true,
  diskCache: true,
  memoryMaxSize: 100,
  diskMaxAge: 7 * 24 * 60 * 60 * 1000  // 7天
};

/**
 * 多级缓存管理器
 * 内存缓存(L1)+ 磁盘缓存(L2)
 */
export class CacheManager {
  private memoryCache: LRUCache<string, any> | null = null;
  private diskCache: DiskCache | null = null;
  private policy: CachePolicy;
  private initialized: boolean = false;

  constructor(private context: Context, policy: Partial<CachePolicy> = {}) {
    this.policy = { ...DEFAULT_POLICY, ...policy };
  }

  /**
   * 初始化缓存管理器
   */
  async init(): Promise<void> {
    if (this.initialized) {
      return;
    }

    // 初始化内存缓存
    if (this.policy.memoryCache) {
      this.memoryCache = new LRUCache<string, any>(this.policy.memoryMaxSize);
    }

    // 初始化磁盘缓存
    if (this.policy.diskCache) {
      this.diskCache = new DiskCache(this.context, {
        dbName: 'MultiCache.db',
        tableName: 'cache_data',
        maxAge: this.policy.diskMaxAge
      });
      await this.diskCache.init();
    }

    this.initialized = true;
  }

  /**
   * 获取缓存数据
   * 查找顺序:内存 → 磁盘 → 返回null
   */
  async get<T>(key: string): Promise<T | null> {
    this.ensureInitialized();

    // 1. 先查内存缓存
    if (this.memoryCache) {
      const memoryValue = this.memoryCache.get(key);
      if (memoryValue !== undefined) {
        console.debug(`[Cache] Hit memory: ${key}`);
        return memoryValue as T;
      }
    }

    // 2. 再查磁盘缓存
    if (this.diskCache) {
      const diskValue = await this.diskCache.get<T>(key);
      if (diskValue !== null) {
        console.debug(`[Cache] Hit disk: ${key}`);
        // 回填内存缓存
        if (this.memoryCache) {
          this.memoryCache.set(key, diskValue);
        }
        return diskValue;
      }
    }

    console.debug(`[Cache] Miss: ${key}`);
    return null;
  }

  /**
   * 设置缓存数据
   * 同时写入内存和磁盘
   */
  async set<T>(key: string, value: T, maxAge?: number): Promise<void> {
    this.ensureInitialized();

    // 写入内存缓存
    if (this.memoryCache) {
      this.memoryCache.set(key, value);
    }

    // 写入磁盘缓存
    if (this.diskCache) {
      await this.diskCache.set(key, value, maxAge);
    }

    console.debug(`[Cache] Set: ${key}`);
  }

  /**
   * 获取或创建缓存
   * 如果缓存不存在,执行factory函数获取数据并缓存
   */
  async getOrSet<T>(
    key: string,
    factory: () => Promise<T>,
    maxAge?: number
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) {
      return cached;
    }

    // 执行factory获取数据
    const value = await factory();
    await this.set(key, value, maxAge);
    return value;
  }

  /**
   * 删除缓存
   */
  async delete(key: string): Promise<void> {
    if (this.memoryCache) {
      this.memoryCache.delete(key);
    }
    if (this.diskCache) {
      await this.diskCache.delete(key);
    }
  }

  /**
   * 清空所有缓存
   */
  async clear(): Promise<void> {
    if (this.memoryCache) {
      this.memoryCache.clear();
    }
    if (this.diskCache) {
      await this.diskCache.clear();
    }
  }

  /**
   * 清理过期缓存
   */
  async cleanup(): Promise<number> {
    if (!this.diskCache) {
      return 0;
    }
    return await this.diskCache.clearExpired();
  }

  /**
   * 获取缓存统计信息
   */
  getStats(): { memorySize: number; policy: CachePolicy } {
    return {
      memorySize: this.memoryCache?.getSize() ?? 0,
      policy: this.policy
    };
  }

  /**
   * 确保已初始化
   */
  private ensureInitialized(): void {
    if (!this.initialized) {
      throw new Error('CacheManager not initialized. Call init() first.');
    }
  }
}

3.4 完整使用示例

import { CacheManager } from './CacheManager';
import http from '@ohos.net.http';

@Entry
@Component
struct CacheDemoPage {
  @State message: string = '缓存策略演示';
  @State cacheStats: string = '';
  
  private cacheManager: CacheManager | null = null;

  async aboutToAppear(): Promise<void> {
    // 初始化缓存管理器
    this.cacheManager = new CacheManager(getContext(this), {
      memoryMaxSize: 50,           // 内存缓存最多50条
      diskMaxAge: 24 * 60 * 60 * 1000  // 磁盘缓存1天过期
    });
    
    await this.cacheManager.init();
    this.updateStats();
  }

  /**
   * 模拟网络请求获取用户信息
   */
  async fetchUserInfo(userId: string): Promise<UserInfo> {
    // 使用缓存的getOrSet方法
    // 缓存命中直接返回,未命中则执行网络请求
    return await this.cacheManager!.getOrSet<UserInfo>(
      `user_${userId}`,
      async () => {
        // 实际的网络请求
        const httpRequest = http.createHttp();
        const response = await httpRequest.request(
          `https://api.example.com/users/${userId}`,
          { method: http.RequestMethod.GET }
        );
        httpRequest.destroy();
        
        return JSON.parse(response.result as string) as UserInfo;
      },
      30 * 60 * 1000  // 缓存30分钟
    );
  }

  /**
   * 测试缓存效果
   */
  async testCache(): Promise<void> {
    const userId = '12345';
    
    // 第一次请求 - 会走网络
    console.info('第一次请求:');
    const user1 = await this.fetchUserInfo(userId);
    console.info(`用户: ${user1.name}`);
    
    // 第二次请求 - 命中内存缓存
    console.info('第二次请求:');
    const user2 = await this.fetchUserInfo(userId);
    console.info(`用户: ${user2.name}`);
    
    // 清空内存缓存,第三次请求会命中磁盘缓存
    this.cacheManager!.clear();
    console.info('清空内存后第三次请求:');
    const user3 = await this.fetchUserInfo(userId);
    console.info(`用户: ${user3.name}`);
    
    this.updateStats();
  }

  /**
   * 更新统计信息
   */
  updateStats(): void {
    const stats = this.cacheManager?.getStats();
    this.cacheStats = `内存缓存: ${stats?.memorySize} 条`;
  }

  build() {
    Column() {
      Text(this.message)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Text(this.cacheStats)
        .fontSize(16)
        .fontColor('#666666')
        .margin({ bottom: 20 })

      Button('测试缓存效果')
        .width('80%')
        .onClick(() => this.testCache())

      Button('清理过期缓存')
        .width('80%')
        .margin({ top: 10 })
        .onClick(async () => {
          const count = await this.cacheManager?.cleanup() ?? 0;
          this.message = `已清理 ${count} 条过期缓存`;
        })

      Button('清空所有缓存')
        .width('80%')
        .margin({ top: 10 })
        .onClick(async () => {
          await this.cacheManager?.clear();
          this.updateStats();
          this.message = '缓存已清空';
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }
}

/**
 * 用户信息类型定义
 */
interface UserInfo {
  id: string;
  name: string;
  avatar: string;
  email: string;
}

四、踩坑与注意事项

4.1 内存缓存容量设置

容量设置太小,缓存命中率低;设置太大,可能OOM。建议根据数据大小动态计算:

// 错误示范:固定容量
const cache = new LRUCache<string, any>(1000);  // 可能存大量图片导致OOM

// 正确做法:根据预估数据大小计算
const avgDataSize = 50 * 1024;  // 假设平均50KB
const maxMemory = 50 * 1024 * 1024;  // 分配50MB内存
const capacity = Math.floor(maxMemory / avgDataSize);  // 约1000条

4.2 磁盘缓存序列化问题

不是所有数据都能JSON序列化,比如Buffer、Map、Set等:

// 错误示范:直接存Buffer
await diskCache.set('image', imageBuffer);  // 序列化失败

// 正确做法:转为base64或直接存文件
const base64 = imageBuffer.toString('base64');
await diskCache.set('image_base64', base64);

4.3 并发访问问题

多线程同时读写缓存可能导致数据不一致。ArkTS是单线程模型,但异步操作仍需注意:

// 潜在问题:两次getOrSet可能同时执行factory
const promise1 = cache.getOrSet('key', expensiveFactory);
const promise2 = cache.getOrSet('key', expensiveFactory);

// 解决方案:使用Promise缓存
private pendingRequests: Map<string, Promise<any>> = new Map();

async getOrSetSafe<T>(key: string, factory: () => Promise<T>): Promise<T> {
  // 检查是否有进行中的请求
  if (this.pendingRequests.has(key)) {
    return this.pendingRequests.get(key) as Promise<T>;
  }
  
  const promise = this.getOrSet(key, factory);
  this.pendingRequests.set(key, promise);
  
  try {
    return await promise;
  } finally {
    this.pendingRequests.delete(key);
  }
}

4.4 缓存key设计

key设计不合理会导致缓存冲突或难以管理:

// 错误示范:简单拼接可能冲突
const key = userId + '_' + type;  // "123_profile" vs "12_3profile"

// 正确做法:使用分隔符或结构化key
const key = `user:${userId}:${type}`;
// 或者
const key = JSON.stringify({ userId, type, version: 'v1' });

4.5 过期时间精度

磁盘缓存过期检查在读取时才触发,可能积累大量过期数据:

// 建议定期清理
setInterval(() => {
  cacheManager.cleanup();
}, 24 * 60 * 60 * 1000);  // 每天清理一次

五、HarmonyOS 6适配说明

5.1 RDB API变更

HarmonyOS 6对RDB的API进行了调整,部分方法签名变化:

// HarmonyOS 5写法
const resultSet = await rdbStore.query(predicates, ['key', 'value']);

// HarmonyOS 6适配
// query方法增加columns参数类型校验,需显式声明
const columns: string[] = ['key', 'value'];
const resultSet = await rdbStore.query(predicates, columns);

5.2 安全等级调整

HarmonyOS 6对数据安全等级要求更严格:

// HarmonyOS 5
const config: relationalStore.StoreConfig = {
  name: 'cache.db',
  securityLevel: relationalStore.SecurityLevel.S1
};

// HarmonyOS 6适配
// 敏感数据需使用S3或S4等级
const config: relationalStore.StoreConfig = {
  name: 'cache.db',
  securityLevel: relationalStore.SecurityLevel.S2,  // 根据数据敏感度调整
  encrypt: false  // 非敏感缓存可关闭加密提升性能
};

5.3 内存管理增强

HarmonyOS 6引入了更精细的内存管理API:

// HarmonyOS 6新增:内存压力监听
import memory from '@ohos.memory';

// 监听系统内存压力,动态调整缓存大小
memory.on('pressure', (level: memory.PressureLevel) => {
  if (level === memory.PressureLevel.CRITICAL) {
    // 内存紧张时,缩减缓存容量
    const currentSize = lruCache.getSize();
    const newSize = Math.floor(currentSize * 0.5);
    lruCache.resize(newSize);  // 新增的resize方法
  }
});

5.4 缓存持久化策略

HarmonyOS 6支持应用冻结时的缓存持久化:

// HarmonyOS 6新增:应用生命周期感知
import abilityLifecycle from '@ohos.app.ability.lifecycle';

// 应用进入后台时,可选择持久化内存缓存
abilityLifecycle.on('background', async () => {
  if (memoryCache) {
    // 将热点数据持久化到磁盘
    const hotKeys = getHotKeys();  // 自定义热度判断
    for (const key of hotKeys) {
      const value = memoryCache.get(key);
      if (value && diskCache) {
        await diskCache.set(key, value);
      }
    }
  }
});

六、总结

维度 评价
学习难度 ⭐⭐⭐⭐
使用频率 ⭐⭐⭐⭐⭐
重要程度 ⭐⭐⭐⭐⭐
调试难度 ⭐⭐⭐

缓存策略设计是性能优化的基石。通过本文的学习,你应该掌握了:

核心收获

  1. LRU算法原理:双向链表+HashMap实现O(1)时间复杂度的缓存淘汰
  2. 多级缓存架构:内存缓存(快)+ 磁盘缓存(持久)的协同工作模式
  3. 策略模式应用:不同数据类型配置不同的缓存策略
  4. 工程化实践:缓存key设计、并发控制、过期清理等实战技巧

最佳实践建议

  • 内存缓存容量根据实际数据大小动态计算,避免OOM
  • 磁盘缓存使用RDB而非Preferences,支持更大数据量
  • 定期清理过期数据,避免存储空间无限增长
  • 使用结构化key设计,便于管理和调试
  • 监听系统内存压力,动态调整缓存策略

缓存不是银弹,用错了反而增加复杂度。记住一个原则:只缓存那些"计算成本高、访问频率高、变化频率低"的数据。其他情况,老老实实请求网络反而更简单可靠。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐