一、什么是redis
Redis(Remote Dictionary Server)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被用作缓存系统,可以显著提高应用的性能。
Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。它提供了5种常用的数据类型,String、Map、Set、ZSet、List。针对不同的结构,可以解决不同场景的问题。因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程序的数据IO效率。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里面,通过hash槽的方式实现了数据分片,进一步提升了性能。
二、Redis为什么这么快?
redis为什么这么快?将有以下几个方面揭晓:
1. 内存数据存储
Redis将所有数据存储在内存中,我们都知道内存存取速度远快于磁盘,因此Redis能提供极高的性能。==省去磁盘I/O的消耗==。虽然它也支持数据持久化,但这并不影响其读写速度,因为数据的读写都是直接与内存交互。
2. 高效的数据结构
Redis支持多种数据结构(如字符串、列表、集合、散列等),这些数据结构都被设计为能够高效地处理数据。这意味着Redis可以更快地执行复杂的操作。
先看看Redis的数据结构&内部编码:
* SDS 简单动态字符串
二进制安全:Redis 可以存储一些二进制数据,在 C 语言中字符串遇到'\0'会 结束,而 SDS 中标志字符串结束的是 len 属性。
* 字典
Redis 作为 K-V 型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如HashMap,通过key 就可以直接获取到对应的 value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。
* 跳跃表
字符串长度处理:Redis 获取字符串长度,时间复杂度为 O(1),而 C 语言中,需要从头开始遍历,复杂度为 O(n);
空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS 修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是 free 记录下多余的空间,后续有变更,直接使用 free 中记录的空间,减少分配。
跳跃表是 Redis 特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率。跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
3. 合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是 redis 设计者总结优化的结果。
String:如果存储数字的话,是用 int 类型的编码;如果存储非数字,小于等于39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。
List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码
Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用ziplist 编码,否则使用 hashtable 编码。
Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。
Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用ziplist 编码,否则使用 skiplist(跳跃表)编码
4. 单线程模型
Redis使用单线程模型来处理命令,避免了常见的多线程或多进程环境下的并发问题和复杂的同步问题,这使得Redis能够更快地处理请求。
5. 优化的网络IO
Redis使用多路复用技术和非阻塞IO来处理并发连接,这使得Redis可以在单个线程内同时处理多个网络连接,从而提供更高的吞吐量。
I/O 多路复用:多路 I/O 复用技术可以让单个线程高效处理多个连接请求,而 Redis 使用用epoll 作为 I/O 多路复用技术实现。并且,Redis 自身事件处理模型将epoll 中连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多时间。
6. 虚拟内存机制
因此,通过优化内存存储、数据结构、线程模型和网络IO,Redis能够提供极高的性能。
7、简单高效的操作
Redis提供了简单、直观且高效的操作接口,使得开发者能够快速地进行数据操作。例如,通过使用Redis的原子性操作来实现分布式锁、计数器等常见的功能。
三、多方面深入的了解I/O多路复用
1、概念
IO多路复用机制,==核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是触发了读/写事件==。
就通知应用程序,去获取这个就绪的连接进行读写操作。
也就是在应用程序里面可以使用单个线程同时处理多个客户端连接,在对系统资源消耗较少的情况下提升服务端的链接处理数量。
在IO多路复用机制的实现原理中,客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理就可以了。
IO多路复用是一种同步的IO模型。利用IO多路复用模型,可以实现一个线程监视多个文件句柄;一旦某个文件句柄就绪,就能够通知到对应应用程序进行相应的读写操作;没有文件句柄就绪时就会阻塞应用程序,从而释放出CPU资源。
IO可以理解为,在操作系统中,数据在内核态和用户态之间的读、写操作,大部分情况下是指网络IO;
多路大部分情况下是指多个TCP连接,也就是多个Socket或者多个Channel;
复用是指复用一个或多个线程资源。IO多路复用意思就是说,一个或多个线程处理多个TCP连接。尽可能地减少系统开销,无需创建和维护过多的进程/线程。
2、实现IO多路复用的三种模型
* Select模型
select模型,它的基本原理是,采用轮询和遍历的方式。也就是说,在客户端操作服务器时,会创建三种文件描述符,简称FD。分别是writefds(写描述符)、readfds(读描述符)和exceptfds(异常描述符)。
而select会阻塞监视这三种文件描述符,等有数据、可读、可写、出异常或超时都会返回;返回后通过遍历fdset,也就是文件描述符的集合,来找到就绪的FD,然后,触发相应的IO操;
它的==优点==是跨平台支持性好,几乎在所有的平台上支持。
它的==缺点==也很明显,由于select是采用轮询的方式进行全盘扫描,因此,随着FD数量增多而导致性能下降。
因此,每次调用select()方法,都需要把FD集合从用户态拷贝到内核态,并进行遍历。而操作系统对单个进程打开的FD数量是有限制的,一般默认是1024个。虽然,可以通过操作系统的宏定义FD_SETSIZE修改最大FD数量限制,但是,在IO吞吐量巨大的情况下,效率提升仍然有限。
* poll模型
poll模型的原理与select模型基本一致,也是采用轮询加遍历,唯一的区别就是poll采用链表的方式来存储FD。
所以,它的==优点==是没有最大FD的数量限制。
它的==缺点==和select一样,也是采用轮询方式全盘扫描,同样也会随着FD数量增多而导致性能下降。
* epoll模型
由于select和poll都会因为吞吐量增加而导致性能下降,因此,才出现了epoll模型。epoll模型是采用时间通知机制来触发相关的IO操作。它没有FD个数限制,而且从用户态拷贝到内核态只需要一次。它主要通过系统底层的函数来注册、激活FD,从而触发相关的IO操作,这样大大提高了性能。主要是通过调用以下三个系统函数:
1、epoll_create()函数,在系统启动时,会在Linux内核里面申请一个B+树结构的文件系统,然后,返回epoll对象,也是一个FD。
2、epoll_ctl()函数,每新建一个连接的时候,会同步更新epoll对象中的FD,并且绑定一个callback回调函数。
3、epoll_wait()函数,轮询所有的callback集合,并触发对应的IO操作
所以,epoll模型最大的==优点==是将轮询改成了回调,大大提高了CPU执行效率,也不会随FD数量的增加而导致效率下降。当然,它也没有FD数量限制,也就是说,它能支持的FD上限是操作系统的最大文件句柄数。一般而言,1G内存大概支持10万个句柄。分布式系统中常用的组件如Redis、Nginx都是优先采用epoll模型。它的==缺点==是只能在Linux下工作。
3、I/O多路复用模型综合对比
select | poll | epoll | |
---|---|---|---|
数据结构 | 数组 | 链表 | B+树 |
最大连接数 | 1024 | 无上限 | 无上限 |
FD拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | FD首次调用epoll_ctl拷贝 每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(N) | 轮询:O(N) | 轮询:O(1) |