数据一致性概念:事务边界、原子性、隔离级别、脏读/幻读
回归测试里,数据一致性是个绕不开的坎。我们一边用 Room 写数据,一边用 Flow 读数据,稍不留神就会读到半截子数据。说白了,得搞懂数据库到底怎么保证“同一个世界,同一个数据”。
我当年接手一个订单同步模块,每次回归测试总有几条数据对不上。排查到凌晨三点,才发现是事务没加边界,Room 的 Flow 提前释放了变化——**事务没提交,数据就“半成品”状态被流给了下游**。嗯,从那以后,我对这四个概念就特别上心。
事务边界:你画个圈,数据库才知道从哪到哪
事务边界就是告诉数据库:“从这句话开始,到那句话结束,中间的事要么全做,要么别做。”没边界,数据库不知道你哪些操作是一伙的。
- 开始:
beginTransaction() - 结束:
setTransactionSuccessful() + endTransaction()
我习惯在 Room 的 @Transaction 注解里显式声明,别靠默认行为猜。你想想看,如果两个插入操作中间出了异常,没边界的话第一条数据就孤零零留在表里了——回归测试一跑,脏数据就冒出来。
@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 收到的数据是不是“半成品”。比如下面这个测试用例,就专门检查脏读是否被防止:
beginTransaction() 手动控制提交时机,在 Flow 订阅之后才真正提交,模拟脏读场景。
脏读:看到了不该看的数据
脏读就是事务 A 写了一半,事务 B 读取了那半截数据。假如 A 最终回滚,B 读到的就是垃圾。Room 默认的读已提交能防住脏读,因为 SQLite 在写操作期间会持有写锁。但我曾经在测试里故意绕过这一点——用 PRAGMA read_uncommitted=1 临时降低隔离级别,然后观察 Flow 是否推送了脏数据。结果真的抓到了。
说实话,回归测试里脏读比较少见,因为 Room 帮我们兜了底。但你如果手动操作 SQLite、或者用
rawQuery并发执行,就得自己盯着了。
幻读:多出来的那几行
幻读更隐蔽。事务 A 查了一个范围,事务 B 插入了一行,然后 A 再查同一个范围,发现多了一行。这在分页、列表加载的场景里特别容易引发断言失败。比如你测试“查询前10条商品”,Flow 第一次拿到10条,第二次却变成11条——你的测试用例可能就跳起来报警。
幻读在默认隔离级别下是无法避免的。我自己的做法是:在回归测试的开始和结束各做一次全量计数,如果总数变了,就标记为“可能存在幻读影响,需要人工排查”。
梳理成一张图:隔离级别与读异常的对应关系
下面我把刚才表格里的逻辑画成结构图,方便你一眼看明白哪个级别管什么事。
这些概念怎么影响 Room + Flow 的回归测试
回归测试里,我一般按下面几步检查数据一致性:
- 看事务边界:每个写操作是否都包裹在
@Transaction里?Flow 的 emit 时机是否在setTransactionSuccessful之后? - 测原子性:故意制造异常(例如让某条插入违反约束),确认没有半残数据留下。
- 测隔离级别:用两个协程并发读写,观察 Flow 收到的数据是否包含未提交的脏数据。用
awaitTransaction可以精确控制并发时序。 - 测幻读:在 Flow 的一个收集周期内,让另一个协程插入新行,然后看同一查询的结果集行数是否变了。
关键结论:回归测试不能只测“正常流程”,必须专门构造并发场景,验证数据一致性边界的防护是否有效。我自己的项目里,把这些测例都写成了参数化测试,每次流水线跑一遍,心里才稳。