同为分布式缓存,为何Redis更胜一筹?

	到了一篇分布式缓存与Redis缓存还不错的文章与自己的一些心的体验,拿来分享一下?最近确实也没怎么写过文章了,以后加油!!!

如今,市面上的缓存解决方案已经逐步成熟了,今天我将选取其中一些代表性的方案包括Redis、Memcached和Tair进行对比,帮助大家在生产实践中更好地进行技术选型。

一、常用的分布式缓存的对比:

在这里插入图片描述

Sentinel 是什么?:Sentinel 作为阿里巴巴“大中台、小前台”架构中的基础模块,覆盖了阿里的所有核心场景,因此积累了大量的流量归整场景以及生产实践。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
➤ 限流:
当我们设计了一个函数,准备上线,这时候这个函数会消耗一些资源,处理上限是1秒服务3000个QPS,但如果实际情况遇到高于3000的QPS该如何解决呢?Sentinel提供了两种流量统计方式,一种是统计并发线程数,另外一种则是统计 QPS,当并发线程数超出某个设定的阀值,新的请求会被立即拒绝,当QPS超出某个设定的阀值,系统可以通过直接拒绝、冷启动、匀速器三种方式来应对,从而起流量控制的作用。
➤ 熔断降级:
接触过Spring Cloud、Service Mesh的同学,都知道熔断降级的概念。服务之间会有相互依赖关系,例如服务A做到了1秒上万个QPS,但这时候服务B并无法满足1秒上万个QPS,那么如何保证服务A在高频调用服务B时,服务B仍能正常工作呢?一种比较常见的情况是,服务A调用服务B时,服务B因无法满足高频调用出现响应时间过长的情况,导致服务A也出现响应过长的情况,进而产生连锁反应影响整个依赖链上的所有应用,这时候就需要熔断和降级的方法。Sentinel通过并发线程数进行限制和响应时间对资源进行降级两种手段来对服务进行熔断或降级。
➤ 塑形
通常我们遇到的流量具有随机性、不规则、不受控的特点,但系统的处理能力往往是有限的,我们需要根据系统的处理能力对流量进行塑形,即规则化,从而根据我们的需要来处理流量。Sentinel通过资源的调用关系、运行指标、控制的效果三个维度来对流量进行控制,开发者可以自行灵活组合,从而达到理想的效果。
➤ 系统负载保护
平时系统运行都没问题,但遇到大促的时候,发现机器的load非常高,这时候对系统的负载保护就显得非常重要,以防止雪崩。Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。需要注意的是,Sentinel在系统负载保护方面的判断机制是根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统load)来做限流。因为我们最终追求的目标是在系统不被拖垮的情况下,提高系统的吞吐率,而不是load一定要到低于某个阀值。
其特点包括:
➤轻巧
轻巧指的是对性能影响小和对应用零入侵。
限流框架是寄宿在应用上的,这时候要求限流框架不能对系统资源有过多的消耗。就像汽车上的安全气囊如果会耗油、导致汽车跑得慢,这就不是一个好气囊,Sentinel的接入对系统资源的消耗极少。
除了对性能的影响要优化到最低以外,还有一个特征,就是需要保证他对应用的零入侵。零入侵是让开发者几乎意识不到这个框架的存在。如果让开发者一边开发,一边还要想着限流降级,这就非常累了。优秀的限流就像是汽车上的安全气囊,平时系统工作正常的时候我们感受不到他的存在,只有当系统出现无法应对当前流量的时候,才会出现,这就是对应用零入侵的体现,开发者无需关心如何接入流量框架,便可调用服务。
对此,Sentinel通过对主流框架,例如Dubbo、Spring Cloud, grpc等,进行默认适配,只要接入我们的适配器,默认的资源就都有了;如果不是用主流框架,也没有关系,只需要很简单,差不多3步,就可以接入,之后还会提供annotation,让用户更简单的用起来。
➤专业
不同的场景下有不同的限流需求。在什么时候减流量,流量减多了影响用户体验、流量减少了影响系统稳定性,陡峭高峰如何限流、销峰填谷如何限流,这里就涉及到限流的算法。不同于 hystrix 只提供一两个维度的限流方式,Sentinel提供了一个灵活的框架,从不同的维度出发,开发者可以根据自身的场景去制定自己的限流策略。
➤ 实时监控
流量具有很强的实时性,之所以需要限流,是因为我们无法对流量的到来作出精确的预判,不然的话我们完全可以通过弹性的计算资源来处理,所以这时候限流框架的实时监控功能就非常重要了。通过Sentinel的实时监控功能,运维人员可以根据实际流量情况,采取不同的措施,限流、降级、塑形、系统保护,所以在我们第一版开源版本中,我们加入了Sentinel的控制台,具备实时监控功能。
下面我们从9个大方面来对比最常用的Redis和Memcached。

