9、数据库事务:事务ACID特性、beginTransaction与setTransactionSuccessful、回滚机制rollback、事务嵌套与保存点、性能对比测试

事务这个东西,说白了就是「要么全做,要么全不做」。我刚开始写Android数据库时,总觉得事务是锦上添花的东西。直到有一次,用户批量导入通讯录时程序突然崩溃,结果数据库里一半联系人进去了,另一半丢了,还留下了一堆脏数据……嗯,从那以后,我再也不敢轻视事务了。

核心一句话:事务是保证数据一致性的最后一道防线。没有事务,你的数据库在异常面前就是纸糊的。

9.1 事务的ACID特性

ACID是事务的四个基本属性。你想想看,如果数据库连这四点都保证不了,谁敢把用户数据往里存?

  • 原子性(Atomicity):事务里的操作,要么全部成功,要么全部失败。不存在「做了一半」的状态。
  • 一致性(Consistency):事务执行前后,数据库的约束、规则都不能被破坏。比如转账后总金额不变。
  • 隔离性(Isolation):并发执行的事务之间互不干扰。每个事务都感觉自己是独占数据库的。
  • 持久性(Durability):事务一旦提交,结果就是永久的。哪怕系统断电,数据也不会丢。

我个人习惯把ACID记成「原一隔持」——虽然不太好听,但确实管用。

避坑指南:我曾经在项目中遇到过一个问题:批量插入1000条数据,每插入一条就调用一次db.insert(),结果中间某条数据格式不对抛了异常,前面插入的500条已经写进去了。这就是典型的「没有事务」的后果。后来我改成事务包裹,异常时统一回滚,问题解决。

事务ACID特性 原子性 全部成功 或 全部失败 没有中间状态 一致性 约束不被破坏 数据逻辑正确 隔离性 事务互不干扰 并发安全 持久性 提交即永久 断电不丢失

9.2 beginTransaction 与 setTransactionSuccessful

在Android的SQLite中,事务的使用非常直接。核心就三个方法:beginTransaction()setTransactionSuccessful()endTransaction()

我见过不少新手写事务时,只调了beginTransactionendTransaction,却忘了调setTransactionSuccessful。结果呢?所有操作都被回滚了,数据一条都没写进去。为什么会这样?因为endTransaction()在没有调用setTransactionSuccessful的情况下,默认执行回滚。

// 标准的事务写法
db.beginTransaction();
try {
    // 执行多条SQL操作
    db.execSQL("INSERT INTO users(name, age) VALUES(?, ?)", new Object[]{"张三", 25});
    db.execSQL("UPDATE accounts SET balance = balance - 100 WHERE user_id = ?", new Object[]{1});
    
    // 标记事务成功
    db.setTransactionSuccessful();
} catch (Exception e) {
    // 异常时自动回滚,不需要手动调用rollback
    Log.e("DB", "事务执行失败", e);
} finally {
    // 结束事务(提交或回滚)
    db.endTransaction();
}

注意:setTransactionSuccessful()必须在endTransaction()之前调用,且只能调用一次。如果你在事务中调用了两次,第二次会抛出异常。

9.3 回滚机制 rollback

回滚,就是把数据库恢复到事务开始之前的状态。在Android SQLite中,你其实很少需要手动调用rollback()方法。因为endTransaction()在没有标记成功时,会自动执行回滚。

但有一种情况需要手动回滚:当你需要在事务中间判断某个条件不满足,提前终止整个事务时。

