Redis系列---集群模式

目录

  • 1. 数据分片
    • 1.1. 哈希算法
      • 1.1.1. 优点
      • 1.1.2. 缺点
    • 1.2. 一致性哈希算法
      • 1.2.1. 优点
      • 1.2.2. 缺点
    • 1.3. 范围算法
      • 1.3.1. 优点
      • 1.3.2. 应用场景
    • 1.4. 虚拟哈希槽算法
      • 1.4.1. 优点
      • 1.4.2. 缺点
    • 1.5. 总述
  • 2. 架构演进
    • 2.1. Replication+Sentinel
      • 2.1.1. 架构图
      • 2.1.2. 工作原理
      • 2.1.3. 缺点
    • 2.2. Proxy+Replication+Sentinel
      • 2.2.1. 架构图
      • 2.2.2 工作原理
      • 2.2.3. 缺点
    • 2.3 Redis Cluster
      • 2.3.1 架构图
      • 2.3.2. 工作原理
      • 2.3.3. 优点
      • 2.3.4. 缺点
  • 3. 面试问题
    • 3.1. 懂Redis事务么?
    • 3.2. Redis的多数据库机制
    • 3.3. Redis集群机制中,你觉得有什么不足的地方吗?
    • 3.4. 懂Redis的批量操作么?
    • 3.5. 那在Redis集群模式下,如何进行批量操作?
    • 3.6. 你们有对Redis做读写分离么?
    • 3.7. Redis Cluster间的节点怎么通信?
    • 3.8. 为什么Redis Cluster的hash slot为16384个?

1. 数据分片

集群的本质其实就是数据分片,也就是所谓的 Sharding。为了使得集群能够水平扩展,首要解决的问题就是如何将整个数据集按照一定的规则分配到多个节点上,常用的数据分片的方法有:范围分片,哈希分片,一致性哈希算法和虚拟哈希槽等。

1.1. 哈希算法

hash算法分片最为简单,是通过hash函数对key进行处理,然后分配在某个partition中,partition 最终又会通过一定的映射规则最终落在machine上。因此当需要查询一个key的value时,要经过key-partition的映射表和partition-machine的映射表去两次寻址,从而实现路由寻址。
一句话概括就是利用hashcode进行简单寻址和存储

1.1.1. 优点

  • 精准查询性能极高,查询效率我O(1)

因此常用在分布式系统的路由寻址中

1.1.2. 缺点

  • 范围查询很慢,因为每个key都需要单独寻址
  • 无法用在存储系统中,因为用做存储系统时,增删节点困难,可用性低。

假设我们有三台缓存服务器,用于缓存图片,我们为这三台缓存服务器编号为 0号、1号、2号,现在有3万张图片需要缓存,我们希望这些图片被均匀的缓存到这3台服务器上,以便它们能够分摊缓存的压力。也就是说,我们希望每台服务器能够缓存1万张左右的图片,那么我们应该怎样做呢?常见的做法是对缓存项的键进行哈希,将hash后的结果对缓存服务器的数量进行取模操作,通过取模后的结果,决定缓存项将会缓存在哪一台服务器上

hash(图片名称)% N

当我们对同一个图片名称做相同的哈希计算时,得出的结果应该是不变的,如果我们有3台服务器,使用哈希后的结果对3求余,那么余数一定是0、1或者2;如果求余的结果为0, 就把当前图片缓存在0号服务器上,如果余数为1,就缓存在1号服务器上,以此类推;同理,当我们访问任意图片时,只要再次对图片名称进行上述运算,即可得出图片应该存放在哪一台缓存服务器上,我们只要在这一台服务器上查找图片即可,如果图片在对应的服务器上不存在,则证明对应的图片没有被缓存,也不用再去遍历其他缓存服务器了,通过这样的方法,即可将3万张图片随机的分布到3台缓存服务器上了,而且下次访问某张图片时,直接能够判断出该图片应该存在于哪台缓存服务器上。

