对于一个集中式缓存的分布式能力构建,必须要额外提供一些机制,来保障数据在各个节点上的安全与一致性。本文以Redis为代表,看下集Redis面对上述问题交出的是怎样一份答卷。

大家好,又见面了。


本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。


在本专栏前面的文章中,我们介绍了各种本地缓存框架,也知晓了本地缓存的常见特性与设计理念。在前两篇文章中,我们介绍了集中式缓存 Redis的一些主流特性与典型使用场景。现在我们来对比一下,分布式缓存相比于本地缓存,在实现层面需要关注的点有哪些不同。梳理如下:

维度 本地缓存 集中式缓存
缓存量 受限于单机内存大小,存储数据有限 需要提供给分布式系统里面所有节点共同使用,对于大型系统而言,对集中式缓存的容量诉求非常的大,远超单机内存的容量大小。
可靠性 影响有限,只有本进程使用,不会影响其他进程的可靠性。 作为整个系统扛压屏障,系统内所有节点共同依赖的通用服务,一旦集中式缓存出问题,会影响与其对接的所有业务节点,对系统的影响是致命性的。
承压性 承载单机节点的压力,请求量有限 承载整个分布式集群所有节点的流量,系统内业务分布式节点部署数量越多、业务体量越大,会导致集中缓存要承载的压力就越大,甚至是上不封顶的。

从上述几个维度的对比可以发现,同样是缓存,但集中式缓存所承担的使命是完全不一样的,业务对集中式缓存的存储容量可靠性承压性等方面的诉求也是天壤之别,不可等同视之。以Redis为例:

  • 如何打破redis缓存容量受限于机器单机内存大小的问题?
  • 如何使得redis能够扛住多方过来的请求压力?
  • 如何保证redis不会成为单点故障源?

其实答案很简单,加机器!通过多台机器的叠加使用,达到比单机更优的效果 —— 现在业务系统的集群化部署,也都是采用的这个思路。Redis的分布式之路亦是如此,但相比于常规的业务系统分布式集群化构建更加复杂:

  1. 很多业务实现集群化部署会很简单,因为每个业务进程节点都是无状态的,只需要部署下然后通过负载均衡的方式对外提供请求应答即可。
  2. Redis作为一个集中式缓存数据库,它是有状态的,不仅需要将进程分别部署在多个节点上,还需要将数据也分散存储在各个节点上,同时还得保证整个Redis集群对外是一个统一整体。

所以对于一个集中式缓存的分布式能力构建,必须要额外提供一些机制,来保障数据在各个节点上的安全与一致性,还需要将分散在各个节点上的数据都组成一个逻辑上的整体。

下面,我们以Redis作为集中式缓存的代表,来看下集Redis面对上述各种难题,交出的是怎样的答卷。

Reids部署方式的演进史

单机部署 —— 原始形态,最简单

单机部署只能算是一个开发或测试场景去小范围使用的场景,它与普通本地缓存无二,在可靠性与承压性上无法得到保证。

虽说Redis的性能很高,但俗话也说双拳难敌四手,单机性能再高,也无法抗住大规模集群中所有节点过来的并发请求。此外,单机部署还有个致命点在于其不具备高可用性,系统容易出现单点故障

所以说,稍微正规点的项目,几乎不会有人天真到会用单机模式去部署线上使用。

主从(master-replica)

前面说过单机节点存在诸多问题,很少在生产环境上使用。在实际项目中,有些项目的存储容量要求其实并不是特别的高(比如常规的16G或者32G就已经足够使用),但是需要保证数据的可靠、并且支持大并发量请求,这种情况下,就可以选择主从部署的方式。

对于redis来说,一主两从是比较常见的搭配,如下所示:

主从模式按照读写分离的策略来提升整体的请求处理能力:

  1. 主节点(Master)同时对外提供读和写操作

  2. 从节点(Slave)通过replicate同步的方式,从主节点复制数据,保持自身数据与主节点一致

  3. 从节点只能对外提供读操作

当然,对于读多写少类的操作,为了提升整体读请求的处理能力,可以采用一主多从的方式:

