资讯详情

03-Redis 凭什么这么快

大家好,我是飓风。

前两篇文章我们认识了redis 今天我们来谈谈数据结构。redis 快的原因是什么?使用时应注意什么?

网上的说法,大多数都是redis 是基于内存的单线程io多路复用,但只说表面,比如为什么单线程快,redis 单线程都干了什么?什么是io 多路复用,redis 怎么和io多路复用组合等都没有说清楚。

为何采用单线程实现?

若采用多线程,则必然会有多线程上线文的费用。

若采用多线程,则对同一线程key 操作中必然会出现线程安全问题。什么是线程安全?当有多个端或线程来竞争公共资源时,就会出现线程安全问题。要解决这个问题,必然会加。如果加锁,会带来锁竞争的消耗,增加redis 的负杂性。

如果你做过性能测试,你应该知道,随着线程数量的增加,你应用的吞吐量会增加,但随着线程的增加,你应用的吞吐量会持平,慢慢下降。redis 这也是因为考虑到这一点,如果你能配置线程参数,性能可能会下降。

根据官网的说法,也就是说redis 基于内存操作,速度非常快,cpu 不是它的瓶颈。

所以redis 不采用多线程,一是会增加锁的竞争 线程切换消耗也会增加redis 的复杂性。

redis 采用io多路复用模型

在介绍io 多路复用前 我们先看下scoket 模型 哪些模型?

基本的阻塞 socket

如果客户端和服务器能够在网络中通信,则必须使用它 Socket 编程是进程间通信中一种特殊的方式,特别是可以跨主机间通信。 建 Socket 指定网络层使用时可以使用 IPv4 还是 IPv6.使用传输层 TCP 还是 UDP。 首先调用服务端 socket() 创建网络协议的函数 IPv以及传输协议 TCP 的 Socket ,接着调用 bind() 函数,给这个 Socket 绑定一个 IP 地址和端口。

绑定端口的目的:当内核收到时 TCP 报文,通过 TCP 找到我们的应用程序,然后将数据传递给我们。 绑定 IP 地址目的:一台机器可以有多张网卡,每张网卡都有相应的 IP 地址,绑定网卡时,核心会在收到网卡上的包后发给我们。

绑定完 IP 地址和端口可以调用 listen() 监控函数,此时对应 TCP 状态图中的 listen。

服务端进入监控状态后,通过调用 accept() 从核心获取客户端连接的函数,如果没有客户端连接,就会客户端连接的到来。

客户端是如何发起连接的?客户端正在创建中 Socket 后,调用 connect()函数启动连接,函数参数应指示服务端 IP 地址和端口号。

在 TCP 在连接过程中,服务器的核心实际上是每个 Socket 维护两个队列:

  • 一个是尚未完全建立连接的队列,称为 TCP 半连接队列,这个队列没有完成三次握手连接,此时服务端在 syn_rcvd 的状态;

  • 一个是建立连接的队列,称为 TCP 全连接队列,这个队列完成了三次握手连接,此时服务端在 established 状态;

当 TCP 全连接队列不空后,服务端 accept()函数将来自内核 TCP 在全连接队列中拿出一个已完成连接的 Socket 这是返回应用程序的后续数据传输 Socket。

  • 一个叫监听 Socket;
  • 一个叫已连接 Socket;

这个模型是最基本的io模型是一种同步阻塞的方式,每次只能处理一个客户端io请求,当读写堵塞时,其他客户端无法与服务端连接。

accept() 当没有客户端请求链接时,它将被阻塞。 read() 当没有可读数据时,它会被阻塞。 write() 当没有写作时,它会被阻塞。

在这里插入图片描述

前面提到的最基本的socket 模型,属于一对一通信,所以你的应用并发量可以想象,同时只能为客户服务,机器资源将是非常浪费的,所以我需要改进我们socket 支持更多客户端连接和读写操作的模型。 这个问题可以通过多个过程来解决,每个客户端的链接都可以分配一个进行读写操作。

一般流程如下:

  1. 服务主流程监控客户端连接,与客户端连接成功后,accept() 函数将返回已连接的函数 socket
  2. 主进程会fork() 一个子过程将复制主过程的文件描述符、内存地址空间、程序计数器、执行代码等。
  3. 已连接的子过程直接用于子过程socket与客户端通信。

看下图,基本流程:

采用多过程实现多客户端连接的服务,当客户数量很高时,您的过程数量会很高,然后系统负担会增加,会占据系统的大部分资源,此外,上午和下午的过程切换非常重,包括虚拟内存、堆栈、全球变量等用户资源,也包括核心堆栈、寄存器等核心资源。因此,这种方法并不是大并发量的最佳解决方案。

基于多线程实现,多线程可以理解为属于某一过程,同一过程中的线程可以共享部分过程资源,如文件描述列表、过程空间、代码、全球数据、堆、共享库等,这些共享资源不需要切换,只需要切换私人数据、寄存器等不共享数据,因此同一过程下的线程切换成本远小于过程。

此外,频繁的线程创建和破坏消耗资源,成本很小,所以我应该使用线程池来实现线程的重用,以避免线程的创建和破坏。

基本流程见下图:

事实上,使用线程处理连接的读写请求与过程处理连接的读写请求相同,这将给系统带来巨大的资源消耗。如果实现了c10k,那么服务端需要维护1万线程,那么此时的操作系统会是什么样子呢?

以上两种方法,无论是过程之间的切换,还是线程之间的切换,创建过程的数量和线程的数量,都会给系统带来负担,所以你可以用一个过程或线程来监控多个过程吗?socket呢? 所以我们今天的主角 就卡咔咔咔 亮相。

那我们怎样才能通俗地理解这个东西呢?

首先要看的是名字 .

重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O同时管理多个流)的状态I/O流. 其原因是尽可能提高服务器的吞吐量。

来来来, 看下图:

每个人都应该听说过select/poll/epoll ,其实他们三个都是 以下是三剑客多路复用的来源和原理。

select 实现多路复用的方法是将连接起来 Socket 文件描述符合集合,然后调用 select 函数将文件描述符合复制到核心,让核心检查是否有网络事件,检查方法非常粗糙,是通过遍历文件描述符合集合,当检查事件发生时, Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后处理。

所以,对于 select 需要这样做 2 次「遍历」文件描述符合集合,一次在内核状态,一次在用户状态 ,而且还会发生 2 次「拷贝」文件描述符合集合,先从用户空间传输到内核空间,由内核修改后再传输到用户空间。

select 使用固定长度 BitsMap,文件描述符合要求,支持的文件描述符的数量有限 Linux 在系统中,由内核中 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 文件描述符。

poll 不再用 BitsMap 存储关注的文件描述符,动态数组,以链表的形式组织,突破 select 当然,文件描述符的数量也会受到系统文件描述符的限制。

epoll

epoll 通过两个方面,很好解决了 select/poll 的问题。

第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

redis利用epoll来实现IO多路复用,内核可以同时存在多个监听socket套接字和已经连接的套接字,内核会一直监听这些socket,只要有socket事件发生,就会交给redis 线程来处理,这样可以看出,一个redis线程就能处理多个io流了。

下图中redis 会利用epoll,让内核帮助它来监听socket,此时redis 并不会阻塞在某一个特定的监听或者已经连接的socket上,如果有事件过来,epoll_wait() 就会返回事件就绪的FD给redis主线程,redis主线程就会将连接信息和事件放到队列中,然后主线程会让事件分派器消费队列.大致流程看下图:

图中FD 是指 有事件就绪的socket。

连接处理器会处理 accept 事件。 命令请求处理器处理 命令请求,也就是可读的事件。 命令回复处理器 处理 命令回复 ,也就是写事件。

IO多路的单线程模型:

  1. redis启动时,向epoll注册还未使用的FD的可连接事件。
  2. 连接事件产生时,epoll机制自动将其放入自己的可连接事件队列中。
  3. redis线程调用epoll的wait,获取所有事件,拷贝存入自己用户线程的内存队列中。
  4. 遍历这些内存队列,通过事件分派器,交由对应事件处理器处理。如是可连接事件则交由对应的连接应答事件处理器处理。可读事件交给命令请求处理器,可写事件交给命令回复处理器处理。
  5. 对应处理器执行相应逻辑。执行完成时,再次向epoll注册对应事件,比如连接事件的那个FD,接下来要注册可读事件,并重新注册可连接事件。命令请求处理器执行完后,要将FD注册可写事件。命令回复处理器执行完后,需要将FD取消注册可写事件。

Redis 底层数据结构

  1. redis 哈希表,在查找key 的时候,能够做o(1) 的复杂度。
  2. 对于有序的集合,采用跳表来实现,复杂度为 o(logN)。
  3. 对于压缩列表 和整数数组 可以对短数据进行压缩,节省了内存空间。
  4. 对于压缩列表和双端链表,可以实现队列, pop 和 push 操作 的时间复杂度为 o(1)。
  5. bitmap,对于只有两种值的判断来说,既能节能内存,也能做到低延迟。
  6. 集合类中元素的个数,redis 已经算好,例如LLEN ,SCARD 复杂度都是o(1)。

redis 速度慢了

  1. 尽量使用自己单个操作的时间很短。
  2. 合理使用redis 的数据结构,每种结构都有自己的应用场景。
  3. 减少对集合的全部查询,如HMGET HGETALL SMEMBBERS LRANGE 等 范围的查找。
  4. 不要使用keys* 之类的操作。
  5. 不要存储bigkey,bigkey 再分配空间(阻塞redis主线程)和删除释放空间(可以设置为异步)、带宽都会受到影响。

总结

今天学习了redis 快的原因,节下来我们来总结下。

Redis是纯内存数据库,相对于读写磁盘,读写内存的速度就不是几倍几十倍了,一般,hash查找可以达到每秒百万次的数量级。

Redis采用了单线程的模型(io解析和读写操作由一个线程来完成),保证了每个操作的原子性,也减少了线程的上下文切换、公共资源锁的开销与竞争。

redis 采用IO多路复用 ,“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。可以直接理解为:单线程的原子操作,避免上下文切换的时间和性能消耗;加上对内存中数据的处理速度,很自然的提高redis的吞吐量。

Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。


今天的分享就到这里了,码字画图不易,期待你的谢谢

欢迎关注 github

微信添加: zookeeper0

标签: 1832zj连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台