在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前置思考

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/ · 加密文件
清理前校验:    二次确认 + 记录清理清单 (可审计)

六、高阶总结与最佳实践

  1. 数据库膨胀靠 VACUUM:空洞率阈值触发,后台执行,先删行再重建。
  2. 缓存垃圾靠生命周期:写入带 TTL/LRU,过期即失效即清理。
  3. 临时文件靠纪律:temp 全清、tmp 清理、日志轮转保保留期。
  4. 主动回收靠触发:定时 + 空间告警 + 退出时,分级清理。
  5. 安全红线:只清可重建数据,用户数据与加密文件绝不动。

一句话记住:数据库 VACUUM 缩文件、缓存 TTL 灭垃圾、临时文件定时清、主动回收分级做——空间只涨不跌的应用留不住用户。

Logo

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

更多推荐