gedis:自己實現go語言的redis客戶端 第五節(集羣模式的支持)

redis3.0之後提供了新的HA的解決方案,即Cluster模式,由多個節點組成的集羣模式。集羣master之間基於crc16算法,對key進行校驗,得到的值對16384取餘,就是key的hash slot(槽)值,每個節點各自存儲一部分的hash槽值,主從節點之間基於異步複製方式同步數據。

基於redis集羣的基本原理,gedis需要提供一下方面的能力:

1、統一的客戶端Cluster;

2、集羣連接池的實現;

3、集羣節點的健康檢查(後續實現);

4、負載均衡機制實現;

5、協議的封裝保證對上層透明。

模型基本設計如下:

模型設計

基礎模型定義

/**
 * 節點
 * master:主節點ip+port
 * slaves:從節點ip+port集合
 */
type Node struct {
	Url        string
	Pwd        string
	InitActive int
}

type ClusterConfig struct {
	Nodes             []*Node
	HeartBeatInterval int
}

/**
 * 集羣客戶端
 * heartBeatInterval 心跳檢測時間間隔,單位s
 * clusterPool key:連接串 value:連接池
 */
type Cluster struct {
	config      *ClusterConfig
	clusterPool map[string]*ConnPool
}

 Cluster初始化

/**
 * 初始化Cluster client
 */
func NewCluster(clusterConfig ClusterConfig) *Cluster {
	nodes := clusterConfig.Nodes

	var cluster Cluster
	clusterPool := make(map[string]*ConnPool)

	for _, node := range nodes {
		var config = ConnConfig{node.Url, node.Pwd}
		pool, _ := NewConnPool(node.InitActive, config)
		clusterPool[node.Url] = pool
	}
	cluster.config = &clusterConfig
	cluster.clusterPool = clusterPool
	//初始化節點健康檢測線程
	defer func() {
		go cluster.heartBeat()
	}()
	if m==nil {
		m = new(sync.RWMutex)
	}
	return &cluster
}

節點心跳檢測

cluster創建後,開啓異步線程定時輪詢各個節點,向節點發出ping請求,若未響應pong,則表示當前節點異常,然後將當前節點退出連接池,並將該節點加入失敗隊列,定時輪詢隊列,檢測是否恢復連接,若恢復,則重新創建連接池,從失敗隊列中退出當前節點。

/**
 * 連接池心跳檢測,定時ping各個節點,ping失敗的,從連接池退出,並將節點加入失敗隊列
 * 定時輪詢失敗節點隊列,檢測節點是否已恢復連接,若恢復,則重新創建連接池,並從失敗隊列中移除
 */
func (cluster *Cluster) heartBeat() {
	clusterPool := cluster.GetClusterPool()
	interval := cluster.config.HeartBeatInterval
	if interval <= 0 {
		interval = defaultHeartBeatInterval
	}
	var nodes = make(map[string]*Node)

	for i := 0; i < len(cluster.GetClusterNodesInfo()); i++ {
		node := cluster.GetClusterNodesInfo()[i]
		nodes[node.Url] = node
	}

	var failNodes = make(map[string]*Node)
	for {
		for url, pool := range clusterPool {
			result, err := executePing(pool)
			if err != nil {
				log.Printf("節點[%s] 健康檢查異常,原因[%s], 節點將被移除\n", url, err)
				//加鎖
				m.Lock()
				time.Sleep(time.Duration(5)*time.Second)
				failNodes[url] = nodes[url]
				delete(clusterPool, url)
				m.Unlock()
			} else {
				log.Printf("節點[%s] 健康檢查結果[%s]\n", url, result)
			}
		}
		//恢復檢測
		recover(failNodes, clusterPool)

		time.Sleep(time.Duration(interval) * time.Second)
	}
}

/**
 * 檢測fail節點是否已恢復正常
 */
func recover(failNodes map[string]*Node, clusterPool map[string]*ConnPool) {
	for url,node:=range failNodes{
		conn := Connect(url)
		if conn != nil {
			//節點重連,恢復連接
			var config = ConnConfig{url, node.Pwd}
			pool, _ := NewConnPool(node.InitActive, config)
			//加鎖
			m.Lock()
			clusterPool[node.Url] = pool
			delete(failNodes,url)
			m.Unlock()
			log.Printf("節點[%s] 已重連\n", url)
		}
	}
}

 測試結果:

loadbalance目前僅實現隨機模式,每次訪問前隨機選擇一個節點進行通信

func (cluster *Cluster) RandomSelect() *ConnPool {
	m.RLock()
	defer m.RUnlock()
	pools := cluster.GetClusterPool()
	for _,pool:= range pools{
		if pool !=nil{
			return pool
		}
	}
	fmt.Errorf("none pool can be used")
	return nil
}

通信模塊的大致流程如下:

1、cluster隨機選擇一個健康的節點,進行訪問;

2、如果節點返回業務數據則通信結束;

3、如果節點返回的消息協議上滿足“-MOVED”,例如 -MOVED 5678 127.0.0.1,則表明當前數據不在該節點;

4、重定向到redis指定的節點訪問;

func (cluster *Cluster) Set(key string, value string) (interface{}, error) {
	result, err := executeSet(cluster.RandomSelect(), key, value)
	if err.Error() != protocol.MOVED {
		return result, err
	}

	//重定向到新的節點
	return executeSet(cluster.SelectOne(result.(string)), key, value)
}

func executeSet(pool *ConnPool, key string, value string) (interface{}, error) {
	conn, err := GetConn(pool)
	if err != nil {
		return nil, fmt.Errorf("get conn fail")
	}
	defer pool.PutConn(conn)
	result := SendCommand(conn, protocol.SET, protocol.SafeEncode(key), protocol.SafeEncode(value))
	return handler.HandleReply(result)
}

這樣,對於應用層來講,無論訪問的哪個節點,都能得到最終的結果,相對是透明的。

調用測試:

package main

import (
	. "client"
	"net"
	"fmt"
)

func main() {
	var node7000 = Node{"127.0.0.1:7000", "123456", 10}
	var node7001 = Node{"127.0.0.1:7001", "123456", 10}
	var node7002 = Node{"127.0.0.1:7002", "123456", 10}
	var node7003 = Node{"127.0.0.1:7003", "123456", 10}
	var node7004 = Node{"127.0.0.1:7004", "123456", 10}
	var node7005 = Node{"127.0.0.1:7005", "123456", 10}

	nodes := []*Node{&node7000, &node7001, &node7002, &node7003, &node7004, &node7005}
	var clusterConfig = ClusterConfig{nodes,10}
	cluster := NewCluster(clusterConfig)
	value,err:=cluster.Get("name")
	fmt.Println(value, err)
}

 響應結果:

項目地址:

https://github.com/zhangxiaomin1993/gedis

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章