redis学习(下)
(0)

主从复制

什么是主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。

前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点
默认情况下,每台redis服务器都是主节点;且一个主节点可以有多个从节点(或者没有),但一个从节点只有一个主

主从复制的作用主要包括

数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从库采用的是读写分离的方式

21.png

原理

分为全量复制与增量复制

全量复制:发生在第一次复制时

增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库

2 全量复制的三个阶段

第一阶段是主从库间建立连接、协商同步的过程。

主要是为全量复制做准备。从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。

具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二阶段,主库将所有数据同步给从库。

从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。

具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。

第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。

具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

哨兵机制

哨兵的核心功能是主节点的自动故障转移

下图是一个典型的哨兵集群监控的逻辑图

22.png

Redis Sentinel包含了若个Sentinel节点,这样做也带来了两个好处:

对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判

即使个别Sentinel节点不可用,整个Sentinel集群依然是可用的。

哨兵实现了一下功能

监控:每个Sentinel节点会对数据节点(Redis master/slave 节点)和其余Sentinel节点进行监控

通知:Sentinel节点会将故障转移的结果通知给应用方

故障转移:实现slave晋升为master,并维护后续正确的主从关系

配置中心:在Redis Sentinel模式中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息

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

1 原理

监控

Sentinel节点需要监控master、slave以及其它Sentinel节点的状态。这一过程是通过Redis的pub/sub系统实现的。Redis Sentinel一共有三个定时监控任务,完成对各个节点发现和监控:

监控主从拓扑信息:每隔10秒,每个Sentinel节点,会向master和slave发送INFO命令获取最新的拓扑结构

Sentinel节点信息交换:每隔2秒,每个Sentinel节点,会向Redis数据节点的__sentinel__:hello频道上,发送自身的信息,以及对主节点的判断信息。这样,Sentinel节点之间就可以交换信息

节点状态监控:每隔1秒,每个Sentinel节点,会向master、slave、其余Sentinel节点发送PING命令做心跳检测,来确认这些节点当前是否可达

主观/客观下线

主观下线

每个Sentinel节点,每隔1秒会对数据节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复时,Sentinel节点会对该节点做失败判定,这个行为叫做主观下线。

客观下线

客观下线,是指当大多数Sentinel节点,都认为master节点宕机了,那么这个判定就是客观的,叫做客观下线。

那么这个大多数是指多少呢?这其实就是分布式协调中的quorum判定了,大多数就是过半数,比如哨兵数量是5,那么大多数就是5/2+1=3个,哨兵数量是10大多数就是10/2+1=6个。

注:Sentinel节点的数量至少为3个,否则不满足quorum判定条件。

哨兵选举

如果发生了客观下线,那么哨兵节点会选举出一个Leader来进行实际的故障转移工作。Redis使用了Raft算法来实现哨兵领导者选举,大致思路如下:

每个Sentinel节点都有资格成为领导者,当它主观认为某个数据节点宕机后,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令,要求自己成为领导者;

收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinelis-master-down-by-addr命令,将同意该请求,否则拒绝(每个Sentinel节点只有1票);

如果该Sentinel节点发现自己的票数已经大于等于MAX(quorum, num(sentinels)/2+1),那么它将成为领导者;

如果此过程没有选举出领导者,将进入下一次选举。

故障转移

选举出的Leader Sentinel节点将负责故障转移,也就是进行master/slave节点的主从切换。故障转移,首先要从slave节点中筛选出一个作为新的master,主要考虑以下slave信息:

跟master断开连接的时长:如果一个slave跟master的断开连接时长已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么该slave就被认为不适合选举为master;

slave的优先级配置:slave priority参数值越小,优先级就越高;

复制offset:当优先级相同时,哪个slave复制了越多的数据(offset越靠后),优先级越高;

run id:如果offset和优先级都相同,则哪个slave的run id越小,优先级越高。

接着,筛选完slave后, 会对它执行slaveof no one命令,让其成为主节点。

最后,Sentinel领导者节点会向剩余的slave节点发送命令,让它们成为新的master节点的从节点,复制规则与parallel-syncs参数有关。

Sentinel节点集合会将原来的master节点更新为slave节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

注:Leader Sentinel节点,会从新的master节点那里得到一个configuration epoch,本质是个version版本号,每次主从切换的version号都必须是唯一的。其他的哨兵都是根据version来更新自己的master配置。

缓存穿透、击穿、雪崩

1 缓存穿透

问题来源

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。

布隆过滤器。类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。

2 缓存击穿

问题来源

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案

1、设置热点数据永远不过期。

2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。

3、加互斥锁

3 缓存雪崩

问题来源

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

设置热点数据永远不过期

Redis 进阶使用

1. 布隆过滤器

