Golang 实现 Redis(7): 集群与一致性 Hash

本文是使用 golang 实现 redis 系列的第七篇, 将介绍如何将单点的缓存服务器扩展为分布式缓存。godis 集群的源码在Github:Godis/cluster

单台服务器的CPU和内存等资源总是有限的,随着数据量和访问量的增加单台服务器很容易遇到瓶颈。利用多台机器建立分布式系统,分工处理是提高系统容量和吞吐量的常用方法。

使用更多机器来提高系统容量的方式称为系统横向扩容。与之相对的,提高单台机器性能被称为纵向扩容。由于无法在单台机器上无限提高硬件配置且硬件价格与性能的关系并非线性的,所以建立分布式系统进行横向扩容是更为经济实用的选择。

我们采用一致性 hash 算法 key 分散到不同的服务器,客户端可以连接到服务集群中任意一个节点。当节点需要访问的数据不在自己本地时,需要通过一致性 hash 算法计算出数据所在的节点并将指令转发给它。

与分布式系统理论中的分区容错性不同,我们仅将数据存在一个节点没有保存副本。这种设计提高了系统吞吐量和容量,但是并没有提高系统可用性,当有一个节点崩溃时它保存的数据将无法访问。

生产环境实用的 redis 集群通常也采取类似的分片存储策略,并为每个节点配置从节点作为热备节点,并使用 sentinel 机制监控 master 节点状态。在 master 节点崩溃后,sentinel 将备份节点提升为 master 节点以保证可用性。

一致性 hash 算法

为什么需要一致性 hash

在采用分片方式建立分布式缓存时,我们面临的第一个问题是如何决定存储数据的节点。最自然的方式是参考 hash 表的做法,假设集群中存在 n 个节点,我们用 node = hashCode(key) % n 来决定所属的节点。

普通 hash 算法解决了如何选择节点的问题,但在分布式系统中经常出现增加节点或某个节点宕机的情况。若节点数 n 发生变化, 大多数 key 根据 node = hashCode(key) % n 计算出的节点都会改变。这意味着若要在 n 变化后维持系统正常运转,需要将大多数数据在节点间进行重新分布。这个操作会消耗大量的时间和带宽等资源,这在生产环境下是不可接受的。

算法原理

一致性 hash 算法的目的是在节点数量 n 变化时, 使尽可能少的 key 需要进行节点间重新分布。一致性 hash 算法将数据 key 和服务器地址 addr 散列到 2^32 的空间中。

我们将 2^32 个整数首尾相连形成一个环,首先计算服务器地址 addr 的 hash 值放置在环上。然后计算 key 的 hash 值放置在环上,顺时针查找,将数据放在找到的的第一个节点上。

Golang 实现 Redis(7): 集群与一致性 Hash

key1, key2 和 key5 在 node2 上,key 3 在 node4 上,key4 在 node6 上

在增加或删除节点时只有该节点附近的数据需要重新分布,从而解决了上述问题。

Golang 实现 Redis(7): 集群与一致性 Hash

新增 node8 后,key 5 从 node2 转移到 node8。其它 key 不变

如果服务器节点较少则比较容易出现数据分布不均匀的问题,一般来说环上的节点越多数据分布越均匀。我们不需要真的增加一台服务器,只需要将实际的服务器节点映射为几个虚拟节点放在环上即可。

Golang 实现一致性 Hash

我们使用 Golang 实现一致性 hash 算法, 源码在 Github: HDT3213/Godis, 大约 80 行代码。

type HashFunc func(data []byte) uint32

type Map struct {
    hashFunc HashFunc
    replicas int
    keys     []int // sorted
    hashMap  map[int]string
}

func New(replicas int, fn HashFunc) *Map {
    m := &Map{
        replicas: replicas, // 每个物理节点会产生 replicas 个虚拟节点
        hashFunc: fn,
        hashMap:  make(map[int]string), // 虚拟节点 hash 值到物理节点地址的映射
    }
    if m.hashFunc == nil {
        m.hashFunc = crc32.ChecksumIEEE
    }
    return m
}

