redis-full-check是阿里云Redis&MongoDB团队开源的用于校验2个redis数据是否一致的工具,通常用于redis数据迁移(redis-shake)后正确性的校验。

基本原理

下图给出了最基本的比较逻辑。

redis-full-check通过全量对比源端和目的端的redis中的数据的方式来进行数据校验,其比较方式通过多轮次比较:每次都会抓取源和目的端的数据进行差异化比较,记录不一致的数据进入下轮对比(记录在sqlite3 db中)。然后通过多伦比较不断收敛,减少因数据增量同步导致的源库和目的库的数据不一致。最后sqlite中存在的数据就是最终的差异结果。
  redis-full-check对比的方向是单向:抓取源库A的数据,然后检测是否位于B中,反向不会检测,也就是说,它检测的是源库是否是目的库的子集。如果希望对比双向,则需要对比2次,第一次以A为源库,B为目的库,第二次以B为源库,A为目的库。
  下图是基本的数据流图,redis-full-check内部分为多轮比较,也就是黄色框所指示的部分。每次比较,会先抓取比较的key,第一轮是从源库中进行抓取,后面轮次是从sqlite3 db中进行抓取;抓取key之后是分别抓取key对应的fieldvalue进行对比,然后将存在差异的部分存入sqlite3 db中,用于下次比较。

不一致类型

redis-full-check判断不一致的方式主要分为2类:key不一致和value不一致。

key不一致

  key不一致主要分为以下几种情况:

  • lack_target : key存在于源库,但不存在于目的库。
  • type: key存在于源库和目的库,但是类型不一致。
  • value: key存在于源库和目的库,且类型一致,但是value不一致。

value不一致

  不同数据类型有不同的对比标准:

  • string: value不同。
  • hash: 存在field,满足下面3个条件之一:

    field存在于源端,但不存在与目的端。
    field存在于目的端,但不存在与源端。
    field同时存在于源和目的端,但是value不同。

  • set/zset:与hash类似。
  • list: 与hash类似。

field冲突类型有以下几种情况(只存在于hashsetzsetlist类型key中):

  • lack_source: field存在于源端keyfield不存在与目的端key
  • lack_target: field不存在与源端keyfield存在于目的端key
  • value: field存在于源端key和目的端key,但是field对应的value不同

比较原理

  对比模式(comparemode)有三种可选:

  • KeyOutline:只对比key值是否相等。
  • ValueOutline:只对比value值的长度是否相等。
  • FullValue:对比key值、value长度、value值是否相等。
      
    对比会进行comparetimes轮(默认comparetimes=3)比较:

  • 第一轮,首先找出在源库上所有的key,然后分别从源库和目的库抓取进行比较。

  • 第二轮开始迭代比较,只比较上一轮结束后仍然不一致的keyfield

    对于key不一致的情况,包括lack_sourcelack_targettype,从源库和目的库重新取key、value进行比较。
    value不一致的string,重新比较key:从源和目的取keyvalue比较。
    value不一致的hashset和zset,只重新比较不一致的field,之前已经比较且相同的filed不再比较。这是为了防止对于大key情况下,如果更新频繁,将会导致校验永远不通过的情况。
    value不一致的list,重新比较key:从源和目的取keyvalue比较。

  • 每轮之间会停止一定的时间(Interval)。
       对于hashsetzsetlistkey处理采用以下方式:

  • len <= 5192,直接取全量fieldvalue进行比较,使用如下命令:hgetallsmemberszrange 0 -1 withscoreslrange 0 -1

  • len > 5192,使用hscansscanzscanlrange分批取fieldvalue

参数说明

redis-full-check中主要参数如下:

