编辑
2023-04-27
摘录
00

目录

redissyncer简介
1. 缓存同步
2. 双向同步
3. 数据双写
4. 双读方案
小结

redissyncer简介

RedisSyncer是京东云自研的redis多任务同步中间件工具集,应用于redis单实例及集群同步。该工具集包括:

redis 同步服务引擎 redissyncer-server redissycner 客户端 redissyncer-cli redis 数据校验工具 redissycner-compare 基于docker-compse的一体化部署方案 redissyncer 目前在github开源:

url
https://github.com/TraceNature/redissyncer-server

1. 缓存同步

缓存同步的定义及必要性

  • 双向同步是指在两个实例都有存量数据和写流量的情况下进行两实例同步,最终达到两实例数据动态一致的过程
  • 缓存数据全局可读,防止缓存击穿
  • 保证缓存命中率,为数据库减压
  • 当单一数据中心发生故障时,保证数据在另一中心完全可见

2. 双向同步

双向同步的操作难度与冷启动问题

  • 原生redis同步无法区分缓存数据来源
  • 由于redis本身没有实例标识(类似mysql的GTID),在双向同步时形成数据回环
  • redis环状缓冲区覆盖后,数据混淆且难于清理

基于数据冲销的双向同步方案: 利用数据冲销的方式破除数据写入环。该方案的必要条件是,同步的实例或集群写入的key无冲突,即在数据中心A写入的key,不会同一时间在B中心写入相异值。

假设我们有两个redis实例redis1和redis2,再分别定义两个冲销池set1和set2,记录key及同步次数。

  1. 启动redis1->redis2的全量同步任务
  2. 启动redis1->redis2增量任务 2-1。增量任务先在set1做冲销(set1中存在的数据删除并丢弃同步) 2-2. 未被冲销数据同步到redis2
  3. 启动redis2->redis1的全量任务,此全量同步数据一定会作为增量形成回环,所以要先写入set1再写入redis1,以便数据作为增量回环同步到redis2时利用set1冲销 3-1,写入set1 3-2,写入redis1
  4. 启动redis2->redis1增量任务,增量任务先在set2做冲销(set2中存在的数据删除并丢弃同步),增量任务先写入set1在写入redis1避免循环复制 4-1。通过set2冲销数据 4-2,数据写入set1 4-3,数据写入redis1
  5. 改变redis1->redis2增量任务行为,增量任务先在set1做冲销(set1中存在的数据删除并丢弃同步),未冲销数据先写入set2再同步到redis2 5-1 写入set2 5-2 写入redis2

为什么增加第五步改变redis1->redis2增量任务行为呢?

因为在第四步完成时set2中并没有从redis1->redis2的增量数据,这会造成从redis1->redis2的增量数据会转换成redis2->redis1增量数据且在本地无法被冲销,只有数据进入set1且被写入redis1后再次作为增量数据向redis2同步时才会被冲销,增加了网络开销同时redis1也增加了一次写入负载。

数据冲销方式及其缺陷

  • 数据冲销方式需要在每次发送数据前对数据进行缓冲,正常情况下缓冲内存占用不大。但当网络阻塞或由于网络不畅无法冲销数据时,会造成缓冲区暴增导致OOM
  • 数据冲销方式带来的冷启动问题
    • 当任务异常中断且redis offset被覆盖的情况下,因为数据矫正依据缺失,需要重建缓存
    • 若采用数据持久化的方式先持久化后发送的方式,那么在冲销过程中会大大降低同步效率

3. 数据双写

数据双写,看似美好其实坑多多。业务双写是最符合人类直觉的双向方案,同一份数据写入两个数据中心以保障数据冗余。但是在实际操作中会遇到数据写入顺序问题。

双写方案中的数据顺序问题

  • 并发环境中同时写入同一个key的情况下,并不能保障key写入redis的顺序。造成key的结果不一致。

  • 通过统一队列解决顺序问题
  • 网络中断导致数据缺失
  • 强一致性会导致单机房不可用的情况下写操作全局不可用,并需要在数据在某一次提交不完全成功的情况下提供回滚机制、及数据补偿机制
  • 队列带来写效率损失,redis失去作为缓存层的意义

4. 双读方案

  • 每个数据中心建立redis 读写实例与只读实例
  • 读写数据中的落地数据通过redissyncer同步到对端只读实例
  • 应用读数据时先读取只读实例若有数据返回则返回;若无数据返回则读取读写实例
  • 双读方案的限制条件
    • key的生成在全局具有唯一性既两个中心不出现重复的key
    • 避免incr 、 lpush 等非幂等操作
    • 由于网络抖动可能造成数据流中断,尽管redissyncer以及对非幂等命令做了处理,但是极端情况仍然可能造成数据不准确影响业务