缺陷:如果服务器已经不能满足缓存需求,就需要增加服务器数量,假设我们增加了一台缓存服务器,此时如果仍然使用上述方法对同一张图片进行缓存,那么这张图片所在的服务器编号必定与原来3台服务器时所在的服务器编号不同,因为除数由3变为了4,最终导致所有缓存的位置都要发生改变,也就是说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端服务器请求数据;同理,假设突然有一台缓存服务器出现了故障,那么我们则需要将故障机器移除,那么缓存服务器数量从3台变为2台,同样会导致大量缓存在同一时间失效,造成了缓存的雪崩,后端服务器将会承受巨大的压力,整个系统很有可能被压垮。为了解决这种情况,就有了一致性哈希算法。

1.2. 一致性哈希算法

hash算法就是为了精准查询,所以精准查询不是优化方向,可优化的方向就是如何优雅的增删节点。即一致性哈希算法。
一致性哈希算法也是使用取模的方法,但是取模算法是对服务器的数量进行取模,而一致性哈希算法是对 2^32 取模(之所以是2 ^ 32是因为hashcode最多是2 ^ 32),具体步骤如下:

  • 步骤一:哈希环的组织,一致性哈希算法将整个哈希值空间按照顺时针方向组织成一个虚拟的圆环,称为 Hash 环;

    我们将 2 ^ 32 想象成一个圆,像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2 ^ 32个点组成的圆,示意图如下:
    Redis系列---集群模式_第1张图片
    圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2 ^ 32-1,也就是说0点左侧的第一个点代表2 ^ 32-1,我们把这个由 2 ^ 32 个点组成的圆环称为hash环。

  • 步骤二:确定服务器在哈希环的位置,接着将各个服务器使用 Hash 函数进行哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,从而确定每台机器在哈希环上的位置
    哈希算法:hash(服务器的IP) % 2^32

    上述公式的计算结果一定是 0 到 2^32-1 之间的整数,那么上图中的 hash 环上必定有一个点与这个整数对应,所以我们可以使用这个整数代表服务器,也就是服务器就可以映射到这个环上,假设我们有 ABC 三台服务器,那么它们在哈希环上的示意图如下:
    Redis系列---集群模式_第2张图片

  • 步骤三:将数据映射到哈希环上,最后使用算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针寻找,第一台遇到的服务器就是其应该定位到的服务器

    我们还是使用图片的名称作为 key,所以我们使用下面算法将图片映射在哈希环上:hash(图片名称) % 2^32,假设我们有4张图片,映射后的示意图如下,其中橘黄色的点表示图片:
    Redis系列---集群模式_第3张图片
    那么,怎么算出上图中的图片应该被缓存到哪一台服务上面呢?我们只要从图片的位置开始,沿顺时针方向遇到的第一个服务器就是图片存放的服务器了。最终,1号、2号图片将会被缓存到服务器A上,3号图片将会被缓存到服务器B上,4号图片将会被缓存到服务器C上。

总结起来,其实就是将写入不再直接与hash算法绑定,而是引入顺序查找的方法,损失部分写入性能,换取优雅增删节点。

1.2.1. 优点

优雅的增删节点

如果简单对服务器数量进行取模,那么当服务器数量发生变化时,会产生缓存的雪崩,从而很有可能导致系统崩溃,而使用一致性哈希算法就可以很好的解决这个问题,因为一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,只有部分缓存会失效,不至于将所有压力都在同一时间集中到后端服务器上,具有较好的容错性和可扩展性。

假设服务器B出现了故障,需要将服务器B移除,那么移除前后的示意图如下图所示:
Redis系列---集群模式_第4张图片
在服务器B未移除时,图片3应该被缓存到服务器B中,可是当服务器B移除以后,按照之前描述的一致性哈希算法的规则,图片3应该被缓存到服务器C中,因为从图片3的位置出发,沿顺时针方向遇到的第一个缓存服务器节点就是服务器C,也就是说,如果服务器B出现故障被移除时,图片3的缓存位置会发生改变,但是,图片4仍然会被缓存到服务器C中,图片1与图片2仍然会被缓存到服务器A中,这与服务器B移除之前并没有任何区别,这就是一致性哈希算法的优点。

1.2.2. 缺点

hash环的倾斜与虚拟节点

