分布式 SVN 代码库同步系统设计方案

7 阅读8分钟

本方案针对跨地域研发场景,基于 CP 原则构建了强一致性同步体系。核心采用“意向锁定”机制,通过数据库原子操作确保全局唯一提交锁(G-Lock),解决并发提交冲突。

1. 物理拓扑图

架构组件说明

1. 研发中心节点(Regional R&D Centers)

  • Local SVN Instance: 存储本地代码镜像,通过 pre-commitpost-commit 钩子脚本触发同步逻辑。
  • Sync Agent (同步代理) :
    • 通信职责:作为客户端通过 HTTPS (RESTful API) 与中央仲裁服务交互。
    • 协议选型:推荐使用 RESTful API,其在跨地域、高延迟网络(200ms+)下具有更好的兼容性,且易于穿透企业级防火墙。
    • 版本控制:执行 svnlook youngest 获取本地物理版本,并执行 svnrdump 进行增量数据的导出与导入。

2. 网络安全边界(Firewall / DMZ)

  • 网络隔离带:各研发中心与中央仲裁中心之间存在严格的网络隔离。
  • 加密传输 (TLS) :所有跨地域流量必须经过 TLS 1.2/1.3 加密,确保代码增量包在公网或专线传输过程中的安全性。
  • 策略配置:防火墙仅放行 Agent 所在 IP 段对中央仲裁服务 443 端口的访问。

3. 中央仲裁中心(Central Arbitration Center)

  • Metadata DB (MySQL/PostgreSQL)
    • 存储全局版本号 group_revision、意向版本 intent_revision 及分布式锁信息。
    • 记录 sync_task 同步任务列表及各节点的版本存证 current_revision
交互动作发起方接收方协议主要数据
请求锁 (Pre-commit)Agent仲裁服务REST/HTTPSnode_id, repo_id, local_rev
确认提交 (Post-commit)Agent仲裁服务REST/HTTPSnew_rev, task_trigger=true
数据同步 (Sync)从节点 Agent主节点 AgentSVN/HTTPSsvnrdump流式增量数据
状态巡检 (Anti-Entropy)Agent仲裁服务REST/HTTPSheartbeat, svnlook_rev

2. 核心实体设计

2.1 SVN 仓库节点 (svn_repo)

属性类型说明
repo_idBIGINT全局唯一 ID,原子生成。
group_idBIGINT所属分布式群组 ID。
repo_nameVARCHAR节点名称。
repo_urlVARCHAR本地 SVN 访问地址。
current_revisionBIGINT节点实际达到的版本号(用于追赶对齐)。
repo_statusENUMONLINE / OFFLINE。
agent_addrVARCHAR同步代理地址 (IP:Port),执行 svnrdump任务。

2.2 分布式群组 (svn_group)

属性类型说明
group_idBIGINT全局唯一群组 ID。
group_revisionBIGINT已确认的全局最高版本号 (Source of Truth)。
intent_revisionBIGINT预分配版本号pre-commit时写入,用于解决断电冲突。
lock_statusENUM0(无占用), 1(锁定中,存在未确认意向)。
lock_holder_idBIGINT当前持有提交锁的节点 ID。
lock_expire_atDATETIME租约到期时间,用于超时自动处理。
statusENUM0-READY, 1-BROKEN(需要人工介入)。

2.3 同步任务 (sync_task)

属性类型说明
task_idBIGINT任务唯一标识。
target_repo_idBIGINT待同步的目标节点 ID。
rev_rangeVARCHAR版本区间(如 r100:r101)。
statusENUM0-CREATED, 1-RUNNING, 2-SUCCESS, 3-FAILED。

2.4 数据库建表 SQL

CREATE TABLE `svn_group` (
  `group_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `group_name` VARCHAR(100) NOT NULL,
  `group_revision` BIGINT DEFAULT 0,
  `intent_revision` BIGINT DEFAULT NULL COMMENT '预分配待确认的版本号',
  `lock_status` ENUM('CLEAN', 'DIRTY') DEFAULT 'CLEAN',
  `lock_holder_id` BIGINT DEFAULT NULL,
  `lock_expire_at` DATETIME(3) DEFAULT NULL,
  `status` ENUM('READY', 'SYNCING', 'BROKEN') DEFAULT 'READY',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE `svn_repo` (
  `repo_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `group_id` BIGINT NOT NULL,
  `repo_name` VARCHAR(100) NOT NULL,
  `repo_url` VARCHAR(255) NOT NULL,
  `current_revision` BIGINT DEFAULT 0,
  `repo_status` ENUM('ONLINE', 'OFFLINE') DEFAULT 'ONLINE',
  `agent_addr` VARCHAR(50),
  FOREIGN KEY (`group_id`) REFERENCES `svn_group`(`group_id`)
) ENGINE=InnoDB;