在这里插入图片描述

Redis Cluster是什么?:Redis3.0以后,节点之间通过去中心化的方式提供了完整的sharding(数据分片)、replication(复制机制、Cluster具备感知准备的能力)、failover解决方案。

Redis Cluster由多个Redis节点组构成。不同节点组服务的数据无交集,每一个节点组对应数据sharding的一个分片。
节点组内分为主备两类节点,两者数据准实时一致,通过异步化的主备复制机制。
master节点对用户提供读写服务,slave节点对用户提供读服务。

Redis Cluster总共有16384个slot,每一个节点负责一部分slot。
Redis Cluster中所有的几点之间两两通过Redis Cluster Bus交互,主要交互以下关键信息:

  • 数据分片(slot)和节点的对应关系
  • 集群中每个节点可用状态
  • 集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、故障发生时的主备切换决策、单点master的发现和其发生主备关系的变更等场景均会导致集群结构变化
  • publish和subscribe(发布/订阅)功能在cluster版的内容实现所需要交互的信息。
    Redis Cluster Bus通过单独的端口进行连接,bus是节点间的内部通信机制,交互的是字节序列化信息,而不是client到Redis服务器的字符序列化以提升交互效率。
    Redis Cluster是去中心化的分布式实现方案,客户端可以和集群中的任一节点连接。
    ➤ 配置一致性
    去中心化意味着集群的拓扑结构并不保存在单独的配置节点上,Redis Cluster通过引入两个自增的epoch变量来使得集群配置在各个节点间达成最终一致。
    Redis Cluster中的每一个节点都保存了集群的配置信息,这些信息存储在clusterState中。
  • clusterState记录了从集群中某个节点的视角看来的集群配置状态
  • currentEpoch表示整个集群中的最大版本号,集群信息每变更一次,该版本号都会自增以保证每个信息的版本号唯一
  • nodes是一个列表,包含了本节点所知的集群所有节点的信息(clusterNode),其中也包含本节点自身
  • clusterNode记录了每个节点的信息,比较关键的信息包括该信息的版本epoch,该版本信息的描述:该节点对应的数据分片(slot),当该节点为master节点时对应的slave节点列表、当该节点为slave时对应的master节点
  • 每个clusterNode还包含了一个全局唯一的nodeId
  • 当集群的数据分片信息发生变更时,Redis Cluster仍然保持对外服务,在迁移过程中,通过分片迁移相关状态的一组变量来管控迁移过程
  • 当集群中的某个master出现宕机时,Redis Cluster会自动发现并触发故障转移的操作,将宕机master的某个slave升级为master,这个过程同样需要一组failover相关状态的变量来管控故障转移。
    Redis Cluster通过epoch作为版本号来实现集群配置的一致性。
    ➤ 信息交互
    去中心化的架构不存在统一的配置中心,各个节点对集群状态的认知来自于节点间的信息交互。在Redis Cluster中,该信息的交互通过Redis Cluster Bus来完成。

