鸿蒙存储碎片高级清理机制:数据库Vacuum/文件碎片整理/缓存过期清理/主动空间回收策略
·



一、前置思考
1.1 存储空间"只涨不跌"是常态
长时间运行的应用,存储占用会持续膨胀:数据库文件越来越大、缓存堆积、临时文件不清理、日志无限增长。用户看到"应用占用 3GB"会直接卸载。
膨胀源1: 数据库 DELETE/UPDATE 产生页碎片, 文件只增不减
膨胀源2: 图片/视频缓存无上限, 磁盘占用失控
膨胀源3: 日志/临时文件从不清理
膨胀源4: 旧版本数据归档缺失
1.2 碎片与垃圾的本质
数据库碎片:
DELETE 后: 页标记空闲但未归还文件 → 文件大小不变
UPDATE 变长: 旧页闲置 + 新页追加 → 空洞
→ VACUUM 重建文件, 回收空洞
文件碎片:
频繁增删文件 → 磁盘块不连续 → 读写变慢
→ 文件整理 + 归并
缓存垃圾:
过期缓存无淘汰 → 占用磁盘
→ TTL + LRU 清理
1.3 本文路线
给出数据库 VACUUM、文件碎片整理、缓存过期清理、主动空间回收四层清理体系。
二、核心原理
2.1 SQLite 页碎片与 VACUUM
SQLite 文件 = 一页页 B-Tree
DELETE 行 → 页内空间标记空闲 → 可复用 (但文件不缩小)
频繁增删 → 页空洞分散 → 查询多页扫描 → 变慢
文件膨胀 → 实际数据 100MB, 文件 300MB
VACUUM:
重建整个数据库文件 → 压缩空洞 → 文件变小 + 查询变快
代价: 重建期间锁库, 大库耗时
2.2 文件碎片整理
碎片产生:
小文件频繁创建/删除 → 文件系统块分散
追加写 → 文件块不连续
整理手段:
① 归并小文件: 多个小文件合并成归档文件
② 重写文件: 读出来写回连续区域
③ 减少碎片: 预留空间 + 批量写入
2.3 缓存过期清理
缓存生命周期:
写入: 带 TTL (绝对过期) 或 LRU (容量淘汰)
读取: 检查是否过期, 过期即失效
清理: 周期扫描 + 触发清理
两种淘汰策略:
TTL: 数据有明确时效 (验证码/临时文件)
LRU: 容量有限, 淘汰最久未用 (图片缓存)
2.4 主动空间回收策略
回收时机:
① 定时 (每天/每周)
② 事件触发 (存储告警/退出时)
③ 条件触发 (空闲时/充电时)
回收流程:
扫描垃圾 → 分级处理 → 记录清理量 → 上报
(可删除: cache/、过期文件、临时文件)
(不可删: files/ 用户数据、数据库)
三、源码/API 深度解析
3.1 数据库 VACUUM
import { relationalStore } from '@kit.ArkData';
async function vacuumDb(rdb: relationalStore.RdbStore): Promise<void> {
const before = await getDbFileSize(rdb); // 查询文件大小
// VACUUM 重建数据库文件
await rdb.executeSql('VACUUM');
const after = await getDbFileSize(rdb);
console.info(`VACUUM: ${before} → ${after}, 回收 ${before - after} bytes`);
}
// 增量检查: 空洞率超阈值才 VACUUM (避免频繁重建)
async function maybeVacuum(rdb: relationalStore.RdbStore,
thresholdPct = 0.3): Promise<void> {
const rs = await rdb.querySql('PRAGMA freelist_count');
const pageCount = await queryPageCount(rdb);
rs.close();
if (pageCount > 0 && freelist / pageCount > thresholdPct) {
await rdb.executeSql('VACUUM');
}
}
3.2 缓存目录清理
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
// 清理 cache 目录: 全部可删 (系统也可能清)
function cleanCacheDir(context: common.UIAbilityContext,
maxAgeMs: number): number {
let freed = 0;
const cacheDir = context.cacheDir;
const entries = fs.listFileSync(cacheDir);
const now = Date.now();
entries.forEach((name: string) => {
const p = `${cacheDir}/${name}`;
const stat = fs.statSync(p);
// 超过 maxAge 的缓存文件删除
if (now - stat.mtime > maxAgeMs) {
fs.unlinkSync(p);
freed += stat.size;
}
});
return freed;
}
3.3 临时文件与日志清理
// 清理规则: temp 目录全部 + files 下带 .tmp 后缀 + 过期日志
function cleanTempAndLogs(context: common.UIAbilityContext,
keepLogDays = 7): number {
let freed = 0;
const tempDir = context.tempDir;
// temp 目录: 全清
fs.listFileSync(tempDir).forEach((name: string) => {
const p = `${tempDir}/${name}`;
freed += fs.statSync(p).size;
fs.unlinkSync(p);
});
// 日志: 只保留最近 N 天
const logDir = `${context.filesDir}/logs`;
if (fs.accessSync(logDir)) {
fs.listFileSync(logDir).forEach((name: string) => {
const p = `${logDir}/${name}`;
const stat = fs.statSync(p);
const ageDays = (Date.now() - stat.mtime) / 86400000;
if (ageDays > keepLogDays) {
freed += stat.size;
fs.unlinkSync(p);
}
});
}
return freed;
}
四、企业级实战落地
4.1 存储清理分层架构
┌──────────────────────────────────────────┐
│ 触发层: 定时 / 告警 / 退出 / 用户手动 │
├──────────────────────────────────────────┤
│ 调度层: 清理任务编排 (优先级/并发控制) │
├──────────────────────────────────────────┤
│ 执行层: │
│ ① 数据库: VACUUM / 删除过期行 │
│ ② 缓存: TTL + LRU 淘汰 │
│ ③ 临时文件: temp + tmp 清理 │
│ ④ 日志: 轮转 + 保留期 │
├──────────────────────────────────────────┤
│ 记录层: 清理量统计 + 埋点上报 │
└──────────────────────────────────────────┘
4.2 数据库行级清理(配合 VACUUM)
// 先删过期数据行, 再考虑 VACUUM
async function cleanExpiredRows(rdb: relationalStore.RdbStore): Promise<number> {
const cutoff = Date.now() - 30 * 86400000; // 30 天前
const predicates = new relationalStore.RdbPredicates('logs');
predicates.lessThan('created_at', cutoff);
const rows = await rdb.delete(predicates);
// 删除后空洞率上升, 满足阈值再 VACUUM
await maybeVacuum(rdb, 0.4);
return rows;
}
4.3 主动回收调度
// 条件触发: 磁盘空间低于阈值时主动清理
class StorageGuard {
private readonly warnThreshold = 2 * 1024 * 1024 * 1024; // 2GB
async check(context: common.UIAbilityContext): Promise<void> {
const free = await getFreeSpace(context); // 获取剩余空间
if (free < this.warnThreshold) {
// 1. 清理 cache (最安全)
const f1 = cleanCacheDir(context, 24 * 3600 * 1000);
// 2. 清理临时文件
const f2 = cleanTempAndLogs(context);
// 3. 数据库 VACUUM
await vacuumIfNeeded();
console.info(`空间告警清理: 回收 ${f1 + f2} bytes`);
}
}
}
4.4 清理效果基准(模拟)
| 清理项 | 清理前 | 清理后 | 回收 |
|---|---|---|---|
| 数据库 VACUUM | 320MB | 118MB | 202MB |
| 缓存 TTL 清理 | 860MB | 240MB | 620MB |
| 临时/日志 | 90MB | 12MB | 78MB |
| 合计 | 1.27GB | 370MB | 900MB |
五、问题排查与性能优化
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 数据库只涨不缩 | 删除后文件不小 | 未 VACUUM | 空洞率触发 VACUUM |
| VACUUM 卡顿 | 大库重建阻塞 | 前台执行 | 后台 TaskPool + 空闲时 |
| 缓存清不掉 | 空间告警仍大 | 无 TTL | 写入带 TTL |
| 误删用户数据 | 清理后数据丢 | files 目录误清 | 只清 cache/temp |
| 清理频繁 | IO 负担 | 每次启动清 | 条件触发 |
| 碎片查询慢 | 增删后变卡 | 空洞分散 | 定期 VACUUM |
| 日志无限增长 | 磁盘被日志占满 | 无轮转 | 保留期 + 轮转 |
5.1 VACUUM 时机选择
❌ 前台/启动时 VACUUM (阻塞 UI)
✅ 后台空闲时 / 充电时 / 空间告警时
✅ 空洞率超阈值才执行 (避免频繁重建)
✅ 大库分两步: 先删数据行, 再 VACUUM
5.2 缓存写入即带生命周期
// 工程规范: 一切缓存写入必须带 TTL/容量
interface CacheEntry {
data: string;
expireAt: number; // 绝对过期
}
function writeCache(kv: KVStore, key: string, data: string, ttlMs: number): void {
const entry: CacheEntry = { data: data, expireAt: Date.now() + ttlMs };
kv.put(key, JSON.stringify(entry));
}
5.3 用户数据安全红线
可清理 (安全): cache/ · temp/ · *.tmp · 过期缓存 · 旧日志
不可清理: files/ 用户数据 · database/ · preferences/ · 加密文件
清理前校验: 二次确认 + 记录清理清单 (可审计)
六、高阶总结与最佳实践
- 数据库膨胀靠 VACUUM:空洞率阈值触发,后台执行,先删行再重建。
- 缓存垃圾靠生命周期:写入带 TTL/LRU,过期即失效即清理。
- 临时文件靠纪律:temp 全清、tmp 清理、日志轮转保保留期。
- 主动回收靠触发:定时 + 空间告警 + 退出时,分级清理。
- 安全红线:只清可重建数据,用户数据与加密文件绝不动。
一句话记住:数据库 VACUUM 缩文件、缓存 TTL 灭垃圾、临时文件定时清、主动回收分级做——空间只涨不跌的应用留不住用户。
更多推荐




所有评论(0)