CREATE TABLE `sync_task` (
  `task_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `target_repo_id` BIGINT NOT NULL,
  `from_rev` BIGINT NOT NULL,
  `to_rev` BIGINT NOT NULL,
  `status` ENUM('CREATED', 'RUNNING', 'SUCCESS', 'FAILED') DEFAULT 'CREATED',
  `retry_count` INT DEFAULT 0,
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

3. 核心流程与极端场景自愈设计

状态迁移和决策矩阵

场景本地状态 (svnlook)数据库状态 (group_rev / lock_status)持有者 (lock_holder)Agent 决策动作
1. 正常提交前NNNN / CLEAN抢占锁:将状态改为 DIRTY,记录 intent_rev=N+1
2. 正常提交后N+1N+1NN / DIRTY自己释放锁:推进 group_rev=N+1,状态改为 CLEAN
3. 正常同步后NNN+1N+1 / CLEAN追赶:执行 svnrdump load,将本地更新至 N+1N+1
4. 提交中途断电N+1N+1NN / DIRTY自己自愈确认:重启后发现 svnlook已达意向版本,强制上报确认
5. 抢锁时冲突NNNN / DIRTY他人等待/报错:当前有其他 Master 正在操作,禁止本地提交。
6. 抢锁时落后NNN+5N+5 / CLEAN拒绝:提示“请先执行 svn update”,本地版本已落后。

自身 NN == 群组 NN,但锁被占用的三种情况:

1. 毫秒级的并发竞争 (Race Condition)

这是最常见的正常业务场景。

  • 过程
    1. 节点 A 和 节点 B 几乎同时发起了 svn commit
    2. 此时数据库中 group_revision100100lock_statusCLEAN
    3. 节点 A 的请求早到了 1 毫秒,成功执行了 UPDATE 语句,将锁抢占,此时 lock_status 变为 DIRTYlock_holder 变为节点 A。
    4. 紧接着 1 毫秒后,节点 B 的请求到达。此时节点 B 本地的版本确实还是 100100(与群组一致),但它看到的锁已经是 DIRTY 了。
  • 结论:这说明别人抢先了一步,你必须等待。

2. 占坑成功但尚未物理写入 (The Gap)

在分布式 SVN 流程中,从“抢锁成功”到“物理写入成功”之间存在一个时间差。

  • 过程
    1. 节点 A 已经抢锁成功(lock_status=DIRTY),此时群组版本还是 100100
    2. 节点 A 正在执行本地的 svn commit 磁盘写入(可能需要 2-5 秒)。
    3. 此时你(节点 B)来查询。你会发现群组版本确实是 100100,和你一样,但节点 A 已经把坑占住了,正在准备变成 101101
  • 结论:这种状态是**意向锁定(Intent-to-Commit)**的正常表现,保证了全局不会有两个节点同时尝试生成 101101

3. “僵尸锁”残留 (Zombie Lock)

这属于异常兜底场景。

  • 过程
    1. 节点 A 抢锁成功,准备提交 101101
    2. 突然:节点 A 还没来得及写 SVN 磁盘,整个服务器就直接断电宕机了。
    3. 此时,数据库里留下的记录是:group_rev=100, lock_status=DIRTY, lock_holder=Node_A
    4. 由于节点 A 根本没写成功,它的本地版本和群组版本依然都是 100100
  • 结论:如果你在节点 A 挂掉期间来抢锁,就会遇到这种情况。此时需要依赖 lock_expire_at (租约过期) 机制来清理这个僵尸锁。
判定结果处理建议
lock_expire_at未过期排队等待。说明 Master 正在忙碌,建议每隔 1-2 秒重试一次。
lock_expire_at已过期触发自愈/报警。说明 Master 可能挂了。如果是你本人,则尝试重置;如果是别人,则提示管理员介入或由系统置为 BROKEN

3.1 意向锁定提交流程 (Standard Workflow)

  1. Pre-commit (准备阶段) :
    • 节点 A 向仲裁中心请求锁。
    • 仲裁中心执行原子操作

SQL

UPDATE svn_group SET 
    lock_status = 'DIRTY', 
    lock_holder_id = Node_A_ID, 
    intent_revision = group_revision + 1,
    lock_expire_at = NOW() + INTERVAL 30 SECOND
WHERE lock_status = 'CLEAN' AND group_revision = Node_A_Local_Rev;
    • 只有上述更新成功,仲裁中心才返回“允许提交”。
  1. Local Commit (执行阶段) :
    • 节点 A 在本地 SVN 库执行真实写入。
  1. Post-commit (确认阶段) :
    • 节点 A 成功后调用确认接口。
    • 仲裁中心执行事务更新
      1. group_revision 更新为 intent_revision
      2. lock_status 重置为 CLEAN,释放锁。
      3. 生成其他节点的 sync_task 记录。

3.2 针对“主节点提交成功后断电”的兜底设计

  • 现象:节点 A 本地 SVN 版本已变 N+1N+1,但数据库 group_revision 仍为 NNlock_statusDIRTY
  • 自愈逻辑
    1. 锁竞争拦截:当节点 B 尝试申请锁时,仲裁中心发现 lock_status == 'DIRTY'
    2. 状态探测:仲裁中心检查 lock_expire_at。若已过期,尝试连接节点 A 的 Agent。
      • 若 Node A 在线且已更新:Agent 上报本地版本为 N+1N+1,仲裁中心自动补填 group_revisionN+1N+1,释放脏锁。
      • 若 Node A 离线:系统将群组状态设为 BROKEN 并封锁所有提交。
    1. 强一致性保障(CP)禁止在脏锁未清除的情况下由其他节点产生新的版本。这确保了绝不会出现两个节点同时拥有不同 N+1N+1 版本的情况。

3.3 弱网环境下的同步补偿

  • 指数退避重试sync_task 失败后按 1min, 5min, 15min 间隔重试。
  • 版本拉取对齐:每个节点的 Agent 定时检查 local_revision 与 DB 的 group_revision。若落后,主动通过 svnrdump load 从 master 节点拉取增量包,不依赖 post-commit 的单次触发。

4. 设计总结

本方案通过将 SVN 的外部操作耦合到数据库的“意向状态”中,解决了分布式系统中“外部执行结果”与“中心元数据”状态不一致的问题。

  • 断电保护intent_revision 记录了正在进行的动作,重启后可对齐。
  • 弱网稳定性sync_task 的持久化保证了只要网络恢复,增量包终将送达。
  • 绝对一致性DIRTY 锁状态牺牲了节点 A 宕机期间的可用性(Availability),换取了安防行业最看重的代码数据绝对强一致性(Consistency)。