redis 雙寫實現策略 && hash取模

吳德寶AllenWu發表於2019-03-02

[TOC]

redis 雙寫實現策略 && hash取模

需求場景

背景

對於redis叢集而言,一般業務方使用的時候,會在服務端對key做hash策略,hash演算法一般可以分為:一致性hash、hash取模等,當然還有其他常用演算法。一致性hash在擴縮容的時候比較麻煩,因此公司層面要求都要使用hash取模,然而,如果當前線上已經是一致性hash,那麼要更改hash演算法為hash取模,那麼我們該如何做?

可能的解決方案

我們的解決方案要能夠平滑過渡,不能影響業務正常執行,因此,我們可以通過雙寫策略來實現,正如我前面的文章《線上redis遷移思路》裡面說的一樣,雙寫是萬能的。 基於此,我們可以通過先雙寫,再去掉一致性hash的方案來解決

實現方案

  • redis的配置,先使用兩套, 一套是原有的一致性hash演算法Ketema, 一套是新增的Compat.
  • 業務層上做雙向方案

從一致性hash過渡為hash取模方式的雙寫方案

舉例說明,假如目前是2個一致性hash節點(例項),那麼要調整為2個取模方式節點的步驟大致如下

  • 業務上雙寫一致性hash的2個節點和取模2個節點,此時,取模節點裡面的資料是新寫的資料,只寫不讀

  • 通過寫遷移工具,掃描所有一致性hash的節點的列表(key列表),從一致性hash節點get資料,然後set到取模節點。這種情況理論上會出現瞬間的併發問題(比如get後有新資料,最終set進去老資料,不過只是在瞬間會產生),不過沒關係,即便有髒資料(資料不一致),也會再下一步的check工具裡面處理好。

  • 資料驗證和check工具修復

    • 這個check的時候,不會有問題,因為check只是check舊的資料,對於新寫入的資料都是最新的,因為新舊節點都是雙寫的
    • 曾經剛開始的時候有想過,如果 check的時候產生了新資料怎麼辦,但是其實是多餘的,這個情況是OK的。
  • 業務切換讀到新的取模節點

    • 這個最終都是需要業務層調整程式碼,使用新的叢集或者方案

從一致性hash過渡為hash取模方式的具體實現

如下程式碼來源於閃聊專案,也是閃聊實際經歷過切換方案

  1. 配置裡面, 針對需要進行調整的redis例項,增加新的redis例項配置(取模相關),如下

    [redis.gunread_new]
    shard = "compat"
    servers = ["192.168.xxx.xxx:6380;;1", "192.168.xxx.xxx:6381;;1"]
    複製程式碼
  2. setupRedis裡面增加新的redis例項配置

    	// 遍歷所有redis pool也就是所有redis類別例項
    	for _, name := range conf.RedisPoolNames {
    		func(instance string) {
    
    			// 原有的redis例項
    			clusterConfig.Configs = conf.Redis[instance]
    			if len(clusterConfig.Configs) == 0 {
    				logger.Errorf(nil, "get redis config for %s failed", instance)
    				return
    			}
    			currentCluster := newRedisCluster(instance, clusterConfig)
    
    			// 同時載入新的redis例項,並通過SetDualWrite賦值給dualWrite. 
    			dualInstance := instance + "_new"
    			clusterConfig.Configs = conf.Redis[dualInstance]
    			if len(clusterConfig.Configs) > 0 {
    				dualWriteCluster := newRedisCluster(dualInstance, clusterConfig)
    				currentCluster.SetDualWrite(dualWriteCluster)
    				logger.Infof(nil, "set redis dual write to %v", instance)
    			}
    
    			redisClusterMap[instance] = currentCluster
    		}(name)
    	}
    
    複製程式碼
  3. 增加開關控制,預設開啟雙寫開關
    這點需要重點說明一下,在實際工程應用中,我們的專案可能有部分功能需要再某個版本啟用,某個版本棄用;或者某個新增的功能,為了防止異常需要能夠有個開關配置,隨時可以開啟這個功能或者關閉這個功能;或者在流量高峰,我們需要關閉掉或者降級某個功能。諸如這型別的需求,一個比較推薦的做法就是增加開關配置,全域性的開關,抽象出一個開關模型出來。

    如:

    
    type Switch struct {
    	Name      string
    	On        bool
    	listeners []ChangeListener
    }
    
    func (s *Switch) TurnOn() {
    	s.On = true
    	s.notifyListeners()
    }
    
    func (s *Switch) TurnOff() {
    	s.On = false
    	s.notifyListeners()
    }
    
    var AsyncProcedure = &Switch{Name: "demo.msg.procedure.async", On: true}
    
    當我們開啟開關的時候執行
    if switches.AsyncProcedure.IsOn() {
      
    }  
    
    複製程式碼
  4. client操作的時候redis例項的時候,如寫資料的時候,對每一個操作都進行雙寫處理

    func (r *Cluster) ZAdd(key string, scoremembers ...interface{}) (int, error) {
    	if len(scoremembers)%2 != 0 {
    		return 0, fmt.Errorf("zadd for %v expects even number of score members", key)
    	}
    	// 如果雙寫開關開啟,並且有雙寫的例項,就非同步寫這個新的例項
    	if r.dualWrite != nil && r.writeDual {
    		go r.dualWrite.ZAdd(key, scoremembers...)
    	}
    	args := append([]interface{}{key}, scoremembers...)
    	return redis.Int(r.doWrite(r.getClient(key), "ZADD", args...))
    }
    複製程式碼

    這樣之後就開始了雙寫,然後需要做的就是check資料

  5. 做一個check工具

    這個要分為兩步走,首先,同步老的資料到新的叢集裡面;同步完之前,要 通過check 工具校驗所有資料是否相等,並進行相關補償調整

  6. 所有這些步驟搞定後,當check完資料後,我們就可以再在配置裡面去掉老一致性hash的配置,只保留新的hash取模的配置

    
        [redis.gunread]  // 把原有的配置的server地址換為_new的地址
        shard = "compat"
        servers = ["192.168.xxx.xxx:6378;;1", "192.168.xxx.xxx:6379;;1"]
        
        [redis.gunread_new]  // 去掉這個_new的配置
        shard = "compat"
        servers = ["192.168.xxx.xxx:6380;;1", "192.168.xxx.xxx:6381;;1"]
        
    複製程式碼

相關文章