redissyncer简介
RedisSyncer是京东云自研的redis多任务同步中间件工具集,应用于redis单实例及集群同步。该工具集包括:
目前在github开源:
https://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 用于与服务器通讯
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
{
"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
{
"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)
}
小结
redis的双向同步方案的机制大致就是以上三种,具体生产中采用哪种方式要根据业务特性进行权衡。从数据安全和维护成本方面考虑,双读方案从运维成本来讲是最少的,且在故障发生时不会引起数据混淆。