Skip to content

Latest commit

 

History

History
84 lines (46 loc) · 5.26 KB

eddie-cache与Redis-IO模型思考.md

File metadata and controls

84 lines (46 loc) · 5.26 KB

IO模型的思考

参考资料:

http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf Author: Doug Lee

IO模型

所有的IO模型都离不开这几个步骤:

  1. read:读取数据
  2. deserialization:反序列化
  3. compute:计算
  4. serialization:序列化
  5. write:输出数据

提升IO性能的目的

  1. 能够在海量负载连接情况下优雅降级;
  2. 能够随着硬件资源的增加,性能持续改进;
  3. 具备低延迟、高吞吐量、可调节的服务质量等特点;

Redis的IO模型

简单来说,多路复用IO + 单线程就是Redis的IO模型即Reactor模型(在上述步骤中1、5阶段使用了多路复用,接下来都是单线程操作),相信大部分读者如果看过Redis的一些介绍都有所印象,不过本文并不是主要为了去介绍多路复用IO与传统的IO有什么优势。

而是从设计者层面来谈,为什么Redis要设计成单线程的。其实这对于我们来说,可能是不可思议的,因为我们的印象中,单线程似乎是不与性能挂钩的,而Redis却偏偏使用他来实现了一个高性能缓存。

其实从Redis4.0开始,Redis就已经将一些命令改为并发执行了,比如清理脏数据、无用连接的释放、大 key 的删除等,Redis6.0正式启动多线程版本(不过也仅仅是在反序列化和序列化阶段是多线程的)。

简单的事情

Redis单线程模型

一般来说,Redis处理的所有的任务都是单一的事情(所有的指令都是由外部的客户端连接发送来的),Redis的执行主线程,只需要通过event_wait函数去等待事件,并处理事件即可。

这个过程是否能切换成多线程模式?答案当然是可以的。

不过我们可以想一想,我们在日常开发中什么时候使用多线程:

  1. 不同业务(并发数可接受)访问同一共享资源(事件不同源)
  2. 秒杀系统的设计(事件不同源转事件同源)

其中第一种情况(事件不同源),一般我们都是直接通过锁进行解决了,因为我们没有必要将其push进一个队列中转成单线程操作吧,这样需要对不同业务的一个整合,进行统一处理,可能又伴随着新的问题出现了。

第二种情况,我们一般都会进行压测,看看系统在单线程的情况下处理得怎么样,然后再来选择是否需要开启一个线程池来处理(需要考虑并发问题我们一般使用具有原子操作的Redis来确保)。

这样说,可能还是有点抽象,可以具体化到生活场景中的工人事件(任务执行均有并发限制):

  1. 事件不同源:工人各自接受到上级的任务,这时候除非是工人太多了,一起干活,会遏制任务整体进度的推进,才会进行任务的整合重新分配;
  2. 事件同源:包工头接到任务后,如果这个任务,他觉得自己干更加快,那么就没必要分发给工人去干,一来要考虑分发任务的问题,二来要考虑任务结果整合的问题,三来这样做复杂度提高了,带来的收益与付出不匹配优先级就没有那么高了。

==这样来讲,多路复用其实就是一个秒杀系统的队列,Redis在单线程执行的情况下,因为基于内存的IO,其实处理的速度都已经非常快了,如果再继续使用多线程,那可能会使得服务器的资源利用更加充分(在线程量适当的情况下)。但是提升的性能与代码的复杂度(共享资源的互斥考虑)、提供给使用者的接口复杂度(根据不同的服务器调节线程池参数),其实是不太乐观的,这是不利于Redis的推广的。==

于是Redis在那之后的更新,都是对功能的多样化,底层数据结构的设计,来进行更新的,这会让Redis的一个成长更加快速,而直到功能日益完善之后,在Redis6.0推出了多线程版本,使得性能又上了一个台阶。

Redis6.0多线程

多线程模型

说说我的缓存设计

我的缓存设计并不是使用单线程模式,因为我的分布式缓存系统是与业务系统耦合在同一个JVM上的,于是便有了1. 本地缓存操作 与 2. 远程缓存操作,这两种操作事件并不同源,所以基于代码简易开发的原则,我则进行加锁,使用并发来进行多线程处理,不仅仅没有使得代码复杂度增加,又充分利用了CPU资源。

服务器IO模型方面,使用了Multiple Reactors,使得服务器IO性能更加高。

但是目前的实现中,IO read / write仍然与memory IO耦合,这将会在下一个版本进行解决。

Multiple Reactors

关于多线程的memory IO是否会额外带来线程上下文切换的开销问题:

  1. eddie-cache使用了线程池控制并发;
  2. JDK对synchronized语句的优化,具备一个锁升级的过程(偏向锁 -> CAS -> 重量级锁);
  3. 锁粒度问题,将锁的粒度尽量减小,使得任务执行更加快;
  4. 读写锁,eddie-cache在磁盘的读写方面使用了读写锁进行并发控制。