RedisSyncer是京东云自研的redis多任务同步中间件工具集,应用于redis单实例及集群同步。该工具集包括:
redis 同步服务引擎 redissyncer-server redissycner 客户端 redissyncer-cli redis 数据校验工具 redissycner-compare 基于docker-compse的一体化部署方案 redissyncer 目前在github开源:
urlhttps://github.com/TraceNature/redissyncer-server
缓存同步的定义及必要性
双向同步的操作难度与冷启动问题
基于数据冲销的双向同步方案: 利用数据冲销的方式破除数据写入环。该方案的必要条件是,同步的实例或集群写入的key无冲突,即在数据中心A写入的key,不会同一时间在B中心写入相异值。
假设我们有两个redis实例redis1和redis2,再分别定义两个冲销池set1和set2,记录key及同步次数。
为什么增加第五步改变redis1->redis2增量任务行为呢?
因为在第四步完成时set2中并没有从redis1->redis2的增量数据,这会造成从redis1->redis2的增量数据会转换成redis2->redis1增量数据且在本地无法被冲销,只有数据进入set1且被写入redis1后再次作为增量数据向redis2同步时才会被冲销,增加了网络开销同时redis1也增加了一次写入负载。
数据冲销方式及其缺陷
数据双写,看似美好其实坑多多。业务双写是最符合人类直觉的双向方案,同一份数据写入两个数据中心以保障数据冗余。但是在实际操作中会遇到数据写入顺序问题。
双写方案中的数据顺序问题
双读方案实践模拟
环境列表
主机名 | 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 |
实施细则
部署 redis 详见 redis 部署文档,这里不累述
部署redissyncer(docker方式); az_a1、az_b1 上执行
clone redissyncer 项目
部署 redissyncer-cli 用于与服务器通讯
下载客户端程序
shellwget https://github.com/TraceNature/redissyncer-cli/releases/download/v0. 1.0/redissyncer-cli-0.1.0-linux-amd64.tar.gz
shelltar zxvf redissyncer-cli-0.1.0-linux-amd64.tar.gz
az_a1 配置同步任务同步到 az_b2
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 文件参数详解
主要代码分析
gofunc 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的双向同步方案的机制大致就是以上三种,具体生产中采用哪种方式要根据业务特性进行权衡。从数据安全和维护成本方面考虑,双读方案从运维成本来讲是最少的,且在故障发生时不会引起数据混淆。
本文作者:Golovin
本文链接:
版权声明:heidsoft