-s, --source=SOURCE               源redis库地址(ip:port)
  -p, --sourcepassword=Password     源redis库密码
      --sourceauthtype=AUTH-TYPE    源库管理权限,开源reids下此参数无用。
  -t, --target=TARGET               目的redis库地址(ip:port)
  -a, --targetpassword=Password     目的redis库密码
      --targetauthtype=AUTH-TYPE    目的库管理权限,开源reids下此参数无用。
  -d, --db=Sqlite3-DB-FILE          对于差异的key存储的sqlite3 db的位置,默认result.db
      --comparetimes=COUNT          比较轮数
  -m, --comparemode=                比较模式
      --id=                         用于打metric
      --jobid=                      用于打metric
      --taskid=                     用于打metric
  -q, --qps=                        qps限速阈值
      --interval=Second             每轮之间的时间间隔
      --batchcount=COUNT            批量聚合的数量
      --parallel=COUNT              比较的并发协程数,默认5
      --log=FILE                    log文件
      --result=FILE                 不一致结果记录到result文件中,格式:'db    diff-type    key    field'
      --metric=FILE                 metric文件
  -v, --version

例如:源redis库是10.1.1.1:1234,目的库是10.2.2.2:5678

./redis-full-check -s 10.1.1.1:1234 -t 10.2.2.2:5678 -p mock_source_password -a mock_target_password --metric metric --log log --result result`

metric信息格式如下:

type Metric struct {
    DateTime     string       `json:"datetime"`      // 时间 格式: 2018-01-09T15:30:03Z
    Timestamp    int64        `json:"timestamp"`     // unix秒级时间戳
    Id           string       `json:"id"`            // run id
    CompareTimes int          `json:"comparetimes"`  // 对比轮次
    Db           int32        `json:"db"`            // db id
    DbKeys       int64        `json:"dbkeys"`        // db里的总key数
    Process      int64        `json:"process"`       // 进度, 百分比
    OneCompareFinished bool                               `json:"has_finished"` // 本次compare是否完成
    AllFinished        bool                               `json:"all_finished"` // 全部compare是否完成
    KeyScan      *CounterStat `json:"key_scan"`      // scan key的数量
    TotalConflict      int64  `json:"total_conflict"` // conflict的总数, 包含key + field
    TotalKeyConflict   int64                              `json:"total_key_conflict"`  // key conflict总数
    TotalFieldConflict int64                              `json:"total_field_conflict"` // field conflict总数
    // 以下两个map第一层key是字段类型, 有string, hash, list, set, zset, 第二层key是冲突类型, 有有type(类型冲突) / value(值冲突) / lack source(源端缺失) / lack target(目标端缺失) / equal(无冲突)
    KeyMetric    map[string]map[string]*CounterStat `json:"key_stat"`  // key metric
    FieldMetric  map[string]map[string]*CounterStat `json:"field_stat"`  // field metric
}

type CounterStat struct {
    Total int64 `json:"total"` // 总量
    Speed int64 `json:"speed"` // 速度
}

sqlite 3 db文件

  结果会保存在sqlite3 db file中,不指定的话,就是当前目录的 result.db 文件:比如有3轮比较,那么会有result.db.1result.db.2result.db.33个文件,

  • key:保存不一致的key
  • field:保存hash,set,zset,list不一致的field, list 存的是下标值
  • feildkey_id字段关联表key的id字段
  • key_<N>和`field_“:保存第N轮比较后的结果,即中间结果。
    使用举例:
$ sqlite3  result.db

sqlite> select * from key;
id          key              type        conflict_type  db          source_len  target_len
----------  ---------------  ----------  -------------  ----------  ----------  ----------
1           keydiff1_string  string      value          1           6           6
2           keydiff_hash     hash        value          0           2           1
3           keydiff_string   string      value          0           6           6
4           key_string_diff  string      value          0           6           6
5           keylack_string   string      lack_target    0           6           0
sqlite>

sqlite> select * from field;
id          field       conflict_type  key_id
----------  ----------  -------------  ----------
1           k1          lack_source    2
2           k2          value          2
3           k3          lack_target    2

开源地址

redis-full-check: https://github.com/aliyun/redis-full-check。
数据迁移工具redis-shake: https://github.com/aliyun/redis-shake。
如果有问题或者建议,请在github issue上进行留言,也欢迎大家一起加入开源开发。

发表评论

电子邮件地址不会被公开。 必填项已用*标注