把一个 SQLite 数据库文件原样丢进 Syncthing 同步目录,理论上听起来很方便:多台设备都有一份数据库,数据不就用上了吗?实际做过的人,基本都会在一段时间后收到一个database disk image is malformed,或者“同步出一堆 conflict 副本”的惊喜。这就是社区里常说的 Syncthing and SQLite Gotcha。
Syncthing 负责在设备之间同步文件内容,SQLite 负责在小文件里帮你做事务、索引、崩溃恢复。两个工具本身都没有问题,问题出在它们对“数据一致”的理解完全不同。Syncthing 看到的是文件块,SQLite 维护的是事务日志和锁。把 SQLite 的活动数据库当成普通文档搬运到另一台设备继续写,等于在没有任何分布式共识机制的情况下搞多副本,损坏只是时间问题。
本文会从底层的文件结构讲起,说明为什么复制单个app.db文件并不能得到一份一致的数据库;然后给出一套可复现的验证流程,用来确认损坏路径;再整理损坏后的恢复顺序和命令;最后给出几套替换方案,包括.stignore忽略规则、定时一致性备份、以及单写者加 Litestream 之类的演进路线。
适合读者:用 Syncthing 同步笔记或网盘目录的 NAS 用户,桌面端或移动端以 SQLite 做本地存储的开发者,以及在 TrueNAS、QNAP 上部署过 Syncthing、准备“顺手”同步数据库文件的朋友。
1. 核心能力速览
先把这篇文章涉及的两个组件和它们“组合之后”的能力边界放一张表里。后面所有分析都围绕这张表展开。
| 维度 | Syncthing | SQLite | 组合使用时的 Gotcha |
|---|---|---|---|
| 项目类型 | 开源文件同步工具 | 嵌入式关系型数据库 | 文件同步与数据库一致性的冲突 |
| 主要功能 | 跨设备增量同步、版本控制、REST API | 事务、索引、SQL 查询、崩溃恢复 | 直接同步活动数据库文件属于高风险操作 |
| 平台支持 | Windows / macOS / Linux / Android / BSD,常见 NAS 可运行 | 几乎所有操作系统 | 取决于客户端平台 |
| 启动方式 | 常驻服务,Web 管理界面或命令行 | 嵌入到应用中,或命令行sqlite3 | 无 |
| 资源占用 | 轻量级,但取决于文件数量与版本控制策略 | 极小,可忽略 | 无 |
| API | 提供 REST API,可管理设备与文件夹 | 提供编程接口,支持多种语言绑定 | 无 |
| 批量任务 | 支持文件夹批量同步 | 支持批量 SQL 操作 | 批量复制数据库文件属于危险操作 |
| 适合场景 | 文档、媒体、代码目录多设备同步 | 本地应用数据存储 | 只建议单写者加一致性导出备份;不建议多设备写同一个数据库 |
关于 Syncthing 的硬件门槛:它本身是轻量级常驻服务,常见 NAS、树莓派级别设备都能跑。但具体内存和磁盘占用取决于同步目录文件数量和版本控制策略,不要用“很省资源”作为唯一判断标准。Web 管理界面默认端口常见为 8384,本地观察时注意端口是否被占用,实际端口以你的配置文件为准。
关于 SQLite 的能力边界:它在单机本地文件系统上表现优秀,官方文档也不建议把数据库文件直接放在 NFS、SMB 这类网络文件系统上运行。Syncthing 同步目录本质上会把数据库文件变得“不再本地”,这已经踩进了 SQLite 的边界之外。多设备同时写同一个 SQLite 文件时,SQLite 自带的文件锁机制并不能跨设备生效,这就是后续所有问题的根因。
2. 适用场景与使用边界
2.1 谁适合继续用这套组合
- 只想做“单向备份”的开发者:本地 SQLite 数据库通过 Syncthing 同步到另一台设备作为冷备,且原数据库所在设备是唯一写入者。此时只要保证同步期间应用处于停止或 checkpoint 后的状态,仍然可以接受。
- 以文档、图片、视频等非数据库文件为主的同步用户:Syncthing 对这类内容完全适用,只需要把数据库文件排除在外。
- 应用不支持服务器数据库、又需要多设备只读副本的场景:可以接受“同步完成后必须断开写入”这种弱一致性。
2.2 谁不适合
- 需要在多台设备上同时打开同一个 SQLite 数据库并写入的团队或开发者。这不是 Syncthing 的 bug,而是文件同步工具无法等价于数据库复制协议。
- 对数据零丢失要求较高的生产业务。即使使用忽略规则,也只能避免问题,不能解决多写者需求。
- 移动端 App 自动备份数据目录的玩家用户。Android 和 iOS 的应用沙箱数据库往往包含未 checkpoint 的 WAL 内容,直接同步非常容易得到不可用文件。
2.3 使用边界与合规提醒
任何同步方案都会涉及数据拷贝。这里的“数据拷贝”不仅仅是文件复制,还可能包含用户隐私、业务数据、版权素材。Syncthing 默认使用 TLS 加密传输,并要求设备之间通过证书完成身份认证,在同类工具里属于基础配置偏严格。但加密传输不等于授权合规,数据到达另一端之后的存储、备份、再分发,仍然需要你自己控制。
如果你是开发者,在设计同步功能前要确认数据授权范围;如果你用 Syncthing 同步办公文档或代码,也要保证设备和链路的访问控制。涉及人脸、声音、隐私字段、受版权保护的素材时,务必确认你有权复制和存储这些数据,再决定是否进入同步链路。
3. 理解 SQLite 的持久化机制
3.1 SQLite 不是一个文件
很多人以为 SQLite 数据库就是单个.db文件,实际上它可能是一组文件。最常见的是主数据库文件app.db,但在不同的日志模式下还会出现陪伴文件:
- 回滚日志模式:写事务开始前会生成
app.db-journal文件,用于崩溃回滚。 - WAL 模式:数据先写入
app.db-wal,同时生成app.db-shm作为 WAL 索引的共享内存映射文件。
主数据库文件内部由数据库头、B-tree 页面、页面结构组成,页面大小通常是 4KB 或更大。数据库头里包含 page size、change counter、schema version 等字段,这些字段必须和文件内容保持一致。如果你只复制了部分页面,或者复制时主文件正处于写入中间状态,文件头和数据页就会错位。
3.2 事务、锁与崩溃恢复
SQLite 使用操作系统文件锁来实现并发控制。同一台机器上的多个进程可以通过文件锁协调读写:读锁可以共享,写锁互斥。但锁信息只存在于本机操作系统,Syncthing 把文件内容推送到另一台设备时,对端进程并不感知“这个文件刚刚被替换”。
原子性方面,SQLite 通过日志重放或回滚来保证崩溃恢复。回滚日志模式会在修改前写旧页面,WAL 模式会在提交后把新页面追加到 WAL。重新打开数据库时,SQLite 会根据日志内容决定是重放还是回滚。关键点是:日志文件必须和主库文件配套,日志里的页面变更必须按顺序应用到同一个主库文件上。
3.3 一句话结论
SQLite 的一致性等于主库文件加日志文件加操作系统的锁,三者必须在一个本地文件系统上同时生效。缺少任何一个,SQLite 都无法按预期保证一致性。Syncthing 的同步目录无法提供这个前提,所以用它同步活动数据库就是踩入 gotcha。
4. 为什么用 Syncthing 同步 SQLite 会翻车
4.1 文件级同步不等于数据库复制
Syncthing 检测到文件块变化后,会把变化块增量传输给对端。它并不理解“这一批变化是整个事务的一部分”。当应用在同一时刻写入了 5 个页面,Syncthing 可能只同步了其中 2 个,对端就临时存储了一个半新半旧的主文件。如果这个半成品被另一个进程加载,SQLite 会把它判定为损坏。
4.2 WAL 文件与主库文件存在时间差
WAL 模式下,提交的数据先进入app.db-wal,主库文件的 change counter 可能很久都不变。Syncthing 同步时可能只同步了 WAL 文件而主库文件还是旧版本,或者反过来。目标设备上的 SQLite 拿到一对不一致的主库和 WAL 时,只能报错或选择回滚,回滚意味着丢失刚刚在其他设备提交的事务。
4.3-shm文件不能当作数据复制
app.db-shm是 WAL 索引的共享内存映射文件,由进程间共享内存机制管理,它应该在每台设备上由各自 SQLite 进程重新创建。如果 Syncthing 把这个文件也从一台设备复制到另一台,反而可能导致对端 SQLite 读到错误索引位置。最稳妥的做法是把它排除掉,不要同步。
4.4 多设备并发写的锁协调问题
SQLite 的文件锁基于本机操作系统。设备 A 和设备 B 各自认为自己拿到了写锁,两个进程在不同机器上写同一个文件,最后落盘结果取决于 Syncthing 最后一次合并覆盖了谁。轻则丢更新,重则产生混合页面,连完整性检查都过不了。