SQLite是一个嵌入式的单文件数据库,不需要独立的服务端进程,天然适合和一些轻量级应用搭配。随着Serverless架构的流行,不少开发者开始尝试在AWS Lambda、Cloudflare Workers、Vercel Functions这类函数计算环境中使用SQLite,希望省掉一台数据库服务器的成本。这个思路是可行的,但前提是你必须理解Serverless平台的运行机制和SQLite的底层工作方式之间的冲突点。本文把实践中的关键注意事项梳理出来,帮助你在动手之前先想清楚。

一、文件系统限制:Serverless下SQLite的第一个门槛
SQLite的一切都建立在本地文件之上,数据库本身就是一个普通的磁盘文件,读写都依赖文件系统调用。而大多数Serverless平台的执行环境恰恰对文件系统做了严格限制。以AWS Lambda为例,除了/tmp目录是可写的之外,其余路径全部只读,而且/tmp目录的容量默认只有512MB,虽然可以调大但上限也有限。更关键的是,/tmp是实例级别的临时空间,实例被回收后数据就消失了。
这就带来第一个必须想清楚的问题:你的数据要放在哪里。如果把数据库文件放在只读路径下,写入操作会直接报错attempt to write a readonly database;如果放在/tmp里,函数一旦冷启动到新实例,之前写入的数据就找不到了,因为Serverless平台不保证请求会路由到同一个实例。很多初学者在本地测试一切正常,部署到线上后数据莫名其妙丢失,基本都栽在这个问题上。
常见的解决思路有三种。第一种是挂载外部文件系统,比如在Lambda上挂载EFS,把数据库文件放到EFS的持久目录里,这样数据可以跨实例存活。第二种是平台自带的持久存储,例如Cloudflare Workers的Durable Objects配合其内置的SQLite API,Fly.io的Volume卷。第三种是把SQLite当作只读缓存用,数据库文件打包在代码包里或启动时下载到/tmp,运行期只做查询不做写入,写入走别的通道。三种方案各有取舍,需要根据读写比例和数据一致性要求来选。
二、多实例并发:数据不一致的隐藏炸弹
Serverless平台为了支撑高并发,会自动水平扩容出多个函数实例。每个实例都有自己独立的文件系统视图,如果数据库放在本地/tmp里,各个实例实际上操作的是各自独立的副本,数据立刻分裂。就算通过EFS这类网络文件系统共享了同一个数据库文件,问题也没有完全解决。
SQLite的并发控制依赖文件锁。在本地磁盘上,文件锁由操作系统内核保证,工作得非常好。但在网络文件系统上,锁的实现往往不完整或者有延迟,AWS官方就明确提示EFS上的fcntl锁支持有限制,多个Lambda实例同时写同一个EFS上的SQLite文件时,可能出现锁失效导致的数据库损坏。这是一个真实的坑,损坏一旦发生往往难以恢复。
如果你的应用确实是多写入者场景,比较稳妥的路线是引入LiteFS。LiteFS是一个基于FUSE的分布式SQLite方案,它在每个节点上维护数据库副本,写入通过主节点复制到各个只读副本。配合Consul或静态租约确定主节点位置,可以让多个函数实例看到一致的数据。另一种更简单的做法是约束写入路径:通过队列或某个固定的单实例(比如Cloudflare的Durable Object本身就保证单点写入)来串行化写操作,其余实例只承担读请求。
# 简单演示在Lambda函数中打开数据库并启用WAL模式
import sqlite3
def handler(event, context):
# 数据库文件必须放在可写的持久路径上
conn = sqlite3.connect("/mnt/data/app.db")
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
try:
conn.execute("CREATE TABLE IF NOT EXISTS hits (id INTEGER PRIMARY KEY, ts TEXT)")
conn.execute("INSERT INTO hits (ts) VALUES (datetime('now'))")
conn.commit()
finally:
conn.close()三、性能与冷启动:连接管理和PRAGMA调优
Serverless函数经常被频繁创建和销毁,如果每次请求都完整地打开数据库、执行查询、再关闭,开销会被放大。一个实践建议是在处理函数之外维持连接:把连接对象初始化放在模块顶层或者全局初始化代码中,实例被复用时连接也跟着复用。SQLite本身没有网络握手开销,连接复用的收益主要省在重复打开文件和页缓存重建上,对冷启动敏感的场景值得做。
PRAGMA配置在Serverless环境里同样重要。建议开启journal_mode=WAL,WAL模式允许读写并发,能减少读操作被写操作阻塞的情况,这对读多写少的典型API场景帮助很大。同时设置busy_timeout,让遇到锁竞争时等待一小段时间而不是立刻抛错,在网络文件系统上尤其必要。还可以开启synchronous=NORMAL,在WAL模式下这个级别的安全性对多数应用足够,而写入延迟明显低于FULL模式。不过要注意,如果数据库放在EFS上且对数据安全要求极高,fsync的语义在网络存储上本来就有折扣,PRAGMA调优无法完全弥补这一点。
冷启动本身也要考虑数据库文件大小的影响。如果采用只读打包的方案,数据库文件会被打进部署包,几十MB的数据库会直接拖慢函数的部署和启动。经验做法是控制打包数据库在合理体积内,或者启动时从对象存储异步下载并做校验,把下载过程放在初始化钩子里,避免占用请求处理时间。
四、备份与迁移:别忘了数据是文件
SQLite的备份本质上是文件操作,这一点在Serverless环境里既是优势也是隐患。优势在于备份和恢复非常直观,复制文件即可;隐患在于直接复制一个正在写入的数据库文件可能得到不一致的快照。正确做法是使用VACUUM INTO语句或者backup API生成一致性副本:
-- 生成一致性备份,适合定时任务执行
VACUUM INTO '/mnt/backup/app-' || strftime('%Y%m%d-%H%M', 'now') || '.db';备份产物建议立即上传到对象存储,例如S3或R2,这样即使函数实例所在的物理机故障,数据也有保障。此外要设计好表结构迁移流程。Serverless环境下没有持久的迁移执行点,常见方案是在部署阶段单独跑一个迁移脚本,或者在函数初始化时检查版本号并执行增量迁移,但要小心多实例同时触发迁移造成的冲突,最好用一张元数据表加锁来保证只有一个实例执行DDL。
五、什么情况下该放弃SQLite
最后要诚实地讨论适用边界。如果你的应用写入频繁、需要在多个区域部署、或者要求强一致的多写入点,SQLite加Serverless的组合会让你不停地在锁和复制问题上打补丁,此时直接使用托管的PostgreSQL或者专门的Serverless数据库(如Durable Objects、Turso、PlanetScale)会更省心。
反过来,如果应用是读多写少的内容型服务、边缘部署追求低延迟、数据量在几个GB以内,SQLite反而是极佳选择:零运维、零网络往返、查询速度极快。边缘计算平台上托管的SQLite方案(如Turso基于libSQL的架构、Cloudflare D1)已经把这些坑封装好了,直接用这类服务比自己硬搭一套要稳妥得多。技术选型的核心不是SQLite好不好,而是你的写入模式、一致性要求和部署形态能不能和它的单文件特性匹配。想清楚这一点,SQLite在Serverless中就能发挥出它简单高效的真正价值。
SQLiteServerlessLiteFS修改时间:2026-09-14 04:52:43