Redis 读书笔记 基础篇

93 阅读17分钟

1. 基础架构

从SimpleKV到Redis image.png image.png 和跟 Redis 相比,SimpleKV 还缺少什么? image.png

2. 数据结构: 快速的redis有哪些慢操作

Redis键值对中值的数据类型:String字符串、List列表、Hash哈希、Set集合、Sorted Set有序集合

2.0 底层数据结构

image.png

2.1 键和值用什么结构组织?

image.png redis使用了一个全局哈希表来保存所有键值对。 一个哈希表其实就是一个数组,数组的每个元素称为一个哈希桶。 哈希桶本身保存的并不是值本身而是指向具体值的指针。 哈希桶里有key和value,value分别是实际的键和值,值的数据类型如String、List、Hash、Set、Sorted Set。

哈希表最大的好处很明显,就是让我们用O(1) 的时间复杂度来查找键值对,然后访问响应的哈希桶元素。

2.2 如果说哈希表数据写入越来越多怎么办?

redis解决哈希冲突的方式是链式哈希。 指的是同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。 image.png 之后随着链路越来越长,Redis会对哈希表做rehash操作。 rehash操作就是增加现有的哈希桶数量,让逐渐增多的entry元素能再更多的桶之间分散保存,从而减少单个桶中的元素数量,再而减少单个桶中的冲突。 为了rehash过程中如果有大量数据进行拷贝迁移而导致redis线程阻塞,采用渐进式rehash。 渐进式rehash就是把一次性大量的拷贝的开销分摊到多次处理请求中。

2.3 集合数据的操作效率

String类型来说,找到哈希桶就可以直接增删改查,对于集合类型来说,通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。

集合的底层数据结构有 整数数组、双向链表、压缩列表、跳表、 整数数组、双向链表:顺序读写,通过数组下标或链表的指针逐个元素访问。

image.png 压缩列表:列表长度、列表尾的偏移量、entry个数、列表结束。O(N)

image.png 跳表:链表的基础上增加多级索引,实现数据快速定位。O(logN) image.png

2.4 不同操作的复杂度

单元操作是基础 Hash(HGET\HSET|HDEL), Set(SADD\SREM\SRANDMEMBER) 范围操作非常耗时 Hash(HGETALL) LRANGE 和 ZSet类型的ZRANGE 统计操作非常高效 例外情况 压缩列表和双向链表会记录表头和表尾的偏移量实现快速操作

2.5 整数数组和压缩列表作为底层数据结构优势是什么?

这两个底层的数据结构的设计是节省内存空间,都是在内存中分配一块地址连续的空间。 把集合中的元素一个接一个地放在这块空间内,非常紧凑。避免而外指针带来的空间开销。 image.png

3. 高性能IO模型: 为什么单线程的Redis能这么快

Redis真的只有单线程吗?为什么用单线程?单线程为什么这么快? Redis单线程是指它对网络IO和数据读写的操作采用了一个线程,而采用单线的一个核心原因是避免多线程开发的并发控制问题。 单线程的Redis也能获得高性能,跟多路复用的IO模型密切相关,因为避免了accept和send、recv潜在的网络IO操作阻塞点。

3.1 Redis 为什么用单线程?

多线程的开销

多线编程模式面临的共享资源的并发访问控制问题 image.png

3.2 单线程 Redis 为什么那么快?

redis使用单线程模型达到每秒十万级别的处理能力。

  • 一方面它采用了高效的数据结构例如哈希表和跳表,这是它实现高性能的原因之一。
  • 另一方面Redis采用了多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现吞吐率。
3.2.1 基本 IO 模型与阻塞点

image.png 潜在阻塞点:accept、recv; 如果redis监听到一个客户端有连接请求,但一直未能成功建立连接时,会阻塞在accept函数里。导致其他客户端无法和Redis建立连接。 当redis通过recv从一个客户端读取数据时,如果数据一致没有到达,redis也会一直阻塞在recv。

3.2.2 非阻塞模式

socket网络模型本身支持非阻塞模式。 image.png

3.3.3 基于多路复用的高性能 I/O 模型

多路:多个socket网络连接 复用:复用一个线程 image.png Redis网络框架调用epoll机制,让内核监听这些套接字,Redis可以同时和多个客户端连接并处理请求,从而提高并发性。

这些事件会被放进事件处理队列,redis单线程对该事件不断进行处理,避免一直轮询导致cpu资源浪费。同时select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

3.3 Redis 基本IO模型中还有哪些潜在的性能瓶颈?

Redis基本IO模型中,主要是主线程执行操作,任何耗时的操作都会导致性能瓶颈,如:bigKey(我生产就遇到过)、全量返回等操作。

4. AOF日志:宕机如何避免数据丢失

redo log 重做日志:记录的是修改后的数据 aof : redis收到的每一条命令,这些命令是以文本形式保存的,写后日志。

