SQLite 事务是怎么工作的?BEGIN 模式该怎么选?
SQLite 的事务用来保证一组操作要么全部成功,要么全部撤销。它支持 ACID,但实现方式和服务器数据库不完全一样:事务最终落在数据库文件、回滚日志或 WAL 日志、文件锁这些机制上。理解这一点很重要,因为很多线上问题不是 SQL 写错,而是事务边界太长、提交太频繁或锁模式选错。
一次事务从 BEGIN 到 COMMIT
最基本的事务写法很简单:
sqlBEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT;
如果第二条语句失败,就应该 ROLLBACK,否则账户状态可能不一致。SQLite 会用日志保证回滚能力:回滚日志模式会先保存旧页面,WAL 模式会先追加新记录。两者目标一样,都是让崩溃恢复时能回到一致状态。
三种 BEGIN 模式的差异
BEGIN DEFERRED 是默认模式,开始时不立刻拿写锁,第一次读或写时再决定。BEGIN IMMEDIATE 会立刻获取写意向锁,避免业务执行到一半才发现无法写。BEGIN EXCLUSIVE 更强,会阻止其他连接读写,在 WAL 模式下表现会略有差异,但仍应谨慎使用。
sqlBEGIN DEFERRED; -- 默认,适合普通读写 BEGIN IMMEDIATE; -- 明确要写,提前抢写锁 BEGIN EXCLUSIVE; -- 维护、迁移、离线任务慎用
应用代码里,IMMEDIATE 常用于库存扣减、计数器更新这类必须写的逻辑。它不是更高级,而是失败更早、更可控。
提交频率影响性能和可靠性
SQLite 每次提交都要确保事务持久化,过于频繁的单条提交会产生大量同步开销。批量导入时,最常见的优化就是把多条写入放到一个事务里。
sqlBEGIN IMMEDIATE; INSERT INTO event_log(type, payload) VALUES ('click', '{}'); INSERT INTO event_log(type, payload) VALUES ('view', '{}'); COMMIT;
但事务也不能无限大。一个包含几十万行写入的事务会长时间占用写锁,WAL 文件也可能膨胀。比较稳的做法是分批提交,比如每 500 或 1000 条一批,根据设备磁盘和业务延迟调参数。
追问
SQLite 的事务隔离级别是不是和 MySQL 一样?
不完全一样。SQLite 默认提供接近串行化的隔离效果,读事务看到的是一致快照,写事务同一时间只能有一个。它没有像 MySQL 那样暴露多种行级隔离级别给你切换。取舍是模型简单、可靠,但并发写入能力不如服务器数据库。
什么时候应该用 SAVEPOINT?
SAVEPOINT 适合在大事务里做局部回滚,比如导入 1000 条记录,其中某几条格式错误不想拖垮整批。它比嵌套事务更贴近 SQLite 的实际能力,因为 SQLite 的普通 BEGIN 不能简单嵌套。示例是 SAVEPOINT one; ...; ROLLBACK TO one; RELEASE one;。踩坑点是 ROLLBACK TO 后还要 RELEASE 或继续处理外层事务,否则事务状态容易混乱。
事务里能不能做耗时业务逻辑?
尽量不要。事务开始后,你持有的读锁或写锁会影响其他连接,尤其是写锁会让后续写入排队。正确做法是先在事务外完成参数校验、网络请求和文件处理,事务内只执行必要 SQL。边界是某些校验必须和写入保持一致,比如唯一性检查,这时要用约束和短事务配合,而不是把整段业务都塞进去。
COMMIT 成功就一定不会丢数据吗?
这取决于 journal_mode、synchronous 和底层文件系统。默认安全配置下,SQLite 会尽力保证提交后的持久性;但如果为了性能把同步级别调低,断电时就可能损失最近事务。WAL 加 synchronous=NORMAL 是常见折中,适合大多数应用缓存和本地业务数据。对强一致账务类数据,应保守配置并做崩溃恢复测试。