本文是使用 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 值放置在环上,顺时针查找,将数据放在找到的的第一个节点上。

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

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

新增 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)
}

Golang 实现 Redis(7): Redis 集群与一致性 Hash的更多相关文章

  1. Redis操作及集群搭建以及高可用配置

    NoSQL - Redis 缓存技术 Redis功能介绍 数据类型丰富 支持持久化 多种内存分配及回收策略 支持弱事务 支持高可用 支持分布式分片集群 企业缓存产品介绍 Memcached: 优点:高 ...

  2. Redis存储Tomcat集群的Session

    Redis存储Tomcat集群的Session 如何 做到把新开发的代码推送到到生产系统中部署,生产系统要能够零宕机.对使用用户零影响. 设想 是使用集群来搞定,通过通知负载均衡Nginx,取下集群中 ...

  3. Redis 3.0 集群搭建

    Redis 3.0 集群搭建 开启两个虚拟机 分别在两个虚拟机上开启3个Redis实例 3主3从两个虚拟机里的实例互为主备 下面分别在两个虚拟机上安装,网络设置参照codis集群的前两个主机 分别关闭 ...

  4. Redis 3.0集群 Window搭建方案

    Redis 3.0集群 Window搭建方案 1.集群安装前准备 安装Ruby环境,安装:rubyinstaller-2.3.0-x64.exe http://dl.bintray.com/onecl ...

  5. Redis+Tomcat+Nginx集群实现Session共享,Tomcat Session共享

    Redis+Tomcat+Nginx集群实现Session共享,Tomcat Session共享 ============================= 蕃薯耀 2017年11月27日 http: ...

  6. Redis 高可用集群

    Redis 高可用集群 Redis 的集群主从模型是一种高可用的集群架构.本章主要内容有:高可用集群的搭建,Jedis连接集群,新增集群节点,删除集群节点,其他配置补充说明. 高可用集群搭建 集群(c ...

  7. Redis进阶实践之十 Redis主从复制的集群模式

    一.引言        Redis的基本数据类型,高级特性,与Lua脚本的整合等相关知识点都学完了,说是学完了,只是完成了当前的学习计划,在以后的时间还需继续深入研究和学习.从今天开始来讲一下有关Re ...

  8. Redis进阶实践之十一 Redis的Cluster集群搭建

    一.引言      本文档只对Redis的Cluster集群做简单的介绍,并没有对分布式系统的详细概念做深入的探讨.本文只是提供了有关如何设置集群.测试和操作集群的说明,而不涉及Redis集群规范中涵 ...

  9. Redis进阶实践之十二 Redis的Cluster集群动态扩容

    一.引言     上一篇文章我们一步一步的教大家搭建了Redis的Cluster集群环境,形成了3个主节点和3个从节点的Cluster的环境.当然,大家可以使用 Cluster info 命令查看Cl ...

随机推荐

  1. openresty下安装luarocks

    wget https://luarocks.org/releases/luarocks-2.4.1.tar.gz tar -xzvf luarocks-2.4.1.tar.gz cd luarocks ...

  2. Java IO相关使用

    date: 2020-06-14 14:42:22 updated: 2020-08-21 17:35:45 Java IO相关使用 1. 文件 创建 File 对象的三种方式 一个路径名:File( ...

  3. 字体图标:Font Awesome

    小图标 Font Awesome Font Awesome 字体为您提供可缩放矢量图标,它可以被定制大小.颜色.阴影以及任何可以用 CSS 的样式,是一款惊艳的字体图标! 可以前往官网进行学习 Fon ...

  4. 蒲公英 · JELLY技术周刊 Vol 27: 平平无奇 React 17

    蒲公英 · JELLY技术周刊 Vol.27 这个热闹的十月终于要走到尾声,React 17 历经 4 个 RC 版本之后,也于数天前正式发布了,而同在几天前发布的 CRA 4.0 也已经完成了 Re ...

  5. web worker的介绍和使用

    目录 简介 Web Workers的基本概念和使用 Web Workers的分类 worker和main thread之间的数据传输 简介 什么是web worker呢?从名字上就可以看出,web w ...

  6. 【总结】docker

    1 docker概述 1.1 docker简介 Docker目标是实现轻量级的操作系统虚拟化解决方案.比虚拟机更轻量级. 虚拟机可以理解成一栋楼里的一个个房间(共享花园基地等),而docker可以理解 ...

  7. JAVA类库之——Math类(持续更新)

    Math类 目录 Math类 1.Math 类中的常量方法 返回π(圆周率)值的方法:PI 返回E(自然对数低)值的方法:E 2.Math 类中的三角函数方法 计算正弦函数值的方法:Sin(radia ...

  8. 使用Azure静态Web应用部署Blazor Webassembly应用

    上一次演示了如何使用Azure静态web应用部署VUE前端项目(使用 Azure静态web应用+Github全自动部署VUE站点).我们知道静态web应用支持VUE,react,angular等项目的 ...

  9. django配置跨域并开发测试接口

    1.创建一个测试项目 1.1 创建项目和APP django-admin startproject BookManage # 创建项目 python manage.py startapp books ...

  10. ASP.NET Core Authentication系列(四)基于Cookie实现多应用间单点登录(SSO)

    前言 本系列前三篇文章分别从ASP.NET Core认证的三个重要概念,到如何实现最简单的登录.注销和认证,再到如何配置Cookie 选项,来介绍如何使用ASP.NET Core认证.感兴趣的可以了解 ...