4.5 KiB
4.5 KiB
BUG: 挖矿分配并发覆盖贡献值同步 (Lost Update)
状态: 待解决 严重程度: 高 - 影响用户挖矿收益 发现日期: 2026-03-03 影响范围: 所有预种用户 (D2602260xxxx),12个账户数据异常
1. 问题描述
用户购买多份预种后,contribution-service 正确记录了累计贡献值,但 mining-service 中只显示第一份的贡献值,导致挖矿收益严重偏低。
示例:
- D26022600018 购买 4 份预种 → contribution 记录 13,117.860 → mining 只显示 3,279.465
- D26022600001 购买 5 份合成 1 棵树 → contribution 记录 16,397.325 → mining 只显示 3,279.465
2. 根因分析
主因:MiningDistributionService 的 save() 覆盖 totalContribution
mining-service 存在两个并发写入路径共享同一个 save() 方法:
路径 A - 贡献值同步(Kafka 事件触发,不定时):
ContributionEventHandler → ContributionSyncService.handleContributionCalculated()
→ account.updateContribution(newValue) // 更新 totalContribution
→ miningAccountRepository.save(account) // upsert 所有字段
路径 B - 挖矿分配(每秒执行):
MiningDistributionService.executeSecondDistribution()
→ findAllWithContribution() // 加载账户到内存(含 totalContribution)
→ account.mine(reward) // 只改 totalMined/availableBalance
→ miningAccountRepository.save(account) // upsert 所有字段(含 stale totalContribution)
竞争时序:
T0 挖矿分配加载 D26022600018, totalContribution=3279.465
T1 贡献值同步更新 D26022600018 → totalContribution=13117.860 ✓ (DB 已更新)
T2 挖矿分配保存 D26022600018, 写回 totalContribution=3279.465 ← 覆盖!
日志证据:
03/01 02:50:30 - Updated contribution for D26022600001: 16397.325 ← 同步成功
03/03 当前 DB - D26022600001 totalContribution = 3279.465 ← 被覆盖回旧值
挖矿分配每秒运行,贡献值同步后几乎必然在下一秒被覆盖。
代码位置:
mining-account.repository.ts:57-62— save() 的 upsert 无条件写入所有字段mining-distribution.service.ts:122-135— 每秒加载→mine()→save()mining-account.aggregate.ts:126-141— mine() 只改余额但 save() 会写回所有字段
次因:DailySnapshot 全量同步完全失效
本应作为安全网的每日全量同步,自上线以来一直 synced 0 accounts:
03/01 01:00 - DailySnapshotCreated for 2026-02-27 → synced 0 accounts
03/02 01:00 - DailySnapshotCreated for 2026-02-28 → synced 0 accounts
03/03 01:00 - DailySnapshotCreated for 2026-03-01 → synced 0 accounts
原因:
- contribution-service 发布的事件 payload 缺少
snapshotId字段 → mining-service 读到 undefined - 字段名不匹配:contribution 发
networkTotalContribution,mining 读totalContribution fetchContributionRatios()返回数据解析失败(result.data路径不对)
代码位置:
contribution-service/snapshot.service.ts:99-110— payload 缺少 snapshotIdmining-service/contribution-sync.service.ts:63-68— 期望 snapshotIdmining-service/contribution-sync.service.ts:126-141— API 调用和解析
3. 受影响用户
| 用户 | contribution 正确值 | mining 当前值 | 差额 |
|---|---|---|---|
| D26022600001 | 16,397.325 | 3,279.465 | -13,117.860 |
| D26022600014 | 13,253.562 | 3,279.465 | -9,974.097 |
| D26022600015 | 6,649.398 | 3,279.465 | -3,369.933 |
| D26022600016 | 13,117.860 | 3,279.465 | -9,838.395 |
| D26022600018 | 13,117.860 | 3,279.465 | -9,838.395 |
| D26022600033 | 3,754.422 | 3,279.465 | -474.957 |
| D26022600034 | 6,988.653 | 3,279.465 | -3,709.188 |
| D26022600035 | 3,799.656 | 3,392.550 | -407.106 |
| D26022600036 | 17,211.537 | 3,279.465 | -13,932.072 |
| D26022600037 | 16,397.325 | 3,279.465 | -13,117.860 |
| D26022600038 | 19,676.790 | 3,279.465 | -16,397.325 |
| D26022600043 | 6,558.930 | 3,279.465 | -3,279.465 |
D26022600000 是唯一正常的预种用户。旧用户 (D2512xxxx) 不受影响。
4. 为什么旧用户不受影响
旧用户在系统稳定后完成了一次性同步,之后没有新的贡献值变更事件。 挖矿分配虽然每秒覆写 totalContribution,但写回的值恰好等于正确值(没有新同步来改变它)。
预种用户是分批多次产生贡献值的(先 1 份 → 2 份 → ... → N 份), 所以挖矿分配总是把后续累加的正确值覆盖回最初同步的旧值。