所有的从节点都从主节点进行数据同步,这样会导致主节点的同步处理压力过大而成为瓶颈。为了解决这个问题,redis还支持了从slave节点分发的能力:

Redis的主从模式重点在于解决整体的承压能力,利用从节点分担读取操作的压力。但是其在容错恢复等可靠性层面欠缺明显,不具备自动的故障转移与恢复能力:

  • 如果slave从节点宕机,整个redis依旧可以正常提供服务,待slave节点重新启动后,可以恢复从master节点的数据同步、然后继续提供服务。
  • 如果master主节点宕机,则redis功能受损,无法继续提供写服务,直到手动修复master节点方可恢复。

当然,master节点故障后,也可以手动将其中一个从节点切换为新的master节点来恢复故障。而原先的master节点恢复后,需要手动将其降级为slave节点,对外提供只读服务。

实际使用的时候,手动故障恢复的时效无法得到保证,为了支持自动的故障转移与恢复能力,Redis在主从模式的基础上进行优化增强,提供了哨兵(Sentinel)架构模式。

哨兵(sentinel)

哨兵模式是在现代自动化系统里面常见的一种模式。比如特斯拉汽车就配置了哨兵模式,当车辆停车锁定并启动哨兵模式时,会通过车辆四周的摄像头持续的监控车辆四周的环境,如果发现异常则启动报警系统。

同样地,在软件架构领域,也可以通过设定一些主进程之外的辅助进程,充当“哨兵”的角色时刻监控着主服务,一旦主服务出现异常则进行报警或者自动介入辅助故障转移,以最大限度的保证系统功能的持续性。

Redis的哨兵模式,就是在主从模式的基础上,额外部署若干独立的哨兵进程,通过哨兵进程去监视者Redis主从节点的状态,一旦发现主节点宕机,则哨兵可以重新从剩余slave节点中推选一个新的节点并将其升级为master节点,以此保证整个系统功能可以正常使用。

比较典型的一个Redis sentinel部署场景是“一主二从三哨兵”的组合,如下:

哨兵可以准实时的监控着组网内所有的节点的状态信息,如果判定master节点宕机之后,所有的sentinel节点会一起推选出一个新的master节点。由于sentinel哨兵节点需要承担着master节点推选的责任,所以实施的时候要去sentinel节点个数必须为基数(比如3个、5个等),这是为了保证投票的时候不会出现平局的情况。

在哨兵模式下:

  1. 如果Redis的master节点宕机之后,Sentinel监控到之后,需要先判定确认master节点已经宕机,然后会从剩余存活的slave节点中投票选出一个新的节点作为master节点。
  2. Sentinel监控到此前宕机的master节点重新恢复之后,会将其作为slave节点,挂到现有的新的master节点下面。

哨兵模式有效的解决了高可用的问题,保证了主节点的自动切换操作,进一步保障了Redis缓存节点的可靠性。但是,不管是哨兵模式还是主从模式,其增加的多台部署机器,都仅仅是扩展其承压能力与可靠性,并没有解决分布式场景下对于集中缓存容量的焦虑 —— 只能适用于数据量有限的场景。

成年人的世界总是贪婪的。如果我们既想要保证Redis的可靠性与承压性,还想要突破容量上的限制,就需要Redis的集群模式登场了。

集群(cluster)

Redis提供了去中心化的集群部署模式,集群内所有Redis节点之间两两连接,而很多的客户端工具会根据key将请求分发到对应的分片下的某一个节点上进行处理。

一个典型的Redis集群部署场景如下图所示:

在Redis集群里面,又会划分出分区的概念,一个集群中可有多个分区。分区有几个特点:

  1. 同一个分区内的Redis节点之间的数据完全一样,多个节点保证了数据有多份副本冗余保存,且可以提供高可用保障。
  2. 不同分片之间的数据不相同。
  3. 通过水平增加多个分片的方式,可以实现整体集群的容量的扩展。

