ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

openGym多设备同步原理:服务器版本号与409冲突合并机制详解

openGym多设备同步原理:服务器版本号与409冲突合并机制详解 openGym多设备同步原理服务器版本号与409冲突合并机制详解【免费下载链接】openGymSelf-hosted gym body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import from FitNotes/Strong/Hevy, passkey login. Your data, your server.项目地址: https://gitcode.com/GitHub_Trending/op/openGymopenGym是一款自托管的健身与体重记录应用支持制定训练计划、记录含超级组与热身的工作量、查看肌肉负荷状态并支持多设备数据同步。当你用手机在健身房记录一组深蹲、用电脑修改训练计划时数据如何不打架、不丢失答案就在 openGym 的服务器版本号_rev乐观锁与409 冲突合并机制里。本文用最小篇幅讲透这套多设备同步方案的完整原理。为什么多设备同步容易丢数据想象一个经典场景你的手机和电脑都读取了同一份档案版本号 rev 7手机先保存成功服务器版本号变为rev 8电脑随后把自己的旧副本推上去——如果服务器直接覆盖手机刚才记录的那次训练就悄无声息地消失了这正是 openGym 在引入版本号之前真实发生过的 Bug一个整天开着的电脑标签页会把手机在期间记录的训练直接冲掉见 sync-merge.js 文件开头的注释。openGym 的解法可以概括为三步服务器发号、条件写入、冲突合并。第一步服务器版本号_rev—— 每个档案的修订号每个用户在服务器上只有一份完整档案JSON 文档里面存着训练记录、计划、体重等所有数据。服务器为这份文档维护一个单调递增的计数器_rev每次成功写入版本号 1并且版本号完全由服务器掌管——客户端上报的值会被忽略body.state._rev curRev 1拉取时GET /api/data返回{ state, rev }把数据和版本号一起交给设备核心代码位于 server.js// rev 是服务器对这份档案写入次数的计数同时存在文档内部的 _rev 里 // 客户端将其作为 baseRev 回传基于从未见过的文档的写入会被拒绝 json(res, 200, { state, state?._rev || 0 });廉价的版本轮询/api/data/rev既然版本号这么重要设备如何低成本地知道服务器变了没openGym 提供了一个只返回一个数字的端点server.jsGET /api/data/rev: async (req, res) { json(res, 200, { rev: readStateCached(user.id)?._rev || 0 }); }设备在页面打开期间每 30 秒轮询一次并在每次回到前台时立即检查。只有当数字变化时才拉取完整文档——解析一份数 MB 的 JSON 只为了读一个数字会浪费 30 毫秒所以这里还叠加了一层 stat 缓存让轮询几乎零成本。第二步条件写入与 409 冲突响应推送到服务器的请求长这样{ state: { ...你的完整档案... }, baseRev: 7 }baseRev表示我这份数据是基于版本号 7 改出来的。服务器执行的是比较并写入compare-and-writeserver.js// 条件写入baseRev 不等于当前版本号说明这个客户端读的是旧文档 // ——另一台设备在期间已经写入过。当前文档随 409 一起返回 // 客户端可以合并后重试不需要第二次请求 const curRev cur?._rev || 0; if (body.baseRev ! null body.baseRev ! curRev) { return json(res, 409, { error: conflict, rev: curRev, state: cur }); }三个设计要点409 Conflict 不是失败而是服务器的一次善意提醒拒绝覆盖同时把当前最新文档整个塞回响应体省掉客户端再发一次 GET 的往返无baseRev的写入视为有意的全量替换如备份导入保持向后兼容比较与写入之间没有任何await是同步原子操作不会出现竞态前端在 api.js 中特意保留了 409 的响应体注释写明The body rides along on the error: a 409 from /api/data carries the servers document.响应体随错误一起抛出来自 /api/data 的 409 携带着服务器的文档。第三步字段级合并 ——mergeStates的精细规则收到 409 后前端不会简单地谁新谁赢而是调用 sync-merge.js 中的mergeStates(a, b)做逐字段的智能合并。核心思想两边的条目都保留并集只有没有天然并集语义的字段才由更新的那份副本决定。各字段的合并规则速览字段类型合并规则标量与设置项取_ts较新的副本训练记录workouts按 id 并集同一 id 保留各自_ts更晚编辑的版本最后按日期与开始时间排序训练照片/视频按 hash 并集——一边加的照片不会因另一边编辑了同一训练而丢失训练计划routines按 id 并集同一 id 保留最后编辑的版本由 stampRoutines 在每次修改时打时间戳体重记录按天并集同一天保留t更晚编辑的条目收藏favEx有序集合并集新副本在前各动作最大重量exWeights普通动作取更大值助力器械取更小值助力器械是越轻越强issue #232 的教训单位kg/lb合并前先把两边换算到同一单位——最先执行的一条规则重置resetAt/resetIds只能向前推进清空一切会记录被清空的条目名单未见过重置的设备会精确剔除这些条目其余数据全部保留两个值得注意的细节单位先行每份档案里的重量都以其unit为准两份不同单位的副本绝不会直接拿数字比较会先把其中一方换算过来。曾经有个 Bug在一台设备上切成 lb 的副本遇到另一台设备的 kg 副本后会变回 kg但底下还是 lb 的数字合并副本的媒体列表mergeWorkoutMedia让保留版本的媒体清单吸纳另一副本中多出的 hash所以一边拍的照片不会因另一边改了备注而消失自动重试设备侧的完整闭环前端 useStore.js 中doPush捕获 409 后自动走合并 → 重推循环最多重试 2 次if (e.status 409 e.data attempt 2) { // 另一台设备在本设备上次读取后写入过。服务器已把当前文档发回来了 // 合并后针对该版本号再推一次 mergeInto(get().S, e.data.state, e.data.rev || 0) return doPush(attempt 1) }mergeInto会记录服务器的版本号writeSync(rev, ts)让紧随其后的重推是精确针对刚合并过的那份文档的条件写入。如果连续两次仍被拒绝说明冲突窗口内又有新写入数据不会丢副本被标记为欠推owed等下次回到前台拉取时从那条路径继续处理。完整循环回到开头的时序图手机推 rev 7 → 服务器升 rev 8笔记本推 rev 7 →409 当前文档笔记本本地合并进 rev 8 → 推 rev 8 → 成功得 rev 9手机轮询发现 rev 9 → 拉取最新。全程没有任何一次数据丢失。已知边界删除的短暂复活sync-merge.js 文件头坦诚地记录了一个已知限制由于没有各端删除了什么的墓碑tombstone记录在冲突窗口内通常只有几秒一端删除的条目可能会从另一端复活回来。openGym 认为这个取舍是对的——一条复活的条目可以一键再删一条丢失的数据则永远没了。而且窗口极短store 在恢复前台时就会拉取且每次推送都是条件写入。小结三行话记住 openGym 多设备同步服务器发号_rev单调递增、仅服务器可写/api/data/rev让轮询便宜到可以每 30 秒一次条件写入baseRev不匹配就返回 409并把最新文档随响应带回精细合并mergeStates按字段做并集/最新编辑获胜单位换算与重置名单是两条前置铁律这套乐观锁 409 携带文档 字段级合并的组合让 openGym 在没有中心化合并服务、没有 WebSocket的极简自托管架构下依然做到了多设备同步的数据零丢失——这也是 SELF_HOSTING.md 中单机部署就能放心多端使用的原因。延伸阅读同步图源码Mermaidsync.mmd合并规则单测sync-merge.test.js服务器端版本机制测试server-data-rev.test.js、server-data-rev-endpoint.test.js【免费下载链接】openGymSelf-hosted gym body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import from FitNotes/Strong/Hevy, passkey login. Your data, your server.项目地址: https://gitcode.com/GitHub_Trending/op/openGym创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进