一致性哈希算法在服务节点太少的情况下,容易因为节点分部不均匀而造成数据倾斜问题,也就是被缓存的对象大部分集中缓存在某一台服务器上,从而出现数据分布不均匀的情况,这种情况就称为 hash 环的倾斜。如下图所示:
Redis系列---集群模式_第5张图片
hash 环的倾斜在极端情况下,仍然有可能引起系统的崩溃,为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点,一个实际物理节点可以对应多个虚拟节点,虚拟节点越多,hash环上的节点就越多,缓存被均匀分布的概率就越大,hash环倾斜所带来的影响就越小,同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射。具体做法可以在服务器ip或主机名的后面增加编号来实现,加入虚拟节点以后的hash环如下:
Redis系列---集群模式_第6张图片

1.3. 范围算法

理解点查询和范围查询
Redis系列---集群模式_第7张图片
如上图所示,点查询和范围查询的关系一句话概括,多个有序的点查询约等于范围查询 之所以说是“约等于”,在这种理解上,范围查询只是点查询的封装,每次范围查询,依然会进行多次寻址,因此并没有本质的变化,如果每次Key查询计数为1(实际上每次key查询代表两次寻址)的话,点查询和范围查询可以解释成1+1=2的关系。

为点查询和范围查询增加分片层

在hash分片中,通过hash函数处理后,一般有序的多个Key,会被存放在多个分片中,尤其是相邻的Key大概率不在同一分片中。

此时会发现查询涉及到的Key越多,查询分片也会越多,如果此时能减少查询分片的数量,在key-parititon映射的效率也会变高
Redis系列---集群模式_第8张图片
我们将Key1对应的分片定为分片a,Key2对应的分片定为分片h,Key(n) 对应分片m,在hash分片模型中会存在多个Key通过hash函数存放于同一个分片中的情况,这里以Key(n-1)对应分片a 进行举例。4个Key在hash分片进行Key-Partition查询时,实际上会路由到3个分片中。此时我们进行两个操作:

  • 将已有的三个分片进行重组
    将3个分片按照Key的顺序进行拆分重组,分片a(包含Key1、Key(n-1))、分片h(包含Key2)、分片m (包含Keyn)重组为分片a(包含Key(n-1))、分片m(包含Keyn)、新分片x(包含Key1和Key2),当然这里如果所有Key都是连续,合并成一个分片当然是最理想的情况。我们在这个示例的基础上向外延伸,分片拆分合并有什么好处?

    无序变有序,针对原本多个无序的分片,按照一定规则,将相近的Key进行合并,之前连续的Key需要在多个分片中查找,而现在可以减少查找的范围,只需要在少量分片中进行查找,提升查询的效率。

  • 优化路由表
    将Key-Partition由4条元数据(Key1->分片a,Key2 ->分片h,Key(n-1)->分片a,Keyn ->分片m)缩减为3条(Key1->新分片x),Key(n-1)->分片a,Keyn-分片m),原来4条元数据缩减成3条,由密集索引转化为稀疏索引。什么是密集索引和稀疏索引呢?
    Redis系列---集群模式_第9张图片
    在路由分片模型中,KeyPartition映射一般是通过路由表来实现的(实际上一致性hash也是另类的路由表,只不过没有固化成index的结构,需要每次计算),在路由表中每个Key都会有其路由到分片的位置,即Key1 ->分片a,Key2->分片h,Key(n-1) ->分片a等。

    同样的将每个Key的分片位置都记录路由表中进行索引的方式叫做密集索引,而路由表优化后,由于Key是有序的,因此只需要记录Key1->新分片x即可,此时在新分片x中就可以找到Key2。这种索引类型叫做稀疏索引。

    上面我们针对分片层进行拆分合并,并且对路由表的元数据进行索引类型转换,更适合于连续Key的范围查询,所以在范围数据寻址效率上,点查询相比于范围查询,可以简单理解成达到了1+1<2的状态。

为点查询和范围查询增加机器层

Redis系列---集群模式_第10张图片
因此每个分片都会对应的一台机器进行承载。范围查询中的分片中数据是有序的,因此在读写时是顺序IO,而不像在点查询时是随机IO ,而对于大数据服务的应用场景来说,顺序IO的提升效果会更加明显。

