在上一篇文章中,我们搭建了 SQLite 数据库的 DAO 层,学会了如何插入数据并将查询结果转换为 TypeScript 对象。当看到“测试项目”出现在日志中时,数据库开发似乎显得简单。

然而,代码能运行与能支撑商业级应用之间,存在着巨大的性能差异。

设想这样一个场景:我们的“会议随记 Pro”发布后,用户尝试导入 500 个联系人以建立人脉图谱。进度条进行到 10% 时界面卡死,随后系统弹出“应用无响应”窗口,导致用户流失。

这就是性能雪崩。对于经验不足的开发者,容易陷入“逻辑正确”的误区,忽略物理限制。使用 for 循环逐条插入数据在逻辑上可行,但在微观物理层面,这迫使硬盘进行数百次无意义的读写切换。

本文将介绍性能优化的关键工具:数据库事务(Transaction)。通过实战代码,我们将对比逐条循环与事务批处理的性能差异,展示如何将 5 秒的卡顿压缩至 50 毫秒。

一、 性能瓶颈分析:逐条插入为何缓慢

首先分析导致卡顿的原因。

在之前的封装中,我们实现了一个 insertProject 方法。若需导入 100 个联系人,直觉的写法是在 async 函数中使用 for 循环并 await 每一次插入。

这种写法在 SQLite 底层存在严重问题。SQLite 是基于文件的数据库。每次调用 insert,不仅是写入数据,SQLite 默认开启一个“隐式事务”以保证数据安全。这意味着每一次插入操作都要执行一整套动作:打开文件锁、写入日志文件(Journal)、写入实际数据、等待磁盘同步(fsync)、删除日志文件、释放文件锁。

循环 100 次意味着这套繁琐的物理动作重复 100 次。这会导致严重的 IO 瓶颈。在移动设备上,尽管 Flash 存储写入速度快,但频繁的 IO 上下文切换会极大消耗性能并导致设备发热。

二、 数据库事务:性能加速器

正确的做法是使用事务(Transaction)。

事务不仅保证数据一致性(原子性),在批量写入场景下更是性能加速器。显式调用 beginTransaction 开启事务后,SQLite 会将操作缓存在内存或临时日志文件中,不会频繁触发磁盘物理同步。只有当调用 commit 时,SQLite 才会一次性将所有数据写入主数据库文件。

这相当于将 100 次“打开锁-写磁盘-释放锁”的过程合并为 1 次。

实测显示,这种优化带来的性能提升通常是数量级的。在老旧机型上,写入速度甚至能提升 50 倍以上。对于处理大量数据的应用,事务是维护用户体验的基础。

三、 实战:性能测试对比

我们在工程中编写测试代码,利用 console.timeconsole.timeEnd 进行精准计时,对比两种方式的差距。

首先生成 500 个虚拟联系人数据。

// entry/src/main/ets/data/db/PerformanceTest.ts

import { Contact } from '../models/Contact';
import { RdbManager } from './RdbManager';
import { TABLE_CONTACT } from './MeetingRdb';
import { relationalStore } from '@kit.ArkData';

/**
 * 生成 500 个虚拟联系人
 */
function generateMockContacts(count: number): Contact[] {
  const contacts: Contact[] = [];
  for (let i = 0; i < count; i++) {
    contacts.push({
      id: `mock_user_${Date.now()}_${i}`,
      name: `User ${i}`,
      title: 'Developer',
      company: 'HarmonyOS Inc.',
      phone: '13800138000',
      avatar: '',
      createdAt: Date.now(),
      updatedAt: Date.now()
    });
  }
  return contacts;
}

接下来编写普通插入方法。为了模拟真实场景,每次插入都构建 ValuesBucket 并调用 insert

// 普通循环插入模式
export async function testNormalInsert() {
  const contacts = generateMockContacts(500); // 准备 500 条数据
  const store = await RdbManager.getInstance().getRdbStore();

  console.info('[Performance] Start Normal Insert...');
  const startTime = Date.now();

  for (const contact of contacts) {
    const valueBucket: relationalStore.ValuesBucket = {
      'id': contact.id,
      'name': contact.name,
      'title': contact.title,
      'company': contact.company,
      'phone': contact.phone,
      'created_at': contact.createdAt,
      'updated_at': contact.updatedAt
    };
    // 每一次循环,都是一次物理 IO
    await store.insert(TABLE_CONTACT, valueBucket);
  }

  const endTime = Date.now();
  console.info(`[Performance] Normal Insert 500 items took: ${endTime - startTime} ms`);
}

然后编写事务批量插入模式。鸿蒙 RdbStore API 中的事务操作如下:

  • beginTransaction() 开启事务。
  • 执行所有 insert 操作(此时未真正写盘)。
  • commit() 提交事务,一次性落盘。
  • rollBack() 出错回滚,保证数据清洁。
