作者:京东零售 李磊

Redis 集群介绍

Redis 集群一般有四种方式,分别为:主从复制、哨兵模式、Cluster 以及各大厂的集群方案。在 3.0 版本之前只支持单实例模式,3.0 之后支持了集群方式。在 3.0 之前各大厂为了解决单实例 Redis 的存储瓶颈问题各自推出了自己的集群方案,其核心思想就是数据分片,主要有客户端分片、代理分片、服务端分片。这里咱们只介绍前三种方式:主从、哨兵、Cluster。

1、主从复制

Redis 单节点的数据是存储在一台服务器上的,如果服务器出现故障,会导致数据不可用,而且读写都是在同一台服务器上,请求量大时会出现 I/O 瓶颈。为了避免单点故障和读写不分离,Redis 提供了复制功能来实现 Master 中的数据向 Slave 数据库的同步。Master 可以有多个 Slave 节点,Slave 节点也可以有 Slave 节点,从节点是级联结构,如下图所示:

主从复制工作原理

一般情况下为了让数据读写分离,Master 节点用来执行写操作,Slave 节点提供读操作,Master 执行写操作时将变化的数据同步到 Slave,其工作原理如下图所示:

Redis 主从复制基本原理有三种:全量复制、基于长连接的命令传播、增量复制。

首先介绍一下全量复制,当主从服务器刚建立连接的时候,会按照三个阶段完成数据的第一次同步。假设现在有实例 1(192.168.1.1)和实例 2(192.168.1.2),当我们在实例 2 上执行 “replicaof 192.168.1.1 6379” 命令后,实例 2 就变成了实例 1 的从库,并开始从实例 1 上复制数据,有如下三个阶段:

第一个阶段,是主从库之间建立连接、协商同步的过程,为全量复制做准备。具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。

•runID:是每个 Redis 实例启动时自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设置为 “?”。

•offset:设置为-1,表示第一次复制。

主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库,从库收到响应后会记录下这两个参数。FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二个阶段,主库将所有数据同步给从库,从库收到数据后,首先清空现有数据,然后在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。

第三个阶段,主库会把第二阶段执行过程中新接收到的写命令,再发送给从库。具体来说,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改和新增操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

以上是全量复制的基本流程,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

长连接是基于网络的,那么它就存在网络断开的风险,在 Redis2.8 之前,如果主从库在命令传播时出现了网络闪断,那么从库会和主库重新进行一次全量复制,开销非常大。在 Redis2.8 开始,网络闪断之后,主从库会采用增量复制的方式继续同步,就只会把主从网络断连期间主库收到的命令同步给从库。

增量复制核心在于 repl_backlog_buffer 这个缓冲区。当主从库断连后,主库会把断连期间收到的写操作命令写入 replication buffer,同时也会写入 repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库记录自己写到的位置,从库也记录自己读到的位置。主从连接恢复之后,从库首先给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距,一般来说 master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。

2、哨兵模式

sentinel,中文名哨兵。Redis 的 sentinel 系统用于管理多个 Redis 实例,该系统主要执行以下四个任务:

1.监控(Monitoring):Sentinel 会不断的检查主服务器和从服务器是否正常运作。

2.自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

3.通知(Notification):哨兵可以将故障转移的结果发送给客户端。

4.配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

哨兵用于实现 Redis 集群的高可用性,本身也是分布式的,作为一个哨兵集群去运行。Sentinel 的进程之间使用流言协议(gossip protocols) 来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。下面分别介绍一下监控和自动故障转移的基本原理:

Sentinel 集群监控原理

1.每个 Sentinel 以每秒一次的频率向它所知的主从服务器以及其它 Sentinel 实例发送一个 PING 命令。

2.如果一个实例距离最后一次有效回复 PING 命令的时间超过指定的值, 那么这个实例会被 Sentinel 标记为主观下线。

3.正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。

4.有足够数量的 Sentinel 在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。

5.每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主从服务器发送 INFO 命令。当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

6.Sentinel 和其它 Sentinel 协商主节点的状态,如果主节点处于 ODOWN(客观下线)状态,则投票自动选出新的主节点,将剩余的从节点指向新的主节点进行数据复制。

7.当没有足够数量的 Sentinel 同意主服务器 下线时, 主服务器的客观下线状态就会被移除。主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器主观下线状态就会被移除 。

哨兵是如何对 Slave 进行监控的呢?当然是通过 Master 来实现的,哨兵向 Master 发送 INFO 命令,Master 收到命令后便将 Slave 列表告诉哨兵。然后哨兵根据 Slave 列表信息与每一个 Slave 建立连接,并且根据这个连接持续监控 Slave。

