3.4 数据同步:分片间对账

最终一致方案的诚实之处在于:它承认中间会有不一致,然后配一套对账系统把不一致找出来修掉。没有对账的最终一致是自欺欺人——不一致在数据库里安安静静躺着,直到客诉爆发。本节讲我们的对账系统怎么设计、怎么跑、发现差异后怎么修。

场景:三类必须对账的数据

分库系统里需要周期性核对的数据有三类。第一类是事务上下游:订单与库存,订单创建后库存是否按量扣减。第二类是路由关系:二级路由索引表与订单主表,索引表里没有的订单就是「孤儿索引」。第三类是跨系统计数:订单总量与积分系统发放量。

对账系统在分库项目里的地位很像「不动产登记」:业务系统天天交易,对账系统负责保证账实相符。它不能阻止错误发生,但能保证错误不积累、不藏匿。我们从上线第一天就把对账排在迭代计划里,而不是等出事再补——等到出事再补的系统通常已经积累了三个月的差异,修复成本是指数级的。

对账类型 比对对象 频率 发现的问题
订单对库存 t_order 对库存流水 每小时 漏扣、少扣、多扣
订单对索引 t_order 对 t_user_order_idx 每天 孤儿索引、重复索引
订单对积分 t_order 对积分流水 每天 漏发、迟发
订单对物流 t_order 对物流单 每天 漏发货
金额汇总 订单明细对汇总表 每小时 汇总漂移

方案:五步对账流水线

对账系统的流程固定为五步:取快照、比对、分类、修复、复核。取快照是关键——两个系统在同一刻的状态在不断变化,必须取同一时间窗口的「结算点」数据比对,否则永远比对不平。我们的做法是对账任务先记录「对账时间戳 T」,订单与库存两边都只取 T 之前的数据。

步骤 动作 关键点
1 取快照 按时间窗口拉取两侧数据 时间戳 T 固定,双方只取 T 前数据
2 比对 按 biz_key 做 Map 差集 双向差集:多有与少有
3 分类 差异按类型归档 业务缺失、系统缺失、金额不符
4 修复 补数据或回滚 幂等修复脚本,人工审核大额
5 复核 对修复结果再跑一轮对账 修复不彻底则升级告警

对账的输出物是一张「差异表」,每条差异记录了比对对象、差异数据、差异原因与修复状态。差异表按 shop 之间无关的主键分片存储,只有处理中的差异才保留,已修复的归档。正常运行下每天的差异在个位数,异常期一天几百条——某次消息队列挂掉半小时,积压了 800 条差异,靠修复脚本两小时清完。

差异治理分级

对账发现的差异不能一刀切处理。我们按影响面把差异分成三级,每级配套不同的修复策略与时效要求。分级之后,自动修复只处理低危类,高危类全部走人工审核:

级别 典型差异 处理方式 时效要求
P0 危急 重复扣款、金额错账 人工审核修复 30 分钟内
P1 重要 漏发积分、漏发货 修复脚本补发 2 小时内
P2 常规 索引缺失、汇总漂移 定时任务修复 当天完成
P0 危急(另一种) 差异数量突增 触发链路告警 立即定位

P0 的第二种形态是「差异数量突增」本身:平时个位数的差异突然变成几百条,说明上游链路出问题了,这时候对账的价值不是修数据而是当「烟雾报警器」。我们给差异量配了突变告警,某次消息队列消费延迟就是靠这个告警提前 40 分钟发现的,比业务告警快得多。

配置:比对脚本的核心片段

对账脚本的核心是「按 Key 聚合后做差集」,用内存 Map 跑效率足够。下面是订单对库存的核心逻辑:

// 两侧都拿到 Map<bizKey, 数量>
Map<String, Integer> orderCnt = orderDao.countByWindow(T);
Map<String, Integer> stockCnt = stockDao.countByWindow(T);
// 双向差集
Set<String> onlyOrder = difference(orderCnt.keySet(), stockCnt.keySet());
Set<String> onlyStock = difference(stockCnt.keySet(), orderCnt.keySet());
// 金额不符
List<Diff> amountDiff = filter(orderCnt, e -> !Objects.equals(e.getValue(), stockCnt.get(e.getKey())));

真实脚本里还要处理窗口内部分交付的订单( 10 分钟内的订单允许差异、跳过不报),否则对账永远有正常噪音。窗口容忍是差异治理的重要经验:刚下完单 10 分钟内的订单,库存消息可能还在路上,算正常态。容忍窗口的长度用「消息表最大重试耗时加 1 分钟」倒推,我们的消息重试退避到第 8 次最多 40 分钟,但对账的容忍窗口只设 10 分钟——超过 10 分钟的差异按真差异处理,由修复脚本补上,因为重试到第 8 次的消息八成已经进死信,等没有意义。

踩坑:对账不能修所有问题

对账系统能发现差异,但修复手段有限。最严重的一次:某个分片的订单因双向网络问题写了两份(写入超时重试造成的重复),对账发现后只能保留一条。重复订单的修复脚本首先要判断「哪一条是业务认可的」,这里需要人工介入而不是自动删除——自动删除可能删错。对账系统最大的价值不是自动修复,而是把差异可见化,让人在客诉之前知道。

第二个坑是对账任务自己把库压挂。全量对账最初在白天跑,扫几千万行数据把分片的 IO 打满,影响线上查询。后来全面改成凌晨低峰窗口加增量对账(只比对近一小时变更的 Key)。全量对账降级到每周日凌晨一次。

第三个坑是对账的「自己查自己」。有段时期对账服务读的也是订单分片,分片故障时对账也挂了,差异彻底无人发现。对策是对账服务具备独立只读实例的降级路径,并且对账任务成功率本身有监控——「对账连续 3 次没跑成功」就是最高级告警,比任何业务告警都严重。

操作清单:对账系统从零建设的顺序

步骤 动作 常见错误
1 选比对对象 从金额与库存两类开始 一上来就全量对账
2 定时间窗口 固定时间戳 T 双方各自取当前时间
3 写比对脚本 双向差集加容忍窗口 无噪音过滤
4 差异分级 按影响面分 P0 到 P2 全部自动修复
5 建告警 差异量突变告警 只在积压后才有告警

顺序的核心思想是「先跑通再完善」:第一版对账只对订单与库存,跑顺了再加索引与积分。一个粗糙但每天跑的对账系统,价值远高于一个完美但三个月才上线的系统。

对账系统上线后的头三个月是「校准期」:总会发现各种没想到的正常态差异,比如优惠券实时核销导致的状态延迟、退款在途的金额差异。每发现一类正常态就在容忍清单里加一条规则,差异量会从每天几百条收敛到个位数。耐心收集这些规则比写更聪明的算法更有用。

对账系统的最后一块拼图是「给业务看的数据看板」。把每天的差异数量、修复时效、遗留积压做成日报推送到群里,所有人对系统一致性有统一预期。看不见的一致性没人信任,看得见的一致性才是最终一致方案的竞争力。

小结

对账是最终一致方案的良心:快照固定时间戳、双向差集、差异分类、幂等修复、复核闭环。设计上把「正常态噪音」与「真差异」分开(时间窗口容忍),把「自动修复」与「人工确认」分开(大额与重复类差异必须人工)。对账系统自己的可用性同样重要,它是整套分库体系的最后一道防线,也是最终一致方案能够理直气壮存在的全部理由。这里再给一个排期建议:对账系统上线后的头三个月,每月安排一次「人肉复核日」,把自动修复的差异逐条人工过一遍。我们的对账规则就是在这样一遍遍人工复核中补齐的,半年后误报率从百分之三降到万分之一以下,团队对自动修复的信任度才真正建立起来。

延伸阅读