1、简述下Redis☆
Redis是一个开源的使用C语言编写,可基于内存,可持久化的Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串),list(链表),set(集合),Zset(sorted set—有序集合),和hash(哈希类型)。
优势:
速度快,性能极高,可持久化,丰富的数据类型,支持数据备份。
Redis和Memcached的区别:
1、类型
Redis是个开源的内存数据结构存储系统,用作数据库,缓存和消息代理。
Memcached是一个免费的开源高性能分布式内存对象缓存系统,它通过减少数据库负载来加速动态Web应用程序。
2、数据结构
Redis支持String,list,set,Zset,hash,位图,超级日志和空间索引,而Memcached 支持字符串和整数。
3、执行速度
Memcached(多线程)读写速度高于Redis
4、复制
Memcached不支持复制。而Redis支持主从复制,允许从属Redis服务器成为主服务器的精确副本;来自任何Redis服务器的数据都可以复制到任意数量的从属服务器。
5、秘钥长度
Redis的秘钥长度最大是2GB,而Memcached的秘钥长度最大为250字节。
6、线程
Redis是单线程的;而Memcached是多线程的。
2、为什么说Redis快或者性能高?☆
- 完全基于内存,数据存在在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 数据结构的专门设计:比如SDS结构中的字符串长度len,压缩链表等;(可以引申到redis的8大数据结构)
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或多线程导致的切换而消耗cpu,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。(严格来讲Redis4.0之后就不在是单线程了,除了主线程之外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据,无用连接的释放,大Key的删除等等)
- 使用多路I/O复用模型,非阻塞IO;多路I/O复用模型是利用select,poll,epoll可以同时检查多个流的I/O事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依顺序处理就绪的流,这种做法就避免了大量的无用操作。
- RESP(redis的序列化协议),文本协议,解析迅速。(但是浪费流量)
- 持久化采用了子线程进行磁盘操作。
3、简述下Redis的单线程模型
Redis基于Reactor模式来设计开发了一套高效的事件处理模型,即Redis中的文件事情处理器(file event handler)。由于文件事件处理器是单线程的方式运行的,所以一般都说Redis是单线程模型。
实现方式:
(1)I/O多路复用:
通过I/O多路复用来监听来自客户端的大量连接(或者说是监听多个socket),将感兴趣的时间及类型(读,写)注册到内核中并监听每个事件是否发生。I/O多路复用技术的使用让redis不需要额外创建多余的线程来监听客户端的大量连接,减低了资源的消耗(引申:NIO中的Selector)
(2)基于事件驱动:
Redis服务器是一个事件驱动程序,服务器需要处理两类事件;文件事件,时间事件。
文件事件处理器使用I/O多路复用程序来同时监听多个套接字2,并根据套接字目前执行的任务来为套接字关联不同的事件处理器,。当被监听的套接字准备好执行连接应答(accept),读取(read),写入(write),关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但是通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其它同样以单线程方式运行的模块进行对接,保持Redis内部单线程设计的简单性。
文件事件处理器(file event handler)主要包括4个部分:多个socket(客户端连接);IO多路线程复用程序(支持多个客户端连接的关键);文件事件分派器(将socket关联到相应的事件处理管理器);事件处理器(连接应答器,命令请求处理器,命令回复处理器)
4、Redis为什么要使用单线而不是多线程?
Redis6.0之前就是单线程,之后才有了多线程。
官方:使用Redis时,几乎不存在CPU成为瓶颈的情况,Redis主要受限于内存和网络。比如在linux系统上,Redis通过使用pipelining每秒可以处理100个请求,所以如果应用程序主要使用O(N)和O(logn)命令时,它几乎不会占用太多CPU。使用单线程后,可维护性高。单线程机制使得Redis内部实现的复杂度大大减低,Hash的惰性Rehash、Lpush等等“线程不安全”的 命令都可以无锁进行。
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,代理了并发读写的一系列问题,增加了系统复杂度,同时可能存在线程切换、甚至加锁解锁,死锁造成的性能损耗。Redis通过AE事件模型以及I/O多路复用技术,处理性能非常高,因此没必要使用多线程。
5、Redis中的5大数据类型、8大数据结构。☆
学习笔记 | Redis 8大核心数据类型
- String;
- Hash;
- list。
- set
- zset
- bitmap 位图类型
- geo 地理位置类型
- HyperLogLog 基数统计类型。
1、不同类型的不同实现:
2、简单动态字符串SDS(Simple Dynamic String)与C语言自带的字符串有什么区别?
(1)常数复杂度获取字符串长度
由于len属性的存在,我们获取SDS的长度只需要读取len属性,时间复杂度为O(1).而C语言是通过遍历来获得len,所以时间复杂度是O(N).
(2)避免缓冲区溢出
对于SDS数据类型,在进行字符修改的时候,会首先根据记录的len属性检查内存空间是否满足需求,如果不满足,会进行相应空间扩展,所以不会溢出。
(3)减少修改字符串的内存重新分配次数。
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减少时会造成内存泄漏。
而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间的预分配和惰性空间释放两种策略:
- 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需要的内存重新分配
- 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后的多余的字节,而是使用free属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要的时候,也可以手动释放这些未使用的空间。)
(4)二进制安全
因为C字符串是以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有SDS的API都是以处理二进制的方式来处理buf里面的元素,并且SDS不是以空字符串来判断是否结束,而是以len属性表示的长度判断字符串是否结束。
(5)兼容部分C字符串函数
虽然SDS是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用C语言库<String.h>中的一部分函数。
(6)总结:
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串N次必然需要执行N次内存重配 | 空间预分配(有分配策略) 惰性空间释放 最多N次 |
只能保存文本数据 | 可以保存二进制或文本数据,以len属性判断结束而不是\0 |
可以使用<string.h>库中的函数 | 可以使用<string.h>库中一部分的函数 |
2、redis字典的底层实现HashTable相关问题
(1)解决冲突:链地址法,与java的HashMap一样;
(2)扩容:复制出另一个hash表,并且会重算Hash值,进行渐进性的Hash。这一点和java不同,特别是Jdk1.8之后不用重算Hash。
什么是渐进性rehash表?也就是说扩容和收缩操作不是一次性的、集中式的完成,而是分多次、渐进式完成。当保存在Redis的键值对很少时,(几个几十个),rehash可以瞬间完成,但是键值对较多时(几百万,几千万,到几亿),那么要一次性进行rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式Rehash,这样在rehash期间,字典的删除和查找更新等操作可能会在两个hash表中进行,第一个hash表中没找到,就会到第二个Hash表中进行查找。但是进行增加操作,一定是在新的Hash表中进行的。
3、压缩链表原理
压缩链表(ziplist)是Redis为了节省内存而开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩链表可以包含任意多的节点(entry),每个节点可以保存一个字节数组或者一个整数值。
- zlbytes: ziplist的长度(单位: 字节),是一个32位无符号整数
- zltail: ziplist最后一个节点的偏移量,反向遍历ziplist或者pop尾部节点的时候有用。
- zllen: ziplist的节点(entry)个数
- entry: 节点
- zlend: 值为0xFF,用于标记ziplist的结尾
4、zset底层跳表原理(为什么不选择平衡树)
本质就是多级链表并有序。
skiplist与平衡树、hash表的比较☆
(1)skiplist和各种平衡二叉树(如AVL、红黑树等)的元素都是有序排列的,而Hash表不是有序排列的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
(2)范围查找时,平衡树比skipList操作要复杂。在平衡树上,我们找到指定范围的最小值之后,还需要以中序遍历的顺序继续寻找其他不超过大值的节点。而在skiplist上进行范围查找就非常简单,只需要在找到最小值之后,对第一层的链表进行若干步遍历就可以实现。
(3)平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
(4)从内存占用来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为·1/(1-p),具体取决于参数p的大小。Redis中p一般取1/4,即平均每个节点包含4/3个指针,比平衡树更有优势。
(5)查找单个key,skiplist和平衡树的时间复杂度都为O(logn),而hash表在保持较低hash值冲突的情况下,查找时间复杂度接近O(1),性能更高一些。所以我们平时使用各种map或者dictionary都是Hash表实现的。
(6)从算法实现难度上,skiplist比平衡树简单的多。
6、红黑树和跳表 ☆
跳表就是带多级索引的链表,时间复杂度为O(logn),所能实现的功能和红黑树差不多,但是跳表有一个区域查找的优势,红黑树没有,所以redis底层就是用的跳表。
一、跳表结构(以空间换时间,来实现快速查找)
面试准备 – Redis 跳跃表
说真的,跳跃表在 Redis 中使用不是特别广泛,只用在了两个地方。一是实现有序集合键,二是集群节点中用作内部数据结构。
从图中可以看到,跳表主要由
表头:head 负责维护跳跃表的节点指针。
跳跃表节点:保存着元素值,以及多个层。
层:保存着指向其他元素的指针。高层指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
表尾:全部由NULL组成,表示跳跃表的末尾。
二、红黑树
红黑树的每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。对于任何有效的红黑树增加了如下要求:
-
节点要么为红,要么为黑
-
根节点为红,
-
叶子节点为黑色的空节点(NIL节点)。
-
每个红色节点必有两个黑色的叶子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
从任意节点到其每个叶子节点的所有简单路径都包含相同数目的黑色节点。
插入和删除时会用到变色和旋转。
7、Redis中过期策略和缓存淘汰机制。
1、策略分为:定期删除+惰性删除
定期删除:redis默认每隔100ms检查,是否有过期的key,有过期的key则删除。需要说明的是,redis不是每隔>100ms将所有key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,Redis会卡死)。因此,如果只采用定期删除的策略,会导致很多Key到时间没有删除。
惰性删除:获取某个key的时候,redis会检查一下,如果过期了就删除。
2、redis内存淘汰机制:
noeviction:当内存不足以容纳新写入的数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的key(这个最常用)。
allkeys-random:当内存不足以容纳新写入的数据时,随机移除某个key。
volatile-lru:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)。
volatile-random:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早期时间的key优先移除。
8、Redis中持久化机制☆
redis–RDB和AOF
Redis的持久化有两种机制,一个是RDB,也就是快照,快照就是一次全量的备份,会把所有Redis的内存数据进行二进制的序列化存储到磁盘上。(Redis会Fork一个子进程,快照的持久化就交给子进程去处理,而父进程继续处理线上业务的请求。)
另一种AOF日志,AOF日志记录的是数据操作修改的指令记录日志,可以类比MySQL的Binlog,AOF日期随着时间的推移只会无限增量。
Redis4.0之后,引入了新的持久化模式,混合持久化,将RDB的文件和局部增量的AOF文件相结合。RDB可以使用间隔较长的时间保存策略,AOF不需要全量日志,只需要保存前一次RDB存储开始到这段时间增量AOF日志即可,一般来说,这个日志量是非常小的。
9、Redis集群的主从复制
采用完整重同步和部分重同步两种模式。
(1)完整同步用于处理初次复制情况:通过让主服务器创建并发送RDB文件,以及向从服务器发送保存到缓冲区的写命令来进行同步。
(2)部分重同步是用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,主服务可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
10、缓存雪崩和缓存穿透问题
常见的缓存处理流程:缓存穿透,缓存击穿,缓存雪崩(附代码)
1、缓存雪崩:
缓存同一时间大面积的失效,所以,后面的请求会落在数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库。
设置热点数据永不过期。
2、缓存穿透:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起id为“-1”或者特别大的不存在的数据。这是用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
接口层增加校检,如用户健全校检,id做基础校验,id<=0,直接拦截。
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒,(设置太长会导致正常情况下也无法使用)。这样可以防止被攻击用户反复用一个id暴力攻击。
3、缓存击穿:
是啥:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
- 设置热点数据永远不过期。
- 加互斥锁。
11、缓存和数据库的数据一致性问题
1. 将不一致分为三种情况:
数据库有数据,缓存没数据。
数据库有数据,缓存也有数据,数据不相等。
数据库没有数据,缓存有数据。
2.缓存+数据库读写的模式
(1)首先尝试从缓存中读取,读到返回,无就读数据库,如果有,写入缓存并返回,无,返回。
(2)更新数据:先更新数据库,然后再把缓存中的对应数据删掉。
更新为什么是删除呢?这个可以根据业务复杂度来衡量,也可以选择更新缓存。
对于第一种情况(数据库 有 缓存 无):在读数据的时候自动把数据库的数据写到缓存上,因此不一致自动消除。
对于第二种情况:两者数据不相同,但之前某一时间点肯定是相同的(懒加载或预加载)。这种不一致肯定是数据库更新数据时,数据库更新了,缓存没更新(删除)。
对于第三种情况:你把数据库数据删了,但是缓存中对应数据没删除。
最终得出结论:不一致产生的原因是数据库更新了,但是删除缓存失败。
解决方案:
-
对删除缓存进行重试,数据一致性越高,重试越快。
-
定期全量更新,简单地说,就是定期把缓存全部清掉,然后再全量加载。
-
给所有缓存一个失效期。
3.常规缓存和数据库设计思路
并发不高的情况:
读:读redis->没有,读mysql->把mysql的数据写回redis,有的话直接从redis取。
写:写mysql->成功,再写redis
并发高的情况:
读:读redis->没有,读mysql->把mysql的数据写回redis,有的话直接从redis取。
写:异步的话,先写入redis缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存。
12、Redis6.0为什么要引入多线程?
Redis将所有数据放在内存中,内存的响应时长大概是100ns,对于小数据包,Redis服务器可以处理80000到100000的QPS,这也是Redis的处理极限,对于大部分公司已经够用。
但是随着越来越复杂的业务逻辑,动不动就上亿交易,需要更大的QPS。常见解决方案是在分布式架构中对数据进行分区并采用多个数据库,但该方案有非常大的缺点,e.g.要管理的Redis数据库太多,维护代价大,某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题,数据偏斜,重新分配和放大/缩小变得更加复杂等等。
从Redis自身角度来说,读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于I/O消耗,优化主要有连个方面
-
提高网络I/O性能:e.g.使用DPDK来代替内核网络栈功能。
-
使用多线程充分利用多核,典型实现如:Memcached,支持多线程。
原因:
- 可以重分利用服务器CPU资源,目前主线程只能利用一个核。
- 多线程的任务可以分摊Redis同步IO读写负荷。
数据进行分区并采用多个数据库,但该方案有非常大的缺点,e.g.要管理的Redis数据库太多,维护代价大,某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题,数据偏斜,重新分配和放大/缩小变得更加复杂等等。