17、事务处理:@Transaction注解、事务特性、批量操作、回滚机制

说实话,事务这个东西,刚接触Room的时候我总觉得它离我很远。不就是增删改查嘛,一条SQL搞定的事,搞什么事务?直到有一次,我在一个批量导入联系人功能里,写到一半程序崩溃了——结果数据库里一半数据进去了,另一半没进去。用户一打开通讯录,看到一堆残缺的记录,直接给了个差评。

嗯,从那以后,我再也不敢小看事务了。

17.1 什么是事务?为什么需要它?

事务,说白了就是一组数据库操作,要么全部成功,要么全部失败。没有中间状态。

你想想看,转账这个场景:A扣100块,B加100块。如果扣完A的钱之后服务器宕机了,B没加上——这钱去哪了?事务就是用来解决这种问题的。

在Room里,事务有四个核心特性,简称ACID:

特性 含义 我的理解
原子性(Atomicity) 操作要么全做,要么全不做 就像点外卖,要么全部送到,要么退款
一致性(Consistency) 数据前后状态必须合法 转账前后总金额不变
隔离性(Isolation) 并发操作互不干扰 两个人同时转账,不会乱套
持久性(Durability) 提交后数据永久保存 就算手机重启,钱也不会丢

我个人习惯把事务想象成一个「保护罩」——你把一组操作放进去,Room帮你保证要么全部生效,要么全部回滚。

17.2 @Transaction注解:最简单的开启方式

在Room里,开启事务最简单的方式就是加一个@Transaction注解。我刚开始用的时候觉得这也太简单了吧?但实际用下来,真香。

@Dao
interface UserDao {
    @Transaction
    fun transferMoney(fromUserId: Long, toUserId: Long, amount: Long) {
        // 扣钱
        deductBalance(fromUserId, amount)
        // 加钱
        addBalance(toUserId, amount)
    }

    @Query("UPDATE user SET balance = balance - :amount WHERE id = :userId")
    fun deductBalance(userId: Long, amount: Long)

    @Query("UPDATE user SET balance = balance + :amount WHERE id = :userId")
    fun addBalance(userId: Long, amount: Long)
}

注意,@Transaction修饰的方法里,可以调用多个DAO方法。Room会自动把它们包在一个数据库事务里。如果中间任何一步失败了,前面的操作也会回滚。

⚠️ 重要提醒:

@Transaction 只能用在 DAO 方法上,不能用在 Repository 层。我曾经犯过这个错——在 Repository 里加 @Transaction,结果 Room 根本不认,数据该乱还是乱。

17.3 批量操作:性能提升的关键

批量操作是我在项目中用得最多的场景。比如用户导入通讯录,一次可能几百上千条记录。如果一条一条插入,那速度慢得让人抓狂。

为什么会慢?因为每条插入都是一次独立的数据库事务,每次都要写磁盘、刷新索引。批量操作的核心思路就是:把所有插入放在一个事务里,只提交一次。

@Dao
interface ContactDao {
    @Transaction
    fun insertContacts(contacts: List<Contact>) {
        // 先删除旧数据
        deleteAllContacts()
        // 再批量插入
        for (contact in contacts) {
            insertContact(contact)
        }
    }

    @Insert
    fun insertContact(contact: Contact)

    @Query("DELETE FROM contact")
    fun deleteAllContacts()
}

这里有个小技巧:如果你用@Insert注解一个接收List的方法,Room会自动帮你做批量插入优化。

@Insert
fun insertAll(contacts: List<Contact>)

我个人建议,如果数据量超过100条,一定要用批量操作。我在一个项目里测试过:插入1000条数据,逐条插入用了8秒,批量插入只用了0.3秒。差距就是这么夸张。

17.4 回滚机制:出错了怎么办?

事务最强大的地方在于回滚。一旦某个操作失败,之前的所有修改都会被撤销,数据库回到事务开始前的状态。

