SSOT 双写架构与崩溃恢复机制
SSOT 双写架构与崩溃恢复机制
学习目标
- 理解 CC Switch 中 SQLite 到 Live 配置的双向同步流程
- 掌握 Live 配置接管的四步流程与标志管理
- 了解崩溃恢复的三层兜底策略
- 识别双写不一致的风险与防护机制
前置知识
- SSOT 双写架构与崩溃恢复:SSOT 模式的通用原理与双写策略
下文聚焦 CC Switch 的具体代码实现。
项目实践
数据库架构
CC Switch 使用 SQLite 作为唯一数据源(SSOT),数据库包含以下核心表:
| 表 | 用途 |
|---|---|
providers | 供应商配置(ID、名称、分类、settings JSON) |
mcp_servers | MCP 服务器配置 |
prompts | 提示词模板 |
skills | Skills 安装记录 |
live_backups | Live 配置备份(接管前存储原始内容) |
proxy_config | 代理配置(地址、端口、接管标志) |
failover_queue | 故障转移队列 |
stream_check_logs | 流式检测日志 |
settings | 通用键值存储 |
Schema 版本:当前为 10 版,每次修改表结构时递增,升级时自动执行增量迁移。
启动导入流程
CC Switch 启动时的配置导入逻辑在 src-tauri/src/lib.rs 的 setup 回调中:
启动流程: 1. 初始化 SQLite 数据库(创建表 + Schema 迁移) 2. 如果数据库为空: 2.1 从 Live 配置导入默认供应商(import_default_config) 2.2 种子官方预设供应商(init_default_official_providers) 2.3 从 Live 导入 MCP 服务器(表空时触发) 2.4 从 Live 导入提示词(表空时触发) 2.5 初始化默认 Skills 仓库 3. OpenCode / OpenClaw: 每次启动都从 Live 同步(幂等) 4. 后台异步: Codex 历史迁移、健康状态同步设计意图:
- 对于 Claude/Codex/Gemini 等”覆盖模式”应用,只在首次导入(数据库非空时跳过),避免覆盖用户后续在 CC Switch 中的修改
- 对于 OpenCode/OpenClaw 等”追加模式”应用,每次启动都同步(幂等操作,按 ID 去重),因为这些应用的配置格式支持多供应商共存
双向同步:正向写入
当用户切换供应商时:
switch_provider(provider_id) { if proxy_active: hot_switch_provider() // 代理模式:热切换 else: 1. 更新数据库 current_provider 2. 从数据库读取供应商完整配置 3. 合并 common_config(共享配置片段) 4. 原子写入 Live 文件}Common Config 合并:CC Switch 支持”共享配置片段”——用户在供应商之间共享的数据(如插件配置)。写入 Live 时,自动将共享配置合并到供应商的专属配置中。
双向同步:反向回填
当活跃供应商的 Live 配置被外部修改时(如用户手动编辑了 settings.json):
read_live_provider_settings(app_type) { 1. 读取 Live 文件 2. 解析为结构化 JSON 3. 提取可同步的字段(API Key、Base URL、环境变量) 4. 跳过不可同步的字段(供应商 ID、名称、分类) 5. 更新数据库中当前供应商的配置}回填保护:回填时不覆盖数据库中的 id、name、category 等关键字段,只同步配置值。
Live 接管四步流程
代理启动接管(start_with_takeover):
步骤 1: backup_live_configs() → 读取 Claude/Codex/Gemini 的 Live 文件 → 序列化后存储到 live_backups 表
步骤 2: sync_live_to_providers() → 从 Live 文件提取 Token → 同步到数据库中当前供应商的 settings_config
步骤 3: set_live_takeover_active(true) → 设置接管标志(在写入接管配置之前!) → 即使后续步骤失败,下次启动也能检测到残留
步骤 4: takeover_live_configs() → 写入代理地址到各应用的 Live 文件 → 写入 PROXY_MANAGED 占位符替换真实 Token → 启动 axum 代理服务器标志先于数据写入是关键设计——即使接管写入中途崩溃,下次启动时 live_takeover_active = true 标志仍然存在,触发自动恢复。
崩溃恢复:三层兜底
应用在 lib.rs 的启动后台任务中检测并恢复:
Layer 1: 从 live_backups 表恢复 → 查询该应用的备份 → 解析原始配置 → 原子写入 Live 文件 → 成功则返回
Layer 2: 从 SSOT 重建 → 读取数据库中当前供应商的配置 → write_live_with_common_config() 写回 Live → 成功则返回
Layer 3: 清理占位符 → 移除 Live 中的代理地址 → 移除 PROXY_MANAGED Token → 记录日志,等待用户修复退出清理
应用退出前(cleanup_before_exit):
1. 检测接管残留(备份存在 或 Live 含占位符)2. 如有残留 → restore_live_configs() 恢复3. 清除 live_takeover_active 标志4. 删除所有 Live 备份5. 保留 proxy_config.enabled 状态(下次启动自动恢复代理)关键决策:使用 stop_with_restore_keep_state 而非 stop_with_restore——前者保留 proxy_config.enabled 状态,后者清除。这意味着如果用户上次使用了代理,正常关闭后下次打开应用时代理会自动恢复。
问题与规避
陷阱 1:数据库迁移失败导致应用无法启动
问题:Schema 升级过程中如果数据库损坏或版本不匹配,应用可能无法启动。
CC Switch 的解决方案:
- 迁移前自动创建预迁移备份(
backup_database_file()) - 迁移失败时弹出系统对话框,提供重试/退出选项
- 对话框内容根据系统 locale 自动切换中英文
- 数据库文件不会被自动删除,用户可以手动回退
陷阱 2:JSON 配置迁移到 SQLite 时解析失败
问题:旧版 CC Switch 使用 config.json 存储配置,迁移到 SQLite 时如果 JSON 格式错误,迁移会失败。
CC Switch 的解决方案:
- 在创建数据库之前先验证 JSON 是否可以加载
- 加载失败时弹出对话框让用户选择重试/退出
- 如果用户选择退出,数据库文件尚未创建,下次启动可以重试
- 迁移成功后归档旧 JSON 文件(rename 为
config.json.migrated)而非删除
陷阱 3:回填时 Live 文件格式异常
问题:用户手动编辑 Live 文件可能导致格式错误(如非 JSON 对象根节点)。
CC Switch 的解决方案:
read_claude_live()等方法在读取后验证根节点类型- 如果根节点不是对象,返回明确错误信息(包含当前类型和文件路径)
- 调用方捕获错误后跳过回填,不覆盖数据库中的有效配置
设计取舍
SQLite vs JSON 文件存储
| 维度 | SQLite | JSON 文件 |
|---|---|---|
| 查询能力 | 复杂查询(按分类筛选、统计) | 需全量加载后内存过滤 |
| 并发安全 | Mutex 保护,线程安全 | 需要文件锁 |
| 迁移能力 | 版本化 Schema,增量升级 | 手动迁移 |
| 备份 | SQL 导出 + 快照 | 直接复制文件 |
| 可编辑性 | 需要工具 | 文本编辑器直接编辑 |
CC Switch 从 JSON 迁移到 SQLite 的原因:管理 50+ 供应商时,JSON 的查询和并发处理能力成为瓶颈,且无法支持用量统计、健康检测等需要聚合查询的功能。
参考来源
- CC Switch 源码:
src-tauri/src/lib.rs(启动流程) - CC Switch 源码:
src-tauri/src/services/proxy.rs(接管/恢复逻辑) - CC Switch 源码:
src-tauri/src/database/schema.rs(Schema 定义)