clusterMsg的type字段指明了消息的类型。配置信息的一致性主要依靠PING和PONG,两者除了type不同,其余字段语义均相同,消息体为Gossip数据。
每一个节点向其他节点较为频繁的周期性发送PING消息和接受PONG响应。在这些下拍戏的Gossip部分,包含了发送者节点(或者响应者节点)所知的集群其他节点信息,接收节点可以根据这些Gossip信息更新自己对于集群的认知。
规模较大的集群可能存在上千个节点,但是这些节点在正常情况下都是稳定的,因此每次都发送全量数据并不必要,而且还会造成网络负担。
作为优化,Redis Cluster在每次的PING和PONG包中,只包含全集群部分节点信息,节点随机选取,以此控制网络流量。由于交互频繁,短时间的几次交互之后,集群状态就会以Gossip协议的方式被扩散到了集群中的所有节点。
➤ 一致性达成
集群结构稳定不发生变化时,各个节点通过Gossip协议在几轮交互之后便可得知全集群的信息并且达到一致的状态。
但是,当发生故障转移、分片迁移等情况将会造成集群结构变更,变更的信息需要各个节点之间自行协调,优先得知变更信息的节点利用epoch变量将自己的最新信息扩散到整个集群,达到最终一致。

  • 配置信息clusterNode的epoch属性描述的粒度是单个节点
  • 配置信息clusterState的currentEpoch属性的粒度是整个集群,它的存在用来辅助epoch自增的生成。由于currentEpoch信息也是维护在各个几点自身的,Redis Cluster结构在发生变更时,通过一定时间窗口控制和更新规则保证每个节点看到的currentEpoch都是最新的。
    集群信息的更新规则:
  • 当某个节点率先知道了信息变更时,这个节点将currentEpoch自增使之成为集群中的最大值,再用自增后的currentEpoch作为新的epoch版本
  • 当某个节点收到了比自己大的currentEpoch时,更新自己的currentEpoch值使之保持最新
  • 当收到的Redis Cluster Bus消息中某个节点信息的epoch值大于接收者自己内部的配置信息存储的值时,意味着自己的信息太旧,此时接收者直接将自己的映射信息更新为消息的内容
  • 当收到的Redis Cluster Bus消息中某个节点信息未包含在接收节点的内部配置信息中时,意味着接受者尚未意识到该节点的存在,此时接收者直接将消息的信息添加到自己的内部配置信息中。
    sharding

不同的节点组服务于相互无交互的数据子集(sharding,分片)。
➤ 数据分片(slot)
Redis Cluster将所有的数据划分为16384个分片(slot),每个分片负责其中一部分。每一条数据根据key值通过数据分布算法映射到16384个slot中的一个。
数据分布算法:slotId=crc(key)%16384
客户端根据slotId决定将请求路由到哪个Redis节点。Cluster不支持跨节点的单命令。
为此,Redis引入HashTag的概念,使得数据分布算法可以根据key的某一部分进行计算,让相关的两条记录落到同一个数据分片,例如:

  • 某条商品交易记录的key值为:product_trade_{prod123}
  • 这个商品的详情记录的key值为:product_detail_{prod123}
    Redis会根据{}之间的子字符串作为数据分布算法的输入。
    ➤ 客户端路由
    Redis Cluster的客户端需要具备一定的路由能力。当一个Client访问的key不在对应Redis节点的slot中,Redis返回给Client一个moved命令,告知其正确的路由信息。
    从Client收到moved响应,到再次向moved响应中指向的节点发送请求期间,Redis Cluster的数据分布可能又发生了变更,此时,指向的节点会继续响应moved。Client根据moved响应更新其内部的路由缓存信息,以便下一次请求时直接路由到正确的节点,降低交互次数。
    当Cluster处在数据重分布(目前由人工触发)过程中时,可以通过ask命令控制客户端路由。
    ask命令和moved命令的不同语义在于,后者会更新路由缓存,前者只是本条操作重定向到新节点,后续的相同slot操作仍路由到旧节点。ask类型将重定向和路由缓存更新分离,避免客户端的路由缓存信息频繁更新。
    ➤ 分片迁移
    在稳定的Redis Cluster下,每一个slot对应的节点是确定的。但是在某些情况下,节点和分片的对应关系要发生变更:
  • 新的节点作为master加入
  • 某个节点分组需要下线
  • 负载不均需要调整slot分布
    此时需要进行分片的迁移。分片迁移的触发和过程由外部系统完成,Redis Cluster只提供迁移过程中需要的原语供外部系统调用。这些原语主要有两种:
  • 节点迁移状态设置:迁移前标记源/目标节点
  • key迁移的原子化命令:迁移的具体步骤

