<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 8683字,需瀏覽 18分鐘

           ·

          2021-06-21 09:41

          不知不覺【數(shù)據(jù)遷移專題】已經(jīng)進行了兩期,在先前《跨越異構鴻溝,Redis 遷移同步過程中的挑戰(zhàn)與解決方案》和《在線數(shù)據(jù)遷移,數(shù)字化時代的必修課》中,我們?yōu)榇蠹医榻B了數(shù)據(jù)遷移挑戰(zhàn)與技術選型,并詳細分享京東云自研開源的RedisSyncer 項目。本篇是系列內(nèi)容第三篇,我們來聊一聊如何用RedisSyncer實現(xiàn)數(shù)據(jù)雙向同步。




          redissyncer簡介


          RedisSyncer是京東云自研的redis多任務同步中間件工具集,應用于redis單實例及集群同步。該工具集包括

          • redis 同步服務引擎 redissyncer-server
          • redissycner 客戶端 redissyncer-cli
          • redis 數(shù)據(jù)校驗工具 redissycner-compare
          • 基于docker-compse的一體化部署方案 redissyncer

          目前在github開源:

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


          緩存同步的定義及必要性


          • 雙向同步是指在兩個實例都有存量數(shù)據(jù)和寫流量的情況下進行兩實例同步,最終達到兩實例數(shù)據(jù)動態(tài)一致的過程
          • 緩存數(shù)據(jù)全局可讀,防止緩存擊穿
          • 保證緩存命中率,為數(shù)據(jù)庫減壓
          • 當單一數(shù)據(jù)中心發(fā)生故障時,保證數(shù)據(jù)在另一中心完全可見
          雙向同步的操作難度與冷啟動問題

          • 原生redis同步無法區(qū)分緩存數(shù)據(jù)來源
          • 由于redis本身沒有實例標識(類似mysql的GTID),在雙向同步時形成數(shù)據(jù)回環(huán)
          • redis環(huán)狀緩沖區(qū)覆蓋后,數(shù)據(jù)混淆且難于清理


          基于數(shù)據(jù)沖銷的雙向同步方案


          利用數(shù)據(jù)沖銷的方式破除數(shù)據(jù)寫入環(huán)。該方案的必要條件是,同步的實例或集群寫入的key無沖突,即在數(shù)據(jù)中心A寫入的key,不會同一時間在B中心寫入相異值。


          假設我們有兩個redis實例redis1和redis2,再分別定義兩個沖銷池set1和set2,記錄key及同步次數(shù)。

          1. 啟動redis1->redis2的全量同步任務
          2. 啟動redis1->redis2增量任務 2-1。增量任務先在set1做沖銷(set1中存在的數(shù)據(jù)刪除并丟棄同步) 2-2. 未被沖銷數(shù)據(jù)同步到redis2
          3. 啟動redis2->redis1的全量任務,此全量同步數(shù)據(jù)一定會作為增量形成回環(huán),所以要先寫入set1再寫入redis1,以便數(shù)據(jù)作為增量回環(huán)同步到redis2時利用set1沖銷 3-1,寫入set1 3-2,寫入redis1
          4. 啟動redis2->redis1增量任務,增量任務先在set2做沖銷(set2中存在的數(shù)據(jù)刪除并丟棄同步),增量任務先寫入set1在寫入redis1避免循環(huán)復制 4-1。通過set2沖銷數(shù)據(jù) 4-2,數(shù)據(jù)寫入set1 4-3,數(shù)據(jù)寫入redis1
          5. 改變redis1->redis2增量任務行為,增量任務先在set1做沖銷(set1中存在的數(shù)據(jù)刪除并丟棄同步),未沖銷數(shù)據(jù)先寫入set2再同步到redis2 5-1 寫入set2 5-2 寫入redis2


          為什么增加第五步改變redis1->redis2增量任務行為呢?因為在第四步完成時set2中并沒有從redis1->redis2的增量數(shù)據(jù),這會造成從redis1->redis2的增量數(shù)據(jù)會轉(zhuǎn)換成redis2->redis1增量數(shù)據(jù)且在本地無法被沖銷,只有數(shù)據(jù)進入set1且被寫入redis1后再次作為增量數(shù)據(jù)向redis2同步時才會被沖銷,增加了網(wǎng)絡開銷同時redis1也增加了一次寫入負載。


          數(shù)據(jù)沖銷方式及其缺陷


          • 數(shù)據(jù)沖銷方式需要在每次發(fā)送數(shù)據(jù)前對數(shù)據(jù)進行緩沖,正常情況下緩沖內(nèi)存占用不大。但當網(wǎng)絡阻塞或由于網(wǎng)絡不暢無法沖銷數(shù)據(jù)時,會造成緩沖區(qū)暴增導致OOM
          • 數(shù)據(jù)沖銷方式帶來的冷啟動問題
            • 當任務異常中斷且redis offset被覆蓋的情況下,因為數(shù)據(jù)矯正依據(jù)缺失,需要重建緩存
            • 若采用數(shù)據(jù)持久化的方式先持久化后發(fā)送的方式,那么在沖銷過程中會大大降低同步效率


          數(shù)據(jù)雙寫,看似美好其實坑多多


          業(yè)務雙寫是最符合人類直覺的雙向方案,同一份數(shù)據(jù)寫入兩個數(shù)據(jù)中心以保障數(shù)據(jù)冗余。但是在實際操作中會遇到數(shù)據(jù)寫入順序問題。


          雙寫方案中的數(shù)據(jù)順序問題


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

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


          雙讀方案



          • 每個數(shù)據(jù)中心建立redis 讀寫實例與只讀實例
          • 讀寫數(shù)據(jù)中的落地數(shù)據(jù)通過redissyncer同步到對端只讀實例
          • 應用讀數(shù)據(jù)時先讀取只讀實例若有數(shù)據(jù)返回則返回;若無數(shù)據(jù)返回則讀取讀寫實例
          • 雙讀方案的限制條件
            • key的生成在全局具有唯一性既兩個中心不出現(xiàn)重復的key
            • 避免incr 、 lpush 等非冪等操作
            • 由于網(wǎng)絡抖動可能造成數(shù)據(jù)流中斷,盡管redissyncer以及對非冪等命令做了處理,但是極端情況仍然可能造成數(shù)據(jù)不準確影響業(yè)務


          雙讀方案實踐模擬


          環(huán)境列表


          主機名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 用于同步到對端數(shù)據(jù)中心

          • 通過 redisdual 模擬雙讀客戶端

          實施細則

          • 部署 redis 詳見 redis 部署文檔,這里不累述
          • 部署redissyncer(docker方式); az_a1、az_b1 上執(zhí)行
          • clone redissyncer 項目
            1git clone https://github.com/TraceNature/redissyncer.git
            2cd redissyncer
            3docker-compose up -d

          • 部署 redissyncer-cli 用于與服務器通訊

            • 下載客戶端程序

              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 訪問地址及端口
              2syncserver: http://127.0.0.1:8080
              3# 訪問 redissyncer-server 的 token??梢酝ㄟ^ redissyncer-cli login 命令獲得。默認用戶名 admin 默認密碼 123456.完整命令 redissyncer-cli login admin 123456
              4token: 379F5E2BD55A4608B6A7557F0583CFC5

          • az_a1 配置同步任務同步到 az_b2
            • 編輯任務文件  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}
            • 啟動任務

              1redissyncer-cli-0.1.0-linux-amd64 -i
              2redissyncer-cli> task create source synctask/a1_to_b2.json;

          • az_b1 配置同步任務同步到 az_a2

            • 編輯任務文件  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}

            • 啟動任務

              1redissyncer-cli-0.1.0-linux-amd64 -i
              2redissyncer-cli> task create source synctask/b1_to_a2.json;

          • 通過redisdual 模擬redis 雙讀

            克隆 https://github.com/TraceNature/redisdual.git 項目自行編譯
          • config.yaml 文件參數(shù)詳解

             1# 日志配置不需改動
            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í)行時間間隔,單位毫秒,可以控制每次執(zhí)行的間隔時長,便于觀察日志
            14execinterval: 1
            15
            16# 循環(huán)執(zhí)行最大步長,當大于步長時歸零;當達到步長時,重新執(zhí)行寫入和雙讀操作
            17loopstep: 30
            18
            19# 本地key前綴,區(qū)分本地寫入key和remote端寫入的key
            20localkeyprefix: a
            21remotekeyprefix: b
            22
            23# redis 讀寫實例
            24redisrw:
            25db: 0
            26addr: '114.67.76.82:16375'
            27password: 'redistest0102'
            28# redis 只讀實例
            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實例,有返回值輸出,無返回值讀RW庫,有返回值輸出,無返回值結束查詢
            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ù)。啟動服務,定時執(zhí)行RW實例寫入。并執(zhí)行雙讀操作
          •  1// -d 后臺啟動
            2if global.RSPViper.GetBool("daemon") {
            3cmd, err := background()
            4if err != nil {
            5panic(err)
            6}
            7
            8//根據(jù)返回值區(qū)分父進程子進程
            9if cmd != nil { //父進程
            10fmt.Println("PPID: ", os.Getpid(), "; PID:", cmd.Process.Pid, "; Operating parameters: ", os.Args)
            11return //父進程退出
            12else { //子進程
            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 讀寫實例
            38redisRW := GetRedisRW()
            39
            40//redis 只讀實例
            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}
          • 啟動redisdual 并觀察日志

            redisdual start

          小結

          redis的雙向同步方案的機制大致就是以上三種,具體生產(chǎn)中采用哪種方式要根據(jù)業(yè)務特性進行權衡。從數(shù)據(jù)安全和維護成本方面考慮,雙讀方案從運維成本來講是最少的,且在故障發(fā)生時不會引起數(shù)據(jù)混淆。


          瀏覽 202
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲日韩欧美动漫 | 囯产精品久久精品 | 成年视频在线 | 永久免费黄色视频 | 欧美精品在线自偷自拍 |