现在有一个场景:要用Redis保存5000万个键值对,每个键值对大约是512B,要怎么部署Redis服务呢?

第一个方案,也是最容易想到的,需要保存5000万个键值对,每个键值对约为512B,一共需要25GB空间,选择一台32GB内存的用品来部署Redis,还剩余7GB空间,可以采用RDB对数据做持续久。

但是Redis服务使用不久后出现Redis的响应有时会非常慢。原因是采用了RDB持久化,在前面介绍RDB原理时,我们知道fork子进程的瞬间会阻塞主线程,而且内存越大,阻塞越长。

第一个方案不太适合,那么有更好的方案吗?Redis提供切片集群机制,多个Redis实例组成一个集群,按照一定的规则,把收到数据划分成多份,每一份用一个实例来保存。这样一来,在生成RDB时,数据量就小了,fork就不会阻塞主线程太长时间。

这里就引出一个问题:该如何保存更多的数据?

如何保存更多数据

通常有两种方案,分别是纵向扩展和横向扩展。

纵向扩展,指通过增加硬件配置来扩展,采用更大的内存,更多的CPU。好处是实施简单,但缺点是受到硬件和成本的限制,不可能无限扩展。

横向扩展,指通过增加机器来组成更大的集群,这也是分布式方案常用的方式。好处是扩展性好,但缺点是管理复杂。

在面向百万、千万级别的用户规模时,横向扩展的Redis切片集群会是一个非常好的选择。

在使用单个实例时,数据保存在哪里,客户端访问哪里,都是非常明确的。但是切片集群不可避免要解决多个实例分布式管理的问题,需要解决两大问题:

  • 数据切片后,在多个实例之间如何分布?
  • 客户端怎么确定想要访问的数据在哪个实例上?

数据切片和实例的对应分布关系

在Redis 3.0之前,官方没有切片集群的方案,从3.0开始,官方提供了一个名为Redis Cluster的方案,用于实现切片集群。

Redis Cluster方案采用哈希槽来处理数据和实例之间的映射关系。这里有两个映射关系:键值对与哈希槽的映射关系和哈希槽与实例的映射关系。下面我们来介绍一下这两个映射关系的映射过程。

键值对与哈希槽的映射过程

根据键值对的key,按照CRC16算法计算一个16bit的值。

再用这个16bit值对16384取模,得到0~16383范围内的模数,每个模数代表一个相应编号的哈希槽。

说明:Redis切片集群最多提供16384个哈希槽。

哈希槽与实例的映射过程

哈希槽与实例的映射关系有两个方案设置,分为自动和手动。

自动映射:使用cluster create命令创建集群,Redis会自动把这些槽平均分布在集群实例上。

手动映射:使用cluster meet命令搬运建立实例间的连接,形成集群,再使用cluster addslots命令,指定每个实例上的哈希槽个数。

说明:在手动分配哈希槽时,需要把16384个槽都分配完,否则Redis集群无法正常工作。

客户端如何定位数据

客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。

集群刚创建时,实例如何互相知道哈希槽信息?Redis实例会扩展哈希槽信息,每个Redis实例都拥有完整的哈希槽信息。

另外,客户端收到哈希槽信息后,会缓存在本地,以便在客户端后续请求直接访问实例。

但在集群中,实例和哈希槽的对应关系不是一成不变的。最常见的变化:

  • 在集群中,实例有新增或删除,Redis需要重新分配哈希槽;
  • 为了负载均衡,Redis需要把哈希槽在所有实例上重新分布一遍。

Redis Cluster提供一种重定向机制,类似于HTTP协议的重定向。

客户端把一个键值对操作请求发给一个实例,如果这个实例没有这个键值对映射的哈希槽,这个实例就会给客户端返回MOVED命令的响应结果,包含新实例的访问地址。

GET hello:key (error)
MOVED 13320 172.16.19.5:6379

