跳转到内容

SSOT 双写架构与崩溃恢复

SSOT 双写架构与崩溃恢复

前置知识

需要了解 SQLite 数据库基本概念和文件系统操作(临时文件、rename 原子操作)。


学习目标

  • 理解 SSOT 模式在客户端配置管理中的应用
  • 掌握”数据库 → Live 配置”的双写同步机制
  • 了解 Live 配置接管与崩溃恢复的完整流程
  • 识别双写架构中的数据一致性风险与防护策略

项目实践

核心问题

在多应用配置管理场景中,存在两种数据源:

  1. 内部数据库(SQLite):应用自己维护的配置存储
  2. Live 配置文件(外部 JSON/TOML/.env):目标应用(如 Claude Code)实际读取的配置文件

问题是:这两个数据源如何保持一致?如果只写数据库,外部应用读不到;如果只写文件,应用自身无法做版本管理和查询。

SSOT 原则

SSOT(Single Source of Truth) 规定:数据库是唯一的真实数据源。Live 配置文件只是数据库的”投影”(Projection)——数据库可以随时将任意配置写入 Live 文件,但反过来不成立(不应直接编辑 Live 文件然后期望数据库自动同步)。

双写同步机制

正向写入(数据库 → Live)

当用户在应用中切换供应商时:

1. 用户选择供应商 B
2. 更新数据库: 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、断电)后,下次启动时:

恢复的核心逻辑

  1. 检查数据库中是否存在 Live 备份(有备份 = 上次可能是代理模式退出)
  2. 检查 Live 文件是否包含占位符(PROXY_MANAGED = 尚未恢复)
  3. 如果两者都满足,从备份恢复原始 Live 配置
  4. 清除接管标志,删除备份

三层兜底恢复

如果备份缺失但 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 的统一管理,数据库是必要的。


参考来源