Redis学习笔记

Redis学习笔记

 Redis学习笔记01-05

  01基本架构

  • 系统观

如果我们想要深入理解和优化 Redis,就必须要对它的总体架构和关键模块有一个全局的认知,然后再深入到具体的技术点。

- 我们理解Redis 经常被用于缓存、秒杀、分布式锁等场景的重要基础
- 	数据模型--里面可以存什么样的数据
-   操作接口--数据可以做什么样的操作
  • Redis能做什么不能做什么?

    • 可以存哪些数据

      • Memcached 支持的 value 类型仅为 String 类型
      • Redis 支持的 value 类型包括了 String、哈希表、列表、集合等。
        Redis 能够在实际业务场景中得到广泛的应用,就是得益于支持多样化类型的 value。
    • 可以做什么操作

      • PUT/GET/DELETE/SCAN 是一个键值数据库的基本操作集合。
    • 存储方面

      • 内存键值数据库:Memcached 和 Redis
      • 保存在内存的好处:读写很快,毕竟内存的访问速度一般都在百 ns 级别。但是,潜在的风险是一旦掉电,所有的数据都会丢失。
      • 保存在外存,虽然可以避免数据丢失,但是受限于磁盘的慢速读写(通常在几 ms 级别),键值数据库的整体性能会被拉低。
    • 访问方式

      • 通常有两种访问模式
        • 通过函数库调用(动态链接库)的方式供外部应用使用(RocksDB)
        • 通过网络框架以 Socket 通信的形式对外提供键值对操作(而 Memcached 和 Redis 则是通过网络框架访问)
    • I/O 模型设计–

      • 网络连接的处理、网络请求的解析,以及数据存取的处理,是用一个线程、多个线程,还是多个进程来交互处理呢?该如何进行设计和取舍呢?(如果一个线程既要处理网络连接、解析请求,又要完成数据存取,一旦某一步操作发生阻塞,整个线程就会阻塞住,这就降低了系统响应速度。如果我们采用不同线程处理不同操作,那么,某个线程被阻塞时,其他线程还能正常运行。但是,不同线程间如果需要访问共享资源,那又会产生线程竞争,也会影响系统效率)

      • Redis 如何做到“单线程,高性能”?

    • 如何定位键值对的位置?索引模块

      • 索引的类型有很多,常见的有哈希表、B+ 树、字典树等。不同的索引结构在性能、空间消耗、并发控制等方面具有不同的特征。
      • Memcached 和 Redis 采用哈希表作为 key-value 索引
      • RocksDB 则采用跳表作为内存中 key-value 的索引
    • 如何实现重启后快速提供服务?存储模块

      • 分配器是键值数据库中的一个关键因素。对于以内存存储为主的 Redis 而言,这点尤为重要。Redis 的内存分配器提供了多种选择,分配效率也不一样。
      • Redis 的持久化模块能支持两种方式:日志(AOF)和快照(RDB),这两种持久化方式具有不同的优劣势,影响到 Redis 的访问性能和可靠性。

      Redis学习笔记_第1张图片
      Redis学习笔记_第2张图片

  02数据结构–快速的Redis有哪些慢操作

  • Redis 的快,到底是快在哪里呢?
    • 它接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。
  • 为啥 Redis 能这么快?
    • 因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快
    • 高效的数据结构
    • Redis学习笔记_第3张图片
    • 集合类型(List、Hash、Set 和 Sorted Set)–一个键对应了一个集合数据
    • String 类型的底层实现只有一种数据结构,也就是简单动态字符串
    • 键和值用什么结构组织?
      • Redis 使用了一个哈希表来保存所有键值对。
      • 一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。
      • Redis学习笔记_第4张图片
      • 全局哈希表–哈希表保存了所有的键值对
        • 好处:让我们可以用 O(1) 的时间复杂度来快速查找到键值对(这个查找过程主要依赖于哈希计算,不管哈希表里有 10 万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。)
        • 缺点:当你往 Redis 中写入大量数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在的风险点,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞
          • 为什么哈希表操作变慢了?
            • 哈希冲突–两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。
            • Redis解决哈希冲突的办法:链式哈希
              • 同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
              • Redis学习笔记_第5张图片
              • 哈希冲突链的问题:哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。
                • rehash–增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
                  • Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。

                  • 一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。

                  • 随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
                    -第一步:给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
                    -第二步:把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
                    -第三步:释放哈希表 1 的空间。

                  • 到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。

                  • rehash问题:第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

                  • 渐进式rehash–每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:

                  • Redis学习笔记_第6张图片

    • 集合数据操作效率
      • 一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。
      • 集合的底层数据结构–>
        • 访问效率:使用哈希表实现的集合,要比使用链表实现的集合访问效率更高
        • 操作效率:操作本身的执行特–>读写一个元素的操作要比读写所有元素的效率高。
      • 5种集合类型低层数据结构–整数数组、双向链表、哈希表、压缩列表和跳表
        • 整数数组和双向链表操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低

        • 压缩列表
          Redis学习笔记_第7张图片

          • 特点:每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
          • 查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)
          • 查找其他元素时,只能逐个查找,此时的复杂度是 O(N) 了。
        • 跳表

          • 在链表的基础上增加了多级索引,通过索引位置的跳转,实现数据的快速定位
            Redis学习笔记_第8张图片
            • 在链表中查找 33 这个元素,只能从头开始遍历链表,查找 6 次。是O(N)的方式
            • 我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。此时,我们只需要 4 次查找就能定位到元素 33 了。
            • 可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。
          • 这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。
            -数据结构的时间复杂度
            Redis学习笔记_第9张图片
        • 操作的时间复杂度

        • 单元素操作是基础;范围操作非常耗时;统计操作通常高效;例外情况只有几个。

          • 单元素操作是基础
            • 单元素操作,是指每一种集合类型对单个数据实现的增删改查操作
            • Eg:Hash 类型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操作的复杂度由集合采用的数据结构决定,例如,HGET、HSET 和 HDEL 是对哈希表做操作,所以它们的复杂度都是 O(1);Set 类型用哈希表作为底层数据结构时,它的 SADD、SREM、SRANDMEMBER 复杂度也是 O(1)。
            • 集合类型支持同时对多个元素进行增删改查,这些操作的复杂度,就是由单个元素操作复杂度和元素个数决定的。例如,HMSET 增加 M 个元素时,复杂度就从 O(1) 变成 O(M) 了。
          • 范围操作
            • 范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据
            • 比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。
            • Redis 从 2.8 版本开始提供了 SCAN 系列操作(包括 HSCAN,SSCAN 和 ZSCAN),这类操作实现了渐进式遍历,每次只返回有限数量的数据。这样一来,相比于 HGETALL、SMEMBERS 这类操作来说,就避免了一次性返回所有元素而导致的 Redis 阻塞。
          • 统计操作
            • 统计操作,是指集合类型对集合中所有元素个数的记录
            • 例如 LLEN 和 SCARD。这类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。
          • 例外情况
            • 是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。
            • 对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。

  03高性能IO模型

  • Redis 是单线程

    • Redis 对外提供键值存储服务的主要流程
      • 网络 IO 和键值对读写是由一个线程来完成的
    • Redis 的其他功能由额外的线程执行
      • 比如持久化、异步删除、集群数据同步等
    • Redis 的单线程设计机制以及多路复用机制
      • “为什么用单线程?为什么单线程能这么快?”
        • 多线程的开销
          • 优点:使用多线程(增加系统中处理请求操作的资源实体),可以增加系统吞吐率(系统能够同时处理的请求数),或是可以增加系统扩展性

          • 缺点:采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。
            Redis学习笔记_第10张图片

          • 以上情况的原因:系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。

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

            • 如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加
        • 单线程 Redis 为什么那么快?
        • 单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力
          • 一方面,Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
          • 另一方面,就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
            • 基本IO模型的阻塞点
              • Redis 采用单线程进行 IO,如果线程被阻塞了,就无法进行多路复用了。

              • E.g.:基本IO模型处理一个Get请求过程

                • 监听客户端请求(bind/listen)
                • 和客户端建立连接(accept)
                • 从 socket 中读取请求(recv)
                • 解析客户端发送请求(parse)
                • 根据请求类型读取键值数据(get)
                • 向 socket 中写回数据(send)
              • 在一个线程中依次执行上面说的操作,阻塞点如下:

                • 当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。
                • 当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。
              • 非阻塞模式:socket 网络模型本身支持非阻塞模式

                • 在 socket 模型中,不同操作调用后会返回不同的套接字类型。
                  • socket() 方法会返回主动套接字,
                  • 然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。
                  • 最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。

    Redis学习笔记_第11张图片

你可能感兴趣的:(redis,学习,笔记)