专栏首页云计算与大数据利用redissyncer实现数据双向同步

利用redissyncer实现数据双向同步

redissyncer简介

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

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

目前在github开源:

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

缓存同步的定义及必要性

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

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

  • 原生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被覆盖的情况下,因为数据矫正依据缺失,需要重建缓存
    • 若采用数据持久化的方式先持久化后发送的方式,那么在冲销过程中会大大降低同步效率

数据双写,看似美好其实坑多多

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

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

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

双读方案

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

双读方案实践模拟

环境列表

主机名

IP地址

部署软件或工具

az_a1

10.0.0.110

redis5.0

az_a1

10.0.0.110

redissyncer

az_a2

10.0.0.111

redis5.0

az_b1

10.0.0.112

redis5.0

az_b1

10.0.0.112

redissyncer

az_b2

10.0.0.113

redis5.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 用于与服务器通讯

  • 下载客户端程序 wget https://github.com/TraceNature/redissyncer-cli/releases/download/v0. 1.0/redissyncer-cli-0.1.0-linux-amd64.tar.gz tar zxvf redissyncer-cli-0.1.0-linux-amd64.tar.gz

az_a1 配置同步任务同步到 az_b2

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

az_b1 配置同步任务同步到 az_a2

  • 编辑任务文件 synctask/b1_to_a2.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 文件参数详解

主要代码分析

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实例写入。并执行双读操作

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

本文分享自微信公众号 - 黑洞日志(heidcloud)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Linux中inotify+unison实现数据双向(多向)实时同步

    楠楠
  • 利用Spark实现Oracle到Hive的历史数据同步

    和上一篇文章Spark通过修改DataFrame的schema给表字段添加注释一样,通过Spark将关系型数据库(以Oracle为例)的表同步的Hive,这里讲...

    董可伦
  • 连仕彤博客利用rsync+sersync实现数据实时同步

    行 者
  • 利用ogg实现oracle到kafka的增量数据实时同步

    转载请务必注明原创地址为:https://dongkelun.com/2018/05/23/oggOracle2Kafka/

    董可伦
  • 如何用Rysnc实现数据同步?

    Rsync(remote sync) 是UNIX及类UNIX平台一款数据镜像备份软件,它不像FTP等其他文件传输服务那样需要进行全备份,Rsync可以根据数据...

    吴柯
  • 使用Mover来实现数据同步

    手里面有了几个OneDrive的号,有个人号,E5管理号,商业管理号,A1子号,A1P子号,世纪互联子号,可是基本都是空的,也不敢存太多东西,毕竟除了E5是自己...

    Erwin
  • 利用Logstash实现ES和MySQL同步

    山海散人
  • 利用反射实现状态同步

    Java反射使我们能在程序运行时动态调用某个对象的方法/构造函数、获取某个对象的属性,经常用于实现动态代理、工厂模式、Java JDBC加载连接驱动类等,近期阅...

    九州暮云
  • JS 实现双向数据绑定

    近几年前端技术栈真是发展的太迅速了,从以前的针对dom操作的框架如jquery,ext.js等框架逐步过渡到当前的mvvm模式,让前端开发者将注意力从dom操作...

    grain先森
  • sersync实现数据实时同步

    1.1 第一个里程碑:安装sersync软件 1.1.1 将软件上传到服务器当中并解压 1、上传软件到服务器上 rz -E 为了便于管理上传位置统一设置为 /s...

    惨绿少年
  • 利用OGG实现Oracle到Kafka到Greenplum的增量数据同步

    墨墨导读:本文来自墨天轮用户 肖杰 的投稿,介绍用OGG实现Oracle到Kafka到Greenplum的增量数据同步的全过程。

    数据和云
  • [经验分享] CentOS7+rsync+sersync实现数据实时同步

    2.rsync配置(千万注意:本人亲自入坑,下面的配置信息,请在添加到配置文件中后千万记得删除掉后面#部分的备注信息,否则同步过程会失败,不知原因,删除后...

    拓荒者
  • CentOS7+rsync+sersync实现数据实时同步

    一、为什么要用Rsync+sersync架构? 1、sersync是基于Inotify开发的,类似于Inotify-tools的工具 2、sersync...

    拓荒者
  • 20行实现Vue双向数据绑定

    Vue.js中最让引人入胜的功能之一就是: 数据的双向绑定,下面使用原生JavaScript, 只须区区20行代码即可实现:

    用户2590762
  • 9. Vue 使用 v-model 实现双向数据绑定

    在Vue框架中,能够绑定表单元素数据的命令有v-bind和v-model,但是v-bind只能单向绑定(将data中的数据绑定到View视图中),而v-mode...

    Devops海洋的渔夫
  • python实现mysql数据同步到elasticsearch

    环境: python3.5 支持包: pymysql elasticsearch_dsl 安装 pymysql elasticsearch_dsl p...

    98k
  • Hive整合HBase实现数据同步

    1、在node3中的hive的配置文件hive-site.xml增加自己的zookeeper集群属性(仅此一步) node3是用于hive集群中用于启动元数据...

    时间静止不是简史
  • 利用Rsync同步备份服务器数据

    转载请备注出处:[狂码一生 ] http://www.sindsun.com/article-details-117.html

    Sindsun
  • 【52ABP实战教程】0.3-- 从GitHub推送代码回VSTS实现双向同步

    需求 在之前的文章中“【52ABP实战教程】0.1-- Devops如何用VSTS持续集成到Github仓库!” 我们有讲述如何将vsts中的代码编译推送到g...

    角落的白板报

扫码关注云+社区

领取腾讯云代金券