Redis 在 4.0 以后支持布隆过滤(准确的来说是支持了布隆过滤器的插件),给 Redis 提供了强大的去重功能。在业务中,我们可能需要查询数据库判断历史数据是否存在,如果数据库的并发能力有限,这个时候我们可以采用 Redis 的 set 做去重。如果缓存的数据过大,这个时候就需要遍历所有缓存数据,另外如果我们的历史数据缓存写不下了,终究要去查询数据库,这个时候就可以使用布隆过滤器。

当然布隆过滤器精确度不是 100% 准确(如果对数据准确度要求很高的话,这里不建议使用),因为对于存在的数据也许这个值不一定存在,当然如果不存在,那肯定 100% 不存在了。

(1)命令使用

bf.add #添加元素
bf.exists #判断元素是否存在
bf.madd #批量添加
bf.mexists #批量判断是否存在

(2)原理
23.png

布隆过滤的组成可以当作一个位数组和几个计算结果比较均匀的 hash 函数,每次添加 key 的时候,会把 key 通过多次 hash 来计算所得到的位置,如果当前位置不是 0 则表示存在。可以看到,这样的计算存在一定误差,这也正是它的不准确性问题的由来。

2. 分布式锁

大家对分布式锁也许也不会陌生,现在市面上主流的实现分布锁的技术有 ZK 和 Redis;下文为大家简单介绍一下 Redis 如何实现分布式锁。

命令

setnx lock:mutex ture #加锁
del  lock:mutex #删除锁

实现分布式锁的核心就是:请求的时候 Set 这个 key,如果其他请求设置失败的时候,即拿不到锁。但是存在一个问题:如果业务 panic 或者忘记调用 del 的话,就会产生死锁,这个时候大家很容易能想到:我们可以 expire 一个过期时间,这样就可以保证请求不会一直独占锁且无法释放锁的逻辑了。

但是假设业务存在这样一种情况:A 请求在获取锁后处理逻辑,由于逻辑过长,这个时候锁到期释放了,A 这个时候刚刚处理完成,而 B 又去改了这个数据,这就存在一个锁失效的问题。解决这种问题参考 CAS 的方式,对锁设置一个随机数,可以理解为版本号,如果释放的时候版本号不一致,则表示数字已经在释放那一刻改掉了。

深入原理

1. IO模型

Redis 是单线程模型(这里的单线程指的是 IO 和键值对的读写是一个线程完成的),当然如果严谨的来说还是可以理解为是多线程,不过这样的多线程不过是在数据备份的时候会 fork 一个子进程对数据进行从磁盘读取数据并组装 RDB,然后同步给 slaver 节点的操作,当然包括备份和持久化也都是通过另外起线程完成的,所以我们可以把 Redis 认作为一个单线程模型。

那么问题来了,为什么单线程的模型能这么快?原因很简单,因为 Redis 本身就是在内存中运算,而对于上游的客户端请求,采用了多路复用的原理。Redis 会给每一个客户端套接字都关联一个指令队列,客户端的指令队列通过队列排队来进行顺序处理,同时 Reids 给每一个客户端的套件字关联一个响应队列,Redis 服务器通过响应队列来将指令的接口返回给客户端。

24.png

2. 通信协议

Redis 采用了 Gossip 协议作为通信协议。Gossip 是一种传播消息的方式,可以类比为瘟疫或者流感的传播方式,使用 Gossip 协议的有:Redis Cluster、Consul、Apache Cassandra 等。Gossip 协议类似病毒扩散的方式,将信息传播到其他的节点,这种协议效率很高,只需要广播到附近节点,然后被广播的节点继续做同样的操作即可。当然这种协议也有一个弊端就是:会存在浪费,哪怕一个节点之前被通知到了,下次被广播后仍然会重复转发。

25.png

3. 持久化

(1)RDB

RDB 是对当前 Redis 的存储数据进行一次快照(具体原理和如何做,限于篇幅这里不做过多复述了)。

(2)AOF

日志只记录 Redis 对内存修改的指令记录,Redis 提供了一个 bgrewriteaif 的指令对 AOF 进行压缩。原理就是:开辟一个子进程对内存进行遍历后,转换成一系列对 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。系列化完成后再将发送的增量 AOF 日志追加到这个新的 AOF 日志中,追加完成后用新的 AOF 日志代替旧的。

(3)混合持久化

由于单纯 RDB 的话,可能存在数据的丢失,而频繁的 AOF 又会影响了性能,在 Redis 4.0 之后,支持了混合持久化,也就是每次启动时候通过 RDB+增量的 AOF 文件来进行回复,由于增量的 AOF 仅记录了开始持久化到持久化结束期间发生的增量,这样日志不会太大,性能相对较高。

4. 主从同步

Redis 的同步方式有:主从同步、从从同步(由于全部都由 master 同步的话,会损耗性能,所以部分的 slave 会通过 slave 之间进行同步)。

