从几个方面来说明Redis为什么能那么快,内存这个就不说了,还有2个很重要的原因是:1、数据结构 2、IO线程模型--IO多路复用
Redis键值库包含什么?
- 基础数据类型是K-V,Key是String类型,Value支持多种数据类型,例如String、List、Set、Hash等
- 键值库支持的操作:PUT(增、改)、GET(查)、DELETE(删),以及SCAN(查询范围)等
- 键值对存储在内存,访问速度快
- 一个键值库包含的模块,访问模块、索引模块、操作模块、存储模块
- 访问模块:有两种访问模式,1、函数库访问 2、网络层socket访问,redis使用第二种。网络层涉及IO模型、协议等复杂操作。
- 索引模块:索引是为Key做索引,方便查找Value,redis使用哈希表做索引。
- 操作模块:GET/SCAN只需要查询key返回对应的value;PUT新增或更新,需要为键值对分配内存空间;DELETE删除键值对后还需要释放对应的内存空间,需要分配器来完成。
- 存储模块:redis提供了多种内存分配器来分配内存空间,还会持久化数据,两种方式持久化:1、每次操作都存盘,有性能影响;2、定期存盘,有丢失数据的风险。
- redis还有其他的功能模块,比如高可用集群、高扩展数据分片。
Redis为什么那么快?有哪些慢操作?
访问速度快的原因有两点:1、数据存储在内存 2、Redis的数据结构
Redis的基本数据类型:String、List、Hash、Set、Sorted Set
这些数据类型对应的数据结构有哪些呢?
- String--简单动态字符串
- List--双向链表、压缩列表
- Hash--压缩列表、哈希表
- Set--整数数组、哈希表
- Sorted Set--压缩列表、跳表
键值对的数据结构是哈希表
- 哈希表是一个数组,数组里的每个元素是个哈希桶entry,entry里存储的是key-value的指针,并不是实际数据
- 由于所有的K-V都存储在这个哈希表上,所以这是redis的全局哈希表
- 数据量多了后就存在哈希冲突,以及rehash阻塞
哈希表有哪些慢操作?
- 当key的数量>哈希桶的数量,必然存在哈希冲突,一个哈希桶就会存储多个元素,多个元素使用链表保存,链表的查询需要从头到尾一个个访问,当链表数据量多了后,查询操作就会变得很慢。
- 进行rehash操作,rehash就是扩容,增加哈希桶的数量,一般是当前哈希桶的两倍,来减少哈希冲突,redis会分配两个哈希表,rehash过程得重新计算key的位置,并把哈希表1里的数据拷贝到哈希表2,如果一次性拷贝会造成线程阻塞,所以redis采用的是渐进式rehash。渐进式rehash就是线程还是可以正常处理请求,同时按照索引位置拷贝一个哈希桶的数据到哈希表2,如此来提高效率。
底层数据结构
集合类型的底层数据结构主要有5种:整数数组、双向链表、压缩列表、哈希表、跳表。
整数数组和双向链表都是顺序访问数据,时间复杂度O(n),哈希表的时间复杂度O(1)(这里是数组的随机访问)
压缩列表类似数组,和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。查找第一个和最后一个数据是O(1),查找其他是O(n)。
跳表在链表的基础上增加了多级索引,比如一级索引是在顺序链表上每隔一个数据增加一个索引,二级索引是在一级索引上再增加索引,三级。。。,所以跳表的时间复杂度是O(logN)。
List的底层数据结构是压缩列表和双向链表,时间复杂度都是O(n),不建议在随机读写里使用,但压缩列表和双向链表都记录了头和尾的偏移量,POP和PUSH效率很高。
单线程的Redis为什么那么快?
Redis真的是单线程吗?
不是,redis的单线程体现在网络IO和键值对的读写,而持久化、集群数据同步都是由其他线程完成的。
Redis为什么使用单线程?
多线程的好处是在某个线程阻塞的时候CPU可以去处理其他线程的task,提高CPU的利用率,从而来提高系统的吞吐率。
但是多线程的缺点也不可避免,主要体现在2点:1、多线程对共享资源的竞争 2、线程的切换很耗费资源。
所以redis使用单线程,再加上内存读写速度快,redis有单线程高性能的说法。
再往下去探究,单线程确实能避免多线程的一些问题,但单线程阻塞的话,相当于redis就不可用了,那么redis怎么还能保证那么快呢?
单线程为什么能那么快?
因为redis采用了多路复用的IO模型。
IO多路复用是怎么保证线程不等待呢?
下面以Linux系统的epoll机制来说明。
一个客户端到服务器的GET请求存在哪些阻塞点呢?
首先得监听客户端的请求(bind/listen)->连接客户端(accept)->从socket里读取数据(receive)->解析请求(parse)->根据请求GET键值对数据(get)->返回数据请求给客户端(send)。
accept和receiv/send存在阻塞点:accept,监听到请求,但一直没连接成功,就阻塞在accept函数;receiv/send,客户端或者服务端数据没就绪时,就会阻塞。
当Redis遇到这些阻塞点怎么办呢?
Linux的io多路复用机制使用epoll机制来解决阻塞,该机制允许内核中存在多个监听套接字和多个已连接套接字。当redis把这些阻塞点交给了操作系统内核来监听这些套接字,redis就可以去做其他事情了,不会阻塞在这里。
当有请求到达时,epoll机制会提供基于事件的回调机制,调用相应的处理函数。这些事件会被放到事件队列,redis单线程对事件队列进行处理,而不用一直轮询是否有请求到达。
虽然这里说redis所有阻塞点都交给内核监听了,对于内核来说即使有很多个监听事件,也是一个个顺序处理的。
以上是基于极客时间Redis相关课程总结,仅供学习。