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

[TOC]redis

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

需求場景

背景

對於redis集羣而言,通常業務方使用的時候,會在服務端對key作hash策略,hash算法通常能夠分爲:一致性hash、hash取模等,固然還有其餘經常使用算法。一致性hash在擴縮容的時候比較麻煩,所以公司層面要求都要使用hash取模,然而,若是當前線上已是一致性hash,那麼要更改hash算法爲hash取模,那麼咱們該如何作?算法

可能的解決方案

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

實現方案

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

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

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

  • 業務上雙寫一致性hash的2個節點和取模2個節點,此時,取模節點裏面的數據是新寫的數據,只寫不讀併發

  • 經過寫遷移工具,掃描全部一致性hash的節點的列表(key列表),從一致性hash節點get數據,而後set到取模節點。這種狀況理論上會出現瞬間的併發問題(好比get後有新數據,最終set進去老數據,不過只是在瞬間會產生),不過不要緊,即使有髒數據(數據不一致),也會再下一步的check工具裏面處理好。app

  • 數據驗證和check工具修復異步

    • 這個check的時候,不會有問題,由於check只是check舊的數據,對於新寫入的數據都是最新的,由於新舊節點都是雙寫的
    • 曾經剛開始的時候有想過,若是 check的時候產生了新數據怎麼辦,可是實際上是多餘的,這個狀況是OK的。
  • 業務切換讀到新的取模節點async

    • 這個最終都是須要業務層調整代碼,使用新的集羣或者方案

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

以下代碼來源於閃聊項目,也是閃聊實際經歷過切換方案工具

  1. 配置裏面, 針對須要進行調整的redis實例,增長新的redis實例配置(取模相關),以下ui

    [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"]
        
    複製代碼

【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】

個人微信公衆號
相關文章
相關標籤/搜索