二、Redis的介绍

二、Redis的介绍

1. Redis 是什么

Redis 是一个用 C 语言开发的 K/V 型的内存数据库,每秒可以处理 15w 的数据,一般我们将它作为缓存数据库来使用,而且由于它对网络 I/O 以及键值对读写是由单线程来完成的,所以可以保证原子性,并且支持持久化。

我们一般说 Redis 是单线程的是指网络 I/O 和键值对的读写,但是其他比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

这些要是单线程的就尬起来了,比如说在做持久化的时候 Redis 直接就不可用了。

这个时候就会有疑问产生了:

  • 为什么是单线程的性能还这么高,多线程不是更快吗?

    1. 事实上在对大批量小容量数据进行处理的时候,单线程并不是性能的制约,网络的 I/O 吞吐量才是影响性能的关键。
    2. 在处理大容量的数据时采用多线程的 Memcached 确实有着比 Redis 更加出色的性能表现,但是作为缓存数据库大批量小容量的数据才是更应该注意的。
    3. 单线程既可以保证原子性,而且在处理小容量数据的时候频繁的上下文切换线程会造成不必要的性能开销。
    4. Redis 是基于内存级别的操作,而且由于是用 C 语言进行编写的越接近底层的语言速度越快。
  • 单线程的 Redis 如何处理高并发呢?

    Redis 利用 epoll 来实现 I/O 多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

那什么是 I/O 多路复用呢?我们简单介绍一下 I/O 模型。

2.简单介绍 I/O 模型

为了阅读流畅性,这里简单介绍一下几个名词。

  • fd 文件描述符

    在 Linux 中一切皆文件,在前面我们介绍过如果我想要快速的找到硬盘上的某一个文件,那么我需要给它一些标识而这个标识如果体现在数据库中那就是索引,同理如果我想要快速的找到内存中的一个文件,我一样需要给它一个索引由或者称为地址,而 fd 就是 Linux 中的内存索引

  • socket 套接字描述符

    前面我们说到过一切皆是文件,那如果我们的 Redis 想要去内存中读取某个数据,我们是否就可以理解为是文件 A 想要操作文件 B ,那么想要实现到这一点就需要我们建立一个通道,文件 A 的socket 与文件 B 的 socket 套在一起建立了一个链接通道,试想一下如果你想要和一个人聊天,那么是否是你需要知道他的联系方式他也需要知道你的联系方式?这样我们就可以称之为你们双方之间建立了一个通道,而 socket 就是联系方式,而我知道了你的 socket 之后我给这个 socket 所发送的消息是否可以理解为给你发送消息?如果你理解了上面的概念,那么剩下的也就好办了,在 Linux 中可以通过调用 socket() 的方式获得 socket 然后我们就可以通过这个 socket 去操作这个文件了。

  • kernel 内核

    内核就是指操作系统空间,无论在哪个操作系统中程序的读写操作都不能直接进行,必须通过内核来实现,因为安全问题。

2.1 BIO 阻塞IO

在 Linux 的默认情况下 socket 是 blocking 阻塞住的,在单线程的情况下也就是说我必须等待内核数据的返回才能继续程序的运行,这也就意味着在进行 Read() 操作的过程中程序是处于了不可用的状态,这显然是不可取的我们不可能让程序处于不可用的状态。

在这里插入图片描述

流程:

  1. 进程调用 read socket 去找内核读取数据,如果没有得到返回则持续阻塞。
  2. 内核准备数据,然后拷贝数据,返回数据给进程,进程结束阻塞。

因为 socket 是内核提供的安全操作的方式我们是无法修改 socket 的,那么在已知 socket 不可变的情况下我们是否能进行优化呢?答案是可以的,我们可以采用多线程的方式。

但是使用多线程的话又会衍生出一个问题,读取的并发可以无上限你的线程能开无限个吗?线程开一个消耗的资源是多少呢?也就意味着随着时代的发展,高并发的来临这种多线程+BIO的方式也就不无法满足我们对性能的要求了。

小总结:

  1. socket 是阻塞的的情况下我们称之为 BIO
  2. 多线程 + BIO 的方式会造成读取并发无限大,但是线程数量不可能无限大。
2.2 NIO 非阻塞同步 I/O

BIO 的方式的缺陷主要原因还是因为 socket 是阻塞住的,只要它是阻塞的我们最佳的方案也只能是多线程 + BIO 了,但是如果socket变为非阻塞那问题不就解决了吗?

伪代码实现:


while (){
	
  socket[] sockets;
  for(socket soc:sockets){

      //读取数据,查看数据是否准备就绪
      //数据准备就绪就读出来,没就绪继续循环
      if(read soc){
          //准备就绪之后先拷贝出来
          socket ok = soc
          //删掉集合里面已经被读取的值,Java没有del方法我随便写的看得懂就行
          del soc
          //然后给值返回出去
          return ok
      }
  }
}

在 socket 能直接返回的变为了非阻塞的情况下,数据虽然依旧需要等待,但是对比 BIO 来说他能继续处理别的事情了,只需要把需要读取的 fd 放到 sockets 集合里面去,然后让它不停的循环就能把准备就绪的数据拿出来,问题岂不是就解决了?

但是又引发了新的问题,无论有没有数据准备就绪你都会去 Read 一下看能不能把数据拿出来,这样做读取到了还算有收获,如果没有读取到那性能就浪费了。这部分的性能浪费如果 1000 个请求你还能抗住,假设有 1w 个呢?10w 个呢?