Sentinel 集群故障自动转移

故障转移简单来说有以下三个流程:

1.Sentinel 系统挑选出现故障的主服务器属下的其中一个从服务器,并将选中的从服务器升级为新的主服务器。

2.Sentinel 系统向出现故障的主服务器属下的所有从服务器发送新的复制命令,让他们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。

3.Sentinel 系统还会继续监听已下线的故障服务器,如果它重新上线时,会将它设置为新的主服务器的从服务器。

示意图

如上图所示,Server1 为 Master 节点,Server2、Server3、Server4 为主服务器 Server1 的从节点,而 Sentinel 系统正在监视所有 4 个服务器

故障转移

如上图所示,主服务 server1 挂掉了,处于下线状态,那么 server2、server3、server4 对主服务器的复制操作将被终止,Server2 被 Sentinel 系统升级为新的 Master,然后将 Server2 和 Server3 转为新 Master 的从服务器,完成故障转移。同时继续监听已下线的 Server1。

如上图所示当 Server1 恢复后,Sentinel 系统将它设置为新的主服务器 Server2 的从服务器,集群恢复原有状态。

3、Cluster 集群

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存。所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容。Redis 集群是由多个主从节点群组成的分布式服务集群,具有复制、高可用和分片特性。这种集群模式没有中心节点,可水平扩展,主要是针对海量数据、高并发、高可用的场景。

Cluster 集群模式主要有以下三个特性:

1.分片存储:Redis3.0 加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的 master 节点上面,从而解决了海量数据的存储问题。

2.指令转换:Redis 集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一 Redis 实例一样,不需要任何代理中间件,当客户端操作的 key 没有分配到该 node 上时,Redis 会返回转向指令,指向正确的 Redis 节点。

3.主从和哨兵:Redis 也内置了高可用机制,支持 N 个 master 节点,每个 master 节点都可以挂载多个 slave 节点,当 master 节点挂掉时,集群会提升它的某个 slave 节点作为新的 master 节点。

如上图所示,Redis 集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点。

Redis 集群数据分片原理

集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。

Key 与哈希槽映射过程可以分为两大步骤:

1.根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值。

2.将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。

另外,Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。

Cluster 集群请求路由方式

客户端直连 Redis 服务,进行读写操作时,若 Key 对应的 Slot 在当前直连的节点上,则可直接读写,但也有可能并不在当前直连的节点上,则经过 “重定向” 才能转发到正确的节点,如下图所示



和普通的查询路由相比,Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis,而是借助客户端转发到正确的节点。实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。

那么客户端具体是怎么确定访问的数据到底分布在哪个实例上呢?

Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。在切片数据的时候是将 key 通过 CRC16 计算出一个值再对 16384 取模得到对应的 Slot,这个计算任务可以在客户端上执行发送请求的时候执行。但是,定位到槽以后还需要进一步定位到该 Slot 所在 Redis 实例。当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。当客户端请求时,会计算出键所对应的哈希槽,在通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。

这个时候大家可能会有个疑问:哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?

集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。Redis Cluster 提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。

Redis 如何告知客户端重定向访问新实例呢?分为两种情况:MOVED 错误、ASK 错误。

MOVED 错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。

(error) MOVED 16330 172.17.18.2:6379

该响应表示客户端请求的键值对所在的哈希槽 16330 迁移到了 172.17.18.2 这个实例上,端口是 6379。这样客户端就与 172.17.18.2:6379 建立连接,并发送 GET 请求。同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确。

ASK 错误:如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?

如果请求的 key 在当前节点找到就直接执行命令,否则就需要 ASK 错误响应了,槽部分迁移未完成的情况下,如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2,实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发送操作命令。

(error) ASK 16330 172.17.18.2:6379

比如客户端请求定位到 key 的槽 16330 在实例 172.17.18.1 上,节点 1 如果找得到就直接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2:6379

注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1 实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。MOVED 指令则更新客户端本地缓存,让后续指令都发往新实例。

Cluster 集群选举算法

1.集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。

2.检测到主节点下线的从节点向集群广播一条

CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

3.这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条

CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。

4.参与选举的从节点都会接收

CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。

5.如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

流程如下图所示:

Cluster 集群故障转移

Redis 集群的故障转移主要有三个流程:故障检测、选主流程、故障转移,下面分别简单介绍一下。

故障检查

一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理 slot 的节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这个节点的失联信息。

如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。

选主流程

