鸿蒙存储碎片高级清理机制:数据库Vacuum/文件碎片整理/缓存过期清理/主动空间回收策略
一、前置思考1.1 存储空间只涨不跌是常态长时间运行的应用存储占用会持续膨胀数据库文件越来越大、缓存堆积、临时文件不清理、日志无限增长。用户看到应用占用 3GB会直接卸载。膨胀源1: 数据库 DELETE/UPDATE 产生页碎片, 文件只增不减 膨胀源2: 图片/视频缓存无上限, 磁盘占用失控 膨胀源3: 日志/临时文件从不清理 膨胀源4: 旧版本数据归档缺失1.2 碎片与垃圾的本质数据库碎片: DELETE 后: 页标记空闲但未归还文件 → 文件大小不变 UPDATE 变长: 旧页闲置 新页追加 → 空洞 → VACUUM 重建文件, 回收空洞 文件碎片: 频繁增删文件 → 磁盘块不连续 → 读写变慢 → 文件整理 归并 缓存垃圾: 过期缓存无淘汰 → 占用磁盘 → TTL LRU 清理1.3 本文路线给出数据库 VACUUM、文件碎片整理、缓存过期清理、主动空间回收四层清理体系。二、核心原理2.1 SQLite 页碎片与 VACUUMSQLite 文件 一页页 B-Tree DELETE 行 → 页内空间标记空闲 → 可复用 (但文件不缩小) 频繁增删 → 页空洞分散 → 查询多页扫描 → 变慢 文件膨胀 → 实际数据 100MB, 文件 300MB VACUUM: 重建整个数据库文件 → 压缩空洞 → 文件变小 查询变快 代价: 重建期间锁库, 大库耗时2.2 文件碎片整理碎片产生: 小文件频繁创建/删除 → 文件系统块分散 追加写 → 文件块不连续 整理手段: ① 归并小文件: 多个小文件合并成归档文件 ② 重写文件: 读出来写回连续区域 ③ 减少碎片: 预留空间 批量写入2.3 缓存过期清理缓存生命周期: 写入: 带 TTL (绝对过期) 或 LRU (容量淘汰) 读取: 检查是否过期, 过期即失效 清理: 周期扫描 触发清理 两种淘汰策略: TTL: 数据有明确时效 (验证码/临时文件) LRU: 容量有限, 淘汰最久未用 (图片缓存)2.4 主动空间回收策略回收时机: ① 定时 (每天/每周) ② 事件触发 (存储告警/退出时) ③ 条件触发 (空闲时/充电时) 回收流程: 扫描垃圾 → 分级处理 → 记录清理量 → 上报 (可删除: cache/、过期文件、临时文件) (不可删: files/ 用户数据、数据库)三、源码/API 深度解析3.1 数据库 VACUUMimport{relationalStore}fromkit.ArkData;asyncfunctionvacuumDb(rdb:relationalStore.RdbStore):Promisevoid{constbeforeawaitgetDbFileSize(rdb);// 查询文件大小// VACUUM 重建数据库文件awaitrdb.executeSql(VACUUM);constafterawaitgetDbFileSize(rdb);console.info(VACUUM:${before}→${after}, 回收${before-after}bytes);}// 增量检查: 空洞率超阈值才 VACUUM (避免频繁重建)asyncfunctionmaybeVacuum(rdb:relationalStore.RdbStore,thresholdPct0.3):Promisevoid{constrsawaitrdb.querySql(PRAGMA freelist_count);constpageCountawaitqueryPageCount(rdb);rs.close();if(pageCount0freelist/pageCountthresholdPct){awaitrdb.executeSql(VACUUM);}}3.2 缓存目录清理import{fileIoasfs}fromkit.CoreFileKit;import{common}fromkit.AbilityKit;// 清理 cache 目录: 全部可删 (系统也可能清)functioncleanCacheDir(context:common.UIAbilityContext,maxAgeMs:number):number{letfreed0;constcacheDircontext.cacheDir;constentriesfs.listFileSync(cacheDir);constnowDate.now();entries.forEach((name:string){constp${cacheDir}/${name};conststatfs.statSync(p);// 超过 maxAge 的缓存文件删除if(now-stat.mtimemaxAgeMs){fs.unlinkSync(p);freedstat.size;}});returnfreed;}3.3 临时文件与日志清理// 清理规则: temp 目录全部 files 下带 .tmp 后缀 过期日志functioncleanTempAndLogs(context:common.UIAbilityContext,keepLogDays7):number{letfreed0;consttempDircontext.tempDir;// temp 目录: 全清fs.listFileSync(tempDir).forEach((name:string){constp${tempDir}/${name};freedfs.statSync(p).size;fs.unlinkSync(p);});// 日志: 只保留最近 N 天constlogDir${context.filesDir}/logs;if(fs.accessSync(logDir)){fs.listFileSync(logDir).forEach((name:string){constp${logDir}/${name};conststatfs.statSync(p);constageDays(Date.now()-stat.mtime)/86400000;if(ageDayskeepLogDays){freedstat.size;fs.unlinkSync(p);}});}returnfreed;}四、企业级实战落地4.1 存储清理分层架构┌──────────────────────────────────────────┐ │ 触发层: 定时 / 告警 / 退出 / 用户手动 │ ├──────────────────────────────────────────┤ │ 调度层: 清理任务编排 (优先级/并发控制) │ ├──────────────────────────────────────────┤ │ 执行层: │ │ ① 数据库: VACUUM / 删除过期行 │ │ ② 缓存: TTL LRU 淘汰 │ │ ③ 临时文件: temp tmp 清理 │ │ ④ 日志: 轮转 保留期 │ ├──────────────────────────────────────────┤ │ 记录层: 清理量统计 埋点上报 │ └──────────────────────────────────────────┘4.2 数据库行级清理配合 VACUUM// 先删过期数据行, 再考虑 VACUUMasyncfunctioncleanExpiredRows(rdb:relationalStore.RdbStore):Promisenumber{constcutoffDate.now()-30*86400000;// 30 天前constpredicatesnewrelationalStore.RdbPredicates(logs);predicates.lessThan(created_at,cutoff);constrowsawaitrdb.delete(predicates);// 删除后空洞率上升, 满足阈值再 VACUUMawaitmaybeVacuum(rdb,0.4);returnrows;}4.3 主动回收调度// 条件触发: 磁盘空间低于阈值时主动清理classStorageGuard{privatereadonlywarnThreshold2*1024*1024*1024;// 2GBasynccheck(context:common.UIAbilityContext):Promisevoid{constfreeawaitgetFreeSpace(context);// 获取剩余空间if(freethis.warnThreshold){// 1. 清理 cache (最安全)constf1cleanCacheDir(context,24*3600*1000);// 2. 清理临时文件constf2cleanTempAndLogs(context);// 3. 数据库 VACUUMawaitvacuumIfNeeded();console.info(空间告警清理: 回收${f1f2}bytes);}}}4.4 清理效果基准模拟清理项清理前清理后回收数据库 VACUUM320MB118MB202MB缓存 TTL 清理860MB240MB620MB临时/日志90MB12MB78MB合计1.27GB370MB900MB五、问题排查与性能优化坑现象原因解决数据库只涨不缩删除后文件不小未 VACUUM空洞率触发 VACUUMVACUUM 卡顿大库重建阻塞前台执行后台 TaskPool 空闲时缓存清不掉空间告警仍大无 TTL写入带 TTL误删用户数据清理后数据丢files 目录误清只清 cache/temp清理频繁IO 负担每次启动清条件触发碎片查询慢增删后变卡空洞分散定期 VACUUM日志无限增长磁盘被日志占满无轮转保留期 轮转5.1 VACUUM 时机选择❌ 前台/启动时 VACUUM (阻塞 UI) ✅ 后台空闲时 / 充电时 / 空间告警时 ✅ 空洞率超阈值才执行 (避免频繁重建) ✅ 大库分两步: 先删数据行, 再 VACUUM5.2 缓存写入即带生命周期// 工程规范: 一切缓存写入必须带 TTL/容量interfaceCacheEntry{data:string;expireAt:number;// 绝对过期}functionwriteCache(kv:KVStore,key:string,data:string,ttlMs:number):void{constentry: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 灭垃圾、临时文件定时清、主动回收分级做——空间只涨不跌的应用留不住用户。