func (m *Map) IsEmpty() bool {
    return len(m.keys) == 0
}

接下来实现添加物理节点的 Add 方法:

func (m *Map) Add(keys ...string) {
    for _, key := range keys {
        if key == "" {
            continue
        }
        for i := 0; i < m.replicas; i++ {
            // 使用 i + key 作为一个虚拟节点,计算虚拟节点的 hash 值
            hash := int(m.hashFunc([]byte(strconv.Itoa(i) + key)))
            // 将虚拟节点添加到环上
            m.keys = append(m.keys, hash)
            // 注册虚拟节点到物理节点的映射
            m.hashMap[hash] = key
        }
    }
    sort.Ints(m.keys)
}

接下来实现查找算法:

func (m *Map) Get(key string) string {
    if m.IsEmpty() {
        return ""
    }

    // 支持根据 key 的 hashtag 来确定分布
    partitionKey := getPartitionKey(key)
    hash := int(m.hashFunc([]byte(partitionKey)))

    // sort.Search 会使用二分查找法搜索 keys 中满足 m.keys[i] >= hash 的最小 i 值
    idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })

    // 若 key 的 hash 值大于最后一个虚拟节点的 hash 值,则 sort.Search 找不到目标
    // 这种情况下选择第一个虚拟节点
    if idx == len(m.keys) {
        idx = 0
    }

    // 将虚拟节点映射为实际地址
    return m.hashMap[m.keys[idx]]
}

实现集群

实现了一致性 hash 算法后我们可以着手实现集群模式了,Godis 集群的代码在 Github:Godis/cluster

集群最核心的逻辑是找到 key 所在节点并将指令转发过去:

// 集群模式下,除了 MSet、DEL 等特殊指令外,其它指令会交由 defaultFunc 处理
func defaultFunc(cluster *Cluster, c redis.Connection, args [][]byte) redis.Reply {
    key := string(args[1])
    peer := cluster.peerPicker.Get(key) // 通过一致性 hash 找到节点
    return cluster.Relay(peer, c, args)
}

func (cluster *Cluster) Relay(peer string, c redis.Connection, args [][]byte) redis.Reply {
    if peer == cluster.self { // 若数据在本地则直接调用数据库引擎
        // to self db
        return cluster.db.Exec(c, args)
    } else {
        // 从连接池取一个与目标节点的连接
        // 连接池使用 github.com/jolestar/go-commons-pool/v2 实现
        peerClient, err := cluster.getPeerClient(peer)
        if err != nil {
            return reply.MakeErrReply(err.Error())
        }
        defer func() {
            _ = cluster.returnPeerClient(peer, peerClient) // 处理完成后将连接放回连接池
        }()
        // 将指令发送到目标节点
        return peerClient.Send(args)
    }
}

func (cluster *Cluster) getPeerClient(peer string) (*client.Client, error) {
    connectionFactory, ok := cluster.peerConnection[peer]
    if !ok {
        return nil, errors.New("connection factory not found")
    }
    raw, err := connectionFactory.BorrowObject(context.Background())
    if err != nil {
        return nil, err
    }
    conn, ok := raw.(*client.Client)
    if !ok {
        return nil, errors.New("connection factory make wrong type")
    }
    return conn, nil
}

func (cluster *Cluster) returnPeerClient(peer string, peerClient *client.Client) error {
    connectionFactory, ok := cluster.peerConnection[peer]
    if !ok {
        return errors.New("connection factory not found")
    }
    return connectionFactory.ReturnObject(context.Background(), peerClient)
}

