我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

直白讲:数据存储选型=体验上限。如果你把“热路径”丢进了“冷介质”,或者把需要结构化查询的内容扔给了“傻大黑粗”的文件系统,性能发飙、迁移跪、备份痛、权限乱……全来找你。今天我们把 Preferences / RDB / KV / 文件 四件套的职责边界热路径优化Schema 变更与迁移备份/恢复策略以及安全存储(加密/沙箱)一口气讲透,最后给一个可落地的“组合仓库层”示例,让你的项目既快、又稳、还能优雅进化。

目录(拿去给评审当骨架)

  1. 选型脑图:四种存储的边界与典型场景
  2. 热路径读写优化:延迟、吞吐与抖动的“三条线”
  3. Schema 变更与迁移:版本门、灰度与回滚
  4. 备份 / 恢复:本地快照、云端同步、断点续传
  5. 安全存储:加密模型、密钥管理、最小权限
  6. 组合拳落地:一套 Repository 把四类存储“拧成绳”
  7. 工程清单 & 踩坑博物馆
  8. 结语与一个小问题

1) 选型脑图:谁该装哪类数据?

存储 强项 不适合 典型数据 读/写延迟 备注
Preferences(键值对,轻量持久) 极快的小配置、开关、最近路由、Token(短文本) 大对象、频繁批量写、查询/排序 布尔开关、字符串、数字、少量 JSON 低/低 原子落盘但别高频多键写
RDB(关系型数据库) 结构化数据、复杂查询、分页、索引 超大二进制、一次性文件 列表、日志、设备表、离线缓存 中/中 有事务、可分区索引、写放大可控
KV(分布式/本地 K-V) 会话态、共享态、订阅变化、弱结构 复杂多维查询、范围扫描 在线状态、房间快照、临时会话 低/低 可做观测点热缓存
文件(顺序/随机 IO) 媒体、导出、离线包、大对象 细粒度查询、频繁小写 图片、加密档、导出 CSV 取决于大小 建议目录分层 + 校验和

经验法则

  • 读多写少的配置”→ Preferences。
  • 要查要筛要分页”→ RDB。
  • 需要多端共享/订阅”→ KV。
  • 大块头/二进制”→ 文件(必要时 RDB 存元数据 + 文件存主体)。

2) 热路径读写优化:把“慢操作”挡在用户感觉之外

2.1 写优化

  • 批量写合并:把多次 preferences 写合并成一次事务(定时 50–100ms 冲洗)。
  • RDB 事务 + 批插BEGIN … INSERT … COMMIT,减少 fsync 次数。
  • 异步落盘:UI 线程只改内存态;写入交给单线程队列
  • 去抖/节流:滑块这类高频输入,落盘用去抖 150–300ms

2.2 读优化

  • 内存缓存(热 Key):启动时一次性拉取关键配置,放 MemoryCache
  • 索引策略:RDB 给查询列上复合索引;避免 %like 前缀模糊造成全表扫。
  • 分页固定步长:例如 20/50 条,避免不可预测的内存抖动。
  • 文件分块与预读:大文件按块读,配合 CRC 校验。

2.3 抖动治理

  • 写放大:日志或计数器高频写→转为内存累积 + 定时 flush
  • 冷路径降级:低优先写任务(如统计)延后到空闲/充电/Wi-Fi 时。

3) Schema 变更与迁移:先立版本门,再谈升级

3.1 版本门

  • 全局 schemaVersion 存在 Preferences;RDB 内部再保留一份 PRAGMA user_version/元表。
  • 启动时:比较版本→顺序执行迁移脚本→标记成功。

3.2 迁移类型

  • 向后兼容:加列(带默认)、加表、加索引 → 线上无感。

  • 破坏性:删列/改类型 → 走“影子表迁移”:

    1. 建新表;
    2. 按批搬数据(可中断/续传);
    3. 切换视图/重命名;
    4. 回收旧表。

3.3 失败与回滚

  • 迁移脚本幂等(可重入、可跳过已完成步)。
  • 设“熔断”:迁移超时/错误 → 退回旧版本运行(读旧表),下次再尝试。