在这里插入图片描述

  1. 向节点B发送状态变更命令,将B的对应slot状态置为IMPORTING
  2. 向节点A发送状态变更命令,将A的对应slot状态置为MIGRATING
  3. 针对A的slot上的所有的key,分别向A发送MIGRATE命令,告知A将对应key的数据迁移到B。
    当节点A的状态被设置为了MIGRATING后,表示对应的slot正在从A迁出,为保证该slot数据的一致性,A此时对slot内部数据提供读写服务的行为和通常状态下有所区别,对于某个迁移中的slot:
  • 如果客户端访问的key尚未迁移出,则正常地处理key
  • 如果key已经被迁移出或者根本不存在该key,则回复客户端ASK信息让其跳转到B执行
    当节点B的状态被设置为了IMPORTING之后,表示对应的slot正在向B迁入中,即使B仍能对外提供该slot的读写服务,但行为和通常状态下也有所区别:
  • 当来自客户端的正常访问不是从ASK跳转而来时,说明客户端尚不知道迁移正在进行,很有可能操作了一个目前尚未迁移完成的正处在A上的key,如果此时key已经在A上被修改了,那么B和A的修改值将在未来发生冲突。
  • 对于该slot上的所有非ASK跳转而来的操作,B不会进行处理,而是通过MOVED命令让客户端跳转至A执行
    这样的状态控制可以保证同一个key在迁移之前总是在源节点执行,迁移后总是在目标节点执行,杜绝了两边同时写导致值冲突的可能性。且迁移过程中新增的key总是在目标节点执行,源节点不会再有新增的key,使得迁移过程时间有界。
    Redis单机对于命令的处理是单线程的,同一个key在MIGRATE的过程中不会处理对该key的其他操作,从而保证了迁移的原子性。
    当slot的所有key从A迁移至B上之后,客户端通过CLUSTER SETSLOT命令设置B的分片信息,使之包含迁移的slot。设置的过程中会自增一个epoch,它大于当前集群中的所有epoch值,这个新的配置信息会传播到集群中的其他每一个节点,完成分片节点映射关系的更新。
    failover

Redis Cluster同Sentinel一样,具备完整的节点故障发现、故障状态一致性保证、主备切换机制。
➤ failover状态变迁
failover的过程如下:

  1. 故障发现:当某个master宕机时,宕机事件如何被集群其他节点感知
  2. 故障确认:多个节点就某个master是否宕机如何达成一致
  3. slave选举:集群确认了某个master确实宕机后,如何将它的slave升级成新的master;如果原master有多个slave,选择谁升级
  4. 集群结构变更:选举成功的slave升级成新的master后如何让全集群的其他节点知道以更新他们的集群结构信息
    ➤ 故障发现
    Redis Cluster节点间通过Redis Cluster Bus两两周期性地进行PING/PONG交互,当某个节点宕机时,其他发向它的PING消息将无法及时响应,当PONG的响应超过一定时间(NODE_TIMEOUT)未收到,则发送者认为接受节点故障,将其置为PFAIL状态,后续通过Gossip发出的PING/PONG消息中,这个节点的PFAIL状态将会被转播到集群的其他节点。
    Redis Cluster的节点间通过TCP保持Redis Cluster Bus连接,当对端无PONG回复时,除了节点故障外,还有可能是TCP连接断开。对于TCP连接断开导致的响应超时,将会产生节点状态误报。因此Redis Cluster通过预重试机制排除此类误报:当NODE_TIMEOUT/2过去了却还未收到PING对应的PONG消息,则重建连接重发PING消息,如果对端正常,PONG会在很短时间内抵达。
    ➤ 故障确认
    对于网络分割的节点,某个节点(假设叫B节点)并没有故障,但可能和A无法连接,但是和C/D等其他节点可以正常联通,此时只有A会将B标记为PFAIL,其他节点扔人认为B是正常的。此时A和C/D等其他节点信息不一致。Redis Cluster通过故障确认协议达成一致。
    A会受到来自其他节点的Gossip消息,被告知节点B是否处于PFAIL状态,当A受到的来自其他master节点的B的PFAIL达到一定数量后,会将B的PFAIL升级为FAIL状态,表示B已确认为故障,后续将会发起slave选举流程
    在这里插入图片描述

➤ slave选举
上例中,如果B是A的master,且B已经被集群公认是FAIL状态,那么A将发起竞选,期望替代B成为新的master。
如果B有多个slave A/E/F都意识到B处于FAIL状态了,A/E/F可能会同时发起竞选,当B的slave数量>=3个时,很有可能因为票数均匀无法选出胜者,延长B上的slot不可用时间。为此,slave间会在选举前协商优先级,优先级高的slave更有可能早地发起选举,优先级较低的slave发起选举的时间越靠后,避免和高优先级的slave竞争,提升一轮完成选举的可能性。
优先级最重要的决定因素是slave最后一次同步master信息的时间,越新标识这个slave的数据越新,竞选优先级越高。
slave通过向其他master节点发送FAILOVER_AUTH_REQUEST消息发起竞选,master收到之后回复FAILOVER_AUTH_ACK消息告知自己是否同意改slave成为新的master。slave发送FAILOVER_AUTH_REQUEST前会将currentEpoch自增并将最新的epoch带入到AILOVER_AUTH_REQUEST消息中,master收到FAILOVER_AUTH_REQUEST消息后,如果发现对于本轮(本epoch)自己尚未投过票,则回复同意,否则回复拒绝。
➤ 主备复制
当slave收到超过半数的master的同意回复时,该slave顺利的替代B成为新master,此时它会以最新的epoch通过PONG消息广播自己成为master的信息,让集群中的其他节点更快地更新拓扑信息。
当B恢复可用之后,它首先仍然认为自己是master,但逐渐得通过Gossip协议得知A已经替代自己的事实之后降级为A的slave。
Redis采用主备复制的方式保持一致性,即所有节点中,有一个节点为master,对外提供写入服务,所有的数据变更由外界对master的写入触发,之后Redis内部异步地将数据从主节点复制到其他节点上。
➤ 主备复制流程
Redis包含master和slave节点:master节点对外提供读写服务;slave节点作为master的数据备份,拥有master的全量数据,对外不提供写服务。主备复制由slave主动触发。

