聊到SQLite,很多人第一反应是“小作坊数据库”,单机用用还行,并发一上来就卡死。这话对了一半——SQLite默认是单线程写入,确实扛不住高并发。但问题不在SQLite本身,而在大多数人没学会怎么用连接池伺候它。连接池这东西,听着高大上,说白了就是提前开几个数据库连接放那儿,谁要用就取,用完放回,省得每次现开现关。道理简单,可SQLite的特殊性在于,它不像MySQL或者PostgreSQL那样跑着独立服务器,它是直接读写文件的。这就意味着,连接池的设计和参数调优,完全是另一套玩法。

先说最核心的一点:并发写入的瓶颈从来不在连接数,而在锁机制。SQLite用的是数据库级别的锁,一个连接在写,其他连接就得等着。你开100个连接,不如只开一个写连接,配合WAL模式(Write-Ahead Logging)来玩。WAL模式下,写操作不会直接锁住整个数据库,而是先写到一个单独的日志文件里,读操作可以继续读旧数据。这时候,连接池里放一个写连接、多个读连接,效果比全混在一起好得多。我见过一个项目,原来开20个连接跑写入,每秒才处理200条,改成单写连接加WAL,直接冲到1500条,数据量不大,但体验天差地别。
讲个具体案例。有个做设备数据采集的团队,SQLite每天要写入几十万条传感器记录,同时还要支持实时查询。他们一开始用默认的“delete+journal”模式,连接池设了30个,结果频繁报“database is locked”。排查下来,是WAL没开,写操作一多,读连接全堵死了。后来我把他们的连接池拆成两组:一组是写池,只放1个连接,专门负责插入;另一组是读池,放10个连接,跑查询。同时把PRAGMA设置成journalmode=WAL,synchronous=NORMAL,cachesize=-8000(大概8MB缓存)。改完当天,写入吞吐量翻了3倍,查询延迟从平均500毫秒降到50毫秒。关键是,WAL模式下,写操作和读操作可以同时进行,不再互相卡脖子。
但WAL也不是万能药。它有个隐藏问题:检查点(checkpoint)机制。WAL文件会越长越大,如果不定期触发检查点,把日志合并回主数据库,读性能会慢慢下降。连接池里最好单独留一个连接,定时跑PRAGMA walcheckpoint(TRUNCATE)。注意别在写入高峰期跑,会短暂锁库。我习惯的做法是,每写入1000条数据或者每分钟触发一次检查点,用后台线程跑,不影响主业务。还有一个细节:WAL模式下,如果有连接一直没关闭,检查点可能无法成功。所以连接池要确保连接生命周期可控,别让某个连接长时间挂着不干活。
连接池的大小怎么定?别迷信网上说的“越大越好”。SQLite的并发瓶颈在IO和锁,不在连接数。读连接可以稍微多一点,但超过10个后,收益递减明显,反而因为上下文切换增加开销。写连接,1个就够,最多2个——一个主写,一个备写,防止主写连接挂了丢数据。我见过最离谱的配置,有人给SQLite配了50个写连接,结果90%的时间都在等锁,CPU全耗在轮询上了。实测下来,读连接5-8个、写连接1个,配合WAL,已经能压榨出SQLite的极限性能。如果你的场景是纯写入、不关心实时读,甚至可以只开一个连接,省掉连接池的开销。
还有一个容易被忽略的点:事务提交频率。连接池里的每个连接,如果不显式开启事务,每次INSERT或UPDATE都会自动提交,这会产生大量小事务,频繁刷盘,拖垮性能。正确的做法是,把一批操作放到一个事务里。比如插入1000条数据,先BEGIN,再循环INSERT,最后COMMIT。连接池可以配合一个“批量提交器”,攒够一定数量或到时间再统一提交。我做过测试,单条提交每秒只能写几百条,批量1000条提交每秒能写几万条,差了不止一个数量级。连接池里的写连接,最好设计成“事务模式”,用完不立即提交,等池子本身触发提交逻辑。
连接池的健康检查也很关键。SQLite连接可能会因为文件损坏、磁盘满、进程崩溃等因素挂掉。池子要定期发一个简单的SELECT 1或者PRAGMA integritycheck,确认连接还能用。发现坏连接,直接丢弃,重新创建。别等业务请求来了才发现连不上,那会儿就晚了。我习惯把健康检查间隔设在30秒到60秒之间,太频繁浪费资源,太长了问题发现不及时。另外,SQLite对并发读写的稳定性,很大程度上取决于磁盘性能。SSD和机械硬盘的差距,在WAL模式下尤其明显。如果预算允许,把数据库文件放到NVMe SSD上,连接池的吞吐能再提一个档次。
聊聊线程模型。SQLite默认是不支持多线程同时使用同一个连接的,必须每个线程用独立的连接,或者加锁串行化访问。连接池天然解决了这个问题——每个线程从池子里取一个独立的连接,用完归还,互不干扰。但要注意,Python的sqlite3模块默认有一个“checksamethread”参数,设为False才能跨线程使用。其他语言类似,得显式关闭线程检查。还有一个坑:连接池如果用了连接复用,多个请求共用同一个连接,一定要确保同一时刻只有一个线程在用,否则数据会乱。所以连接池的实现,要么每个连接加锁,要么用队列串行化访问,别贪图性能搞成无锁状态,早晚出事。
总结一下,SQLite连接池优化的核心就四点:开WAL模式、拆读写池、控制事务粒度、定期检查点。别把它当成MySQL来调,也别因为它是文件型数据库就轻视它。调好了,百万级并发读、万级并发写,完全扛得住。调不好,几千条请求就能把SQLite锁成死狗。连接池不是银弹,但用对了,绝对能让SQLite从“玩具”变成“利器”。下次再有人跟你说SQLite不适合并发,你可以笑着告诉他:你只是没学会怎么伺候它。


