1.redis基础概念
1.redis是一种键值数据库,基本数据类型是key-value,value类型包括string,哈希表,列表,集合等。
2.redis保存在内存中,读写很快,但是潜在的风险是一旦断电,所以数据都会丢失。保存在外存避免数据丢失,但是受限于磁盘的慢速读写,性能会被拉低。缓存场景下的数据需要能够快速访问但允许丢失。
3.redis定位键值对的位置依赖于索引模块,索引的作用是让键值数据库根据key找到相应value的存储位置。索引的类型有哈希表,b+树,字典树等。不同索引结构在性能空间消耗,并发控制方面不同,redish memcached采用哈希表,rocksdb采用跳表。
4.redis的数据类型和底层数据结构
- string 简单动态字符串
- list 双向链表和压缩列表
- hash 压缩列表和哈希表
- sorted set 调表和压缩列表
- set 整数数组和哈希表
5.数组和压缩列表作为底层数据结构的优势是什么
节省空间。整数数组和压缩列表都是在内存中分配一块连续的空间,然后把集合中的元素一个接一个放在这块空间,非常紧凑,因为元素是连续放置,不用通过额外的指针把元素串接起来,避免了额外指针带来的空间开销。
6.键和值使用哈希表来保存所以的键值对,一个哈希表其实就是一个数组,每个元素称为哈希桶,一个哈希表由多个哈希桶组成的,每个哈希桶中保存了键值对数据,哈希桶中保存的不是值本身而是具体值的指针。哈希表的最大好处是用时间O(1)的时间复杂度,快速查找到键值对。
7.当插入大量数据后,哈希表变慢了?
主要是哈希冲突,多个key的哈希值经过计算后落在同一个哈希桶中。
redis解决哈希冲突就是链式哈希,链式哈希就是指一个桶中的多个元素用一个链表来保存,它们之间用指针连接,也叫做哈希冲突链。
但是哈希冲突链上的元素只能通过指针逐一查找,如果数据过多,哈希冲突也可能越来越多,导致哈希冲突链过长,导致这个链上的元素查找耗时长,效率降低。
所以redis会对哈希表做rehash操作。即增加现有的哈希桶数量。
具体做法:redis使用了两个全局哈希表,哈希表1和哈希表2,一开始默认使用哈希表1,哈希表2没有被分配空间,随着数据增多,redis开始执行rehash,过程如下:
- 给哈希表2分配更大的空间
- 把哈希表1的数据重新映射并拷贝到哈希表2中
- 释放哈希表1的空间
但是第二部涉及大量的数据拷贝,不能一次性迁移,否则会造成线程阻塞,所以采用了渐进式rehash,简单来说就是第二步拷贝数据时,redis仍然正常处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带把这个索引位置上的所有entry拷贝到哈希表2中,等处理下一个请求的时候,拷贝下一个所以位置上的entry。
执行rehash时除了根据键值对操作进行数据迁移,redis还会有一个定时任务执行rehash,没有键值对操作时,这个任务会周期性的批量迁移。rehash过程中,删除,查找,更新会在两个哈希表上进行,和golang的map都是渐进式扩容。
8.集合数据操作效率
第一步通过哈希表找到对于哈希桶位置,第二步在集合中增删改查。
9.底层数据结构
-
整数数组和双向链表顺序读写,通过数组下标或者链表的指针逐个访问元素,复杂度O(n),效率比较低。
-
压缩列表类似数组,不过区别是压缩列表再表头有三个字段zlbytes,zltail,zllen表示列表长度,列表尾的偏移量和列表中entry个数,表尾还有一个zlend表示列表结束,在压缩列表中查找第一个元素和最后一个元素,通过表头字段直接查找复杂度O(1),其他元素没有这个高效
-
跳表,有序列表只能逐一查找元素非常缓慢,跳表是在链表基础上增加多级索引,通过索引位置的跳转实现快速定位,数据量大的时候,复杂度O(logn).,类似于二分查找
10.时间复杂度
- 哈希表 O(1)
- 跳表 O(logn)
- 双向链表O(n)
- 压缩列表O(n)
- 整数数组O(n)
11.redis不同操作的复杂度
- 单元素操作是基础
hash的hset,hget,hdel是对哈希表操作,复杂度是O(1),set用哈希表作为底层数据结构时,它的sadd,srem,srandmember复杂度都是O(1)。HMSET和SADD操作又元素个数决定,增加m个元素复杂度就是O(M)
- 范围操作非常耗时
hash类型的HGETALL和set类型的SMEMBERS,或者list的lrange和zset的zrange,操作复杂度都是O(n),比较耗时,尽量避免。redis从2.8版本开始提供了scan操作,(hsan,scan,zcan)实现了渐进式遍历,每次只返回有限数量的数据,这样,避免一次性返回所以元素导致redis阻塞
- 统计操作通常高效
统计操作是对集合中所有元素个数的记录,比如llen和scard,复杂度只有O(1),集合类型采用压缩列表双向链表整数数组这些数据结构时,这些结构中专门记录了元素的个数统计。
- 例外情况只有几个
指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样,对于list类型的lpop,rpop,lpush,rpush这四个操作,都是在列表的头尾增删,通过偏移量可以直接定位,复杂度O(1)
12.整数数组和压缩列表在查找时间复杂度方面没有很大的优势为什么还会作为底层数据结构
内存利用率,作为紧凑的数据结构,比链表内存占用要少,提高内存利用率。数据对cpu高速缓存支持更友好,当元素较少的情况下,采用内存紧凑排列的方式存储,同时利用cpu高速缓存不会降低访问速度,当数据元素超过设定阙值后,避免查询时间复杂度过高,转为哈希或者跳表数据结构存储,保证查询效率。
13.redis采用单线程还是多线程的问题。
单线程的话一个线程要处理网络连接,解析请求完成数据存取,一旦某一步发生阻塞,整个线程都会阻塞住,降低系统响应速度,如果采用多线程,不同线程间需要访问共享资源,又会产生资源竞争,也会影响系统效率,redis是单线程,高性能。
14.为什么单线程的redis能那么快
redis是单线程主要是指redis的网络io和键值对读写是一个线程来完成的,但是redis的其他功能,比如持久化,异步删除,集群数据同步等,其实是由额外的线程执行的。
多线程可以增加系统吞吐率,但是系统中通常会存在被多线程同时访问的共享资源,为了保证正确性,需要额外的机制保证,就会带来额外开销,多线程编程模式面临的共享资源的并非访问控制问题。如果只是简单采用一个粗粒度互斥锁,就会出现不理想结果,即使增加线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,并没有提高系统吞吐率。采用多线程开发一般引入同步原语来保护共享资源的并非访问,降低系统代码的易调试性和可维护性
采用单线程的一个核心原因是避免多线程开发的并非控制问题,单线程的redis也能获得高性能跟多路复用的io模型相关,避免了accept()和send()/recv()潜在的网络io操作阻塞点。
redis快的原因一方面redis的大部分操作都是在内存上完成,加上它采用了高效数据结构,比如哈希表和跳表,另一方面,redis采用了多路复用机制,使其在网络io操作中能处理大量的客户端请求,实现高吞吐率。
15.基于多路复用的高性能io模型
redis在只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求,一旦有请求达到,就会交给redis线程处理,实现了redis线程处理多个io流的效果。为了请求到达时能通知到redis线程,select,epoll提供了基于事件的回调机制,针对不同事件的发生,调用相应的处理函数。这些事件被放到一个事件队列,redis单线程对该事件队列进行处理,这样redis不需要轮询是否有请求发生,避免造成cpu资源浪费。redis在对事件队列处理时,会调用相应处理函数,这就是基于事件的回调。