在这里插入图片描述

  1. slave向master发起SYNC命令。这一步在slave启动后触发,master被动地将新进slave节点加入自己的主备复制集群
  2. master收到SYNC后,开启BGSAVE操作。BGSAVE是Redis的一种全量模式的持久化机制
  3. BGSAVE完成后,master会将快照信息发送给slave
  4. 发送期间,master收到的来自客户端的新的写命令,除了正常响应外,都再存入一份到backlog队列
  5. 快照信息发送完成后,master继续发送backlog队列信息
  6. backlog发送完成后,后续的写操作同时发送给slave,保持实时地异步复制
    slave侧的处理逻辑:
  7. 发送完SYNC后,继续对外提供服务
  8. 开始接收master的快照信息,此时,将slave现有数据清空,并将master快照写入自身内存
  9. 接收backlog内容并执行它,即回放,期间对外提供读请求
  10. 继续接收后续来自master的命令副本并继续回放,以保证数据和master一致
    如果有多个slave节点并发发送SYNC命令给master,只要第二个slave的SYNC命令发生在master完成BGSAVE之前,第二个slave将受到和第一个slave相同的快照和后续的backlog;否则,第二个slave的SYNC将触发master的第二次BGSAVE。
    ➤ 断点续传
    slave通过SYNC命令和master进行数据同步时,master都会dump全量数据。假设master和slave断开很短的时间,数据只有很少的差异,重连后也会发送这些全量数据导致大量的无效开销。最好的方式就是,master-slave只同步断开期间的少量数据。
    Redis的PSYNC可用于替代SYNC,做到master-slave基于断点续传的主备同步协议。master-slave两端通过维护一个offset记录当前已经同步过的命令,slave断开期间,master的客户端命令会保持在缓存中,在slave命令重连后,告知master断开时的最新offset,master则将缓存中大于offset的数据发送给slave,而断开前已经同步过的数据,则不再重新同步,这样减少了数据传输开销。
    可用性和性能

➤ Redis Cluster读写分离
对于有读写分离需求的场景,应用对于某些读的请求允许舍弃一定的数据一致性,以换取更高的读吞吐量,此时希望将读的请求交由slave处理以分担master的压力。
默认情况下,数据分片映射关系中,某个slot对应的节点一定是一个master节点,客户端通过MOVED消息得知的集群拓扑结构也只会将请求路由到各个master中,即便客户将读请求直接发送到slave上,后者也会回复MOVED到master的响应。
Redis Cluster引入了READONLY命令。客户端向slave发送该命令后,slave对于读操作,将不再MOVED回master而不是直接处理,这被称为slave的READONLY模式。通过READWRITE命令,可将slave的READONLY模式重置。
➤ master单点保护
集群只需要保持2*master+1个节点,就可以在任一节点宕机后仍然自动地维持,称为master的单点保护。

1.数据类型

Redis一共支持5种数据类型,每种数据类型对应不同的数据结构,有简单的String类型、压缩串、字典、跳跃表等。跳跃表是比较新型的数据结构,常用于高性能的查找,可以达到log2N的查询速度,而且跳跃表相对于红黑树,在更新时变更的节点较少,更易于实现并发操作。
Memcache只支持对键值对的存储,并不支持其它数据结构。
那下面先讲讲Redis的5种数据类型:
字符串string:

字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key-value,一般来存字符串,整数和浮点数。Value最多可以容纳的数据长度为512MB

