[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取模方式的具體實現
如下程式碼來源於閃聊專案,也是閃聊實際經歷過切換方案
-
配置裡面, 針對需要進行調整的redis例項,增加新的redis例項配置(取模相關),如下
[redis.gunread_new] shard = "compat" servers = ["192.168.xxx.xxx:6380;;1", "192.168.xxx.xxx:6381;;1"] 複製程式碼
-
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) } 複製程式碼
-
增加開關控制,預設開啟雙寫開關
這點需要重點說明一下,在實際工程應用中,我們的專案可能有部分功能需要再某個版本啟用,某個版本棄用;或者某個新增的功能,為了防止異常需要能夠有個開關配置,隨時可以開啟這個功能或者關閉這個功能;或者在流量高峰,我們需要關閉掉或者降級某個功能。諸如這型別的需求,一個比較推薦的做法就是增加開關配置,全域性的開關,抽象出一個開關模型出來。如:
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() { } 複製程式碼
-
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資料
-
做一個check工具
這個要分為兩步走,首先,同步老的資料到新的叢集裡面;同步完之前,要 通過check 工具校驗所有資料是否相等,並進行相關補償調整
-
所有這些步驟搞定後,當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"] 複製程式碼