03-事务高频面试总结
目录
1. 解释事务的 ACID 特性,并说明 SQLite 是如何保证每一项的。
2. 什么是自动提交模式?为什么在批量写入时要显式使用 BEGIN...COMMIT?
3. 解释 BEGIN DEFERRED、BEGIN IMMEDIATE、BEGIN EXCLUSIVE 的区别和使用场景。
4. SQLite 的锁机制是怎样的?为什么说它适合“多读少写”的场景?
5. 遇到 database is locked 错误时,有哪些解决办法?(设计 + 配置)
设计层面(架构/SQL 习惯)
配置 / 代码层面
6. 解释 SQLite 的隔离级别,它能否读未提交的数据?PRAGMA read_uncommitted 有什么作用?
7. 在高并发读多写少场景下,你会如何配置 SQLite 来提升并发能力?
8. 什么是 WAL 模式?它对读写并发和崩溃恢复有什么影响?
9. 多线程访问 SQLite 时,你如何管理连接和事务?
1. 解释事务的 ACID 特性,并说明 SQLite 是如何保证每一项的。
ACID:
-
A 原子性(Atomicity)
要么全成功,要么全失败。
SQLite:-
用事务日志(rollback journal / WAL)记录修改
-
出错或
ROLLBACK时,用日志把数据恢复到事务开始前的状态
-
-
C 一致性(Consistency)
事务前后,数据库从一个合法状态到另一个合法状态(约束不被破坏)。
SQLite:-
通过约束(PRIMARY KEY、UNIQUE、NOT NULL、CHECK、FOREIGN KEY)+ 事务
-
约束失败 → 语句报错 → 事务可整体回滚,避免留下“半合法”状态
-
-
I 隔离性(Isolation)
并发事务互不干扰,看起来像串行执行。
SQLite:-
使用文件锁和多版本读(特别是 WAL 模式)
-
读事务看到的是一个一致快照,不会看到别的事务的中间状态(不读未提交)
-
-
D 持久性(Durability)
提交后的数据崩溃也不会丢。
SQLite:-
提交时把日志 / WAL 同步写入磁盘(受
PRAGMA synchronous控制) -
崩溃后启动时,根据日志/WAL 恢复到一致状态
-
2. 什么是自动提交模式?为什么在批量写入时要显式使用 BEGIN...COMMIT?
自动提交模式(auto-commit):
只要你没有显式
BEGIN事务,SQLite 会把每一条写语句当成一个独立事务:
-
语句开始前:隐式
BEGIN -
语句成功:隐式
COMMIT -
语句失败:隐式
ROLLBACK
批量写时为什么要显式 BEGIN...COMMIT?
-
不写
BEGIN:
每条INSERT/UPDATE/DELETE都是
BEGIN → 执行 → COMMIT→ 每条都刷一次磁盘,非常慢 -
显式事务:
BEGIN;INSERT ...;INSERT ...;INSERT ...; COMMIT;-
多条语句共享一个事务
-
只在最后
COMMIT时落盘一次 -
大幅提升写入性能
-
同时保证“这几条要么一起成功,要么一起失败”(原子性)
-
3. 解释 BEGIN DEFERRED、BEGIN IMMEDIATE、BEGIN EXCLUSIVE 的区别和使用场景。
三种模式都是“开始事务”,区别在于什么时候抢锁、锁多大:
-
BEGIN DEFERRED(默认)-
开始事务 不立即加写锁
-
真正需要读/写时再申请相应锁
-
优点:锁持有时间短,更灵活
-
**使用场景:**大多数普通事务
-
-
BEGIN IMMEDIATE-
一开始就尝试获取写锁(RESERVED)
-
阻止其他连接再开启写事务,但读仍可进行
-
使用场景:你已经确定本事务会写,希望尽早占住写权限,避免做到一半才发现写不了
-
-
BEGIN EXCLUSIVE-
一开始就尝试获取排他锁(EXCLUSIVE)
-
阻止其他读写,几乎“独占数据库文件”
-
**使用场景:**大批量迁移、重建索引、一次性大改动,能接受这段时间其他访问都暂停
-
4. SQLite 的锁机制是怎样的?为什么说它适合“多读少写”的场景?
锁机制(非 WAL 模式下):
-
锁是加在整个数据库文件上的(不是行锁)
-
主要锁级别:
-
SHARED(共享锁):读事务用,多个读可以并发 -
RESERVED:写事务准备写时占位,只允许一个写者 -
PENDING:写事务准备升级为排他锁,阻止新读者进来 -
EXCLUSIVE(排他锁):写事务真正写回数据库时独占文件,挡住其他读写
-
读/写流程(简化):
-
读:
无锁 → SHARED → 释放 -
写:
无锁 → (可能 SHARED)→ RESERVED → PENDING → EXCLUSIVE → 释放
为什么适合“多读少写”?
因为:
可以有很多并发读(多个 SHARED 锁)
但同一时刻只能有一个写者(写要升级到 EXCLUSIVE),
写的时候会影响其他读写
所以:
-
读多写少 → 冲突少,表现很好
-
写多并发 → 写锁竞争严重,频繁
database is locked
(使用 WAL 后,读写并发会好很多,但“单写者”本质不变)
5. 遇到 database is locked 错误时,有哪些解决办法?(设计 + 配置)
可以从设计层面和配置/代码层面两块回答:
设计层面(架构/SQL 习惯)
-
缩短事务时间
-
事务里只做必要的 SQL
-
不要在事务中做复杂计算 / 网络 IO
-
尽快
COMMIT或ROLLBACK,减少锁持有时间
-
-
避免长时间大查询(长读事务)
-
使用分页查询
LIMIT/OFFSET -
避免一个 SELECT 持有读锁很久,阻塞写
-
-
控制写的粒度和频率
-
批量写时用显式事务,减少事务次数
-
尽量把高频写挪到队列/后台任务
-
-
读多写少时开启 WAL 模式
-
减少写对读的影响(写不那么挡读)
-
配置 / 代码层面
-
设置
busy_timeoutPRAGMA busy_timeout = 3000; -- 最多等 3000ms 再报错-
避免刚好撞锁时立刻失败,给对方一点时间释放锁
-
-
应用层重试机制
-
捕获
database is locked/busy异常 -
sleep一小会儿后重试几次
-
-
合理使用 WAL 模式
-
PRAGMA journal_mode = WAL; -
提升读写并发能力,减少锁冲突
-
6. 解释 SQLite 的隔离级别,它能否读未提交的数据?PRAGMA read_uncommitted 有什么作用?
隔离级别:
-
SQLite 官方默认隔离行为接近 SERIALIZABLE / snapshot isolation:
-
读事务看到的是一个一致的“快照”
-
不会读到其他事务的未提交数据(不支持脏读)
-
能否读未提交的数据?
不能。
即使你设置了PRAGMA read_uncommitted = 1,SQLite 实际上也不会真的让你看到未提交的数据,更多是为了兼容 SQL 标准接口。
PRAGMA read_uncommitted 的作用?
PRAGMA read_uncommitted = 1;
PRAGMA read_uncommitted;
-
语义上是“允许脏读”
-
但在 SQLite 的实现中:
-
读未提交的数据并不是真的被允许
-
官方文档说明它基本是无效/被忽略的开关
-
SQLite 仍然提供一致性快照,避免脏读
-
可以在面试中简单说:
SQLite 的隔离级别类似于快照隔离,默认不允许读未提交数据。
PRAGMA read_uncommitted在 SQLite 中基本不会真的打开脏读,只是为了兼容 SQL 标准接口。
7. 在高并发读多写少场景下,你会如何配置 SQLite 来提升并发能力?
可以从配置 + 使用习惯两块回答:
-
启用 WAL 模式
PRAGMA journal_mode = WAL;-
写入追加到 WAL 文件
-
大部分场景下:写不阻塞读,读写并发能力大幅提升
-
-
适当设置
busy_timeoutPRAGMA busy_timeout = 3000; -- 例如 3 秒-
避免短暂锁冲突导致立刻报错
-
给写事务一些时间完成并释放锁
-
-
合理设置
synchronous策略(视场景而定)PRAGMA synchronous = NORMAL; -- 读多写少、允许一点点风险 -- 或 FULL(更安全,稍微慢一些) -
优化查询与索引
-
给频繁查询的条件列建索引,减少查询时间
-
查询快 → 事务短 → 锁持有时间短
-
-
设计上保证“写少、短事务”
-
批量写用显式事务
-
避免长时间读事务
-
8. 什么是 WAL 模式?它对读写并发和崩溃恢复有什么影响?
WAL(Write-Ahead Logging)模式:
写前日志模式:写操作不直接改主库文件(
xxx.db),
而是先把修改追加写到xxx.db-wal日志文件里,
后面再通过 checkpoint 把 WAL 内容合并回主库。
对读写并发的影响:
-
写:写入 WAL 文件,主库暂时不动
-
读:从主库 + WAL 合成一个当前视图
→ 大部分情况下:“有人在写,别人仍然可以读” -
结果:读写并发明显提升,尤其适合读多写少场景
对崩溃恢复的影响:
-
崩溃时:
-
WAL 中已有事务记录可以用来恢复一致状态
-
-
恢复过程:
-
重放 WAL → 修复主库
-
-
一般来说,WAL 模式下:
-
崩溃恢复效率高
-
一致性更容易保证
-
可以简短总结:
WAL 模式通过把写操作先记录到独立日志文件,实现“写不阻塞多数读”,显著提升读写并发;同时也提供良好的崩溃恢复能力,通过重放 WAL 日志保证事务的持久性和一致性。
9. 多线程访问 SQLite 时,你如何管理连接和事务?
建议做法:
-
一个线程一个连接(推荐)
-
每个线程拥有自己的 SQLite 连接对象(
sqlite3*或对应语言的 Connection) -
避免多个线程同时操作同一个连接
-
-
如果必须共享连接,外层加锁
-
用互斥锁(mutex)包裹所有对该连接的操作:
lock(mutex)// 使用 conn 执行 SQL unlock(mutex) -
保证同一时刻只有一个线程使用这个连接
-
-
配合显式事务管理
-
在线程内明确使用
BEGIN...COMMIT/ROLLBACK控制事务边界 -
尽量缩短事务时间,避免长时间持有锁
-
-
结合前面提到的并发优化:
-
开启 WAL 模式
-
设置
busy_timeout -
读多写少,控制写频率
-
面试时可以这样说:
在多线程环境下,我通常让每个线程使用自己的 SQLite 连接,避免跨线程共享同一个连接对象。如果业务上必须共享,会在外层加 mutex 保证串行访问。事务上会显式使用
BEGIN...COMMIT控制范围,尽量缩短事务时间,并在读取多写入少的场景下开启 WAL +busy_timeout来提升并发能力。