4.1 AOF 日志是如何实现的?

4.1.1 set testkey testvalue

*3 3set3 set 7 testkey 9testvalue:3表示有三个命令9 testvalue :*3 表示有三个命令 +数字:表示有多少字节 image.png

4.1.2 AOF存在的风险
  • Redis 刚执行完一个命令还没记录日志宕机,有数据丢失的风险
  • AOF避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险,AOF日志在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大就会导致写盘很慢,进而导致后续的操作也无法执行

4.2 三种写回策略?

以上AOF存在的风险,AOF机制给我们提供了三个选择,配置项appendfsync三个可选值

image.png

image.png

4.3 日志文件太大了怎么办

为了避免日志文件太大,AOF有一个重写机制,也就是说在重写时,读取数据库中的所有键值对,根据这个键值对当前最新的状态,为它生成对应的写入命令。多变一的思想。

4.4 AOF 重写会阻塞吗?

AOF重写过程是由后台子进程bgrewriteaof来完成的,这也是为例避免阻塞主线程,导致数据库性能下降。

4.5 重写过程

一个拷贝,两处日志。 image.png 每次AOF重写时,Redis会先执行一个内存拷贝,用于重写; 然后使用两个日志保存在重写过程中,新写入的数据不会丢失。 第一处日志:正在使用的AOF日志。这样一来即使宕机了,AOF的操作仍然是齐全的。 第二处日志:新的AOF重写日志,重写日志也不会丢失最新的操作。等到所有操作记录重写完成后,重写日志的这些最新操作也会写入新的AOF文件。 Redis采用额外的线程进行数据重写,所以这个过程不会阻塞主线程。

4.6 AOF重写过程中潜在风险

image.png

5. 内存快照:宕机后如何实现快速恢复

所谓内存快照就是指内存中的数据在某一个时刻的状态记录。

5.1 给哪些内存数据做快照?

为了提供所有数据的可靠性保证,它执行的是全量快照,把内存中的所有数据都记录到磁盘中。

Redis提供了两个命令来生成RDB文件 save:在主线程中执行,会导致阻塞 bgsave:创建一个子进程,专门用于写入RDB,避免主线程的阻塞,redis默认的配置。

5.2 快照时数据能修改吗?

主线程的确没有阻塞,可以接受正常请求,为了保证快照完整性,它只能处理读操作,不能修改正在执行快照的数据。

Redis就会借助操作系统的写时复制技术(Copy-On-Write,COW),在执行快照的同时正常处理写操作。 image.png bgsave子进程由主线程fork生成,可以共享主线程的所有数据,如果主线也是读操作那么和bgsave子进程互不影响。如果是修改操作。那么这块数据会被复制一份生产副本,主线程在这个副本修改。

5.3 可以每秒做一次快照吗?

bgsave执行不会阻塞主线程,如果频繁地执行全量快照,也会带来两方面的开销。 频繁将全量写入磁盘,会给磁盘带来很大压力。 创建bgsave由主线程fork生成,fork这个创建工程本身会阻塞主线程。

Redis4.0提出一个混合使用AOF日志和内存快照的方法。 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。 这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,颇有点“鱼和熊掌可以兼得”的感觉。

最后,关于 AOF 和 RDB 的选择问题,我想再给你提三点建议: 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择; 如果允许分钟级别的数据丢失,可以只使用 RDB; 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

6. 主从实现数据同步

Redis具有高可靠性具有两层含义:一是数据尽量少丢失,二是服务少中断。 上面讲的AOF和RDB是Redis发生宕机的时候可以通过回放日志和重新读入RDB文件恢复。 对于后者Redis是增加副本冗余量,采用读写分离。 image.png

6.1 主从库间如何进行第一次同步?全量复制

image.png fullresync响应表示第一次复制采用的全量复制。 为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer记录RDB文件生成后的所有写操作。

6.2 主从级联模式分担全量复制时的主库压力 基于长连接的命令传播

如果从库很多,都要由主库进行全量复制的话,忙于fork子进程生成RDB文件,进行数据全量同步。传输RDB也会占用主库的网络带宽。采用 主从从。 image.png

6.3 主从库间网络断了怎么办? 增量复制

image.png 当主从库断联之后,主库会把断连期间收到的写操作命令,写入replication buffer,同时也会把这些操作命令写入repl_backlog_buffer这个缓冲区。 image.png 主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。

在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。

6.4 总结

我给你一个小建议:

  • 一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销。
  • 另外,为了避免多个从库同时和主库进行全量复制,给主库过大的同步压力,我们也可以采用“主 - 从 - 从”这一级联模式,来缓解主库的压力。
  • 这期间如果遇到了网络断连,增量复制就派上用场了。我特别建议你留意一下 repl_backlog_size 这个配置参数。如果它配置得过小,在增量复制阶段,可能会导致从库的复制进度赶不上主库,进而导致从库重新进行全量复制。所以,通过调大这个参数,可以减少从库在网络断连时全量复制的风险。