一句话:利用手动干预的方法进行分片,再加上顺序读写磁盘

1.3.1. 优点

  • 批量读取数据时,可以通过缓存分片的物理地址位置,直接访问,提升读命中率。

  • 物理上使用顺序IO,相比随机IO,提升批量读写效率。

  • 元数据(路由表)优化,使用稀疏索引,元数据压力降低。

  • 范围分片更加灵活,不再受hash函数的限制,可以灵活的调整范围分片中每个分片的大小(分片拆分)和位置。

1.3.2. 应用场景

主要就是非关系型数据库
Redis系列---集群模式_第11张图片
HBase分片模型
Redis系列---集群模式_第12张图片
在HBase中,分片是基于rowkey排序后,按照不同范围进行拆分的,即[startKey,endKey)这样一个左闭右开区间,每个分片称为一个Region。

  • 一个HBase集群中有多张表,每张表包含1个或者多个Region,每个Region有且只有一台机器进行映射,换言之,每台机器会承载0个或者多个Region,这里的机器在HBase中叫做RegionServer。

  • 由于Region中rowkey是已经排序好的,因此后一个Region的startKey实际上是前一个Region的endKey。并且在第一个Region中是没有startKey的,同理最后一个Region也是没有endkey的。所以当所有region组合在一起,就可以覆盖这个表中任意的rowkey数值。

元数据路由策略
在HBase中数据的查询,涉及两个层级的路由:一是rowkey到region的路由,二是region到RS的路由。两级路由信息均存放在.meta表中,meta表实际上也是稀疏索引,只记录了startKey和endKey的的值,通过稀疏索引可以定位key对应Region的位置。
Redis系列---集群模式_第13张图片

LSM存储结构与优化

  • HBase使用LSM(Log-Structured Merge Tree)的存储结构,将磁盘的随机IO转化为顺序IO来提高批量读写的性能,代价就是在点查询上性能有所牺牲。

  • 在HBase中当写入一条数据后,率先会写入WAL,然后写入MemStore中。当Mem-Store满足一定条件后,开始flush数据到磁盘中,随着写入的不断增加,磁盘文件HFile也会越来越多,由于数据位置不确定,所以要遍历所有的HFile,因此在点查询时LSM树读性能时没有B+树好(这也是为什么在点查询上HBase不如Mysql的主要原因)。但是HBase也做了一定的优化,会定期合并若干个HFile,即多个文件合并成1个文件,以此来提高读性能。

Redis系列---集群模式_第14张图片
更加灵活的调度

  • 在HBase中每个Region内部都是有序的,当Region过大或者有Hot Key出现时,会按照相应规则切分Region,此时就不必受hash函数的制约,Region可以自由的拆分和迁移。

  • HBase存储层和计算层实际上是分离的,这也是现在主流架构,因此当Region迁移时不需要迁移物理数据,因此迁移成本很低。

1.4. 虚拟哈希槽算法

哈希槽其实就是虚拟的很小粒度的分区,通过hash算法将数据存放对应的槽里面。
一般来说,都是节点的概念,而hash槽,则是利用hash算法,虚拟出来一些槽位,然后再维护节点与槽位之间的关系。

1.4.1. 优点

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
  • 支持节点、槽和键之间的映射查询,用于数据路由,在线集群伸缩等场景。

1.4.2. 缺点

  • 二次映射,hash找到槽,再找到节点

