鸿蒙ArkData事务与备份恢复实战:转账场景与数据救援
鸿蒙ArkData事务与备份恢复实战:转账场景与数据救援
上篇讲了 CRUD 基础,这里攻两个难题:一是"多个操作要么全成功要么全失败"——典型场景是转账,扣款和加款必须原子完成;二是"数据丢了怎么救"——数据库异常、用户误删、升级失败,备份恢复是最后的保险。这两块都是出事时才显出价值的能力,平时不练,出事就抓瞎。
一、事务实战:转账场景
转账是最经典的事务场景:A 给 B 转 100 元,要扣 A 的余额、加 B 的余额、记一笔流水。这三步必须原子完成——任何一步失败,全部回滚。如果不用事务,扣了 A 的钱却没加给 B,就是资损事故。
1.1 用 createTransaction 实现转账
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
async function transfer(store: relationalStore.RdbStore, fromId: number, toId: number, amount: number) {
try {
// 创建事务对象,开始事务
const transaction = await store.createTransaction();
try {
// 1. 查询转出方余额
const fromPredicates = new relationalStore.RdbPredicates('ACCOUNT');
fromPredicates.equalTo('ID', fromId);
const fromRs = await transaction.query(fromPredicates, ['BALANCE']);
fromRs.goToNextRow();
const fromBalance = fromRs.getDouble(fromRs.getColumnIndex('BALANCE'));
fromRs.close();
// 2. 余额不足,回滚
if (fromBalance < amount) {
await transaction.rollback();
hilog.info(DOMAIN, 'rdb', '余额不足,转账取消');
return false;
}
// 3. 扣款
const deductPredicates = new relationalStore.RdbPredicates('ACCOUNT');
deductPredicates.equalTo('ID', fromId);
await transaction.update({ BALANCE: fromBalance - amount }, deductPredicates);
// 4. 加款
const toPredicates = new relationalStore.RdbPredicates('ACCOUNT');
toPredicates.equalTo('ID', toId);
const toRs = await transaction.query(toPredicates, ['BALANCE']);
toRs.goToNextRow();
const toBalance = toRs.getDouble(toRs.getColumnIndex('BALANCE'));
toRs.close();
await transaction.update({ BALANCE: toBalance + amount }, toPredicates);
// 5. 记流水
await transaction.insert('TRANSACTION_LOG', {
FROM_ID: fromId,
TO_ID: toId,
AMOUNT: amount,
TIME: Date.now()
});
// 6. 全部成功,提交
await transaction.commit();
hilog.info(DOMAIN, 'rdb', '转账成功');
return true;
} catch (error) {
// 业务异常,回滚
await transaction.rollback();
hilog.error(DOMAIN, 'rdb', `转账失败,已回滚: ${(error as BusinessError).message}`);
return false;
}
} catch (error) {
hilog.error(DOMAIN, 'rdb', `创建事务失败: ${(error as BusinessError).message}`);
return false;
}
}
1.2 事务的三种类型
createTransaction 支持配置事务类型,默认 DEFERRED:
| 类型 | 行为 | 适用场景 |
|---|---|---|
| DEFERRED(默认) | 延迟获锁,不立即冲突 | 大多数场景 |
| IMMEDIATE | 立即获取写锁 | 确定要写的场景 |
| EXCLUSIVE | 独占锁,阻塞其他读写 | 需要强隔离的场景 |
// 指定 IMMEDIATE 事务
const transaction = await store.createTransaction({ type: relationalStore.TransactionType.IMMEDIATE });
1.3 事务里也能用冲突解决
插入时可以指定冲突解决策略:
const rowId = await transaction.insert(
'EMPLOYEE',
{ NAME: 'Lisa', AGE: 18, SALARY: 100.5 },
relationalStore.ConflictResolution.ON_CONFLICT_REPLACE // 冲突时替换
);
1.4 事务里执行原生 SQL
// 事务里也能跑原生 SQL,参数化绑定
await transaction.execute('DELETE FROM EMPLOYEE WHERE age = ? OR age = ?', [21, 20]);
二、备份实战:给数据上个保险
数据库异常(错误码 14800011)、用户误删、升级失败,这些场景下有备份就能救回来。关系型数据库支持手动备份和自动备份(自动备份仅系统应用可用)。
2.1 手动备份
async function backupDatabase(store: relationalStore.RdbStore) {
// 'Backup.db' 是备份文件名,默认在 RdbStore 同路径下
// 也可指定路径:customDir + 'Backup.db'
store.backup('Backup.db', (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN, 'rdb', `备份失败: ${err.message}`);
return;
}
hilog.info(DOMAIN, 'rdb', '备份成功');
});
}
2.2 什么时候该备份
几个建议备份的时机:
- 应用升级前:升级有风险,先备份。升级失败可以用备份回退
- 批量删除/修改前:大操作前备份,操作错了能恢复
- 定期备份:重要数据每天/每周备份一次
- 用户主动触发:提供"导出备份"功能让用户安心
三、恢复实战:从备份还原
async function restoreDatabase(store: relationalStore.RdbStore) {
// 从 Backup.db 恢复数据
store.restore('Backup.db', (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN, 'rdb', `恢复失败: ${err.message}`);
return;
}
hilog.info(DOMAIN, 'rdb', '恢复成功');
});
}
restore 会用备份文件的内容覆盖当前数据库。注意:恢复是覆盖操作,当前数据库里备份之后新增的数据会丢失。所以恢复前要想清楚,或者先对当前数据再做一次备份。
四、数据库异常重建:错误码 14800011
官方文档特别提到:数据库操作或存储过程中,可能因为各种原因抛出 14800011 异常。这时需要对数据库重建并恢复数据。
一个完整的异常处理流程:
async function safeOperation(store: relationalStore.RdbStore, operation: () => Promise<void>) {
try {
await operation();
} catch (error) {
const err = error as BusinessError;
if (err.code === 14800011) {
// 数据库异常,需要重建
hilog.error(DOMAIN, 'rdb', '数据库异常,尝试重建');
// 1. 先尝试从备份恢复
try {
await new Promise<void>((resolve, reject) => {
store.restore('Backup.db', (e) => {
if (e) reject(e);
else resolve();
});
});
hilog.info(DOMAIN, 'rdb', '从备份恢复成功,重试操作');
await operation(); // 恢复后重试
} catch (restoreErr) {
hilog.error(DOMAIN, 'rdb', `恢复失败: ${(restoreErr as BusinessError).message}`);
// 2. 备份也救不了,只能重建空库
// 走 initDatabase 流程重新建库建表
}
} else {
throw error; // 其他错误向上抛
}
}
}
五、事务使用的几个原则
5.1 事务要短
事务持有锁,时间越长越容易阻塞其他操作。事务里不要做耗时操作(网络请求、大文件读写、复杂计算)。把准备工作做完,事务里只做数据库操作。
5.2 必须处理回滚
事务里任何一步失败,都要 rollback()。忘了回滚,事务会一直挂着,锁不释放,其他写操作全阻塞。
5.3 结果集在事务里也要 close
事务里查询返回的结果集,用完一样要 close()。事务提交/回滚不会自动关闭结果集。
5.4 不要嵌套事务
关系型数据库同一时间只支持一个写操作,嵌套事务会死锁。需要多段操作时,串行执行多个独立事务。
六、备份恢复的实战策略
给一个生产环境建议的备份策略:
| 场景 | 策略 |
|---|---|
| 应用启动 | 检查上次备份是否存在,异常时自动恢复 |
| 版本升级前 | 强制备份一次 |
| 用户清空数据前 | 备份一次,提供"撤销"窗口 |
| 定期(如每天首次启动) | 静默备份,保留最近 N 份 |
| 关键操作(转账、支付) | 操作前备份,操作后确认 |
七、一个完整的"安全转账"封装
把事务、备份、异常处理组合起来,封装一个生产可用的转账函数:
async function safeTransfer(
store: relationalStore.RdbStore,
fromId: number, toId: number, amount: number
): Promise<boolean> {
// 1. 操作前备份
await new Promise<void>((resolve) => {
store.backup('pre_transfer.db', () => resolve());
});
// 2. 执行转账事务
const success = await transfer(store, fromId, toId, amount);
if (success) {
// 3. 成功后备份
await new Promise<void>((resolve) => {
store.backup('post_transfer.db', () => resolve());
});
return true;
} else {
return false;
}
}
八、总结一下下
事务和备份恢复是数据库能力的"安全网",平时不起眼,出事时救命。
- 事务保证原子性:多步操作用
createTransaction,失败rollback,成功commit - 事务要短、要处理回滚、不嵌套:避免死锁和长时间阻塞
- 关键操作前备份:升级、批量删除、转账前都该备份
- 14800011 异常要重建:捕获这个错误码,尝试从备份恢复
- 恢复是覆盖操作:恢复前考虑当前数据是否要先备份
更多推荐


所有评论(0)