SSOT 双写架构与崩溃恢复
SSOT 双写架构与崩溃恢复
前置知识
需要了解 SQLite 数据库基本概念和文件系统操作(临时文件、rename 原子操作)。
学习目标
- 理解 SSOT 模式在客户端配置管理中的应用
- 掌握”数据库 → Live 配置”的双写同步机制
- 了解 Live 配置接管与崩溃恢复的完整流程
- 识别双写架构中的数据一致性风险与防护策略
项目实践
核心问题
在多应用配置管理场景中,存在两种数据源:
- 内部数据库(SQLite):应用自己维护的配置存储
- Live 配置文件(外部 JSON/TOML/.env):目标应用(如 Claude Code)实际读取的配置文件
问题是:这两个数据源如何保持一致?如果只写数据库,外部应用读不到;如果只写文件,应用自身无法做版本管理和查询。
SSOT 原则
SSOT(Single Source of Truth) 规定:数据库是唯一的真实数据源。Live 配置文件只是数据库的”投影”(Projection)——数据库可以随时将任意配置写入 Live 文件,但反过来不成立(不应直接编辑 Live 文件然后期望数据库自动同步)。
双写同步机制
正向写入(数据库 → Live)
当用户在应用中切换供应商时:
1. 用户选择供应商 B2. 更新数据库: UPDATE providers SET is_current = true WHERE id = 'B'3. 从数据库读取供应商 B 的完整配置4. 原子写入到 Live 配置文件(临时文件 + rename)5. 外部应用检测到文件变化,使用新配置反向回填(Live → 数据库)
当活跃供应商的 Live 配置被外部修改时:
1. 检测到 Live 文件变更(文件监听或定期检查)2. 读取 Live 文件,解析为结构化数据3. 更新数据库中当前供应商的配置字段4. 保留非关键字段(如用户手动添加的插件配置)回填保护机制:回填时不应覆盖数据库中的关键字段(如供应商 ID、名称、分类),只同步配置值(API Key、Base URL、环境变量等)。
Live 配置接管(Proxy Takeover)
当本地代理启动时,需要”接管” Live 配置:
步骤 1: 备份当前 Live 配置到数据库 → live_backups 表 { app_type, original_config, backed_up_at }
步骤 2: 同步 Live Token 到数据库 → 将 Live 文件中的 API Key 同步到数据库中的当前供应商
步骤 3: 写入接管配置 → base_url = "http://127.0.0.1:<proxy-port>" → api_key = "PROXY_MANAGED" (占位符) → 代理层从数据库读取真实 Token 并注入
步骤 4: 标记接管状态 → proxy_config.enabled = true → live_takeover_active = true关键点:接管写入的是占位符 Token(PROXY_MANAGED),真实 Token 已安全存储在数据库中。代理层在转发请求时从数据库读取真实 Token 并注入到上游请求中。
崩溃恢复流程
应用异常退出(崩溃、kill -9、断电)后,下次启动时:
恢复的核心逻辑:
- 检查数据库中是否存在 Live 备份(有备份 = 上次可能是代理模式退出)
- 检查 Live 文件是否包含占位符(
PROXY_MANAGED= 尚未恢复) - 如果两者都满足,从备份恢复原始 Live 配置
- 清除接管标志,删除备份
三层兜底恢复
如果备份缺失但 Live 文件仍包含占位符(极端情况),使用三层兜底:
第 1 层: 从 SSOT(当前供应商配置)重建 Live → 将数据库中当前供应商的配置写回 Live 文件
第 2 层: 如果无当前供应商,清理占位符 → 移除代理地址和 PROXY_MANAGED Token
第 3 层: 记录日志,等待用户手动修复问题与规避
陷阱 1:双写不一致
问题:数据库写入成功但 Live 文件写入失败(磁盘满、权限不足),导致两个数据源不一致。
规避策略:
- 先写 Live 文件,成功后再更新数据库标记(写回顺序很重要)
- 如果 Live 写入失败,回滚数据库操作或标记为”待同步”
- 启动时检测不一致并自动修复
陷阱 2:并发写入竞争
问题:应用和外部程序同时写入 Live 文件,导致配置被覆盖。
规避策略:
- 使用原子写入(临时文件 + rename),确保写入是原子的
- 在代理接管期间,阻止外部程序修改 Live 文件(通过文件锁或权限)
- 定期检查 Live 文件的哈希值,检测未预期的修改
陷阱 3:Token 泄露风险
问题:在接管过程中,如果备份文件未清理,真实 Token 会残留在数据库中。
规避策略:
- 恢复 Live 配置后立即删除备份
- 备份使用加密存储(或在 SQLite 的 encrypted 扩展中存储)
- 用户手动关闭代理时,确保清理所有备份
陷阱 4:部分写入导致配置损坏
问题:写入 Live 文件过程中进程被中断(kill -9),文件可能处于不完整状态。
规避策略:
- 原子写入:先写入临时文件,验证内容正确后 rename 到目标路径
- rename 在同一个文件系统上是原子操作,不会出现”半写”状态
- 写入后读取验证,确保内容与预期一致
设计取舍
SSOT vs 双向同步
| 维度 | SSOT(单向投影) | 双向同步 |
|---|---|---|
| 一致性 | 强(只有一个真相源) | 弱(两个源可能冲突) |
| 实现复杂度 | 低 | 高(需要冲突解决) |
| 灵活性 | 低(外部修改会被覆盖) | 高(外部修改会被同步) |
| 适用场景 | 代理接管等强控制场景 | 用户手动编辑场景 |
实践建议:在代理接管期间使用 SSOT(单向投影),平时使用双向同步(回填)。两种模式可以动态切换。
替代方案:直接操作 Live 文件
最简方案:不维护数据库,直接读写 Live 配置文件。
局限性:
- 无法做多版本管理和快速切换
- 无法做复杂的查询(如”哪些供应商用同一个 API Key”)
- 无法做云端同步(需要同步整个文件而非增量变更)
对于只管理 1-2 个供应商的场景可行,但对于 50+ 供应商 + MCP + Skills 的统一管理,数据库是必要的。
参考来源
- Single Source of Truth 模式. https://en.wikipedia.org/wiki/Single_source_of_truth
- 原子文件操作:POSIX rename() 语义保证. https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html