6.5 为什么主从库间的复制不使用AOF

  • RDB 文件是二进制文件,无论是要把 RDB 写入磁盘,还是要通过网络传输 RDB,IO 效率都比记录和传输 AOF 的高。
  • 在从库端进行恢复时,用 RDB 的恢复效率要高于用 AOF。

7. 哨兵机制:主库挂了,如何不间断服务?

在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制。 它有效地解决了主从复制模式下故障转移的这三个问题。

7.1 哨兵机制的基本流程

image.png

7.2 主观下线和客观下线

image.png

哨兵进程会使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。 如果哨兵发现主库或从库对PING命令的响应超时了,那么哨兵就会先把它标记为“主观下线”。 如果是从库,简单地标记为“主观下线”就行。因为从库下线影响不太大。 主库的话不能这样做,因为可能会存在误判。一旦启动主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。

哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。 引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。 只有大多数 N/2 + 1 的哨兵实例都判断主观下线了,主库才会被标记为客观下线。

7.3 如何选定新主库?

image.png 使用配置项 down-after-milliseconds * 10。 其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。 如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。 如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

第一轮:优先级最高的从库得分高。 用户可以通过 slave-priority 配置项,给不同的从库设置不同优先级.

第二轮:和旧主库同步程度最接近的从库得分高。 主从库同步时有个命令传播的过程。 在这个过程中,主库会用 master_repl_offset 记录当前的最新写操作在 repl_backlog_buffer 中的位置,而从库会用 slave_repl_offset 这个值记录当前的复制进度。 此时,我们想要找的从库,它的 slave_repl_offset 需要最接近 master_repl_offset。 如果在所有从库中,有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高,可以作为新主库。 image.png

第三轮:ID 号小的从库得分高。 每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。

image.png

8. 哨兵集群:哨兵挂了,主从库还能切换吗?

哨兵集群:sentinel monitor pub/sub 机制,哨兵和哨兵之间,哨兵和客户端之间就都能建立起连接。 基于info命令的从库列表,帮助哨兵和从库建立连接。

8.1 基于 pub/sub 机制的哨兵集群组成

多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的IP地址和端口。 只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。 image.png

哨兵如何知道从库的IP地址和端口? 哨兵向主库发生info命令,主库发送从库列表给他。 image.png

8.2 基于 pub/sub 机制的客户端事件通知

哨兵可以类似于一个运行在特定模式下的redis实例,完成监控、选主、通知的任务,每个哨兵实例也提供pub/sub机制,客户端可以从哨兵订阅消息。 image.png

8.3 由哪个哨兵执行主从切换?

image.png

  • 任何一个实例只要自身判断主库“主观下线”后就会给其他哨兵发送 is-master-down-by-addr命令,其他哨兵会根据自己和主库的连接情况作出Y或N响应。
  • 比如哨兵配置文件中的quorum配置是3那么一个哨兵需要3赞成就可以标注为客观下线了。
  • 之后哨兵想要执行主从切换,进行leader选举
  • 第一个拿到半数以上的赞成票,第二拿到的票数同时还需要大于等于哨兵配置文件的quorum值

image.png

9. 切片集群:数据增多了,是该加内存还是加实例?

image.png Redis数据量越来越多的时候,Redis响应变慢,fork耗时变高,fork执行会阻塞主线程。

9.1 如何保存更多数据?

image.png 在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。

9.2 数据切片和实例的对应分布关系

Redis Cluster 方案采用哈希槽(一个切片有16384个哈希槽),来处理数据和实例之间的映射关系。每个键值对都会根据它的 key,被映射到一个哈希槽中。 image.png

9.3 客户端如何定位数据?

Redis实例之间会把自己的哈希槽信息互相发送来完成哈希槽分配的扩散。实例互相连接之后,每个实例都有哈希槽的映射关系了。

客户端会缓存哈希槽信息在本地。当客户端请求键值对时,会先计算键所对应的哈希槽。然后就可以给相应的实例发送请求了。

实例与哈希槽对应关系变了之后,Redis-cluster提供一个中重定向机制。 image.png GET hello:key (error) MOVED 13320 172.16.19.5:6379 MOVED表示:客户端请求的键值对所在的哈希槽 13320,实际是在 172.16.19.5 这个实例上。通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。这样一来,客户端就可以直接和 172.16.19.5 连接,并发送操作请求了。

但此时,Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息,如下所示: GET hello:key (error) ASK 13320 172.16.19.5:6379 ASK 命令就表示,客户端请求的键值对所在的哈希槽 13320,在 172.16.19.5 这个实例上,但是这个哈希槽正在迁移。此时,客户端需要先给 172.16.19.5 这个实例发送一个 ASKING 命令。这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。 image.png