双读方案实践模拟

环境列表

主机名IP地址部署软件或工具
az_a110.0.0.110redis5.0
az_a110.0.0.110redissyncer
az_a210.0.0.111redis5.0
az_b110.0.0.112redis5.0
az_b110.0.0.112redissyncer
az_b210.0.0.113redis5.0
  • az_a1 代表 a 中心的 redis RW 实例;az_a2 代表 a 中心 redis RO 实例;az_b1 代表 b 中心 redis RW 实例;az_b2 代表 b 中心 redis RO 实例
  • 分别在 az_a1、az_b1上部署 redissyncer 用于同步到对端数据中心
  • 通过 redisdual 模拟双读客户端

实施细则

部署 redis 详见 redis 部署文档,这里不累述

部署redissyncer(docker方式); az_a1、az_b1 上执行

clone redissyncer 项目

部署 redissyncer-cli 用于与服务器通讯

  • 下载客户端程序

    shell
    wget https://github.com/TraceNature/redissyncer-cli/releases/download/v0. 1.0/redissyncer-cli-0.1.0-linux-amd64.tar.gz
    shell
    tar zxvf redissyncer-cli-0.1.0-linux-amd64.tar.gz

az_a1 配置同步任务同步到 az_b2

  • 编辑任务文件 synctask/a1_to_b2.json
json
{ "sourcePassword": "redistest0102", "sourceRedisAddress": "10.0.0.112:16375", "targetRedisAddress": "10.0.0.111:16375", "targetPassword": "redistest0102", "taskName": "b1_to_a2", "targetRedisVersion": 5.0, "autostart": true, "afresh": true, "batchSize": 100 }

通过redisdual 模拟redis 双读

config.yaml 文件参数详解

主要代码分析

go
func dual(rw *redis.Client, ro *redis.Client, key string) { roResult, err := ro.Get(key).Result() if err == nil && roResult != "" { global.RSPLog.Sugar().Infof("Get key %s from redisro result is:%s ", key, roResult) return } rwResult, err := rw.Get(key).Result() if err != nil || rwResult == "" { global.RSPLog.Sugar().Infof("key %s no result return!", key) return } global.RSPLog.Sugar().Infof("Get key %s from redisrw result is: %s ", key, rwResult) }

redisdual/cmd/start.go;func startServer() 函数。启动服务,定时执行RW实例写入。并执行双读操作

go
// -d 后台启动 if global.RSPViper.GetBool("daemon") { cmd, err: =background() if err != nil { panic(err) } //根据返回值区分父进程子进程 if cmd != nil { //父进程 fmt.Println("PPID: ", os.Getpid(), "; PID:", cmd.Process.Pid, "; Operating parameters: ", os.Args) return //父进程退出 } else { //子进程 fmt.Println("PID: ", os.Getpid(), "; Operating parameters: ", os.Args) } } global.RSPLog = core.Zap() global.RSPLog.Info("server start ... ") pidMap: =make(map[string] int) // 记录pid pid: =syscall.Getpid() pidMap["pid"] = pid pidYaml, _: = yaml.Marshal(pidMap) dir, err: = filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { panic(err) } if err: =ioutil.WriteFile(dir + "/pid", pidYaml, 0664); err != nil { global.RSPLog.Sugar().Error(err) panic(err) } global.RSPLog.Sugar().Infof("Actual pid is %d", pid) //redis 读写实例 redisRW: =GetRedisRW() //redis 只读实例 redisRO: =GetRedisRO() //清理RW redisRW.FlushAll() global.RSPLog.Sugar().Info("execinterval:", global.RSPViper.GetInt("execinterval")) loopstep: =global.RSPViper.GetInt("loopstep") i: =0 for { if i > loopstep { i = 0 } key: =global.RSPViper.GetString("localkeyprefix") + "_key" + strconv.Itoa(i) redisRW.Set(key, key + "_" + strconv.FormatInt(time.Now().UnixNano(), 10), 3600 * time.Second) dual(redisRW, redisRO, global.RSPViper.GetString("localkeyprefix") + "_key" + strconv.Itoa(i)) dual(redisRW, redisRO, global.RSPViper.GetString("remotekeyprefix") + "_key" + strconv.Itoa(i)) i++ time.Sleep(time.Duration(global.RSPViper.GetInt("execinterval")) * time.Millisecond) }
  • 启动redisdual 并观察日志

    redisdual start

小结

redis的双向同步方案的机制大致就是以上三种,具体生产中采用哪种方式要根据业务特性进行权衡。从数据安全和维护成本方面考虑,双读方案从运维成本来讲是最少的,且在故障发生时不会引起数据混淆。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Golovin

本文链接:

版权声明:heidsoft