应用场景:很常见的场景用于统计网站访问数量,当前在线人数等。incr命令(++操作)
在这里插入图片描述

列表list:

Redis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。List中可以包含的最大元素数量是4294967295。

应用场景:1.最新消息排行榜。2.消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)
在这里插入图片描述

散列hash:

Redis中的散列可以看成具有String key和String value的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。

应用场景:例如存储、读取、修改用户属性(name,age,pwd等)
在这里插入图片描述

集合set:

Redis的集合是无序不可重复的,和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。

在这里插入图片描述
应用场景:1.利用交集求共同好友。2.利用唯一性,可以统计访问网站的所有独立IP。3.好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。

有序集合sorted set:

和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是卫衣的,但是分数(score)却可以重复。

应用场景:可以用于一个大型在线游戏的积分排行榜,每当玩家的分数发生变化时,可以执行zadd更新玩家分数(score),此后在通过zrange获取几分top ten的用户信息。

在这里插入图片描述

通用操作:

在这里插入图片描述

2.线程模型

Redis使用单线程实现,Memcache等使用多线程实现,因此我们不推荐在Redis中存储太大的内容,否则会阻塞其它请求。
因为缓存操作都是内存操作,只有很少的计算操作,所以在单线程下性能很好。Redis实现的单线程的非阻塞网络I/O模型,适合快速地操作逻辑,有复杂的长逻辑时会影响性能。对于长逻辑应该配置多个实例来提高多核CPU的利用率,也就是说,可以使用单机器多端口来配置多个实例,官方的推荐是一台机器使用8个实例。
它实现的非阻塞I/O模型基于Libevent库中关于Epoll的两个文件加上自己简单实现的事件通知模型,简单小巧,作者的思想就是保持实现简单、减少依赖。由于在服务器中只有一个线程,因此提供了管道来合并请求和批量执行,缩短了通信消耗的时间。
Memcache也使用了非阻塞I/O模型,但是使用了多线程,可以应用于多种场景,请求的逻辑可大可小、可长可短,不会出现一个逻辑复杂的请求阻塞对其它请求的响应的场景。它直接依赖Libevent库实现,依赖比较复杂,损失了在一些特定环境下的高性能。

3.持久机制

Redis提供了两种持久机制,包括RDB和AOF,前者是定时的持久机制,但在出现宕机时可能会出现数据丢失,后者是基于操作日志的持久机制。
Memcahe并不提供持久机制,因为Memache的设计理念就是设计一个单纯的缓存,缓存的数据都是临时的,不应该是持久的,也不应该是一个大数据的数据库,缓存未命中时回源查询数据库是天经地义的,但可以通过第三方库MemcacheDB来支持它的持久性。

4.客户端

常见的Redis Java客户端Jedis使用阻塞I/O,但可以配置连接池,并提供了一致性哈希分片的逻辑,也可以使用开源的客户端分片框架Redic。
Memecache的客户端包括Memcache Java Client、Spy Client、XMemcache等,Memcache Java Client使用阻塞I/O,而Spy Client/XMemcache使用非阻塞I/O。
我们知道,阻塞I/O不需要额外的线程,非阻塞I/O会开启额外的请求线程(在Boss线程池里)监听端口,一个请求在处理后就释放工作者线程(在Worker线程池中),请求线程在监听到有返回结果时,一旦有I/O返回结果就被唤醒,然后开始处理响应数据并写回网络Socket连接,所以从理论上来讲,非阻塞I/O的吞吐量和响应能力会更高。
5.高可用

Redis支持主从节点复制配置,从节点可使用RDB和缓存的AOF命令进行同步和恢复。Redis还支持Sentinel和Cluster(从3.0版本开始)等高可用集群方案。
Memecache不支持高可用模型,可使用第三方Megagent代理,当一个实例宕机时,可以连接另外一个实例来实现。

6.对队列的支持

Redis本身支持lpush/brpop、publish/subscribe/psubscribe等队列和订阅模式。
Memcache不支持队列,可通过第三方MemcachQ来实现。
7.事务

Redis提供了一些在一定程度上支持线程安全和事务的命令,例如:multi/exec、watch、inc等。由于Redis服务器是单线程的,任何单一请求的服务器操作命令都是原子的,但跨客户端的操作并不保证原子性,所以对于同一个连接的多个操作序列也不保证事务。
Memcached的单个命令也是线程安全的,单个连接的多个命令序列不是线程安全的,它也提供了inc等线程安全的自加命令,并提供了gets/cas保证线程安全。
8.数据淘汰策略

