4、Room 的 ACID 保证:SQLite 如何撑起数据一致性
做 Android 本地存储,最怕什么?
我个人的答案是:数据写到一半,App 闪退了。或者两个线程同时在写同一个表,数据乱成一锅粥。Room 之所以敢叫板「官方持久化库」,底气就在于它底层依托的 SQLite 提供了完整的 ACID 保证。说白了,你只管用 DAO 操作,脏活累活 SQLite 全干了。今天我们就掰开揉碎,看看 Room 到底怎么利用 SQLite 实现原子性、一致性、隔离性和持久性。
原子性:要么全做,要么全不做
原子性说白了就是:一组操作要么全部成功,要么看起来像没发生过。SQLite 靠的是 rollback journal(回滚日志)或 WAL 模式下的原子提交协议。
当你执行 BEGIN TRANSACTION,SQLite 会把原始数据页复制到日志文件。如果中途 App 崩溃了,下次打开数据库时,SQLite 检测到日志文件存在且没有 COMMIT,就会自动回滚。Room 的 @Transaction 注解本质上就是在帮你生成这些代码。
看看 Room 帮我们生成的 SQLite 代码大概长什么样:
// Room 自动生成的代码(简化版)
_sqlite.beginTransaction();
try {
// 你的 DAO 方法内的所有操作
_sqlite.execSQL("INSERT INTO users ...");
_sqlite.execSQL("UPDATE profile ...");
_sqlite.setTransactionSuccessful();
} finally {
_sqlite.endTransaction(); // 没有 setSuccessful 就会自动 ROLLBACK
}
一致性:数据永远合法
一致性的含义是:事务开始前和结束后,数据库的约束条件都成立。SQLite 通过以下几种方式保证:
- 声明式约束:NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY。举个例子,如果你定义了外键,插入一条指向不存在父记录的子记录,SQLite 直接拒绝。
- 触发器:可以在 INSERT/UPDATE/DELETE 前后自动执行校验逻辑。
- 原子性自动回滚:当事务内的操作违反约束时,SQLite 会回滚整个事务,保证不会留下「半截子」不一致的状态。
Room 里你在 Entity 上标注 @NonNull 或 @ColumnInfo(index = true),最后都会映射到 SQLite 的约束上。我个人的习惯是:能交给数据库校验的,绝不写在应用层。因为应用层可能漏掉,但数据库的一致性保证是铁打的。
PRAGMA foreign_keys = ON,但你如果自己写 raw SQL 或迁移脚本,别忘了手动设置。
隔离性:并发不打架
隔离性解决的是「多个事务同时读写同一行数据」的问题。SQLite 的隔离级别是 Serializable(可串行化),这是最严格的级别。但在 WAL 模式下,它又允许读写并发。
底层原理:
- 读锁(SHARED):多个读事务可以同时持有,互不阻塞。
- 写锁(RESERVED / EXCLUSIVE):写事务需要独占锁。在回滚日志模式下,读写完全互斥;但在 WAL 模式下,写操作写入 WAL 文件,读操作从原数据库文件读取,所以读不阻塞写,写也不阻塞读。
- MVCC(多版本并发控制):WAL 模式下每个读事务看到的是开始时刻的快照,写事务的修改不会影响正在进行的读。
SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING),所以你的 App 可以一边读一边写,性能大幅提升。但要注意,如果多个线程同时写同一个表,还是会有锁竞争,建议用 @Transaction 让它们排队。
我曾经踩过一个坑:在后台线程批量插入数据时,UI 线程查询列表,结果偶尔出现「数据瞬间消失又出现」的错觉。后来发现是读事务在 WAL 模式下看到的是旧版本,等 WAL checkpoint 之后新版本才可见。解决办法是用 Flow 监听数据库变化,或者调高 wal_autocheckpoint 间隔。
持久性:写成功就不丢
持久性要求:一旦事务提交成功,数据必须被永久保存,即使系统崩溃也不会丢失。SQLite 通过以下机制保证:
- WAL(Write-Ahead Logging):修改先写入 WAL 文件,待安全后再合并回主数据库。这是默认推荐模式。
- fsync:每次 COMMIT 后,SQLite 会调用操作系统层面的
fsync()将数据从页面缓存强制刷到磁盘。这是保证持久性的关键。 - fullfsync / PRAGMA synchronous:你可以配置
synchronous = FULL(最安全)或synchronous = NORMAL(略快但极端情况下可能丢失部分数据)。Room 的默认设置是FULL。
| PRAGMA 选项 | 写性能 | 持久性保证 |
|---|---|---|
| synchronous = OFF | 极快 | 💀 可能丢数据(不推荐) |
| synchronous = NORMAL | 较快 | 绝大多数情况可靠 |
| synchronous = FULL(默认) | 较慢 | ✅ 最安全 |
Room 如何串联这四者?
Room 没有自己实现 ACID,它只是个「翻译官」和「管家」——
- 自动生成
beginTransaction() / endTransaction()代码(原子性) - 自动开启外键约束、把注解映射成表约束(一致性)
- 默认启用 WAL 模式,并提供
@Transaction控制隔离边界(隔离性) - 使用 SQLite 原生的 checkpoint 机制和 fsync(持久性)
你唯一要做的,就是正确使用这些注解和配置。嗯,其实说到底,ACID 是 SQLite 的功劳,但 Room 替我们省掉了大量手写事务的样板代码。下次如果面试官问「Room 怎么保证 ACID」,你就可以底气十足地告诉他:Room 自己不干活,它只是 SQLite 的优秀代理人。