bigywater.tech
分布式事务 · 31

跨服务的数据一致性校验

跨服务的数据校验怎么做才又快又不漏?比总数够吗?全量比较要传多少数据?分桶校验和怎么省?增量、抽样各有什么坑?这篇用两个各有 100 万行的 MySQL,把这几种校验方法的发现能力和代价量出来。

1.67 s vs 3.6 s
分桶校验和与全量比较发现全部 50 处差异所用的时间(传输量 1095 KiB 对 31.4 MiB)
33.7%
增量校验窗口不重叠时,300 个改动里被漏掉的比例(重叠 1 秒是 0)
32%
从 100 万行里抽 1% 做校验,发现 35 处差异中至少一处的概率

总数校验的盲区

实测 · 本机执行输出
  A:count = 1000000, sum(amount) = 587881504;B:count = 1000000, sum(amount) = 587881504 —— count 和 sum 都一样,汇总校验认为“一致”
  逐行哈希再异或(bit_xor(crc32(id|amount|status))):A = 2763208693,B = 3801738515 —— 不同,发现了

count 和 sum 对一加一减、只改状态这类错误完全看不出。

分桶校验和

实测 · 本机执行输出
  做法                          | 用时                             | 传输的数据量                             | 报出的差异行 | 其中真差异 | 漏报
  ----------------------------+--------------------------------+------------------------------------+--------+-------+---
  全量比较(两边所有行拉出来)              | 3.6 s                          | 31.4 MiB                           | 50     | 50    | 0 
  分桶校验和(每 1000 行一个桶)+ 只拉有差异的桶 | 1.67 s(校验和 1.01 s + 下钻 0.66 s) | 1095 KiB(校验和 38 KiB + 下钻 1057 KiB) | 50     | 50    | 0 
  有差异的桶 34 个(共 1002 个桶);注入的差异 50 处。校验和在数据库内部算,只有每个桶的 (桶号, count, 哈希) 离开数据库

在库里按桶算 count 与行哈希,只传校验和,只下钻有差异的桶:用时和传输量都小得多,发现率不降。

哪种汇总能发现什么

实测 · 本机执行输出
  差异类型(在 id 1~20000 的范围内) | count   | sum(amount) | 行哈希 bit_xor | 行哈希 sum | 按 id 排序的行哈希串再 SHA-256
  ------------------------+---------+-------------+-------------+---------+----------------------
  金额 +1                   | 0 / 60  | 60 / 60     | 60 / 60     | 60 / 60 | 60 / 60              
  少一行                     | 60 / 60 | 60 / 60     | 60 / 60     | 60 / 60 | 60 / 60              
  一加一减(抵消)                | 0 / 60  | 0 / 60      | 58 / 60     | 60 / 60 | 60 / 60              
  状态被改(金额不变)              | 0 / 60  | 0 / 60      | 60 / 60     | 60 / 60 | 60 / 60              
  count 只能发现行数变化;sum(amount) 发现不了“一加一减”和“只改状态”;行哈希 bit_xor 在“一加一减”上漏了 2 / 60 次(异或把两行的哈希变化互相抵消了;我的解释是 CRC32 是线性的,两处改动的比特差异模式相同时哈希差相同,这个解释没有单独验证);行哈希 sum 和按 id 排序的 SHA-256 链在这四类上都没有漏,代价是 SHA-256 链要排序、拼接,算起来最重

count、sum 管不了的情形,行哈希能管;bit_xor 偶尔漏(CRC 线性),sum 或 SHA-256 链更稳。

增量校验

实测 · 本机执行输出
  增量窗口            | 提交的改动 | 一直没被检查到的改动 | 漏报率  
  ----------------+-------+------------+------
  不重叠(只看上次检查开始之后) | 300   | 101        | 33.7%
  窗口往前重叠 100 ms   | 300   | 40         | 13.3%
  窗口往前重叠 300 ms   | 300   | 9          | 3.0% 
  窗口往前重叠 1000 ms  | 300   | 0          | 0.0% 
  不重叠时,提交得比较晚的事务(持有超过一个检查周期的)会在检查器两次窗口之间漏掉;窗口往前重叠一段(要大于最长事务的持有时间)可以补上。更稳的做法是用提交顺序单调的位点(binlog 位点 / LSN / 自增版本号),而不是 updated_at

updated_at 不是提交时刻,事务提交晚的行会漏;窗口要重叠,或者用提交顺序单调的位点。

抽样校验

实测 · 本机执行输出
  抽样行数   | 占总行数  | 抽到至少一处差异的试验数 | 实测发现概率 | 理论值(35 处差异)
  -------+-------+--------------+--------+------------
  1000   | 0.1%  | 14 / 300     | 5%     | 3%         
  10000  | 1.0%  | 96 / 300     | 32%    | 30%        
  100000 | 10.0% | 290 / 300    | 97%    | 97%        
  500000 | 50.0% | 300 / 300    | 100%   | 100%       
  差异只有总量的 0.0035% ~ 0.005% 时,抽样要抽到相当大的比例才有把握;而且按 A 的 id 抽样根本看不到 B 里“多出来”的行(那 15 处)。罕见的错误靠抽样找不出来

罕见错误要抽到很大比例才有把握,还看不到另一边多出来的行。

分步演示

数据一致性校验
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用两个独立的 MySQL 26.7.0 实例(各 100 万行的 orders 表);行哈希是 crc32(concat_ws("|", id, amount, status)),分桶 = 每 1000 个 id 一个桶,校验和在库内用 GROUP BY 算;第三节的“造差异”是对 id 1 到 20000 这一段的行改一次、测一次、还原一次;第四节的检查器与 300 个事务是我写的线程,不是真实业务;抽样用的是均匀随机抽 id
    • 没有验证:两个库跨网络时传输时间(这里在同一台机器上,只统计字节数);校验时源库仍在写入的快照一致性(本篇校验期间没有写入);更大的表(亿级)上校验和的计算成本与对主库的影响;Merkle 树多层下钻的最优桶大小(只试了 1000);bit_xor 漏报的原因(CRC 线性)只是解释,没有单独验证;没有主键的明细表里重复行互相抵消的情形(带主键的表里不会出现)
    京ICP备2026067502号-1京公网安备11010502063487号