利用redissyncer實(shí)現(xiàn)數(shù)據(jù)雙向同步

不知不覺(jué)【數(shù)據(jù)遷移專(zhuān)題】已經(jīng)進(jìn)行了兩期,在先前《跨越異構(gòu)鴻溝,Redis 遷移同步過(guò)程中的挑戰(zhàn)與解決方案》和《在線數(shù)據(jù)遷移,數(shù)字化時(shí)代的必修課》中,我們?yōu)榇蠹医榻B了數(shù)據(jù)遷移挑戰(zhàn)與技術(shù)選型,并詳細(xì)分享京東云自研開(kāi)源的RedisSyncer 項(xiàng)目。本篇是系列內(nèi)容第三篇,我們來(lái)聊一聊如何用RedisSyncer實(shí)現(xiàn)數(shù)據(jù)雙向同步。
RedisSyncer是京東云自研的redis多任務(wù)同步中間件工具集,應(yīng)用于redis單實(shí)例及集群同步。該工具集包括:
redis 同步服務(wù)引擎 redissyncer-server redissycner 客戶端 redissyncer-cli redis 數(shù)據(jù)校驗(yàn)工具 redissycner-compare 基于docker-compse的一體化部署方案 redissyncer
目前在github開(kāi)源:
https://github.com/TraceNature/redissyncer-server
雙向同步是指在兩個(gè)實(shí)例都有存量數(shù)據(jù)和寫(xiě)流量的情況下進(jìn)行兩實(shí)例同步,最終達(dá)到兩實(shí)例數(shù)據(jù)動(dòng)態(tài)一致的過(guò)程 緩存數(shù)據(jù)全局可讀,防止緩存擊穿 保證緩存命中率,為數(shù)據(jù)庫(kù)減壓 當(dāng)單一數(shù)據(jù)中心發(fā)生故障時(shí),保證數(shù)據(jù)在另一中心完全可見(jiàn)
原生redis同步無(wú)法區(qū)分緩存數(shù)據(jù)來(lái)源 由于redis本身沒(méi)有實(shí)例標(biāo)識(shí)(類(lèi)似mysql的GTID),在雙向同步時(shí)形成數(shù)據(jù)回環(huán) redis環(huán)狀緩沖區(qū)覆蓋后,數(shù)據(jù)混淆且難于清理
利用數(shù)據(jù)沖銷(xiāo)的方式破除數(shù)據(jù)寫(xiě)入環(huán)。該方案的必要條件是,同步的實(shí)例或集群寫(xiě)入的key無(wú)沖突,即在數(shù)據(jù)中心A寫(xiě)入的key,不會(huì)同一時(shí)間在B中心寫(xiě)入相異值。
假設(shè)我們有兩個(gè)redis實(shí)例redis1和redis2,再分別定義兩個(gè)沖銷(xiāo)池set1和set2,記錄key及同步次數(shù)。
啟動(dòng)redis1->redis2的全量同步任務(wù) 啟動(dòng)redis1->redis2增量任務(wù) 2-1。增量任務(wù)先在set1做沖銷(xiāo)(set1中存在的數(shù)據(jù)刪除并丟棄同步) 2-2. 未被沖銷(xiāo)數(shù)據(jù)同步到redis2 啟動(dòng)redis2->redis1的全量任務(wù),此全量同步數(shù)據(jù)一定會(huì)作為增量形成回環(huán),所以要先寫(xiě)入set1再寫(xiě)入redis1,以便數(shù)據(jù)作為增量回環(huán)同步到redis2時(shí)利用set1沖銷(xiāo) 3-1,寫(xiě)入set1 3-2,寫(xiě)入redis1 啟動(dòng)redis2->redis1增量任務(wù),增量任務(wù)先在set2做沖銷(xiāo)(set2中存在的數(shù)據(jù)刪除并丟棄同步),增量任務(wù)先寫(xiě)入set1在寫(xiě)入redis1避免循環(huán)復(fù)制 4-1。通過(guò)set2沖銷(xiāo)數(shù)據(jù) 4-2,數(shù)據(jù)寫(xiě)入set1 4-3,數(shù)據(jù)寫(xiě)入redis1 改變r(jià)edis1->redis2增量任務(wù)行為,增量任務(wù)先在set1做沖銷(xiāo)(set1中存在的數(shù)據(jù)刪除并丟棄同步),未沖銷(xiāo)數(shù)據(jù)先寫(xiě)入set2再同步到redis2 5-1 寫(xiě)入set2 5-2 寫(xiě)入redis2

