Redis集群和脑裂问题

Redis主从复制、Redis如何解决缓存雪崩、击穿、穿透难题,脑裂

Redis面试:《我们一起进大厂》系列- Redis基础 - 掘金 (juejin.cn)

主从复制

当启动一个slave node的时候,从节点会在本地保存master node的信息,包括master node的host和IP,slave node内部有一个定时任务,每秒检查是否有新的master node需要连接和复制,如果发现需要连接的master node,则与其建立socket连接,然后发送一个PSYNC命令给master node。如果这是slave node首次连接到master node上,则会进行全量复制。此时master会启动一个后台线程执行bgsave,开始生成一份RDB快照文件,同时还会把从客户端新收到的命令缓存在内存中。RDB文件生成后,master会将这个RDB文件发送给slave node,slave会先写入到本地磁盘,然后再从本地磁盘加载到内存中,接着master会将内存中缓存的命令发送到slave,slave也会同步这些数据。在复制过程中,master node照样可以进行服务(copy on write)。
在这里插入图片描述

Redis 支持三种集群方案

  • 主从复制模式
  • Sentinel(哨兵)模式
  • Cluster 模式

img

主从复制的作用

通过持久化功能,Redis保证了即使在服务器重启的情况下也不会丢失(或少量丢失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。 但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。

为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。

为此, Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上

在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库

总结:引入主从复制机制的目的有两个

  • 一个是读写分离,分担 “master” 的读写压力
  • 一个是方便做容灾恢复

主从复制原理

img

从数据库启动成功后,连接主数据库,发送 SYNC 命令;

主数据库接收到 SYNC 命令后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;

主数据库 BGSAVE 执行完后,向所有从数据库发送快照文件,并在发送期间继续记录被执行的写命令;

从数据库收到快照文件后丢弃所有旧数据,载入收到的快照;

主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令;

从数据库完成对快照的载入,开始接收命令请求,并执行来自主数据库缓冲区的写命令;(从数据库初始化完成

主数据库每执行一个写命令就会向从数据库发送相同的写命令,从数据库接收并执行收到的写命令(从数据库初始化完成后的操作

出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库,增量复制。

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主从复制优缺点

主从复制优点

  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
  • 为了分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成;
  • Slave 同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力;
  • Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在 Master-Slave 同步期间,客户端仍然可以提交查询或修改请求;
  • Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据;

主从复制缺点

  • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复(也就是要人工介入);
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
  • 如果多个 Slave 断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要 Slave 启动,就会发送sync 请求和主机全量同步,当多个 Slave 重启的时候,可能会导致 Master IO 剧增从而宕机。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;

当缓存数据量超过一定的数量时,我们就要对Redis集群做分库分表的操作。

来个栗子,我们有一个电商平台,需要使用Redis存储商品的图片资源,存储的格式为键值对,key值为图片名称,Value为该图片所在的文件服务器的路径,我们需要根据文件名,查找到文件所在的文件服务器上的路径,我们的图片数量大概在3000w左右,按照我们的规则进行分库,规则就是随机分配的,我们以每台服务器存500w的数量,部署12台缓存服务器,并且进行主从复制,架构图如下图1-2所示:

img

​ 图1-2:Redis分库分表

过期key的处理

slave不会过期key,只会等待msater过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。

Heartbeat

主从节点互相发送heatbeat信息。master默认每隔10秒发送一次心跳,slave node每隔1秒发送一个心跳。

RDB、AOF 与COW

Redis有两种数据持久化的方式:AOF和RDB。

简单来说,AOF是记录数据增量的方式,将每次对服务器写的操作存入日志(类似MySQL的binlog);而RDB是记录全量数据,根据指定的时间间隔对数据进行快照存储,以二进制格式文件(后缀RDB)保存在硬盘当中。

  • 主从复制时,自动生成RDB文件
  • shutdown会自动生成RDB文件

具体见:(96条消息) Redis 中 bgsave 方式持久化的细节问题_zmflying8177的博客-CSDN博客_redis的bgsave

Sentinel(哨兵)模式

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个 Redis 实例

单哨兵

哨兵模式的作用

  • 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;
  • 当哨兵监测到 master 宕机,会自动将 slave 切换成 master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;
  • 然而一个哨兵进程对Redis服务器进行监控,也可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

多哨兵

故障切换的过程

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。

如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)

如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态

当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)

在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、Slave 从服务器发送 INFO 命令。

当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

哨兵模式的优缺点

优点:

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高(可以看作自动版的主从复制)。

缺点:

  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

哨兵的核心知识:

  • 哨兵至少需要3个实例,来保证自己的健壮性。
  • 哨兵+redis主从的部署架构,是不保证数据的零丢失的,只能保证Redis集群的高可用。
  • 对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
    哨兵集群必须部署2个以上节点,如果哨兵集群仅仅部署了两个哨兵实例,则quorum=1。

Cluster 集群模式(Redis官方)

Redis Cluster是一种服务器 Sharding 技术,3.0版本开始正式提供。

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容

image-20200531184321294

在这个图中,每一个蓝色的圈都代表着一个 redis 的服务器节点。它们任何两个节点之间都是相互连通的。客户端可以与任何一个节点相连接,然后就可以访问集群中的任何一个节点。对其进行存取和其他操作。

集群的数据分片

Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念。

Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5460 号哈希槽
  • 节点 B 包含 5461 到 10922 号哈希槽
  • 节点 C 包含 10923 到 16383 号哈希槽

​ 这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

Redis 集群的主从复制模型

为了保证高可用,redis-cluster集群引入了主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了,那么该集群就无法再提供服务了。

集群的特点

  • 所有的 redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  • 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。
  • 客户端与 Redis 节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

脑裂问题

概述

脑裂问题,听起来很高端的样子.其实说白了就是主从拓扑结构,在主节点被误切换,导致原主节点还在处理请求,导致新主节点这部分数据缺失的一个现象.一般来说,主从结构多少有脑裂问题.

脑裂问题

image.png

说明:redis主从结构正常情况,正常同步,正常处理客户端请求.

image.png

说明: master由于处理数据量比较大,或者网络拥堵,没有ack心跳包,哨兵误以为主节点下线,这个时候 选举了新的master,而且这个时候旧master依然在处理 set a 10这个请求

image.png

说明:旧的master已经恢复网络,发现已经选举了新master,于是清空本地数据,开始全量同步新master数据 ,导致set a 10这个数据丢失,新master并不知道发生过这次请求

解决方案

1
2
3
4
5
6
1.配置min-slaves-to-write 1, 要求master必须有一个slaver

2.配置min-slaves-max-lag 10,要求主从同步延时不能超过10s

3.只要以上两个配置都不满足,那么当前master会拒绝接受客户端请求,这样可以防止由于脑裂
问题一直丢失数据,可以将数据损失减少到10s以内

Redis如何解决缓存雪崩、击穿、穿透难题

概述

针对雪崩、击穿、穿透问题,前提都是在高并发情况下,如果你做的场景流量在mysql数据库承载范围内,那么你并不需要关心这些问题。你只要保证你使用缓存,保证缓存与数据库数据一致性即可。所以今天讨论的是在高流量情况,如何应对使用缓存中一些常见的问题。

缓存雪崩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 背景:大量缓存同时过期,直接访问数据库刷新缓存数据,访问量超过mysql并发处理阈值,导致mysql长时间
无响应,导致依赖mysql服务都长时间不可用,无响应,服务超时.

解决方案:
1.错开缓存过期时间,设置业务满足的过期时间之后,再加个过期时间的随机数

2.采用服务降级,核心业务允许访问,非核心业务直接返回预定义信息直接返回

背景: redis单台机器部署,直接宕机了,那么很明显,这个时候缓存全部失效,直接大量请求到mysql,在redis没
恢复之前,这些大量请求是长期存在的,和上面允许部分先刷缓存慢慢恢复服务(短时间)还有点不一样

解决方案:
1.redis高可靠集群部署,当主节点宕机,哨兵直接切换新节点(从节点),缓存瞬间全部恢复

2.熔断,限流

对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

这就是缓存雪崩。

img

缓存雪崩的事前事中事后的解决方案如下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
  • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
img

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 Redis。如果 ehcache 和 Redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。

限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空值。

好处:

  • 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
  • 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
  • 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来了。

缓存击穿

1
2
3
4
5
6
7
8
9
   背景:热点数据,也就是单条数据访问量非常高,只要缓存一过期,马上有大量访问请求到数据库,导致缓存击穿.
比如微博热搜数据,如果缓存一过期,那么将承受巨大流量

解决方案:

1.对于热点数据直接不设置缓存数据即可.
2.加锁,拦截不必要的请求

复制代码

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

不同场景下的解决方式可如下:

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。

  • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。

  • 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

缓存穿透

1
2
3
4
5
6
7
8
9
10
  背景: 非正常用户故意发起缓存中不存在的数据,那么必然不会中缓存,并且开启多线程刷你接口,一般这种都是
恶意请求,并且持续高流量发起攻击,导致mysql承受巨大流量压力,影响正常业务访问数据库

解决方案:
1.对于明显不存在的数据,在缓存中设置一个空值,这样直接走缓存,保护了数据库

2.在应用程序代码中,做合法参数校验

3.使用布隆过滤器,用来判断真实存在的数据,不存在的直接返回错误信息即可
复制代码

缓存穿透

对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

img

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN 。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

总结

上述情况都是在高流量的情况才会发生的,如果流量不大,不存在会影响服务不可用的情况

连接到docker的redis

1
docker exec -it redis redis-cli