【鸿蒙原生开发会议随记 Pro】 事务的力量 毫秒级导入 100 个联系人
本文将介绍性能优化的关键工具:数据库事务(Transaction)。通过实战代码,我们将对比逐条循环与事务批处理的性能差异,展示如何将 5 秒的卡顿压缩至 50 毫秒。
在上一篇文章中,我们搭建了 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.time 和 console.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 倍。
四、 封装通用批量处理方法
为了避免代码重复和遗漏 rollback 或 commit 导致的数据库死锁,我们将事务逻辑封装进 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 事务时需注意以下几点:
- 避免嵌套过深 SQLite 支持嵌套事务(Savepoint),但在移动端应用中,逻辑复杂容易出错。建议保持事务逻辑扁平化,避免在
runInTransaction中再次调用包含runInTransaction的方法。 - 避免耗时非数据库操作 不要在
beginTransaction和commit之间执行网络请求或复杂计算。事务开启期间数据库处于锁定状态,耗时操作会阻塞应用其他部分的数据库读取,导致 App 假死。事务代码块应尽可能简短,仅包含数据库写入。 - 控制单次事务数据量 不要一次性插入过大数据量(如 10 万条)。大事务会占用大量内存并可能导致 SQLite 日志文件爆满。建议采用分片处理策略,例如每 1000 条开启一个新事务。
六、 总结
我们介绍了如何通过数据库事务优化数据写入性能。我们对比了普通循环插入与事务批处理的性能差异,验证了事务对减少 IO 操作、提升性能的显著效果。
通过封装 runInTransaction 方法,我们将这一系统级能力集成到工程架构中,确保应用能够毫秒级处理大数据导入,保障流畅的用户体验。
更多推荐





所有评论(0)