為什么增加第五步改變r(jià)edis1->redis2增量任務(wù)行為呢?因?yàn)樵诘谒牟酵瓿蓵r(shí)set2中并沒(méi)有從redis1->redis2的增量數(shù)據(jù),這會(huì)造成從redis1->redis2的增量數(shù)據(jù)會(huì)轉(zhuǎn)換成redis2->redis1增量數(shù)據(jù)且在本地?zé)o法被沖銷(xiāo),只有數(shù)據(jù)進(jìn)入set1且被寫(xiě)入redis1后再次作為增量數(shù)據(jù)向redis2同步時(shí)才會(huì)被沖銷(xiāo),增加了網(wǎng)絡(luò)開(kāi)銷(xiāo)同時(shí)redis1也增加了一次寫(xiě)入負(fù)載。
數(shù)據(jù)沖銷(xiāo)方式及其缺陷
數(shù)據(jù)沖銷(xiāo)方式需要在每次發(fā)送數(shù)據(jù)前對(duì)數(shù)據(jù)進(jìn)行緩沖,正常情況下緩沖內(nèi)存占用不大。但當(dāng)網(wǎng)絡(luò)阻塞或由于網(wǎng)絡(luò)不暢無(wú)法沖銷(xiāo)數(shù)據(jù)時(shí),會(huì)造成緩沖區(qū)暴增導(dǎo)致OOM 數(shù)據(jù)沖銷(xiāo)方式帶來(lái)的冷啟動(dòng)問(wèn)題 當(dāng)任務(wù)異常中斷且redis offset被覆蓋的情況下,因?yàn)閿?shù)據(jù)矯正依據(jù)缺失,需要重建緩存 若采用數(shù)據(jù)持久化的方式先持久化后發(fā)送的方式,那么在沖銷(xiāo)過(guò)程中會(huì)大大降低同步效率
業(yè)務(wù)雙寫(xiě)是最符合人類(lèi)直覺(jué)的雙向方案,同一份數(shù)據(jù)寫(xiě)入兩個(gè)數(shù)據(jù)中心以保障數(shù)據(jù)冗余。但是在實(shí)際操作中會(huì)遇到數(shù)據(jù)寫(xiě)入順序問(wèn)題。
雙寫(xiě)方案中的數(shù)據(jù)順序問(wèn)題

并發(fā)環(huán)境中同時(shí)寫(xiě)入同一個(gè)key的情況下,并不能保障key寫(xiě)入redis的順序。造成key的結(jié)果不一致。

通過(guò)統(tǒng)一隊(duì)列解決順序問(wèn)題 網(wǎng)絡(luò)中斷導(dǎo)致數(shù)據(jù)缺失 強(qiáng)一致性會(huì)導(dǎo)致單機(jī)房不可用的情況下寫(xiě)操作全局不可用,并需要在數(shù)據(jù)在某一次提交不完全成功的情況下提供回滾機(jī)制、及數(shù)據(jù)補(bǔ)償機(jī)制 隊(duì)列帶來(lái)寫(xiě)效率損失,redis失去作為緩存層的意義