参考上一节的 “Cluster 集群选举算法”

故障转移

当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。

1.从下线的 Master 节点的 Slave 节点列表选择一个节点成为新主节点。

2.新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。

3.新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。

4.新的主节点开始接收处理槽有关的命令请求,故障转移完成。

Cluster 集群扩容与缩容

扩容

Redis 集群主要有两种扩容方式:垂直扩容和水平扩容。

垂直扩容:增加内存方式来增加缓存实例的系统容量,比如从 2G 增加到 4G。

水平扩容:通过增加节点的方式来增加整个缓存系统的容量。

垂直扩容比较方便,但是受制于机制内存的限制,一个机器不可能无限增大内存, 所以到了一定阶段肯定要进行水平扩容。下面我们主要讲一下水平扩容。

水平扩容又有两种方式:1、主节点数量不变;2、增加主节点数量

1、主节点数量不变:比如,当前有一台物理机 A,构建了一个包含 3 个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是 3 个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下:

1.将新增节点加入集群;

2.将新增节点设置为某个主节点的从节点,进而对其进行复制;

3.进行主备倒换,将新增的节点调整为主。

2、增加主节点数量:不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下:

1.将新增节点加入集群;

2.将集群中的部分 Slot 迁移至新增的节点。

缩容

•如果下线的是 slave,那么通知其他节点忘记下线的节点

•如果下线的是 master,那么将此 master 的 slot 迁移到其他 master 之后,通知其他节点忘记此 master 节点

•其他节点都忘记了下线的节点之后,此节点就可以正常停止服务了

Redis 集群测试思路及常见问题

集群搭建好之后,就可以对集群的各种功能和使用进行测试了。一般我们会从两个方面来制定测试计划:1、集群功能测试;2、集群调优测试

1、集群功能测试

集群功能测试属于最基本的测试,是为了验证集群所提供的各种功能是否能正常使用,主要有以下方面的内容:

•主从节点的数据备份是否正常

•主从节点的切换功能是否正常

•监控及故障转移功能是否正常

•集群扩缩容功能是否正常

集群的功能测试类似于黑盒测试,内容比较简单,在这里我们就不展开介绍了,下面主要介绍一下集群在使用过程中的调优测试。

2、集群调优测试

集群调优测试,是为了验证集群在提供服务时,如何最大限度避免因各种异常导致数据丢失或者缓存功能失效,提前对配置进行调优或者提前预案,从而保证缓存架构设计是最优的。需要注意的点主要有:集群脑裂、缓存穿透、缓存击穿、缓存雪崩、缓存预热、缓存降级、缓存更新,下面分别介绍一下定义以及解决方案。

集群脑裂

定义

当 Redis 主从集群环境出现两个主节点为客户端提供服务,这时客户端请求命令可能会发生数据丢失的情况。脑裂产生的场景主要有两个:

1.如果哨兵正在进行选举,故障转移的过程中原主节点恢复和客户端的通信,那么证明原主节点没有真正的故障,这时客户端依旧可以向原主节点正常通信,但是当故障转移结束后,就又产生了一个主节点,这就是脑裂产生的第一个场景,如下图所示:



2. 网络分区,主节点和客户端,哨兵和从库分割为了两个网络,主库和客户端处在一个网络中,从库和哨兵在另外一个网络中,此时哨兵也会发起主从切换,出现两个主节点的情况,如下图所示:



脑裂出现后带来最严重的后果就是数据丢失,为什么会出现数据丢失的问题呢?

主要原因是新主库确定后会向所有的实例发送 slave of 命令,让所有实例重新进行全量同步,而全量同步首先就会将实例上的数据先清空,所以在主从同步期间在原主库执行的命令将会被清空(上面场景二是同样的道理,在网络分区恢复后原主节点将被降级为从节点,并且执行全量同步导致数据丢失),所以这就是数据丢失的具体原因,如下图所示:

解决方案

应对脑裂的解决办法应该是去限制原主库接收请求,Redis 提供了两个配置项:

1.min-replicas-to-write:主库能进行数据同步的最少从库数量,否则主节点拒绝写入。

2.min-replicas-max-lag:主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(单位 s),否则主节点拒绝写入。

这两个配置项必须同时满足,不然主节点拒绝写入。

即使原主假故障,假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然就无法和从库进行 ACK 确认。这俩配置项组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写新数据。等新主上线,就只有新主能接收和处理客户端请求,此时,新写的数据会被直接写到新主。而原主会被哨兵降为从库,即使它的数据被清空,也不会有新数据的丢失。示例如下:

