前言

高性能服务器 一定是 多线程来实现的?

  • 以前一直有个误区,以为:高性能服务器 一定是 多线程来实现的,原因很简单:因为误区二导致的:多线程 一定比 单线程 效率高,其实不然

  • 在说这个事前希望大家都能对 CPU 、 内存 、 硬盘的速度都有了解了,这样可能理解得更深刻一点

    • 1.顺序访问:这种情况下,内存访问速度仅仅是硬盘访问速度的6~7倍(358.2M / 53.2M = 6.7)
    • 2.随机访问:这种情况下,内存访问速度就要比硬盘访问速度快上10万倍以上 (36.7M / 316 = 113,924)
  • redis 核心就是 如果我的数据全都在内存里,我单线程的去操作 就是效率最高的,为什么呢,因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。redis 用 单个CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案 —— 阿里 沈询

    • 因为一次CPU上下文的切换大概在 1500ns 左右。
    • 从内存中读取 1MB 的连续数据,耗时大约为 250us,假设1MB的数据由多个线程读取了1000次,那么就有1000次时间上下文的切换,
    • 那么就有1500ns * 1000 = 1500us ,我单线程的读完1MB数据才250us ,你光时间上下文的切换就用了1500us了,我还不算你每次读一点数据 的时间,

那什么时候用多线程的方案呢?

  • 答案是:下层的存储等慢速的情况。比如磁盘

  • 内存是一个 IOPS 非常高的系统,因为我想申请一块内存就申请一块内存,销毁一块内存我就销毁一块内存,内存的申请和销毁是很容易的。而且内存是可以动态的申请大小的。

  • 磁盘的特性是:IPOS很低很低,但吞吐量很高。这就意味着,大量的读写操作都必须攒到一起,再提交到磁盘的时候,性能最高。为什么呢?

  • 如果我有一个事务组的操作(就是几个已经分开了的事务请求,比如写读写读写,这么五个操作在一起),在内存中,因为IOPS非常高,我可以一个一个的完成,但是如果在磁盘中也有这种请求方式的话,

    • 我第一个写操作是这样完成的:我先在硬盘中寻址,大概花费10ms,然后我读一个数据可能花费1ms然后我再运算(忽略不计),再写回硬盘又是10ms ,总共21ms
    • 第二个操作去读花了10ms, 第三个又是写花费了21ms ,然后我再读10ms, 写21ms ,五个请求总共花费83ms,这还是最理想的情况下,这如果在内存中,大概1ms不到。
    • 所以对于磁盘来说,它吞吐量这么大,那最好的方案肯定是我将N个请求一起放在一个buff里,然后一起去提交。
    • 方法就是用异步:将请求和处理的线程不绑定,请求的线程将请求放在一个buff里,然后等buff快满了,处理的线程再去处理这个buff。然后由这个buff 统一的去写入磁盘,或者读磁盘,这样效率就是最高。java里的 IO不就是这么干的么~
  • 对于慢速设备,这种处理方式就是最佳的,慢速设备有磁盘,网络 ,SSD 等等,

  • 多线程 ,异步的方式处理这些问题非常常见,大名鼎鼎的netty 就是这么干的。

  • 顺便再提一句:redis 的瓶颈在网络上 。。。。

解析

  • Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。

  • 文件事件处理器的结构包含 4 个部分:

    • 多个 Socket 。
    • IO 多路复用程序。
    • 文件事件分派器。
    • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
  • 多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
  • 上图是客户端与 redis 的一次通信过程:
    • 客户端 Socket01 向 Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 Socket01,并将该 Socket01 的 AE_READABLE 事件与命令请求处理器关联。
    • 假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 Socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 Socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 Scket01 的 set key value 并在自己内存中完成 set key value 的设置。操作完成后,它会将 Scket01 的 AE_WRITABLE 事件与令回复处理器关联。
    • 如果此时客户端准备好接收返回结果了,那么 Redis 中的 Socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 Socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

总结

  • Redis被说成是单线程是操作数据,处理客户端连接是单线程的,redis本身还是有其他的线程,比如,持久化进行,hash散列重hash,主从心跳检测,等等。

  • Redis 是非阻塞 IO ,使用多路复用模式,一个线程就能够处理多个客户端的多个请求。

  • 多线程Redis的改进点

    • 由于是IO是单个线程处理的,所以一旦某次IO比较久的话,其他的IO请求就会挂在队列中,会影响整体的性能。
    • 多线程只是在执行IO任务阶段是多线程的,但是整体用于命令分发队列的依旧是单线程串行,这样就解决了单个IO耗时影响其他IO请求了,同时也避免了多线程环境下的数据的一致性。

为什么 Redis 效率这么高

  • 1、C 语言实现:我们都知道,C 语言的执行速度非常快。
  • 2、纯内存操作:Redis 为了达到最快的读写速度,将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 Redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 Redis 的性能。
  • 3、基于非阻塞的 IO 多路复用机制。
  • 4、单线程,避免了多线程的频繁上下文切换问题。
    • Redis 利用队列技术,将并发访问变为串行访问,消除了传统数据库串行控制的开销。
    • 单线程无需考虑并发的问题。
  • 5、底层实现数据结构是hash散列表,随机访问数据的时间复杂度为O(1),hash冲突的方式同样是采取拉链法,计算出同一个hash值,用链表去存储。
  • 6、丰富的数据结构。
    • Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化。例如,压缩表,对短数据进行压缩存储

参考