1.5. 总述

  • 范围算法:假设数据集是有序,将顺序相临近的数据放在一起,可以很好的支持遍历操作。范围分片的缺点是面对顺序写时,会存在热点。比如日志类型的写入,一般日志的顺序都是和时间相关的,时间是单调递增的,因此写入的热点永远在最后一个分片。对于关系型的数据库,因为经常性的需要表扫描或者索引扫描,基本上都会使用范围的分片策略。

  • 哈希算法:为了将不同的 key 分散放置到不同的 redis 节点,通常的做法是获取 key 的哈希值,然后根据节点数来求模,但这种做法有其明显的弊端,当我们需要增加或减少一个节点时,会造成大量的 key 无法命中

  • 一致性哈希算法:一致性哈希有四个重要特征,其实就是哈希分片的改良,让其易于增删节点

    • 均衡性:也有人把它定义为平衡性,是指哈希的结果能够尽可能分布到所有的节点中去,这样可以有效的利用每个节点上的资源。
    • 单调性:当节点数量变化时哈希的结果应尽可能的保护已分配的内容不会被重新分派到新的节点。
    • 分散性和负载:这两个其实是差不多的意思,就是要求一致性哈希算法对 key 哈希应尽可能的避免重复。
  • 虚拟哈希槽算法:Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,每个key通过CRC16校验后对16384取模来决定放置哪个槽(Slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。

    计算公式:slot = CRC16(key) & 16383。
    这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。
    当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
    当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了。

2. 架构演进

你是否使用过Redis集群呢?那Redis集群的原理又是什么呢?记住下面两句话:

  • Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
  • Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

2.1. Replication+Sentinel

2.1.1. 架构图

这套架构使用的是社区版本推出的原生高可用解决方案

Replication:复制(之前一直以为是副本的意思,原来是复制)
Sentinel:哨兵(监控)
在我看来就是一主多从,相当于mysql的读写分离,只有主节点可以被写入,然后同步数据至从节点,所有查询走子节点。
Redis系列---集群模式_第15张图片
这里Sentinel的作用有三个:

  • 监控:Sentinel 会不断的检查主服务器和从服务器是否正常运行。
  • 通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。
  • 自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系 的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。

2.1.2. 工作原理

当Master宕机的时候,Sentinel会选举出新的Master,并根据Sentinel中client-reconfig-script脚本配置的内容,去动态修改VIP(虚拟IP),将VIP(虚拟IP)指向新的Master。我们的客户端就连向指定的VIP即可!
故障发生后的转移情况,可以理解为下图
Redis系列---集群模式_第16张图片

2.1.3. 缺点

既然是集群模式,优点不必多谈,无非就是高可用及提升反应能力及存储能力,故主要分析其缺点

  • 主从切换的过程中会丢数据
  • Redis只能单点写,不能水平扩容

2.2. Proxy+Replication+Sentinel

2.2.1. 架构图

这里的Proxy目前有两种选择:Codis和Twemproxy。我经历这套架构的时间为2015年,当时我好像咨询过我的主管为啥不用Codis和Redis官网的Redis Cluster。原因有二:

  • 据说是因为Codis开源的比较晚,考虑到更换组件的成本问题。毕竟本来运行好好的东西,你再去换组件,风险是很大的。
  • Redis Cluster在2015年还是试用版,不保证会遇到什么问题,因此不敢尝试。

所以我没接触过Codis,之前一直用的是Twemproxy作为Proxy。
这里以Twemproxy为例说明,如下图所示
Redis系列---集群模式_第17张图片
确实是真正的多主多从,水平扩展也没问题。但是配置应该很麻烦,且增删节点很困难。因为增删节点依旧存储节点的选择都是自己处理的,相当于自己用多个单redis搭了一套集群且自己管理。

2.2.2 工作原理

  • 前端使用Twemproxy+KeepAlived做代理,将其后端的多台Redis实例分片进行统一管理与分配
  • 每一个分片节点的Slave都是Master的副本且只读
  • Sentinel持续不断的监控每个分片节点的Master,当Master出现故障且不可用状态时,Sentinel会通知/启动自动故障转移等动作
  • Sentinel 可以在发生故障转移动作后触发相应脚本(通过 client-reconfig-script 参数配置 ),脚本获取到最新的Master来修改Twemproxy配置

2.2.3. 缺点

其实从架构图就可以看出缺点,因为整个集群的管理完全是自己管理。

  • 部署结构超级复杂
  • 可扩展性差,进行扩缩容需要手动干预
  • 运维不方便

2.3 Redis Cluster

redis官方的redis集群
我经历这套架构的时间为2017年,在这个时间Redis Cluster已经很成熟了!你们在网上能查到的大部分缺点,在我接触到的时候基本已经解决!
比如没有完善的运维工具?可以参照一下搜狐出的CacheCloud。
比如没有公司在生产用过?我接触到的时候,百度贴吧,美团等大厂都用过了。
比如没有Release版?我接触到的时候距离Redis Cluster发布Release版已经很久。
而且毕竟是官网出的,肯定会一直维护、更新下去,未来必定会更加成熟、稳定。换句话说,Redis不倒,Redis Cluster就不会放弃维护。
也是目前主流的redis集群

2.3.1 架构图

Redis系列---集群模式_第18张图片
跟其他集群模式的区别是,它不仅有节点的概念,还有插槽的概念(hash slot),其核心特点

  • 去中心化:集群中的每个节点都存有所有的插槽与节点的对应信息
  • 核心为管理插槽而非管理节点:每个插槽其实都可以理解为一个分区表,一个hash slot中会有很多key和value。

2.3.2. 工作原理

  • 客户端与Redis节点直连,不需要中间Proxy层,直接连接任意一个Master节点
  • 根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上
  • 如果需要操作的是该分片,则直接操作
  • 如果需要操作的不是该分片,则移动至相应的节点进行操作(Redirected to slot [12539] located at 172.10.20.33:7003)

客户端可以连接集群中任意一个Redis 实例,发送读写命令,如果当前Redis 实例收到不是自己负责的Slot的请求时,会将该slot所在的正确的Redis 实例地址返回给客户端。客户端收到后,自动将原请求重新发到这个新地址,自动操作,外部透明。
是不是有点似曾相识的感觉,HTTP 协议也有重定向功能。玩法跟这个差不多。HTTP 响应头有一个Location字段,当状态码是301或者302时,客户端会自动读取 Location中的新地址,自动重定向发送请求。

2.3.3. 优点

  • 无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master
  • 可以快速方便的进行水平扩容
  • 支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一master也宕机了,咋办呢? 针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。

2.3.4. 缺点

  • 批量操作是个坑,只要批量操作不在一个hash slot便会报错
    ((error)CROSSSLOT Keys in request don 't hash to the same slot)

  • 资源隔离性较差,容易出现相互影响的情况。

  • Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。

  • 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。

  • 数据通过异步复制,不保证数据的强一致性。

  • 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。

  • Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。

  • Key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个Key分布于不同的节点上时无法使用事务功能。

  • Key作为数据分区的最小粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点。

  • 不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db 0。

  • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

  • 避免产生hot-key,导致主库节点成为系统的短板。

  • 避免产生big-key,导致网卡撑爆、慢查询等。

  • 重试时间应该大于cluster-node-time时间。

  • Redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

3. 面试问题

3.1. 懂Redis事务么?

  • 正常版:
    Redis 事务的本质是一组命令的集合:事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
    redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
    Redis事务没有隔离级别的概念:批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
    Redis不保证原子性:Redis中,单条命令是原子性执行的,但整个事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
    Redis事务相关命令
    watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
    multi : 标记一个事务块的开始( queued )
    exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 ) 
    discard : 取消事务,放弃事务块中的所有命令
    unwatch : 取消watch对所有key的监控

    总体来说,redis的事务本质上来说不算事务,整体非原子性。非严格符合事务四大特征原子性、隔离性、持久性、一致性

  • 高调版:
    我们在生产上采用的是Redis Cluster集群架构,不同的key是有可能分配在不同的Redis节点上的,在这种情况下Redis的事务机制是不生效的。其次,Redis事务整体不是原子性操作,且不支持回滚操作,简直是鸡肋!所以基本不用!

3.2. Redis的多数据库机制

  • 正常版:
    字典:一个Redis实例提供了多个用来存储数据的字典,客户端可指定将数据存储到哪个字典中。字典和我们理解的数据库类似,所以可将每个字典都理解成一个独立的数据库。
    命名:每个数据库对外都是以0开始递增的数字命名,Redis默认支持16个数据库,可以通过配置参数database来修改这个数字。客户端与Redis建立连接后,会自动选择0号数据库,不过可以随时通过SELECT命令更换数据库。
    限制:Redis不支持自定义的数据库名,每个数据库都以数字编号命名,开发者必须自己记录哪些数据库记录了哪些数据。另外Redis也不支持为每个数据库设置不同的密码,所以数据库不能针对客户端区分权限访问。重要的是多个数据库之间数据不能共享但是又不是完全隔离的,比如FLUSHALL命令可以清空一个Redis实例中所有数据库中的数据。
    开销:Redis非常轻量级,一个空Redis实例占用的内存只有1MB左右,所以不用担心多个Redis实例会额外占用很多内存。

    总体来说:redis的多数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用db0存储生产环境的数据,使用db1存储测试环境的数据,但不适宜db0存应用A的数据,db1存储应用B的数据,不同应用应该使用不同的Reids实例存储数据。

  • 高调版:
    redis的各种集群模式基本都不支持多数据库机制,redis cluster也不支持,在Redis Cluster集群架构下只有一个数据库空间,即db0。因此,我们没有使用Redis的多数据库功能!

3.3. Redis集群机制中,你觉得有什么不足的地方吗?

  • 假设我有一个key,对应的value是Hash类型的。如果Hash对象非常大,是不支持映射到不同节点的!只能映射到集群中的一个节点上
  • 批量操作比较麻烦,因为集群模式下只能同时操作一个hash slot。

3.4. 懂Redis的批量操作么?

  • 正常版:
    基本就是带m的命令,比如mset、mget、hmget、hmset操作等,简单说就是单命令的累计,严格来说其实不属于批量操作,而是在一个指令中处理多个key。

    • 优势:性能优异,因为是单条指令操作,因此性能略优于其他批量操作指令
    • 劣势:批量命令不保证原子性,存在部分成功部分失败的情况,需要应用程序解析返回的结果并做相应处理 批量命令在key数目巨大时存在RRT与key数目成比例放大的性能衰减,会导致单实例响应性能(RRT)严重下降

    类似于管道操作:管道(pipelining)方式意味着客户端可以在一次请求中发送多个命令。

    • 优势:
      通过管道,可以将多个redis指令聚合到一个redis请求中批量执行
      可以使用各种redis命令,使用更灵活
      客户端一般会将命令打包,并控制每个包的大小,在执行大量命令的场景中,可以有效提升运行效率
      由于所有命令被分批次发送到服务器端执行,因此相比较事务类型的操作先逐批发送,再一次执行(或取消),管道拥有微弱的性能优势
    • 劣势:
      没有任何事务保证,其他client的命令可能会在本pipeline的中间被执行
  • 高调版:
    我们在生产上采用的是Redis Cluster集群架构,不同的key会划分到不同的slot中,因此直接使用mset或者mget等操作是行不通的。

3.5. 那在Redis集群模式下,如何进行批量操作?

如果执行的key数量比较少,就不用mget了,就用串行get操作。如果真的需要执行的key很多,就使用Hashtag保证这些key映射到同一台redis节点上。简单来说语法如下:

对于key为{foo}.student1、{foo}.student2,{foo}student3,这类key一定是在同一个redis节点上。因为key中“{}”之间的字符串就是当前key的hash tags, 只有key中{ }中的部分才被用来做hash,因此计算出来的redis节点一定是同一个

HashTag机制可以影响key被分配到的slot,从而可以使用那些被限制在slot中操作。通常情况下,HashTag不支持嵌套,即将第一个{和第一个}中间的内容作为HashTag。若花括号中不包含任何内容则会对整个key进行散列,如{}user:。

HashTag可能会使过多的key分配到同一个slot中,造成数据倾斜影响系统的吞吐量,务必谨慎使用。
mset在集群模式设置值时错误 CROSSSLOT Keys in request don’t hash to the same slot
在这里插入图片描述
Redis系列---集群模式_第19张图片

3.6. 你们有对Redis做读写分离么?

  • 正常版:
    没有做,至于原因额。。。额。。。额。。没办法了,硬着头皮扯~
  • 高调版:
    不做读写分离。我们用的是Redis Cluster的架构,是属于分片集群的架构。而redis本身在内存上操作,不会涉及IO吞吐,即使读写分离也不会提升太多性能Redis在生产上的主要问题是考虑容量,单机最多10-20G,key太多降低redis性能.因此采用分片集群结构,已经能保证了我们的性能。其次,用上了读写分离后,还要考虑主从一致性,主从延迟等问题,徒增业务复杂度
    总结来说: redis根本没必要做读写分离,读写分离是因为io瓶颈,redis根本不存在,与其费劲搞redis读写分离还不如直接使用redis cluster。

3.7. Redis Cluster间的节点怎么通信?

Redis 集群采用 Gossip(流言)协议, Gossip 协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,类似流言传播,也就是最开始时每个节点是不知道所有的对应信息的。但是这是无所谓的,只要set过之后一定能get到目标节点。

集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出现故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的 ping/pong 消息通信,经过一段时间后所有的节点都会知道整个集群 全部节点的最新状态,从而达到集群状态同步的目的。

具体规则:

  • 每秒会随机选取5个节点,找出最久没有通信的节点发送ping消息
  • 每隔 100毫秒 都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster-node-timeout/2 ,则立刻发送ping消息,因此,每秒单master节点发出ping消息数量:
    = 1 + 10 * num(node.pong_received>cluster_node_timeout/2)

也就是说redis节点之间无时无刻不再发送心跳,所谓心跳其实就是节点之间在交互信息,交换的数据信息,由消息体和消息头组成。

  • 消息体无外乎是一些节点标识啊,IP啊,端口号啊,发送时间
  • 消息头其实最重要的就是槽信息
    Redis系列---集群模式_第20张图片

消息头有一个myslots的char类型数组,unsigned char myslots[CLUSTER_SLOTS/8];,数组长度为 16384/8 = 2048 。底层存储其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点。

消息体中,会携带一定数量的其他节点信息用于交换,约为集群总节点数量的1/10,至少携带3个节点的信息。节点数量越多,消息体内容越大。10个节点的消息体大小约1kb。

例如,一个总节点数为200的Redis集群,部署在20台物理机上每台划分10个节点,cluster-node-timeout采用默认15秒,这时ping/pong消息占用带宽达到25Mb,把cluster-node-timeout设为20秒,对带宽的消耗降低到15Mb以下。

3.8. 为什么Redis Cluster的hash slot为16384个?

对于客户端请求的key,根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作!而CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

作者回答:
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

翻译:
正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。
这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。
同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。
因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。
请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。

一个hash slot可以存多少数据:摘自Redis官网的Data type章节,意思是内存允许的情况下,可以存超过40亿数据
redis cluster哈希槽数量能改变吗? 不能,因为代码算法写死了,固定是2的14次方这个数字上,作者自己有过分析

原因是因为需要把所有的槽放到心跳包里面便于让节点知道当前的全部信息。16348=16k,用bitmap来压缩心跳包的话,就相当于使用2810=2KB大小的心跳包。而如果用crc16算法(redis使用这个而不是用哈希一致性算法)来确定哈希槽的分配。他的最大值是是2的16次方。用上面的算法换算需要8KB的心跳包来传输,作者自己认为这样不划算。而一个redis节点一般不会有超过1000个master(这个是作者自己说的),用16k来划分是比较合适的

1.如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。

如上所述,在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。

2.redis的集群主节点数量基本不可能超过1000个。

如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。

3.槽位越小,节点少的情况下,压缩率高

Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。

而16384÷8÷1024=2kb,怎么样,神奇不!

综上所述,作者决定取16384个槽,不多不少,刚刚好!

总结:其实以上的解释并没有说出为什么是16384!但是网上只能查到这一种解释,没有人深入考虑过这个问题,按照以上理论,其实哈希槽数理论范围应该是1000-65536,作者应该是经过一些实验,他觉得16834在性能和可用性上是一个比较好的选择,所以就选择了这个。
也许有必须不可的原因,也许真的只是随便选择的,当你看过很多架构的源码时,你会发现,其实代码也有写的非常差的,很多东西也没有那么的高高在上,或者有些架构也很早写的,经过这么多年,硬件,软件都在变化和进步,再看那些东西,也不是非常好了,但大家可能一直在使用。就像java一样之所以用的多,因为轮子太多了,而不是说比别的语言有什么突出优点。
对于这个16384目前确实没有找到确切的原因,包括请教公司的架构师,也许随着经验的丰富会有答案,期待那个时候。

你可能感兴趣的:(redis,redis,数据库,服务器)