每個(gè)數(shù)據(jù)中心建立redis 讀寫(xiě)實(shí)例與只讀實(shí)例 讀寫(xiě)數(shù)據(jù)中的落地?cái)?shù)據(jù)通過(guò)redissyncer同步到對(duì)端只讀實(shí)例 應(yīng)用讀數(shù)據(jù)時(shí)先讀取只讀實(shí)例若有數(shù)據(jù)返回則返回;若無(wú)數(shù)據(jù)返回則讀取讀寫(xiě)實(shí)例 雙讀方案的限制條件 key的生成在全局具有唯一性既兩個(gè)中心不出現(xiàn)重復(fù)的key 避免incr 、 lpush 等非冪等操作 由于網(wǎng)絡(luò)抖動(dòng)可能造成數(shù)據(jù)流中斷,盡管redissyncer以及對(duì)非冪等命令做了處理,但是極端情況仍然可能造成數(shù)據(jù)不準(zhǔn)確影響業(yè)務(wù)
環(huán)境列表
| 主機(jī)名 | 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 實(shí)例;az_a2 代表 a 中心 redis RO 實(shí)例;az_b1 代表 b 中心 redis RW 實(shí)例;az_b2 代表 b 中心 redis RO 實(shí)例
分別在 az_a1、az_b1上部署 redissyncer 用于同步到對(duì)端數(shù)據(jù)中心
通過(guò) redisdual 模擬雙讀客戶端
實(shí)施細(xì)則
部署 redis 詳見(jiàn) redis 部署文檔,這里不累述 部署redissyncer(docker方式); az_a1、az_b1 上執(zhí)行 clone redissyncer 項(xiàng)目 1git clone https://github.com/TraceNature/redissyncer.git
2cd redissyncer
3docker-compose up -d部署 redissyncer-cli 用于與服務(wù)器通訊
下載客戶端程序
1wget https://github.com/TraceNature/redissyncer-cli/releases/download/v0. 1.0/redissyncer-cli-0.1.0-linux-amd64.tar.gz
2
3tar zxvf redissyncer-cli-0.1.0-linux-amd64.tar.gz配置.config.yaml 文件
1# redissyncer-server 訪問(wèn)地址及端口
2syncserver: http://127.0.0.1:8080
3# 訪問(wèn) redissyncer-server 的 token??梢酝ㄟ^(guò) redissyncer-cli login 命令獲得。默認(rèn)用戶名 admin 默認(rèn)密碼 123456.完整命令 redissyncer-cli login admin 123456
4token: 379F5E2BD55A4608B6A7557F0583CFC5az_a1 配置同步任務(wù)同步到 az_b2 編輯任務(wù)文件 synctask/a1_to_b2.json 1{
2"sourcePassword": "redistest0102",
3"sourceRedisAddress": "10.0.0.110:16375",
4"targetRedisAddress": "10.0.0.113:16375",
5"targetPassword": "redistest0102",
6"taskName": "a1_to_b2",
7"targetRedisVersion": 5.0,
8"autostart": true,
9"afresh": true,
10"batchSize": 100
11}啟動(dòng)任務(wù)
1redissyncer-cli-0.1.0-linux-amd64 -i
2redissyncer-cli> task create source synctask/a1_to_b2.json;az_b1 配置同步任務(wù)同步到 az_a2
編輯任務(wù)文件 synctask/b1_to_a2.json
1{
2 "sourcePassword": "redistest0102",
3 "sourceRedisAddress": "10.0.0.112:16375",
4 "targetRedisAddress": "10.0.0.111:16375",
5 "targetPassword": "redistest0102",
6 "taskName": "b1_to_a2",
7 "targetRedisVersion": 5.0,
8 "autostart": true,
9 "afresh": true,
10 "batchSize": 100
11}啟動(dòng)任務(wù)
1redissyncer-cli-0.1.0-linux-amd64 -i
2redissyncer-cli> task create source synctask/b1_to_a2.json;通過(guò)redisdual 模擬redis 雙讀
克隆 https://github.com/TraceNature/redisdual.git 項(xiàng)目自行編譯 config.yaml 文件參數(shù)詳解
1# 日志配置不需改動(dòng)
2zap:
3level: 'debug'
4format: 'console'
5prefix: '[redisdual]'
6director: 'log'
7link-name: 'latest_log'
8show-line: true
9encode-level: 'LowercaseColorLevelEncoder'
10# stacktrace-key: 'stacktrace'
11log-in-console: true
12
13# 執(zhí)行時(shí)間間隔,單位毫秒,可以控制每次執(zhí)行的間隔時(shí)長(zhǎng),便于觀察日志
14execinterval: 1
15
16# 循環(huán)執(zhí)行最大步長(zhǎng),當(dāng)大于步長(zhǎng)時(shí)歸零;當(dāng)達(dá)到步長(zhǎng)時(shí),重新執(zhí)行寫(xiě)入和雙讀操作
17loopstep: 30
18
19# 本地key前綴,區(qū)分本地寫(xiě)入key和remote端寫(xiě)入的key
20localkeyprefix: a
21remotekeyprefix: b
22
23# redis 讀寫(xiě)實(shí)例
24redisrw:
25db: 0
26addr: '114.67.76.82:16375'
27password: 'redistest0102'
28# redis 只讀實(shí)例
29redisro:
30db: 0
31addr: '114.67.120.120:16375'
32password: 'redistest0102'主要代碼分析 redisdual/cmd/start.go;func dual(rw *redis.Client, ro *redis.Client, key string) 函數(shù).雙讀的主要邏輯,先讀RO實(shí)例,有返回值輸出,無(wú)返回值讀RW庫(kù),有返回值輸出,無(wú)返回值結(jié)束查詢 1func dual(rw *redis.Client, ro *redis.Client, key string) {
2 roResult, err := ro.Get(key).Result()
3
4 if err == nil && roResult != "" {
5 global.RSPLog.Sugar().Infof("Get key %s from redisro result is:%s ", key, roResult)
6 return
7 }
8
9 rwResult, err := rw.Get(key).Result()
10 if err != nil || rwResult == "" {
11 global.RSPLog.Sugar().Infof("key %s no result return!", key)
12 return
13 }
14
15 global.RSPLog.Sugar().Infof("Get key %s from redisrw result is: %s ", key, rwResult)
16
17}redisdual/cmd/start.go;func startServer() 函數(shù)。啟動(dòng)服務(wù),定時(shí)執(zhí)行RW實(shí)例寫(xiě)入。并執(zhí)行雙讀操作 1// -d 后臺(tái)啟動(dòng)
2if global.RSPViper.GetBool("daemon") {
3cmd, err := background()
4if err != nil {
5panic(err)
6}
7
8//根據(jù)返回值區(qū)分父進(jìn)程子進(jìn)程
9if cmd != nil { //父進(jìn)程
10fmt.Println("PPID: ", os.Getpid(), "; PID:", cmd.Process.Pid, "; Operating parameters: ", os.Args)
11return //父進(jìn)程退出
12} else { //子進(jìn)程
13fmt.Println("PID: ", os.Getpid(), "; Operating parameters: ", os.Args)
14}
15}
16
17global.RSPLog = core.Zap()
18global.RSPLog.Info("server start ... ")
19
20pidMap := make(map[string]int)
21// 記錄pid
22pid := syscall.Getpid()
23pidMap["pid"] = pid
24
25pidYaml, _ := yaml.Marshal(pidMap)
26dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
27if err != nil {
28panic(err)
29}
30
31if err := ioutil.WriteFile(dir+"/pid", pidYaml, 0664); err != nil {
32global.RSPLog.Sugar().Error(err)
33panic(err)
34}
35global.RSPLog.Sugar().Infof("Actual pid is %d", pid)
36
37//redis 讀寫(xiě)實(shí)例
38redisRW := GetRedisRW()
39
40//redis 只讀實(shí)例
41redisRO := GetRedisRO()
42
43//清理RW
44redisRW.FlushAll()
45
46global.RSPLog.Sugar().Info("execinterval:", global.RSPViper.GetInt("execinterval"))
47loopstep := global.RSPViper.GetInt("loopstep")
48i := 0
49for {
50if i > loopstep {
51i = 0
52}
53key := global.RSPViper.GetString("localkeyprefix") + "_key" + strconv.Itoa(i)
54redisRW.Set(key, key+"_"+strconv.FormatInt(time.Now().UnixNano(), 10), 3600*time.Second)
55dual(redisRW, redisRO, global.RSPViper.GetString("localkeyprefix")+"_key"+strconv.Itoa(i))
56dual(redisRW, redisRO, global.RSPViper.GetString("remotekeyprefix")+"_key"+strconv.Itoa(i))
57i++
58time.Sleep(time.Duration(global.RSPViper.GetInt("execinterval")) * time.Millisecond)
59}
啟動(dòng)redisdual 并觀察日志
redisdual start
redis的雙向同步方案的機(jī)制大致就是以上三種,具體生產(chǎn)中采用哪種方式要根據(jù)業(yè)務(wù)特性進(jìn)行權(quán)衡。從數(shù)據(jù)安全和維護(hù)成本方面考慮,雙讀方案從運(yùn)維成本來(lái)講是最少的,且在故障發(fā)生時(shí)不會(huì)引起數(shù)據(jù)混淆。