26.png
27.png

同步过程

建立连接,然后从库告诉主库:“我要同步啦,你给我准备好”,然后主库跟从库说:“收到”。

从库拿到数据后,要把数据保存到库里。这个时候就会在本地完成数据的加载,会用到 RDB 。

主库把新来的数据 AOF 同步给从库。

5. Sentinel

Redis 的主从切换是通过哨兵来解决的。这里哨兵主要解决的问题就是:当 master 挂了的情况下,如果在短时间内重新选举出一个新的 master 。

28.png

Sentinel 集群是一个由 3-5 个(可以更多)节点组成的,用来监听整个 Redis 的集群,如果发现 master 不可用的时候,会关闭和断开全部的与 master 相连的旧链接。这个时候 Sentinel 会完成选举和故障转移,新的请求则会转到新到 master 中。

6. Redis集群工作原理

Redis 集群通过槽指派机制来决定写命令应该被分配到那个节点。整个集群对应的槽是由 16384 大小的二进制数组组成,集群中每个主节点分配一部分槽,每条写命令落到二进制数组中的某个位置,该位置被分配给了哪个节点,则对应的命令就由该节点去执行。槽指派对应的二进制数组如下图所示:

29.png

从上图可以看到:节点 1 只负责 执行 0 - 4999 的槽位,而节点 2 负责执行 5000 - 9999,节点 3 执行 9999- 16383 。当进行写的时候:

set key value

命令通过 CRC16(key) & 16383 = 6789(假设结果),由于节点 2 负责 5000~9999 的槽位,则该命令的结果 6789 最终由节点 2 执行。当然如果在节点 2 执行一条命令时,假设通过 CRC 计算后得到的值为 567,则其应该由节点 1 执行,此时命令会进行转向操作,将要执行的命令流转到节点 1 上去执行。

30.png

集群节点同步

集群中每个主节点都会定时发送信息到其他主节点进行同步,如果其他主节点在规定时间内响应了发送消息的主节点,则发送消息的主节点认为响应了消息的主节点正常,反之则认为响应消息的主节点疑似下线,则发送消息的主节点在其节点上将其标记“疑似下线”。

当集群中超过一半以上的节点认为某个主节点被标记为“疑似下线”,则其中某个主节点将疑似下线节点标记为下线状态,并向集群广播一条下线消息,当下线节点对应的从节点接收到该消息时,则从从节点中选举出一个节点作为主节点继续对外提供服务。

Redis为什么变慢了

业务场景中,不知道大家是否碰到过 Redis 变慢的情况:

执行 SET、DEL 命令耗时也很久;
偶现卡顿,之后又恢复正常了;
在某个时间点,突然开始变慢了。

原因分析

查看慢查询,由于笔者本身机器没有慢查询,所以这里看到是空(实在尴尬,这里没有可用的例子~~)

31.png

由于 Redis 在 IO 操作和对键值对的操作是单线程的,所以直接在客户端 Redis-cli 上执行的 Redis 命令有可能会导致操作延迟变大;
使用复杂的命令会让 Redis的处理变慢,以及CPU过高,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令(时间负责度O(N) );

查询的数据量过大,使得更多时间花费在数据协议的组装和网络传输过程中;

大 key 查询,比如对于一个很大的 hash、zset 等,这样的对象对 Redis 的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个 key 太大,会导致数据迁移卡顿;

另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

集中过期,变慢的时间统一,所以业务中的 Key 过期时间尽量在统一的一个时间点加上一个随机数时间;

内存使用达到上限,当内存达到内存上限的时候,就不许淘汰一些数据,这个时候也可能导致 Redis 查询效率低;

碎片整理,Redis 在 4.0 版本后会自动整理碎片(由于内存回收过程中存在大量的碎片空间,不整理会导致 Redis 的空间少量浪费),而在整理碎片的过程中会消耗 CPU 的资源,从而影响了请求得到性能;
网络带宽,Redis 集群和业务混部,或者并发量过大以及每次返回的数据也很大,网卡带宽跑满的情况容易导致网络阻塞;

AOF 的频率过高,由于 AOF 需要将全部的写命令同步,如果同步的间隔比较短,也会影响到 Redis 的性能;

Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是导致 Redis 缓慢的操作。

Redis安全

默认会监听 6379 端口,最好在 Redis 的配置文件中指定监听的 IP 地址,更进一步还可以增加 Redis 的 ACL 访问控制,对客户指定群组,并限限制用户对数据的读写权限。

访问 Redis 尽量走公司代理,由于 Redis 本身不支持 SSL 的链接,所以走公司代理可以保证安全。客户端登陆 Redis 必须设置 Auth 秘密登陆。

本文为作者admin发布,未经允许禁止转载!
上一篇 下一篇
评论
暂无评论 >_<
加入评论