db.beginTransaction();
try {
    // 第一步:插入订单
    long orderId = db.insert("orders", null, orderValues);
    if (orderId == -1) {
        // 插入失败,手动回滚
        db.endTransaction(); // 这里会触发回滚
        return false;
    }
    
    // 第二步:扣减库存
    int rows = db.update("products", values, "id = ?", new String[]{productId});
    if (rows == 0) {
        // 库存扣减失败,回滚
        db.endTransaction();
        return false;
    }
    
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

个人经验:我曾经在项目中遇到一个诡异的bug——事务回滚后,自增主键的计数器并没有回滚。也就是说,你回滚了插入操作,但下一次插入时主键会跳过被回滚的那个值。这不是bug,是SQLite的设计如此。如果你对主键连续性有要求,需要自己处理。

9.4 事务嵌套与保存点

事务嵌套,说白了就是事务里面再开事务。Android SQLite支持事务嵌套,但它的实现方式有点特殊——嵌套的事务实际上共享同一个数据库连接。

当你调用beginTransaction()时,SQLite会维护一个事务计数器。只有最外层的事务提交或回滚时,才会真正影响数据库。内层事务的setTransactionSuccessful()只是标记一个「保存点」(Savepoint)。

// 外层事务
db.beginTransaction();
try {
    // 做一些操作
    db.execSQL("INSERT INTO log VALUES('开始批量操作')");
    
    // 内层事务
    db.beginTransaction();
    try {
        db.execSQL("INSERT INTO users(name) VALUES('内层插入')");
        db.setTransactionSuccessful(); // 这只是标记保存点
    } finally {
        db.endTransaction(); // 内层结束,不影响外层
    }
    
    // 如果这里发生异常,内层的插入也会被回滚
    db.setTransactionSuccessful();
} finally {
    db.endTransaction(); // 真正提交或回滚
}

你想想看,这种设计有什么好处?最大的好处是:你可以在一个大的事务中,把某些操作「打包」成子事务。即使子事务成功了,只要外层事务回滚,子事务的数据也会被撤销。

保存点的本质:保存点就是事务中的「书签」。你可以回滚到任意一个保存点,而不影响其他部分。这在复杂的数据迁移、批量处理场景中非常有用。

事务嵌套与保存点流程 外层事务 beginTransaction() 操作1: 插入日志 内层事务 beginTransaction() 操作2: 插入用户 setTransactionSuccessful endTransaction() → 保存点标记 setTransactionSuccessful 外层 endTransaction() → 真正提交或回滚所有操作

9.5 性能对比测试

事务对性能的影响有多大?我直接说结论:差距是数量级的

为什么?因为每次insert()update()操作,如果不放在事务里,SQLite都会自动开启一个隐式事务,操作完立即提交。这意味着每次操作都要经历「写日志 → 刷磁盘 → 更新索引」这一整套流程。而事务把这些操作攒在一起,最后一次性提交,磁盘I/O次数大大减少。

我做过一个简单的测试:插入1000条记录。

测试场景 耗时(毫秒) 说明
逐条插入(无事务) 约 8500 ms 每次插入都触发磁盘写入
单条事务包裹 约 8200 ms 和逐条插入差不多,因为每条还是独立事务
批量事务(1000条一次提交) 约 120 ms 快了70倍!
批量事务 + 预编译 约 45 ms 事务+预编译,性能最佳

性能优化建议:

  • 批量操作(插入、更新、删除)一定要用事务包裹
  • 事务不要太大,建议每500-1000条提交一次,避免长时间锁表
  • 配合SQLiteStatement预编译,性能还能再翻倍
// 性能最优的批量插入写法
SQLiteStatement stmt = db.compileStatement("INSERT INTO users(name, age) VALUES(?, ?)");
db.beginTransaction();
try {
    for (User user : userList) {
        stmt.bindString(1, user.getName());
        stmt.bindLong(2, user.getAge());
        stmt.executeInsert();
        stmt.clearBindings();
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
    stmt.close();
}

注意:事务虽然快,但也不是越大越好。一个事务包含几万条操作时,内存占用会飙升,而且一旦失败,回滚的成本也很高。我建议根据数据量灵活控制,比如每1000条提交一次。

好了,关于事务的核心内容就这些。ACID特性是理论基础,beginTransactionsetTransactionSuccessful是实战工具,回滚机制是安全保障,事务嵌套和保存点是进阶技巧,性能对比测试则告诉你——事务不是可有可无的,它是高性能数据库操作的基石


公众号:蓝海资料掘金营,微信deep3321