随着读取的并发进一步的升级,因为不管有没有数据准备就绪都会不停的循环造成性能不必要的浪费,在读取数不是很多的情况下这种性能的浪费还是可以接收的,但是一旦在高并发的情况下积少成多也会产生极大的性能开销。

2.3 NIO 多路 I/O 复用之select

非阻塞同步 I/O 的主要缺陷还是在于无论有没有数据准备就绪我都会不停的轮询,造成性能不必要的浪费,那我们给它加上一个监听器不就好了,如果监听到有数据准备就绪了我在去轮询。

伪代码实现:

while (){
  socket[] sockets;
  //代码在select这里就阻塞住了,只有当它返回有数据准备就绪的情况下才能往下走。
  //select(sockets)这个代码发生在内核空间,也就是阻塞在内核,而不是进程里面
  select(sockets);
  //在内核空间中如果被调起回调方法,才会通知进程过来取数据。
  for(socket soc:sockets){

      //读取数据,查看数据是否准备就绪
      //数据准备就绪就读出来,没就绪继续循环
      if(read soc){
          //准备就绪之后先拷贝出来
          socket ok = soc;
          //删掉集合里面的值,Java没有del方法我随便写的看得懂就行
          del soc;
          //然后给值返回出去
          return ok;
      }
  }
}

因为上述原因考虑,内核就开放了一个叫做 select() 的接口,它可以监听有没有数据准备就绪,如果有的话再去轮询读取数据。

select 的流程如下:

  1. 把 fd_set 从用户空间拷贝到内核空间

    ( fd_set 可以理解是 fd 的集合,这一步可以理解为把所有需要读取的 fd 的集合全部拷贝到内核)

  2. 注册回调函数,**注意:**回调函数没有被触发则 socket 一直阻塞住。

    (只有 socket 被阻塞住,进程在把 fd 放到 socket 中之后就不管了,而 socket 是由内核提供的)

  3. 将当前进程挂到等待列表中。

    (这一步你可以理解为网购的时候给商家的电话号码,快递到了之后如何联系你)

  4. 如果回调函数被调起则返回一个文件是否准备就绪。

  5. 遍历 fd_set 如果在遍历的过程中没有另外一个回调函数被触发则继续进入等待队列。

  6. 将数据从内核空间拷贝到用户空间。

注:在操作系统中,用户空间与内核空间的数据是不共享的,所以需要拷贝。

可以看到这种方式在对比 非阻塞同步 I/O有了很明显的提升。只有当数据准备就绪的时候才去读取数据,这样就避免了资源不必要的浪费,但是这样做就完美了吗?不能继续优化了吗?并不是。

它还是有着一些缺陷的:

  1. 它监听的是所有放到 socket 里面的 fd_set 有没有某一个数据准备就绪,如果有那就遍历一遍,相当于 1000 个 fd 里面如果好了 1 个,就会把 1000 个都查了一遍还是会有资源的浪费
  2. 每次都需要把 fd_set 往内核空间拷贝一次,拷来拷去的如果 fd 的数量变多了资源消耗也不少。
  3. select 的 fd_set 的大小是有限制的,在 Linux 中默认的 fd_setSize 为 1024 而 select 采用的也是 1024 但是问题在于 Linux 中默认的 size 是可以更改的,但是 select 的你没办法改除非去更改内核代码。
2.4 NIO 多路 I/O 复用之 epoll

其实还有一种技术是 poll 但是这个就不讲了和 select 区别不大。

针对以上的不足,操作系统对 select 以及 poll 做了一次改进升级也就是 epoll 的诞生。

epoll 也是采用的监听,但是与 select 不同在于 select 只有一个方法 select()

它有三个方法 epoll_create 、epoll_ctl 和 epoll_wait 。

create为创建、ctl为监听事件、wait是等待事件发生。

整体流程大致相当于是:

  1. 将 fd 丢给了 epoll 创建了之后在每个 socket 上面都加了一个ctl监听事件
  2. 监听事件被触发之后wait就被触发了,然后将准备就绪的数据丢到一个就绪链表。
  3. 通知我这个 socket 好了把它读走,然后我去读取就绪链表。

缺点的解决方法:

  1. 全量遍历的解决方案:新加一个就绪链表,只读取就绪链表的数据。
  2. 重复拷贝的解决方案:原来需要重复拷贝的主要原因在于内核空间与用户空间不共享,于是引入了 mmap 用户和内核的共享空间,只需要往共享空间里面拷一次就可以了,不需要每次都拷贝了。
  3. epoll支持的数量没有限制,纯看配置。

注:io模型还有其他的,我只讲了三种因为redis用的就是最后一种,讲到这里也就结束了感兴趣自己去了解

2.5 小总结
  • 多路 I/O 复用技术解只是能同时处理多个读取任务,相当于 BIO 而言在读取单个数据的时候效率其实有所下降。
  • Redis 采用的是 epoll 的方式来做的。
  • 事实上在 Redis6.0 之后也引入了网络 I/O 多线程的方式来提高入口的效率。
  • 因为 Redis 执行命令是采用的单线程的方式,所以在执行耗时较长的命令时应该需要小心。

Any problem in computer science can be solved by another layer of indirection.

计算机科学中的任何问题都可以通过另一层间接方法来解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值