SQLite 如何处理并发读写?WAL 模式能解决什么问题?
SQLite 的并发能力要先说清一个前提:它不是服务器数据库,而是把数据库引擎嵌进应用进程,多个连接最终都在争用同一个数据库文件。所以它的设计目标不是让几百个写请求同时冲进去,而是让读操作尽量轻、写操作尽量安全。实际项目里,只要写入不是持续高峰,SQLite 的多读单写模型通常够用;一旦把它当成 MySQL 那样承接高并发写入,就会很快遇到 database is locked。
SQLite 的锁是怎么变化的
默认回滚日志模式下,SQLite 常见锁状态可以理解为从宽到严逐步升级:SHARED 允许多个连接读,RESERVED 表示某个连接准备写,PENDING 阻止新的读进入,EXCLUSIVE 才真正独占写入。读连接之间不互相阻塞,写连接之间只能排队。最容易踩坑的是长时间读事务:一个后台导出任务开着游标不关,写事务提交时就可能一直等锁。
sqlPRAGMA journal_mode; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;
busy_timeout 不是提升并发的魔法,它只是让连接在遇到锁时等一会儿,而不是立刻报错。如果写事务本身很慢,等待时间再长也只是把问题往后拖。
WAL 为什么常被推荐
WAL(Write-Ahead Logging)把写入先追加到 -wal 文件,读连接可以继续读旧快照,写连接也不用立刻覆盖主数据库文件。这样读写冲突明显减少,尤其适合“读很多、偶尔写”的桌面应用、移动端缓存和小型服务。取舍也很明确:WAL 仍然只有一个写者,而且会多出 wal/shm 文件,需要定期 checkpoint。
sqlPRAGMA wal_checkpoint(TRUNCATE); PRAGMA synchronous = NORMAL;
synchronous=NORMAL 在 WAL 下通常是性能和安全的折中;如果是账务落库或断电风险很敏感,仍应评估 FULL。不要为了跑分把可靠性配置关到最低。
写事务要短,连接要克制
SQLite 并发优化最有效的办法不是开更多连接,而是缩短写锁占用时间。批量写入要包进事务,避免每条语句都单独提交;但事务里不要夹杂网络请求、文件上传或复杂计算。写入前先准备好数据,拿到锁后只做数据库操作。
sqlBEGIN IMMEDIATE; INSERT INTO logs(message, created_at) VALUES ('ok', unixepoch()); UPDATE counters SET value = value + 1 WHERE name = 'daily'; COMMIT;
BEGIN IMMEDIATE 会提前抢写锁,适合已经确定要写的场景。好处是失败更早,不会在做了一半业务逻辑后才发现提交不了;代价是会更早挡住其他写者。
追问
WAL 开了以后是不是就能高并发写入?
不能。WAL 主要改善读写并发,让读者不必因为写者追加日志而停下来,但同一时间仍然只有一个写事务能提交。它适合读多写少或短写事务场景,不适合订单撮合、聊天消息洪峰这类持续写入压力。踩坑点是只改了 journal_mode=WAL,却把大批量导入放在一个很长事务里,结果其他写请求仍然全部排队。
为什么经常遇到 database is locked?
常见原因是事务没有及时提交、查询游标没有关闭、多个进程同时写,或者把网络调用放进事务中。SQLite 不会替你拆分写锁,它只会按文件锁规则等待或失败。排查时可以先加 PRAGMA busy_timeout=5000 缓解瞬时冲突,再检查哪段代码持有连接太久。边界是 busy timeout 只能处理短暂竞争,不能解决架构级写入过载。
BEGIN DEFERRED、IMMEDIATE 和 EXCLUSIVE 怎么选?
DEFERRED 是默认模式,直到真正读写时才拿锁,适合不确定是否写入的普通逻辑。IMMEDIATE 一开始就拿写意向锁,适合明确要写且希望早失败的业务。EXCLUSIVE 会更强势地独占数据库,通常只在迁移、批量维护或离线工具里使用。取舍在于锁越早越强,失败越可控,但对其他连接越不友好。
多线程里能共享同一个 SQLite 连接吗?
要看驱动和编译配置,但工程上更稳的是每个线程或请求使用独立连接,并通过连接池控制数量。共享连接容易出现游标交叉、事务边界混乱和锁释放不及时。连接也不是越多越好,SQLite 的瓶颈常常是写锁而不是连接数。移动端和桌面应用里,单写队列加多个只读连接通常更容易维护。