4) 备份 / 恢复:别把“数据生死”交给玄学

  • 本地快照:RDB 用 VACUUM INTO / 只读拷贝得到一致性快照;Preferences、KV 导出为 JSON;文件打清单 + 校验
  • 云备份端到端加密,断点续传(chunk + offset),小文件聚合减少请求数。
  • 选择性恢复:支持按模块恢复(避免把旧垃圾也抬回来)。
  • 校验:快照包含 manifest.json(版本、时间、校验和、加密参数)。

5) 安全存储:加密/沙箱/最小权限三板斧

  • 沙箱:运行时默认应用私有目录;导出时走系统分享通道,避免裸写公共目录。

  • 加密模型

    • 小文本(Token、密钥种子)→ Keystore/Keychain(硬件/TEE)
    • RDB 敏感列 → 列级加密(应用密钥包裹数据密钥,数据密钥轮换)
    • 文件 → 流式 AES-GCM 加密(带随机盐与 AAD)
  • 密钥管理

    • 主密钥存 Keystore;
    • 数据密钥随机生成,定期轮换(manifest 里记录版本);
    • 备份前再包裹一层用户口令(PBKDF2/Argon2)
  • 最小化:只请求你真的需要的读写权限;临时授权优先。

注意:加密≠安全,如果日志/崩溃上报把明文打出,也白给。敏感值打印前脱敏


6) 组合拳落地:用 Repository 把四类存储“拧成绳”

以下为示意代码(接口与命名示例化,便于迁移到你项目的具体 API)。思路是:内存缓存兜顶、Preferences装微配置、RDB放结构化数据、KV做共享态与订阅、文件存大对象;统一经由 Repository 访问,读写都有去抖与批处理

6.1 接口与基础设施

// storage/types.ts
export type Json = string | number | boolean | null | Json[] | { [k: string]: Json };

export interface Prefs {
  get<T=Json>(key: string, def?: T): Promise<T>;
  set(entries: Record<string, Json>): Promise<void>; // 批量
}

export interface Rdb {
  query<T>(sql: string, args?: unknown[]): Promise<T[]>;
  exec(sql: string, args?: unknown[]): Promise<void>;
  tx<T>(fn: (db: Rdb) => Promise<T>): Promise<T>;
}

export interface Kv {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, val: T): Promise<void>;
  subscribe(key: string, cb: (v: unknown) => void): () => void;
}

export interface BlobStore {
  read(uri: string): Promise<ArrayBuffer>;
  write(uriHint: string, data: ArrayBuffer, opts?: { encrypt?: boolean }): Promise<string>; // 返回最终 uri
}

6.2 Repository:热路径、去抖与分层

// storage/repository.ts
import { Prefs, Rdb, Kv, BlobStore } from './types';
import { debounce } from './utils';

export class Repo {
  constructor(private prefs: Prefs, private rdb: Rdb, private kv: Kv, private blob: BlobStore) {}

  // === Preferences:启动热 Key 一次性加载到内存 ===
  private hot: Map<string, any> = new Map();
  async warmHotKeys(keys: string[]) {
    const pairs = await Promise.all(keys.map(k => this.prefs.get(k, null)));
    keys.forEach((k, i) => this.hot.set(k, pairs[i]));
  }
  getPref<T>(key: string, def: T): T { return (this.hot.get(key) ?? def) as T; }
  setPref = debounce(async (obj: Record<string, any>) => {
    Object.entries(obj).forEach(([k, v]) => this.hot.set(k, v));
    await this.prefs.set(obj);
  }, 80);

  // === RDB:分页读取 & 批插入 ===
  async listDevices(offset=0, limit=50) {
    return this.rdb.query(
      `SELECT id, name, room, online, updated_at 
       FROM device ORDER BY updated_at DESC LIMIT ? OFFSET ?`, [limit, offset]
    );
  }
  async upsertDevices(devs: Array<{id:string,name:string,room:string,online:number}>) {
    return this.rdb.tx(async db => {
      await db.exec('BEGIN');
      for (const d of devs) {
        await db.exec(
          `INSERT INTO device(id,name,room,online,updated_at)
           VALUES(?,?,?,?,strftime('%s','now'))
           ON CONFLICT(id) DO UPDATE SET name=excluded.name, room=excluded.room, online=excluded.online, updated_at=strftime('%s','now')`,
          [d.id, d.name, d.room, d.online]
        );
      }
      await db.exec('COMMIT');
    });
  }