Original: https://www.cnblogs.com/Finley/p/14038398.html
Author: -Finley-
Title: Golang 实现 Redis(7): 集群与一致性 Hash

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/529388/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Mysql 安全加固经验总结

    本文为博主原创,转载请注明出处: 1.内网部署Mysql mysql 数据库在使用过程中,需要给服务提供连接和访问的权限,而不需要进行公网连接和访问,所以在安全环境和现网环境部署m…

    Linux 2023年6月14日
    087
  • Centos7 找回root密码

    在开机界面,按”e”进入编辑界面 按”e”进图下图界面后,找到开头为”linux16″行,在行后面加入 &#…

    Linux 2023年5月27日
    0130
  • 【微服务】- Nacos-注册中心

    微服务 – 注册中心 – Nacos 😄生命不息,写作不止🔥 继续踏上学习之路,学之分享笔记👊 总有一天我也能像各位大佬一样🏆 一个有梦有戏的人 @怒放吧德…

    Linux 2023年6月6日
    0123
  • Linux上安装并启动tomcat

    1、下载tomcat安装包 官网链接:https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.57/bin/ 2、将tomcat上…

    Linux 2023年6月6日
    0107
  • Windows 11 绕过 TPM 方法

    在 Windows 11 安装界面按 Shift + F10 打开命令行界面,执行如下命令: REG ADD HKLM\SYSTEM\Setup\LabConfig /v Bypa…

    Linux 2023年6月13日
    093
  • CentOS8 AnolisOS8 yum安装 No match for argument: htop Error: Unable to find a match: htop

    镜像下载、域名解析、时间同步请点击阿里云开源镜像站 CentOS8 AnolisOS8 yum安装失败 今天有人反馈服务器卡,登录上服务器,想看下CPU以及内存使用情况,觉得top…

    Linux 2023年5月27日
    062
  • Xshell小技巧

    鼠标右键粘贴 工具->选项->鼠标->向右按钮->(paste the clipboard contents.) 选定文本自动复制到剪贴板 工具->选…

    Linux 2023年5月28日
    0104
  • DQL

    查询语法 select 字段列表 from 表名列表 where 条件列表 group by 分组字段 having 分组后条件 order by 排序字段 limit 分页限定 …

    Linux 2023年6月7日
    063
  • 设计模式——行为型设计模式

    行为型设计模式 针对对象之间的交互 解释器模式 java中用的很。JVM编译的时候就是对我们写的代码进行了解释操作;数据库SQL语句亦是如此 解释器:对语言进行解释,根据不同语义来…

    Linux 2023年6月7日
    0100
  • docker网络模型

    [root@iZuf620p8rsr3faul3zsx6Z ~]# docker network –help Usage: docker network COMMAND Mana…

    Linux 2023年6月13日
    0107
  • python操作MySQL;MySQL补充知识

    目录 1.python操作MySQL 2.SQL注入及解决方式 3.二次确认 4.修改表SQL语句补充 5.视图 6.触发器 7.事务 8.存储过程 9.函数 10.流程控制 11…

    Linux 2023年6月7日
    089
  • podman对容器映像签名和分发

    熟悉podman 如何使用 Podman 对容器映像进行签名和分发 熟悉podman 此示例容器将运行一个非常基本的 httpd 服务器,该服务器仅为其索引页提供服务 [root@…

    Linux 2023年6月7日
    091
  • java反射机制

    1..获取Class实例的方式 1 @Test 2 public void test3() throws ClassNotFoundException { 3 //方式一:调用运行…

    Linux 2023年6月6日
    0106
  • ThinkPHP5 远程命令执行漏洞

    一、ThinkPHP介绍 轻量级框架,内部OOP和面向过程代码都存在,是国人自己开发的框架。ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架,诞生于2006年初,…

    Linux 2023年6月14日
    068
  • JAVA设计模式-工厂模式

    JAVA设计模式-工厂模式 简单工厂模式 介绍 简单工厂模式就是定义一个工厂类,工厂类提供获取实例的方法,方法会根据传入的参数不同来返回不同的实例。不同的实例基本都有共同的父类。对…

    Linux 2023年6月6日
    085
  • Nginx/Tengine安装配置详解

    1 概念 Nginx (“engine x”) 是一个高性能的 HTTP 和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。官方测试…

    Linux 2023年5月27日
    0120
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球