其中,MOVED命令表示,客户端请求的键值对所在的哈希槽13320,实际是在172.16.19.5这个实例上。

如果哈希槽没有完成迁移,客户端请求的数据并不在哈希槽时,客户端就会收到一条ASK报错信息,如下所示:

GET hello:key (error)
ASK 13320 172.16.19.5:6379

这个结果中的ASK命令就表示,客户端请求的键值对所在的哈希槽13320,在172.16.19.5这个实例上,但是这个哈希槽正在迁移。

和MOVED命令不同,ASK命令并不会更新客户端缓存的哈希槽分配信息。

Redis Cluster为什么不采用把key直接映射到实例的方式

整个集群存储key的数量是无法预估的,key的数量非常多时,直接记录每个key对应的实例映射关系,这个映射表会非常庞大,这个映射表无论是存储在服务端还是客户端都占用了非常大的内存空间。

Redis Cluster采用无中心化的模式(无proxy,客户端与服务端直连),客户端在某个节点访问一个key,如果这个key不在这个节点上,这个节点需要有纠正客户端路由到正确节点的能力(MOVED响应),这就需要节点之间互相交换路由表,每个节点拥有整个集群完整的路由关系。如果存储的都是key与实例的对应关系,节点之间交换信息也会变得非常庞大,消耗过多的网络资源,而且就算交换完成,相当于每个节点都需要额外存储其他节点的路由表,内存占用过大造成资源浪费。

当集群在扩容、缩容、数据均衡时,节点之间会发生数据迁移,迁移时需要修改每个key的映射关系,维护成本高。

而在中间增加一层哈希槽,可以把数据和节点解耦,key通过Hash计算,只需要关心映射到了哪个哈希槽,然后再通过哈希槽和节点的映射表找到节点,相当于消耗了很少的CPU资源,不但让数据分布更均匀,还可以让这个映射表变得很小,利于客户端和服务端保存,节点之间交换信息时也变得轻量。

当集群在扩容、缩容、数据均衡时,节点之间的操作例如数据迁移,都以哈希槽为基本单位进行操作,简化了节点扩容、缩容的难度,便于集群的维护和管理。

小结

  • 数据扩容有两种方式:纵向扩展和横向扩展。Redis切片集群提供了横向扩展的模式。
  • 集群的实例增减或者数据重新分布,会导致哈希槽和实例的映射关系发生变化。当客户端发送请求时,会收到命令执行报错信息。
  • 在Redis3.0之前,Redis官方并没有提供切片集群方案。业界提供了一些成熟的方案,例如基于客户端分区的ShardedJedis,基于代理的Codis、Twemproxy等。

参考资料