按照Cluster模式进行部署的时候,要求最少需要部署6个Redis节点(3个分片,每个分片中1主1从),其中集群中每个分片的master节点负责对外提供读写操作,slave节点则作为故障转移使用(master出现故障的时候充当新的master)、对外提供只读请求处理。

集群数据分布策略

Redis Sharding(数据分片)

Redis Cluster前,为了解决数据分发到各个分区的问题,普遍采用的是Redis Sharding(数据分片)方案。所谓的Sharding,其实就是一种数据分发的策略。根据key的hash值进行取模,确定最终归属的节点。

使用Redis Sharding方式进行数据分片的时候,当集群内数据分区个数出现变化的时候,比如集群扩容的时候,会导致请求被分发到错误节点上,导致缓存命中率降低

如果需要解决这个问题,就需要对原先扩容前已经存储的数据重新进行一次hash计算和取模操作,将全部的数据重新分发到新的正确节点上进行存储。这个操作被称为重新Sharding,重新sharding期间服务不可用,可能会对业务造成影响。

一致性Hash

为了降低节点的增加或者移除对于整体已有缓存数据访问的影响,最大限度的保证缓存命中率,改良后的一致性Hash算法浮出水面。

通过一致性Hash算法,将所有的存储节点排列在首尾相接的Hash环上,每个key在计算Hash后会顺时针找到最近的存储节点存放。而当有新的分区节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续一个节点。

当然咯,如果Hash圆环上的分区节点数太少,可能会出现数据在各个分片中分布不均衡的情况,也即出现数据倾斜

为了解决这个问题,引入了虚拟节点的机制,通过增加虚拟节点,来实现数据尽可能的均匀分布在各个节点上。

上图中,A1、A2实际上都对应真实的A分区节点,而B1、B2则对应真实的B分区节点。通过虚拟节点的方式,尽可能让节点在Hash环上保持均分,实现数据在分区内的均分。

Hash槽

不管是原本的Hash取模,还是经过改良后的一致性Hash,在节点的新增或者删减的时候,始终都会出现部分缓存数据丢失的问题 —— 只是丢失的数据量的多少区别。如何才能实现扩展或者收缩节点的时候,保持已有数据不丢失呢?

既然动态变更调整的方式行不通,那就手动指定咯!Hash槽的实现策略因此产生。何为Hash槽?Hash槽的原理与HashMap有点相似,共有16384个槽位,每个槽位对应一个数据桶,然后每个Redis的分区都可以负责这些hash槽中的部分区间。存储数据的时候,数据key经过Hash计算后会匹配到一个对应的槽位,然后数据存储在该槽位对应的分片中。然后各个分区节点会与Hash槽之间有个映射绑定关系,由指定的Redis分区节点负责存储对应的Has槽对应的具体分片文件。

数据查询的时候,先根据key的Hash值进行计算,确定应该落入哪个Hash槽,进而根据映射关系,确定负责此Hash槽数据存储的redis分区节点是哪个,然后就可以去做对应的查询操作。

执行数据节点增加的时候,需要手动执行下处理:

  • 为新的节点分配新其负责的Hash槽位区间段;
  • 调整已有的节点的Hash槽位负责区间段;
  • 将调整到新节点上的hash槽位区间段对应的数据分片文件拷贝到新的节点上。

这样,就不会出现已有数据无法使用的情况了。鉴于Hash槽的自主可控性以及节点伸缩场景下的优势,其也成为了Redis Cluster中使用的方案。

小结回顾

好啦,关于Redis部署模式的演进探讨,就聊到这里了。通过本篇文章,我们也可以感受出集中式缓存相对本地而言,在实现与设计机制上要更加的复杂,因为需要考虑与解决多方面的问题,比如可靠性、承压性、容量以及后期的水平扩容能力等等,而这些也都是一个合格的集中式缓存所必须要具备的基本品格。

那么,了解Redis对于集中式缓存在节点安全性与扩展性上的实现后,如果让你来设计一个集中缓存的话,你会采用何种方式来保证其可靠性与后续的扩展性呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

📣 补充说明

本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。

如果有兴趣,也欢迎关注此专栏。

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点赞 + 关注让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。