Redis使用单线程为什么还这么快

    Redis为什么用单线程?多线程的开销

    如果没有良好的系统设计,采用多线程通常会导致如右图所示的结果(注意纵坐标)。刚开始增加线程数时,系统吞吐率会增加,再进一步增加线程时,系统吞吐率就增长迟缓了,甚至还会出现下降的情况。

    关键瓶颈在于: 系统中通常会存在会被多线程同时访问的共享资源,为了保证共享资源的正确性,就需要有额外的机制保证线程安全性,例如加锁,这会带来额外的开销。

    Redis为何使用单线程却能取得如此高效速度

    比如拿最常用的List类型来举例吧,假设Redis采用多线程设计,有两个线程A和B分别对List做LPUSH和LPUSH操作,为了使得每次执行都是相同的结果,即【B线程取出A线程放入的数据】就需要让这两个过程串行执行。这就是多线程编程模式面临的共享资源的并发访问控制问题。

    并发访问控制一直是多线程开发中的一个难点问题:如果只是简单地采用一个互斥锁,就会出现即使增加了线程,大部分线程也在等待获取互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。

    同时加入并发访问控制后也会降低系统代码的可读性和可维护性,所以Redis干脆直接采用了单线程模式。

    Redis使用单线程为什么还这么快?

    之所以使用单线程是Redis设计者多方面衡量的结果。

    • Redis的大部分操作在内存上完成

    • 采用了高效的数据结构,例如哈希表和跳表

    • 采用了多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率

    既然Redis使用单线程进行IO,如果线程被阻塞了就无法进行多路复用了,所以不难想象,Redis肯定还针对网络和IO操作的潜在阻塞点进行了设计。

    网络与IO操作的潜在阻塞点

    在网络通信里,服务器为了处理一个Get请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从socket中读取请求(recv),解析客户端发送请求(parse),最后给客户端返回结果(send)。

    最基本的一种单线程实现是依次执行上面的操作。

    上面标红的accept和recv操作都是潜在的阻塞点:

    • 当Redis监听到有连接请求,但却一直不能成功建立起连接时,就会阻塞在accept()函数这里,其他客户端此时也无法和Redis建立连接

    • 当Redis通过recv()从一个客户端读取数据时,如果数据一直没有到达,也会一直阻塞

    基于多路复用的高性能IO模型

    为了解决IO中的阻塞问题,Redis采用了Linux的IO多路复用机制,该机制允许内核中,同时存在多个监听套接字和已连接套接字(select/epoll)。

    内核会一直监听这些套接字上的连接或数据请求。Redis会处理到达的请求,从而实现了一个线程处理多个IO流的效果。

    此时,Redis线程就不会阻塞在某一个特定的客户端请求处理上,所以它可以同时和多个客户端连接并处理请求。

    回调机制

    select/epoll一旦监测到FD上有请求到达时,就会触发相应的事件被放进一个队列里,Redis线程对该事件队列不断进行处理,所以就实现了基于事件的回调。

    例如,Redis会对Accept和Read事件注册accept和get回调函数。当Linux内核监听到有连接请求或读数据请求时,就会触发Accept事件和Read事件,此时,内核就会回调Redis相应的accept和get函数进行处理。

    Redis的性能瓶颈点

    经过上面的分析,虽然通过多路复用机制可以同时监听多个客户端的请求,但Redis仍然有一些性能瓶颈点,这也是我们平时编程需要极力避免的情况。

    1. 耗时操作

    如果任何一个请求在Redis中耗时较长,就会对整个服务器的性能产生影响。后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。

    这一点需要我们在设计业务场景时去规避;Redis的lazy-free机制也把释放内存的耗时操作放在了异步线程中去执行了。

    2. 高并发场景

    并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但还是只能单线程依次读取客户端的数据,无法利用到CPU多核。

    Redis在6.0可以利用CPU多核多线程读写客户端数据,但只是针对客户端的读写是并行的,每个命令的真正操作还是单线程。

    其他Redis相关的有趣问题

    借此机会也提几个和redis相关的有意思的问题。

    • 为什么要用Redis,直接访问内存不好吗?

    这一条其实并没有很明确的界定,对于一些不经常变动的数据,可以直接放到内存里,不一定要放到Redis里,可以放到内存里。在更新数据时可能存在一致性问题,即可能只有某一台服务器上的数据被修改,因此数据仅在本地内存中存在。访问Redis服务器可以解决一致性问题,用Redis的话。

    • 数据太多内存放不下怎么办?比如我要缓存100G的数据,怎么办?

    这里也要打一个广告Tair是淘宝开源的分布式KV缓存系统,它从Redis继承了丰富的操作,理论上总数据量无限制,针对可用性、可扩展性、可靠性也进行了升级,感兴趣的小伙伴们可以了解一下~



    Redis,内存高效的Key-Value数据库,以其高效、快速的读写能力迅速走红。但是,很多人都对Redis高效的本质不解,甚至抱以怀疑的态度,认为凭借单线程性能如此优异的服务端服务不靠谱。那么,其实现机制究竟是什么呢?
    一、为什么单线程的Redis如此出色?
    Redis的单线程架构,曾经被很多人认为是其性能的瓶颈,但是实际上及其反之。单线程虽然并不能真正处理所有的请求,但Redis能够利用现代软硬件所获得的高并发性能,以及CPU在高速运行时随时打断恢复处理,并且充分利用了OS自带的多进程负载均衡,在其中也可以实现不同程度的并发。此外,Redis通过高质量的数据结构将常见的操作时间精简到了O(1),因此它能够在输入输出操作上更有效地利用系统资源。
    二、Redis的单线程架构如何处理I/O操作?
    虽然Redis依托单线程,但其实现I/O的方式却十分高效。对于输出操作,Redis将发送的数据先存储到缓存区,并利用write函数一次性将数据全部发送,这样能够极大缩短输出操作所需时间。同时,在输入操作中,Redis利用异步操作,让线程在慢卡IO负载如recv()的操作时由主线程分离,这样就可以保持Redis主线程的响应能力,可以及时地处理请求。当然,输入输出操作的高速以及对异步处理的依赖都被诸如EventLoop的事件驱动程序间接支持。
    三、如何充分利用单线程带来的性能优势?
    Redis所采用的单线程架构,虽然可能会引起实现瓶颈的疑惑,但是实际使用效果却完全不同。Redis能够极大的充分利用当代计算机系统资源的高速读写和CPU计算等各种能力,容易写出高效的代码实现,在大型企业环境中得到了广泛应用。此外,在实际架构过程中,利用好Redis的数据和API,还能够实现数据分布以及实时响应等高效业务。因而Redis的单线程架构依然是其高效性的基础所在。
    综上所述,Redis依托单线程架构所实现的高效性应该被更好的理解和认识。其性能高效完全不是虚构的,而是来自于严谨的设计与实现以及对当下现代IT科技的合理利用。在如此高效地性能竞争中,Redis的发展历程将会不断地打破成见,继续创新在技术的发展道路上书写新的篇章。