假设:

min-replicas-to-write=1

min-replicas-max-lag 设为 12s

哨兵的 down-after-milliseconds 设为 10s

主库因某原因卡住 15s,导致哨兵判断主库客观下线,开始进行主从切换。 同时,因原主库卡住 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求。主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失。

但是,在实际应用中,真的能完全避免数据的丢失吗?我们看下面的例子:

假设:

min-replicas-to-write 置 1

min-replicas-max-lag 设置为 15s

哨兵的 down-after-milliseconds 设置为 10s 哨兵主从切换需要 5s,主库因为某些原因卡住 12s,此时,还会发生脑裂吗?主从切换完成后,数据会丢失吗?

主库卡住 12s,达到哨兵设定的切换阈值,所以哨兵会触发主从切换。但哨兵切换时间 5s,即哨兵还未切换完成,主库就会从阻塞状态中恢复回来,且没有触发 min-slaves-max-lag 阈值,所以主库在哨兵切换剩下的 3s 内,依旧可以接收客户端的写操作,如果这些写操作还未同步到从库,哨兵就把从库提升为主库了,那么此时也会出现脑裂的情况,之后旧主库降级为从库,重新同步新主库的数据,新主库也会发生数据丢失。

所以,即使 Redis 配置了 min-replicas-to-write 和 min-replicas-max-lag,当脑裂发生时,还是无法严格保证数据不丢失,只是尽量减少数据的丢失。我们需要根据被测系统的性能、网络情况、流量情况来调优这两个参数的配置,当出现异常的时候尽可能最小的缩减数据丢失的时间。

缓存穿透

定义

当查询 Redis 中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过 Redis 访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透 Redis 的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

解决方案

1.在接口访问层对用户做校验,如接口传参、登陆状态、n 秒内访问接口的次数;

2.利用布隆过滤器,将数据库层有的数据 key 存储在位数组中,以判断访问的 key 在底层数据库中是否存在;核心思想是布隆过滤器,在 redis 里也有 bitmap 位图的类似实现,布隆过滤器不能实现动态删除,有时间可以研究下布谷鸟过滤器,是布隆过滤器增强版本。布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将 99.99% 的穿透查询给屏蔽在 Redis 层了,极大的降低了底层数据库的压力,减少了资源浪费。

基于布隆过滤器,我们可以先将数据库中数据的 key 存储在布隆过滤器的位数组中,每次客户端查询数据时先访问 Redis:

•如果 Redis 内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;

•如果布隆过滤器告诉我们该 key 在底层库内不存在,则直接返回 null 给客户端即可,避免了查询底层数据库的动作;

•如果布隆过滤器告诉我们该 key 极有可能在底层数据库内存在,那么将查询下推到底层数据库即可;

缓存击穿

定义

缓存击穿和缓存穿透从名词上可能很难区分开来,它们的区别是:穿透表示底层数据库没有数据且缓存内也没有数据,击穿表示底层数据库有数据而缓存内没有数据。当热点数据 key 从缓存内失效时,大量访问同时请求这个数据,就会将查询下沉到数据库层,此时数据库层的负载压力会骤增,我们称这种现象为"缓存击穿"。

解决方案

•延长热点 key 的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口;

•利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至 Redis 内,避免其他大量请求同时穿过 Redis 访问底层数据库;

缓存雪崩

定义

缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到 Redis 内的热点数据失效导致大量并发查询穿过 redis 直接击打到底层数据库,而缓存雪崩是指 Redis 中大量的 key 几乎同时过期,然后大量并发查询穿过 redis 击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。

事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时 key 的场景的确太少见了,而大量 key 同时过期,大量用户访问这些 key 的几率相比缓存击穿来说明显更大。

解决方案

1.在可接受的时间范围内随机设置 key 的过期时间,分散 key 的过期时间,以防止大量的 key 在同一时刻过期;

2.对于一定要在固定时间让 key 失效的场景 (例如每日 12 点准时更新所有最新排名),可以在固定的失效时间时在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来;

3.延长热点 key 的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样;

缓存预热

如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至 Redis 内再提供出去使用,这种操作就成为"缓存预热"。

缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。

缓存降级

•缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。

•降级的目的是保证核心服务可用,即使是有损的。如某年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。

•降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。

缓存更新

缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。

参考文档:《Redis 设计与实现》- 黄健宏著

https://www.cnblogs.com/yizhiamumu/p/16704556.htmlhttps://cloud.tencent.com/developer/article/1981186


↙↙↙阅读原文可查看相关链接,并与作者交流