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)
}
 响应结果:

心跳检查和其他loadbalance机制后续补充实现。

项目地址:

https://github.com/zhangxiaomin1993/gedis

go语言的redis客户端的更多相关文章

  1. Redis客户端——Jedis的使用

    本文介绍基于Java语言的Redis客户端——Jedis的使用,包括Jedis简介.获取Jedis.Jedis直连.Jedis连接池以及二者的对比的选择. Jedis简介 Jedis 是 Redis  ...

  2. 用C、python手写redis客户端,兼容redis集群 (-MOVED和-ASK),快速搭建redis集群

    想没想过,自己写一个redis客户端,是不是很难呢? 其实,并不是特别难. 首先,要知道redis服务端用的通信协议,建议直接去官网看,博客啥的其实也是从官网摘抄的,或者从其他博客抄的(忽略). 协议 ...

  3. Redis客户端之Spring整合Jedis,ShardedJedisPool集群配置

    Jedis设计 Jedis作为推荐的java语言redis客户端,其抽象封装为三部分: 对象池设计:Pool,JedisPool,GenericObjectPool,BasePoolableObjec ...

  4. redis客户端(三)

    redis客户端 一.>redis自带的客户端 启动 启动客户端命令:[root@ming bin]# ./redis-cli -h xxx.xxx.xx.xxx-p 6379 注意: -h:指 ...

  5. Java语言访问Redis数据库之Set篇

    如果想通过Java语言对Redis数据库进行访问. 首先,需要安装Redis数据库,可以是Windows系统,或者Linux系统.(本文以Windows系统的本地Redis数据库为例,代码说明如何操作 ...

  6. Redis学习笔记--Redis客户端(三)

    1.Redis客户端 1.1 Redis自带的客户端 (1)启动 启动客户端命令:[root@kwredis bin]# ./redis-cli -h 127.0.0.1 -p 6379 -h:指定访 ...

  7. spring 5.x 系列第8篇 —— 整合Redis客户端 Jedis和Redisson (代码配置方式)

    文章目录 一.说明 1.1 Redis 客户端说明 1.2 Redis可视化软件 1.3 项目结构说明 1.3 依赖说明 二.spring 整合 jedis 2.1 新建基本配置文件和其映射类 2.2 ...

  8. spring 5.x 系列第7篇 —— 整合Redis客户端 Jedis和Redisson (xml配置方式)

    文章目录 一.说明 1.1 Redis 客户端说明 1.2 Redis可视化软件 1.3 项目结构说明 1.3 依赖说明 二.spring 整合 jedis 2.1 新建基本配置文件 2.2 单机配置 ...

  9. 一文彻底理解Redis序列化协议,你也可以编写Redis客户端

    前提 最近学习Netty的时候想做一个基于Redis服务协议的编码解码模块,过程中顺便阅读了Redis服务序列化协议RESP,结合自己的理解对文档进行了翻译并且简单实现了RESP基于Java语言的解析 ...

随机推荐

  1. uva 10189 扫雷

    简单的输入 判断周围上下左右组合的八个方向的雷 然后输出 代码 #include <iostream> #include <memory.h> using namespace ...

  2. C++ 标准库,可变参数数量,参数类型相同

    #include <iostream> // 可变模板参数 // 此例:可以构造可变数量,可变类型的函数输入. // 摘自:https://www.cnblogs.com/qicosmos ...

  3. 3. 语法"陷阱"

    1. C运算符优先级 运算符(优先级从高到低) 结合律 ++(后置).--(后置).()(函数调用).[].{}.(复合字面量).. .-> 从左往右 ++(前置).--(前置).-.+.~.! ...

  4. 分析Runtime的属性Property

    一.介绍 在OC中我们可以给任意的一个类以@property的格式声明属性,当然对于这个属性也会采用某一些属性关键字进行修饰,那么属性的真正的面目是啥样子的呢?其实,runtime源码中可以看到,pr ...

  5. php date获取前一天的时间

    结果: 结论: 第二种方式只使用了一个函数,所以更快一些,速度大约是第一种的两倍

  6. 转载-ThreadPoolExecutor里面4种拒绝策略(详细)

    原文链接:https://blog.csdn.net/wjs19930820/article/details/79849050 1 /** * 定义异步任务执行线程池 */ @Configuratio ...

  7. Java修饰符作用域

    作用域 当前类 同一package 子孙类 其他package public √ √ √ √ protected √ √ √ × friendly √ √ × × private √ × × × 修饰 ...

  8. idea插件备份

  9. 知识图谱辅助金融领域NLP任务

    从人工智能学科诞生之初起,自然语言处理(NLP)就是人工智能核心的研究问题之一.NLP的重要性是毋庸置疑的,它能够实现以自然语言交流为特征的高级人机交互,使机器能“阅读”所有以文字形式记录的人类知识, ...

  10. Docker - 快速入门(一)

    概念 下面这三个概念一开始可能不好理解,等大家跟着博客把例子做完了,再回头来看应该就能理解了. docker image  # docker镜像 镜像就是一个只读的模板.镜像可以用来创建Docker容 ...