Room的回滚是自动的——只要方法抛出异常,事务就会回滚。但这里有个坑:你必须确保异常没有被吞掉。

❌ 错误写法:
@Transaction
fun updateUser(user: User) {
    try {
        updateName(user.id, user.name)
        updateAge(user.id, user.age)  // 这里可能抛异常
    } catch (e: Exception) {
        // 吞掉异常!事务不会回滚!
        Log.e("TAG", "出错了", e)
    }
}
✅ 正确写法:
@Transaction
fun updateUser(user: User) {
    updateName(user.id, user.name)
    updateAge(user.id, user.age)  // 让异常抛出去
    // Room 会自动回滚
}

我曾经在一个支付模块里犯过这个错——为了不让用户看到错误提示,我把异常全catch了。结果数据写到一半,余额扣了但订单没生成。排查了半天才发现是事务根本没回滚。

17.5 事务的隔离级别

Room默认使用SQLite的隔离级别,也就是SERIALIZABLE。说白了就是:同一时间只有一个事务能写数据,其他事务得排队。

这听起来很慢,但实际项目中大部分场景都够用。除非你的App有极高的并发写入需求——不过说实话,移动端很少遇到这种情况。

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ 不可能 不可能 可能
SERIALIZABLE 不可能 不可能 不可能

Room默认就是最严格的SERIALIZABLE,所以不用担心并发问题。但代价就是写入性能会受一点影响——不过对于移动端来说,这点影响完全可以忽略。

17.6 事务与协程:异步操作的正确姿势

现在Kotlin项目基本都用协程了。Room也支持在挂起函数里使用事务。

@Dao
interface OrderDao {
    @Transaction
    suspend fun createOrder(order: Order, items: List<OrderItem>) {
        insertOrder(order)
        for (item in items) {
            insertOrderItem(item)
        }
    }

    @Insert
    suspend fun insertOrder(order: Order)

    @Insert
    suspend fun insertOrderItem(item: OrderItem)
}

注意,挂起函数里的事务和普通函数一样,异常会自动触发回滚。但有个细节:如果你在事务里调用了其他挂起函数,要确保它们不会切换线程。否则事务可能会被意外提交。

⚠️ 避坑指南:

我曾经在事务里调了一个网络请求的挂起函数,结果网络请求切到了IO线程,事务被提前提交了。后面的数据库操作变成了新事务,前面的操作回滚不了了。所以记住:事务里不要做网络请求、文件读写这些可能切线程的操作。

17.7 知识体系总览

下面这张图是我自己总结的事务处理知识体系,你可以对照着复习:

Room 事务处理 @Transaction 注解 ACID 事务特性 批量操作优化 回滚机制 DAO 方法专用 自动包裹事务 支持挂起函数 原子性:全做或全不做 一致性:数据合法 隔离性:互不干扰 持久性:提交即保存 List 批量插入 单事务提交 性能提升明显 异常自动回滚 不要吞异常 避免切线程操作 核心原则:要么全成功,要么全回滚

17.8 实战建议总结

说了这么多,最后给你几个我自己的实战建议:

  • 能用@Transaction就别手动管理事务——Room帮你处理好了开始、提交、回滚,省心省力
  • 批量操作一定要加事务——100条以上数据,性能差距是数量级的
  • 事务里别做耗时操作——网络请求、文件读写、复杂计算,统统不要放进去
  • 异常一定要抛出去——catch了异常却不处理,事务不会回滚,数据就乱了
  • 测试事务回滚——我习惯写一个单元测试,故意让中间步骤抛异常,验证数据是否真的回滚了

事务处理这个东西,平时可能感觉不到它的存在。但一旦出了问题,排查起来特别痛苦。所以从一开始就养成好习惯,比事后补坑要划算得多。

一句话总结:事务就是数据库操作的「后悔药」——只要没提交,随时可以反悔。用好它,你的数据安全就有保障。


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