// 事务批量插入模式
export async function testTransactionInsert() {
  const contacts = generateMockContacts(500);
  const store = await RdbManager.getInstance().getRdbStore();

  console.info('[Performance] Start Transaction Insert...');
  const startTime = Date.now();

  // 1. 开启事务
  store.beginTransaction();

  try {
    for (const contact of contacts) {
      const valueBucket: relationalStore.ValuesBucket = {
        'id': contact.id,
        'name': contact.name,
        'title': contact.title,
        'company': contact.company,
        'phone': contact.phone,
        'created_at': contact.createdAt,
        'updated_at': contact.updatedAt
      };
      // 这里的 insert 只是写到了内存缓冲区
      await store.insert(TABLE_CONTACT, valueBucket);
    }

    // 2. 提交事务(原子性提交)
    store.commit();
  } catch (e) {
    console.error(`[Performance] Transaction failed: ${e}`);
    // 3. 遇到错误,全盘回滚
    store.rollBack();
  }

  const endTime = Date.now();
  console.info(`[Performance] Transaction Insert 500 items took: ${endTime - startTime} ms`);
}

分别调用这两个方法。测试结果显示,普通模式插入 500 条数据耗时约 3500ms 至 5000ms,会导致明显卡顿;而事务模式耗时通常在 50ms 至 100ms 之间,性能提升达 50 倍。

四、 封装通用批量处理方法

为了避免代码重复和遗漏 rollbackcommit 导致的数据库死锁,我们将事务逻辑封装进 RdbManager

entry/src/main/ets/data/db/RdbManager.ts 中添加 runInTransaction 方法。

// entry/src/main/ets/data/db/RdbManager.ts

export class RdbManager {
  // ... 之前的代码

  /**
   * 在事务中执行任务
   * 自动处理 begin、commit 和 rollback
   * @param task 需要在事务中执行的异步函数
   */
  public async runInTransaction(task: (store: relationalStore.RdbStore) => Promise<void>): Promise<void> {
    const store = await this.getRdbStore();
    
    try {
      store.beginTransaction();
      // 执行业务逻辑
      await task(store);
      // 没报错就提交
      store.commit();
    } catch (e) {
      console.error(`[RdbManager] Transaction failed, rolling back. Error: ${e}`);
      // 报错了就回滚
      store.rollBack();
      // 继续向上抛出异常,让 UI 层知道失败了
      throw e;
    }
  }
}

使用封装后的方法,ContactRepo.ts 中的批量导入逻辑变得简洁。

// entry/src/main/ets/data/db/ContactRepo.ts

import { RdbManager } from './RdbManager';
import { Contact } from '../models/Contact';
import { TABLE_CONTACT } from './MeetingRdb';
import { relationalStore } from '@kit.ArkData';

/**
 * 批量导入联系人
 * @param contacts 联系人数组
 */
export async function batchInsertContacts(contacts: Contact[]): Promise<void> {
  // 使用封装好的事务方法
  await RdbManager.getInstance().runInTransaction(async (store) => {
    for (const contact of contacts) {
      const valueBucket: relationalStore.ValuesBucket = {
        'id': contact.id,
        'name': contact.name,
        // ... 其他字段
        'updated_at': Date.now()
      };
      await store.insert(TABLE_CONTACT, valueBucket);
    }
  });
}

这种封装使业务代码专注于数据转换和插入,屏蔽了繁琐的事务控制逻辑。

五、 事务使用注意事项

使用 SQLite 事务时需注意以下几点:

  1. 避免嵌套过深 SQLite 支持嵌套事务(Savepoint),但在移动端应用中,逻辑复杂容易出错。建议保持事务逻辑扁平化,避免在 runInTransaction 中再次调用包含 runInTransaction 的方法。
  2. 避免耗时非数据库操作 不要在 beginTransactioncommit 之间执行网络请求或复杂计算。事务开启期间数据库处于锁定状态,耗时操作会阻塞应用其他部分的数据库读取,导致 App 假死。事务代码块应尽可能简短,仅包含数据库写入。
  3. 控制单次事务数据量 不要一次性插入过大数据量(如 10 万条)。大事务会占用大量内存并可能导致 SQLite 日志文件爆满。建议采用分片处理策略,例如每 1000 条开启一个新事务。

六、 总结

我们介绍了如何通过数据库事务优化数据写入性能。我们对比了普通循环插入与事务批处理的性能差异,验证了事务对减少 IO 操作、提升性能的显著效果。

通过封装 runInTransaction 方法,我们将这一系统级能力集成到工程架构中,确保应用能够毫秒级处理大数据导入,保障流畅的用户体验。

Logo

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

更多推荐