  // === KV:共享态与订阅(例如“家是否有人”) ===
  subscribePresence(cb: (present: boolean) => void) {
    return this.kv.subscribe('presence/home', v => cb(Boolean(v)));
  }
  setPresence(present: boolean) { return this.kv.set('presence/home', present); }

  // === 文件:大对象(比如本地快照/导出) ===
  async exportSnapshot(payload: Uint8Array) {
    // 这里可接入 AES-GCM 加密再写
    const uri = await this.blob.write('snapshots/snap-', payload.buffer, { encrypt: true });
    return uri;
  }
}

6.3 Schema 迁移脚手(顺序化 + 幂等)

// storage/migrations.ts
type Step = { id: number; up: (db: Rdb) => Promise<void> };

export const steps: Step[] = [
  { id: 1, up: async db => {
      await db.exec(`CREATE TABLE IF NOT EXISTS device(
        id TEXT PRIMARY KEY, name TEXT, room TEXT, online INTEGER, updated_at INTEGER
      )`);
      await db.exec(`CREATE INDEX IF NOT EXISTS idx_device_updated ON device(updated_at DESC)`);
    }},
  { id: 2, up: async db => {
      await db.exec(`ALTER TABLE device ADD COLUMN vendor TEXT DEFAULT ''`);
    }},
];

export async function migrate(db: Rdb, getVer:()=>Promise<number>, setVer:(n:number)=>Promise<void>) {
  const cur = await getVer(); // from preferences or meta
  const pending = steps.filter(s => s.id > cur).sort((a,b)=>a.id-b.id);
  for (const s of pending) {
    await db.tx(d => s.up(d));
    await setVer(s.id);
  }
}

6.4 备份 manifest(跨介质统一)

{
  "version": 2,
  "createdAt": 1730000000,
  "rdb": { "schemaVersion": 2, "file": "backup/device.sqlite.cpk", "checksum": "sha256:..." },
  "prefs": { "file": "backup/prefs.json.enc", "keys": ["theme","lang","tokenHash"] },
  "kv": { "subset": ["presence/home"] },
  "files": [
    { "uri": "snapshots/snap-2025-10-27.cpk", "size": 1048576, "checksum": "sha256:..." }
  ],
  "crypto": { "alg": "AES-256-GCM", "keyVersion": 3, "salt": "base64..." }
}

7) 工程清单 & 踩坑博物馆

工程清单

  • 读写分层:UI→UseCase→Repository→Driver(Prefs/RDB/KV/FS);UI 不直接碰驱动。
  • 热 Key 预热:启动把常用配置拉到内存,写有去抖。
  • RDB 索引与分页:每个可过滤字段评估是否上索引;长列表一律分页。
  • 迁移脚本可回放/幂等:失败可重入;置“已执行标记”。
  • 备份断点续传:分块 + 偏移 + 校验;manifest 记录一切。
  • 密钥生命周期:主密钥在 Keystore,数据密钥可轮换;日志脱敏。
  • 压测脚本:构造 10k/100k 记录,测 P50/P95 读写;弱网恢复测试。

踩坑博物馆

  • Preferences 高频多键写:同步落盘颠簸明显 → ✅ 合并/去抖 + 单次提交。
  • 把缩略图二进制塞 RDB:文件膨胀、WAL 压力大 → ✅ 文件系统存 BLOB,RDB 存元数据+URI。
  • 集合原地改不触发刷新(UI 层)→ ✅ 不可变赋值/替换引用。
  • 迁移里直接删列 → ✅ 影子表渐进迁移 + 回滚预案。
  • 备份不校验 → ✅ 每块带校验和,恢复先验再写。
  • 把 Token 明文放 Preferences → ✅ Keystore 包裹 + 过期与吊销机制。

8) 结语:快与稳,不是二选一

存储选型做对了,你的应用 “冷启动像热启动,热路径像内存访问”;迁移与备份也能“稳如老狗”。Preferences 定参、RDB 做骨、KV 做魂、文件扛大件——再辅以迁移脚本、快照与加密,这就是一套能陪你版本迭代的“组合拳”。下次有人问“用哪个存?”——请你笑着回一句:**“都用,但各安其位。”**😉

(未完待续)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