配置丢在 Preferences、列表塞进 RDB、状态放 KV、文件走加密?——别猜,用一套组合拳把读写打到飞起!
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
直白讲:数据存储选型=体验上限。如果你把“热路径”丢进了“冷介质”,或者把需要结构化查询的内容扔给了“傻大黑粗”的文件系统,性能发飙、迁移跪、备份痛、权限乱……全来找你。今天我们把 Preferences / RDB / KV / 文件 四件套的职责边界、热路径优化、Schema 变更与迁移、备份/恢复策略以及安全存储(加密/沙箱)一口气讲透,最后给一个可落地的“组合仓库层”示例,让你的项目既快、又稳、还能优雅进化。
目录(拿去给评审当骨架)
- 选型脑图:四种存储的边界与典型场景
- 热路径读写优化:延迟、吞吐与抖动的“三条线”
- Schema 变更与迁移:版本门、灰度与回滚
- 备份 / 恢复:本地快照、云端同步、断点续传
- 安全存储:加密模型、密钥管理、最小权限
- 组合拳落地:一套 Repository 把四类存储“拧成绳”
- 工程清单 & 踩坑博物馆
- 结语与一个小问题
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 迁移类型
-
向后兼容:加列(带默认)、加表、加索引 → 线上无感。
-
破坏性:删列/改类型 → 走“影子表迁移”:
- 建新表;
- 按批搬数据(可中断/续传);
- 切换视图/重命名;
- 回收旧表。
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 做魂、文件扛大件——再辅以迁移脚本、快照与加密,这就是一套能陪你版本迭代的“组合拳”。下次有人问“用哪个存?”——请你笑着回一句:**“都用,但各安其位。”**😉
…
(未完待续)
更多推荐




所有评论(0)