数据一致性概念:事务边界、原子性、隔离级别、脏读/幻读

回归测试里,数据一致性是个绕不开的坎。我们一边用 Room 写数据,一边用 Flow 读数据,稍不留神就会读到半截子数据。说白了,得搞懂数据库到底怎么保证“同一个世界,同一个数据”。

我当年接手一个订单同步模块,每次回归测试总有几条数据对不上。排查到凌晨三点,才发现是事务没加边界,Room 的 Flow 提前释放了变化——**事务没提交,数据就“半成品”状态被流给了下游**。嗯,从那以后,我对这四个概念就特别上心。

事务边界:你画个圈,数据库才知道从哪到哪

事务边界就是告诉数据库:“从这句话开始,到那句话结束,中间的事要么全做,要么别做。”没边界,数据库不知道你哪些操作是一伙的。

  • 开始:beginTransaction()
  • 结束:setTransactionSuccessful() + endTransaction()

我习惯在 Room 的 @Transaction 注解里显式声明,别靠默认行为猜。你想想看,如果两个插入操作中间出了异常,没边界的话第一条数据就孤零零留在表里了——回归测试一跑,脏数据就冒出来。

注意:Room 的 @Transaction 注解只对函数体加事务,但如果你在函数里又嵌套了 Flow,Flow 的订阅可能在事务提交之前就触发了。这是我的亲身经历——解法是等事务完成后再 emit 数据。

原子性:要活一起活,要死一起死

原子性就一个意思:**操作不可分割**。写错了就回滚,写对了才提交。我在项目里遇到过一个大坑:批量插入 1000 条记录,中间第 500 条违反唯一约束,前 499 条已经写进去了。如果没有原子性,回归测试的校验结果就会显示“多出 499 条脏数据”。

// Room 中的原子性示例
@Transaction
fun insertUsers(users: List<User>) {
    userDao.insertAll(users) // 如果有异常,整个回滚
}

你看,@Transaction 确保要么全部写入,要么一张表都不动。测试的时候验证总数,心里才踏实。

隔离级别:四档“防干扰”模式

隔离级别解决了多个事务同时运行时,互相能看到什么。Room 默认用的是 **读已提交(READ COMMITTED)** 级别,这也是 SQLite 的默认。不过我们得知道每档到底防什么、漏什么。

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交(Room 默认) 避免 可能 可能
可重复读 避免 避免 可能
串行化 避免 避免 避免

我平时写回归测试,会特意造几个并发事务的场景,看看 Flow 收到的数据是不是“半成品”。比如下面这个测试用例,就专门检查脏读是否被防止:

经验:利用 Room 的 beginTransaction() 手动控制提交时机,在 Flow 订阅之后才真正提交,模拟脏读场景。

脏读:看到了不该看的数据

脏读就是事务 A 写了一半,事务 B 读取了那半截数据。假如 A 最终回滚,B 读到的就是垃圾。Room 默认的读已提交能防住脏读,因为 SQLite 在写操作期间会持有写锁。但我曾经在测试里故意绕过这一点——用 PRAGMA read_uncommitted=1 临时降低隔离级别,然后观察 Flow 是否推送了脏数据。结果真的抓到了。

说实话,回归测试里脏读比较少见,因为 Room 帮我们兜了底。但你如果手动操作 SQLite、或者用 rawQuery 并发执行,就得自己盯着了。

幻读:多出来的那几行

幻读更隐蔽。事务 A 查了一个范围,事务 B 插入了一行,然后 A 再查同一个范围,发现多了一行。这在分页、列表加载的场景里特别容易引发断言失败。比如你测试“查询前10条商品”,Flow 第一次拿到10条,第二次却变成11条——你的测试用例可能就跳起来报警。

幻读在默认隔离级别下是无法避免的。我自己的做法是:在回归测试的开始和结束各做一次全量计数,如果总数变了,就标记为“可能存在幻读影响,需要人工排查”。

梳理成一张图:隔离级别与读异常的对应关系

下面我把刚才表格里的逻辑画成结构图,方便你一眼看明白哪个级别管什么事。

事务隔离级别与读异常关系 事务隔离级别 读未提交 读已提交 可重复读 串行化 脏读 ✅ 避免 ✅ 避免 ✅ 避免 不可重复读 ❌ 可能 ✅ 避免 ✅ 避免 幻读 ❌ 可能 ❌ 可能 ✅ 避免 Room 默认隔离级别=读已提交,防脏读不防幻读 回归测试建议额外校验范围计数防止幻读

这些概念怎么影响 Room + Flow 的回归测试

回归测试里,我一般按下面几步检查数据一致性:

  1. 看事务边界:每个写操作是否都包裹在 @Transaction 里?Flow 的 emit 时机是否在 setTransactionSuccessful 之后?
  2. 测原子性:故意制造异常(例如让某条插入违反约束),确认没有半残数据留下。
  3. 测隔离级别:用两个协程并发读写,观察 Flow 收到的数据是否包含未提交的脏数据。用 awaitTransaction 可以精确控制并发时序。
  4. 测幻读:在 Flow 的一个收集周期内,让另一个协程插入新行,然后看同一查询的结果集行数是否变了。

关键结论:回归测试不能只测“正常流程”,必须专门构造并发场景,验证数据一致性边界的防护是否有效。我自己的项目里,把这些测例都写成了参数化测试,每次流水线跑一遍,心里才稳。