Redis提供了丰富的淘汰策略,包括maxmemory、maxmemory-policy、volatile-lru、allkeys-lru、volatile-random、allkeys-random、volatile-ttl、noeviction(return error)等。
Memecache在容量达到指定值后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。在某些情况下LRU机制反倒会带来麻烦,会将不期待的数据从内存中清除,在这种情况下启动Memcache时,可以通过“M”参数禁止LRU算法。
体外之音:如何Redis内存满了的几种解决方案(内存淘汰策略与Redis集群)
内存淘汰策略:

LRU算法,least RecentlyUsed,最近最少使用算法。也就是说默认删除最近最少使用的键。

但是一定要注意一点!redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取3个键,删除这三个键中最近最少使用的键。

那么3这个数字也是可以设置的,对应位置是配置文件中的maxmeory-samples.
集群:
Redis仅支持单实例,内存一般最多1020GB。对于内存动辄100200GB的系统,就需要通过集群来支持了。

Redis集群有三种方式:客户端分片、代理分片、RedisCluster(在之后一篇文章详细说一下。)

· 客户端分片

通过业务代码自己实现路由

优势:可以自己控制分片算法、性能比代理的好

劣势:维护成本高、扩容/缩容等运维操作都需要自己研发

· 代理分片

代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis实例并返回给业务程序。使用类似Twemproxy、Codis等中间件实现。

优势:运维方便、程序不用关心如何链接Redis实例

劣势:会带来性能消耗(大概20%)、无法平滑扩容/缩容,需要执行脚本迁移数据,不方便(Codis在Twemproxy基础上优化并实现了预分片来达到Auto Rebalance)。

· Redis Cluster

优势:官方集群解决方案、无中心节点,和客户端直连,性能较好

劣势:方案太重、无法平滑扩容/缩容,需要执行相应的脚本,不方便、太新,没有相应成熟的解决案例

9.内存分配

Redis为了屏蔽不同平台之间的差异及统计内存占用量等,对内存分配函数进行了一层封装,在程序中统一使用zmalloc、zfree系列函数,这些函数位于zmalloc.h/zmalloc.c文件中。封装就是为了屏蔽底层平台的差异,同时方便自己实现相关的统计函数。具体的实现方式如下:

  • 若系统中存在Google的TC_MALLOC库,则使用tc_malloc一族的函数代替原本的malloc一族的函数。
  • 若当前系统是Mac系统,则使用系统的内存分配函数。
  • 对于其它情况,在每一段分配好的空间前面同时多分配一个定长的字段,用来记录分配的空间大小,通过这种方式来实现简单有效的内存分配。
    Memcache采用slab table的方式分配内存,首先把可得的内存按照不同的大小来分类,在使用时根据需求找到最接近于需求大小的块分配,来减少内存碎片,但是这需要进行合理配置才能达到效果。
    从上面的对比可以看到,Redis在实现和使用上更简单,但是功能更强大,效率更高,应用也更广泛。下面将对Redis进行初步介绍,给初学者一个初体验式的学习引导。

二:Redis初体验
Redis是一个能够存储多种数据对象的开源Key-Value存储系统,使用ANSI C语言编写,可以仅仅当作内存数据库使用,也可以作为以日志为存储方式的数据库系统,并提供多种语言的API。
1.使用场景
我们通常把Redis当作一个非本地缓存来使用,很少用到它的一些高级功能。在使用中最容易出问题的是用Redis来保存JSON数据,因为Redis不像Elasticsearch或者PostgreSQL那样可以很好地支持JSON数据。所以我们经常把JSON当作一个大的String直接放到Redis中,但现在的JSON数据都是连环嵌套的,每次更新时都要先获取整个JSON,然后更改其中一个字段再放上去。
一个常见的JSON数据的Java对象定义如下:
public class Commodity {
private long price;
private String title;
……
}
在海量请求的前提下,在Redis中每次更新一个字段,比如销量字段,都会产生较大的流量。在实际情况下,JSON字符串往往非常复杂,体积达到数百KB都是有可能的,导致在频繁更新数据时使网络I/O跑满,甚至导致系统超时、崩溃。
因此,Redis官方推荐采用哈希来保存对象,比如有3个商品对象,ID分别是123、124和12345,我们通过哈希把它们保存在Redis中,在更新其中的字段时可以这样做:
HSET commodity:123 price 100

HSET commodity:124 price 101
HSET commodity:12345 price 101

