Redis为什么那么快?(一)

113 阅读6分钟

从几个方面来说明Redis为什么能那么快,内存这个就不说了,还有2个很重要的原因是:1、数据结构 2、IO线程模型--IO多路复用

Redis键值库包含什么?

  1. 基础数据类型是K-V,Key是String类型,Value支持多种数据类型,例如String、List、Set、Hash等
  2. 键值库支持的操作:PUT(增、改)、GET(查)、DELETE(删),以及SCAN(查询范围)等
  3. 键值对存储在内存,访问速度快
  4. 一个键值库包含的模块,访问模块、索引模块、操作模块、存储模块
  • 访问模块:有两种访问模式,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相关课程总结,仅供学习。