Redis基础篇(八)数据分片的更多相关文章

  1. 老司机带你玩转面试(1):缓存中间件 Redis 基础知识以及数据持久化

    引言 今天周末,我在家坐着掐指一算,马上又要到一年一度的金九银十招聘季了,国内今年上半年受到 YQ 冲击,金三银四泡汤了,这就直接导致很多今年毕业的同学会和明年毕业的同学一起参加今年下半年的秋招,这个 ...

  2. c# 扩展方法奇思妙用基础篇八:Distinct 扩展(转载)

    转载地址:http://www.cnblogs.com/ldp615/archive/2011/08/01/distinct-entension.html 刚看了篇文章 <Linq的Distin ...

  3. c# 扩展方法奇思妙用基础篇八:Distinct 扩展

    刚看了篇文章 <Linq的Distinct太不给力了>,文中给出了一个解决办法,略显复杂. 试想如果能写成下面的样子,是不是更简单优雅 var p1 = products.Distinct ...

  4. 在 Istio 中实现 Redis 集群的数据分片、读写分离和流量镜像

    Redis 是一个高性能的 key-value 存储系统,被广泛用于微服务架构中.如果我们想要使用 Redis 集群模式提供的高级特性,则需要对客户端代码进行改动,这带来了应用升级和维护的一些困难.利 ...

  5. redis基础篇

    1.redis常见的数据结构 redis是一种以键值对存储的高性能内存数据库,有五种常用的数据类型,string,list,hash,set,zset. 2.redis的过期时间 redis中的key ...

  6. Redis基础篇(四)持久化:内存快照(RDB)

    AOF好处是每次执行只需要记录操作命令,记录量不大.但在故障恢复时,需要逐一执行AOF的操作命令,如果日志很大,恢复就很慢. 今天学习另一种持久化方式:内存快照.内存快照,是Redis某一时刻的状态, ...

  7. Redis基础篇(六)数据同步:主从复制

    Redis具有高可靠性,体现在两方面: 一是数据尽量少丢失,通过前面介绍的持久化方式AOF和RDB,在宕机时可以恢复数据. 二是服务尽量少中断,通过副本冗余来实现. 今天我们学习的就是通过主从复制实现 ...

  8. Redis基础篇(二)高性能IO模型

    我们经常听到说Redis是单线程的,也会有疑问:为什么单线程的Redis能那么快? 这里要明白一点:Redis是单线程,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对 ...

  9. Redis基础篇(三)持久化:AOF日志

    Redis是内存数据库,但是一旦服务器宕机,内存中的数据将会全部丢失. 最简单的恢复方式是从后端数据库恢复,但这种方式有两个问题: 频繁访问数据库,会给数据库带来巨大的压力: 从数据库中读取相比从Re ...

随机推荐

  1. PyQt(Python+Qt)学习随笔:Qt Designer中怎么给toolBar添加按钮

    在Designer中创建了一个MainWindow窗体,当想在其中的toolBar中添加toolButton时发现怎么也放不上去,最终才发现toolBar中的按钮只能通过直接拖拽Action编辑器中的 ...

  2. PHP代码审计分段讲解(8)

    20 十六进制与数字比较 源代码为: <?php error_reporting(0); function noother_says_correct($temp) { $flag = 'flag ...

  3. VS Code 搭建 Rust 开发环境

    VS Code 搭建 Rust 开发环境 上一篇文章安装和配置好了 Rust 环境后,我们是使用的是简单的文本工具编写 Hello World 入门代码,但是为了提高我们的学习效率,下面安利大家 VS ...

  4. ThreadLocal原理记录,别被坑了!!

    简介 ThreadLocal的用处 ThreadLocal是为了将数据记录一份到某个线程里,确保该数据线程安全 例如数据库的Connection放入ThreadLocal,一个事务会用到很多DAO,但 ...

  5. hashmap为什么是二倍扩容?

    这个很简单,首先我们考虑一个问题,为什么hashmap的容量为2的幂次方,查看源码即可发现在计算存储位置时,计算式为: (n-1)&hash(key) 容量n为2的幂次方,n-1的二进制会全为 ...

  6. nginx学习之——虚拟主机配置

    例子1: 基于域名的虚拟主机 server { listen 80;  #监听端口 server_name a.com; #监听域名 location / { root /var/www/a.com; ...

  7. Navicat操作Oracle

    1.出现connection to server failed, probable Oracle Net admin error 更换oci.dll,Navicat有自己默认的oci.dll,更换成你 ...

  8. 关于Switch Case的优化

    switch case虽然是代替if else而出现的,并不好维护,有时候使用switch还不如使用if else. 但没有别的东西能代替switch case了吗?答案当然是否定的,现在就有两种方式 ...

  9. Docker部署Portainer搭建轻量级可视化管理UI

    1. 简介   Portainer是一个轻量级的可视化的管理UI,其本身也是运行在Docker上的单个容器,提供用户更加简单的管理和监控宿主机上的Docker资源. 2. 安装Docker   Doc ...

  10. Linux系统-scp简介&坑

    文件请见这里: https://blog.csdn.net/xingxingzhilong/article/details/82909015