HSET commodity:123 title banana
HSET commodity:124 title apple
HSET commodity:12345 title orange
也就是说,用商品的类型名和ID组成一个Redis哈希对象的KEY。在获取某一属性时只需这样做就可以获取单独的属性:HGET commodity: 12345。
2.Redis的高可用方案:哨兵
Redis官方推出了一个集群管理工具,叫作哨兵(Sentinel),负责在节点中选出主节点,按照分布式集群的管理办法来操作集群节点的上线、下线、监控、提醒、自动故障切换(主备切换),且实现了著名的RAFT选主协议,从而保证了系统选主的一致性。
这里给出一个哨兵的通用部署方案。哨兵节点一般至少要部署3份,可以和被监控的节点放在一个虚拟机中,常见的哨兵部署如图所示。

在这里插入图片描述
在这个系统中,初始状态下的机器A是主节点,机器B和机器C是从节点。
由于有3个哨兵节点,每个机器运行1个哨兵节点,所以这里设置quorum = 2,也就是在主节点无响应后,有至少两个哨兵无法与主节点通信,则认为主节点宕机,然后在从节点中选举新的主节点来使用。
在发生网络分区时,若机器A所在的主机网络不可用,则机器B和机器C上的两个Sentinel实例会启动failover并把机器B选举为主节点。
Sentinel集群的特性保证了机器B和机器C上的两个Sentinel实例得到了关于主节点的最新配置。但机器A上的Sentinel节点依然持有旧的配置,因为它与外界隔离了。
在网络恢复后,我们知道机器A上的Sentinel实例将会更新它的配置。但是,如果客户端所连接的主机节点也被网络隔离,则客户端将依然可以向机器A的Redis节点写数据,但在网络恢复后,机器A的Redis节点就会变成一个从节点,那么在网络隔离期间,客户端向机器A的Redis节点写入的数据将会丢失,这是不可避免的。
如果把Redis当作缓存来使用,那么我们也许能容忍这部分数据的丢失,但若把Redis当作一个存储系统来使用,就无法容忍这部分数据的丢失了,因为Redis采用的是异步复制,在这样的场景下无法避免数据的丢失。
在这里,我们可以通过以下配置来配置每个Redis实例,使得数据不会丢失:
min-slaves-to-write 1
min-slaves-max-lag 10
通过上面的配置,当一个Redis是主节点时,如果它不能向至少一个从节点写数据(上面的min-slaves-to-write指定了slave的数量),则它将会拒绝接收客户端的写请求。由于复制是异步的,所以主节点无法向从节点写数据就意味着从节点要么断开了连接,要么没在指定的时间内向主节点发送同步数据的请求。
所以,采用这样的配置可排除网络分区后主节点被孤立但仍然写入数据,从而导致数据丢失的场景。
3.Redis集群
Redis在3.0中也引入了集群的概念,用于解决一些大数据量和高可用的问题,但是,为了达到高性能的目的,集群不是强一致性的,使用的是异步复制,在数据到主节点后,主节点返回成功,数据被异步地复制给从节点。
首先,我们来学习Redis的集群分片机制。Redis使用CRC16(key) mod 16384进行分片,一共分16384个哈希槽,比如若集群有3个节点,则我们按照如下规则分配哈希槽:

  • A节点包含0-5500的哈希槽;
  • B节点包含5500-11000的哈希槽;
  • C节点包含11000-16384的哈希槽。
    这里设置了3个主节点和3个从节点,集群分片如图所示。

在这里插入图片描述

图中共有3个Redis主从服务器的复制节点,其中任意两个节点之间都是相互连通的,客户端可以与其中任意一个节点相连接,然后访问集群中的任意一个节点,对其进行存取和其他操作。
那Redis是怎么做到的呢?首先,在Redis的每个节点上都会存储哈希槽信息,我们可以将它理解为是一个可以存储两个数值的变量,这个变量的取值范围是0-16383。根据这些信息,我们就可以找到每个节点负责的哈希槽,进而找到数据所在的节点。
Redis集群实际上是一个集群管理的插件,当我们提供一个存取的关键字时,就会根据CRC16的算法得出一个结果,然后把结果除以16384求余数,这样每个关键字都会对应一个编号为0-16383的哈希槽,通过这个值找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。但是这些都是由集群的内部机制实现的,我们不需要手工实现。

猜你喜欢

转载自blog.csdn.net/chajinglong/article/details/100130479
今日推荐