1、Redis有哪些基本数据结构?
String、Hash、List、Set、SortedSet
它还有三种特殊的数据结构类型Geospatial(地理空间,经纬度)、Hyperloglog(基数统计)、Bitmap(位图)
1.字符串
Redis的数据结构中,字符串主要用来存储一些String类型的数据,Redis中实现字符串类型是通过SDS来实现的,SDS是一个动态字符串类型。在Redis中字符串类型长度以及字符串扩容则是通过定义的对象来实现的,在Redis中定义了字符串对象,这个对象中主要存储的数据为字符串长度,未使用字符串容量,以及字符串数据等。
因此在Redis中获取字符串长度的时间复杂度是O1,同时为了解决存储可变字符串的问题,Redis则使用了动态扩容来解决,当Redis向一个字符串中新增字串时,则会判断字符串的容量是否足够,如果容量不足以存储新增的字串,那么Redis则对字符串执行空间预分配策略,也就是当SDS为字符串分配空间时,同时会为SDS分配一个未使用字符串容量,这个容量可以在后续中为新增字符串时做铺垫。
如果SDS中存储的数据容量长度为10字节,同时Redis分配了10字节未使用的空间,那么SDS的总长度就为10+10+1=21,之所以最后加1是因为Redis遵循C语言的准则在末尾添加一个空字节。
2.链表
Redis中的链表结构跟数据结构中的链表大体相同
3.字典
字典在Redis中就是我们平时使用Java中的map,有key-value形式的数据结构,但是这个结构在Redis中的实现有一些不同,首先Redis中是通过一个table属性来保存key-value的值,table属性是一个数组,这个数组的类型是dictEntry类型,这个类型就是我们所说的key-value
Redis中也是通过哈希算法来进行存储字典值的,既然使用了哈希算法,那么也不可避免的要出现哈希冲突的问题,也就是两个或以上的键被分配到了一个数组上。对于这种情况Redis则使用了链地址法来解决,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单项链表,被分配到同一个索引上的多个节点可以通过这个单向链表连接起来
4.跳跃表
Redis中的ZSet的底层实现就是通过跳跃表来实现的,通过跳跃表来实现有序集合
跳跃表的实现基于一种特殊的数据结构,这种结构的时间复杂度为Ologn,这里可以跟平衡树做个对比,在平衡树中查找一个数的时候所走过的路径是从根部向下搜索,直到找到这个数据,如果数据过多,那么查找路径就会边长,影响一定的性能,而跳跃表则很好的解决了这个问题
跳跃表有几个概念,层、长度、头部、尾部、分值、成员对象,跳跃表的结构分为多个节点,每个节点中包含分值、成员对象、层,分值代表每个节点存储的值,并且数据在节点中是顺序存储的,成员对象可以理解为每个节点保存的成员对象,层在跳跃表中是一个比较特色的部分,跳跃表之所以能实现快速查询就是依靠了层
可以将节点理解为一层楼,成员对象存储在第一层,分值存储在第二层,第二层以上则就是层,每层都可以跟前面的节点进行连接
节点1指向了节点4的第九层,这样如果要访问6这个数据,那么可以直接从节点1指向节点4这个这个路径查找节点4中的数据6,这样走过的路径只有一次
5.整数
Redis中存储整数时则使用了整数型的数据结构,Redis中的整数会区分16位,32位以及64位的数据,因此为了解决数据变换的问题,Redis中则使用了升级方法来实现数据的存储,如果一个键值中存储的数据是16位的数据,当对这个数据更改到32位时,那么16位的就不足以存储下这些数据,就需要将16位的键值升级,升级到32位进行存储更改的数,同时32位存储64位的数据也会执行升级,并且这个升级过程是不可逆的,这样也保证了以后数据跟更改之后不需要再进行升级。
2、如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些
3、Redis为什么这么快?
基于内存存储实现
内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗
高效的数据结构
Mysql索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快
合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果
合理的线程模型
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求
单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗
虚拟内存机制
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题
4. 什么是缓存穿透、缓存雪崩、缓存击穿?
缓存穿透问题
先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力
通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透
缓存雪崩问题
缓存雪崩:指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机
缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值 + 一个较小的随机值,5 小时 +0 到1800 秒酱紫
Redis 故障 down 机也可能引起缓存雪奔。这就需要构造 Redis 高可用集群啦。
缓存击穿问题
缓存击穿: 指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到DB
和缓存雪崩看着有点像,其实它两区别是,缓存雪崩是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪崩的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key缓存,缓存雪崩则是很多key
5.Redis的过期策略和内存淘汰策略
定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存
定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
Redis中同时使用了惰性过期和定期过期两种过期策略
假设Redis当前存放30万个key,并且都设置了过期时间,如果你每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。
因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的
但是呢,最后可能会有很多已经过期的key没被删除。这时候,redis采用惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除
6.Redis 内存淘汰策略
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错
7.Redis 的持久化机制有哪些?
RDB
就是把内存数据以快照的形式保存到磁盘上。
什么是快照?
可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来
RDB的概述
RDB模式的持久化分为两个实现方式,SAVE和BGSAVE,SAVE实现比较粗暴,当执行SAVE命令进行持久化时,Redis会通过主线程来进行持久化操作,因此当进行持久化操作时其它任务就必须停止,必须等待持久化操作结束,其它任务才会进行,这种持久化方式会影响Redis任务的执行,一般不推荐使用。另一种是BGSAVE,BGSAVE比较温和,这种方式并不会占用Redis主线程,而是在主线程的基础上fork一个子线程来进行持久化操作,所以并不影响主线程处理Redis任务,只是会消耗一些性能。一般都使用BGSAVE来进行RDB的持久化。
另一方面,RDB持久化的是数据,也就是所谓的数据库状态,当我们向Redis数据库执行对数据的操作时,RDB持久化会将这些数据持久化到磁盘中,但是这种方式的持久化会丢失一部分数据,因为RDB持久化是从一个时间点开始持久化,某个时间点执行了RDB持久化,如果之后发生了宕机,那么该时间点之后的数据将会丢失。
最后对于RDB文件的载入,在Redis启动时会自动检测是否存在RDB文件,如果存在则对RDB文件进行载入,恢复RDB文件中的数据,但是Redis中如果开启了AOF持久化,那么Redis会优先载入AOF持久化文件。
RDB持久化
是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据
在 m 秒内数据集存在 n 次修改,自动触发 RDB 机制
RDB优缺点
RDB 的优点
适合大规模的数据恢复场景,如备份,全量复制等
RDB缺点
没办法做到实时持久化/秒级持久化
新老版本存在RDB格式兼容问题
AOF(append only file)
持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的
ADF优缺点
AOF的优点
数据的一致性和完整性更高
AOF的缺点
AOF记录的内容越多,文件越大,数据恢复变慢
AOF概述
为了弥补RDB持久化会丢失某个时间点数据的问题,Redis也存在另一个持久化方式AOF,AOF持久化与RDB持久化不同的地方在于持久化数据的不同,RDB持久化的是数据,而AOF持久化的是写命令,当对Redis执行写命令时,命令执行成功,如果开启了AOF持久化,那么就会将这些写命令存储到AOF缓冲区中。
AOF持久化的开启在Redis的配置文件中,通过设置appendonly参数来开启AOF持久化。
同时AOF持久化还具有三个模式,always、everysec、no,
always的意思是执行一个写命令就会将aof缓冲区中的数据写入到AOF文件中,并且同步AOF文件,
everysec的意思是执行一个写命令就将aof缓冲区中的数据写入到AOF文件中,并且每隔一秒就同步依次AOF文件,
no的意思是执行一个写命令就会将aof缓冲区中的数据写入到AOF文件中,但是不进行AOF文件的同步,同步AOF文件由操作系统决定。
从以上可以看到AOF的持久化会丢失更少的数据,因此当使用Redis时可以将两个持久化都开启,这样保证数据最少的丢失
8.怎么实现Redis的高可用?
我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。 Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式
主从模式
主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制
主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址
主从模式的概述
Redis中有时需要同步其它服务器的数据,这时候就可以通过Redis中主从复制的功能来实现,对于Redis的主从复制有两个版本,2.8之前与2.8之后 ,先说2.8之前的
2.8版本之前的主从复制是通过SYNC来实现,在Redis中SYNC的实现过程有两步,分别是同步和命令传播,同步主要实现的是将主数据库的持久化文件发送到从数据库,通过文件进行同步数据,命令传播实现的是在发送持久化文件之后,主数据库执行的各种命令会同步发送到从数据库中,这样保证了主从数据库的数据一致
旧版:SYNC
1、同步
当从数据库执行SYNC发送给主数据库,主数据库收到SYNC命令,然后执行BGSAVE命令进行RDB持久化,然后将持久化的RDB文件发送给从数据库,从数据库接收文件并载入。主数据库在持久化之后执行的命令则会同步到缓冲区中。
2、命令传播
主从数据库完成同步操作之后,从数据库就有了主数据库中从BGSAVE持久化之前的所有数据了,那么主数据库之后执行产生的数据则会存放到缓冲区中,将缓冲区中的数据再同步给从数据库就实现了命令传播的功能,这样就保证了主数据库执行命令产生的数据也会及时发送给从数据库保证数据的一致性。
但是这种主从复制的方式会存在问题,就是当从数据库宕机之后,那么再进行主从复制时就需要再次发送RDB文件,然后再进行命令传播,但是从数据库中已经有了之前的数据,缺失的只是宕机之后的数据,这样执行全部的主从复制对于Redis来讲比较消耗性能,所以在2.8之后进行了优化。
新版:PSYNC
对于新版的主从复制的功能则是通过PSYNC来实现的,PSYNC有两种模式,一种是完整重同步,一种是部分重同步。当从数据库从来没有复制过主数据库的数据时,执行PSYNC命令则会使用完整重同步来进行同步数据
1、完整重同步
完整重同步的过程跟旧版的SYNC的过程类似,也是先发送RDB文件,然后再进行命令传播
2、部分重同步
而部分重同步则是为了处理断线重连的问题,对于部分重同步需要知道几个概念,复制偏移量,复制积压缓冲区、容器ID
复制偏移量分为两个,一个是主数据库的偏移量指的是主数据库发送给从数据库数据偏移量,从数据库的偏移量指的是从数据库接收主数据库的数据偏移量,当从数据库的偏移量小于主数据库的偏移量时则证明需要进行主从复制
复制积压缓冲区,当主数据库发送命令给从数据库时同时会将这些命令写入到复制积压缓冲区中,写入到缓冲区中的命令也同步了主数据库的偏移量,这个缓冲区也是为了进行同步最近的数据做的处理
容器ID,容器ID指的是主数据库的ID,这个ID是为了防止当主数据库宕机,那么主数据库会变更,原来的主数据库就会失效,那么从数据库就无法从原来的主数据库进行复制,这时就需要容器ID来解决,当从数据库中保存的容器ID与要请求的主数据库的ID不一致时,则证明主数据库更改了,需要对新的主数据库执行完整重同步
哨兵模式
哨兵模式由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控
哨兵模式就三个作用
发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态
哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机
哨兵之间还会相互监控,从而达到高可用
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容
哨兵的概念
Redis中哨兵的功能主要是管理Redis集群,当一个Redis集群中存在一个master和多个slave,当master出现断开连接的情况,就需要有一个管理员来进行维护整个集群的正常运行,而不是master服务器断开连接,那么整个集群就会崩溃的情况,这就是哨兵所要做的事情。
哨兵可以监控Redis集群中的所有Redis服务器,当出现master服务器断开连接的情况,那么哨兵就会发挥作用维护整个集群的运行
哨兵的实现
哨兵监控到master断开连接,那么会将master服务器从监控列表中移除,并且从slave服务器中选举一个服务器作为新的master服务器,然后将旧的master服务器设置为slave服务器,这样当旧的master服务器重新恢复连接后会成功新master服务器的从服务器,而不会 导致旧master服务器重新连接需要跟新master抢master角色的问题,并且也保证了整个集群的稳定性。
在使用哨兵时一般是多个哨兵一同工作,并且哨兵之间进行一个投票机制,只有多个哨兵共同决定一个Redis服务器断开连接,这个服务器才会从哨兵的监视中去除,例如当有三个哨兵对一个集群进行监控,当master服务器断开连接,只有当两个哨兵都认可该服务器已下线,那么这个master服务器才会从集群中剔除掉。然后哨兵完成故障转移操作,选举新的master服务器,将旧的服务器转换为新master的从服务器
哨兵监控服务器的状态是每十秒一次的频率进行发送监控命令
Cluster集群模式
Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能
Cluster的过程:
Redis的集群开启是通过设定配置文件中的cluster-enable参数来实现的,当打开这个参数时,执行cluster meet命令就可以就可以将数据库加入到集群中
集群的底层实现是基于一种clusterNode结构来实现的,集群中的每个节点都会存在一个clusterNode,该结构保存着节点的名称、节点IP、节点端口、当前纪元等。
并且每个节点还会通过clusterState结构来保存该节点与集群中其它数据库的连接,clusterNode保存着集群的当前纪元、集群的状态、以及一个nodes数组,该数组的结构是clusterNode结构,所以nodes数组中就保存着其它数据库的clusterNode结构
9.Redis 事务机制
Redis通过MULTI、EXEC、WATCH等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
简言之,Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令
事务机制的用法
MULTI命令是用来开启事务,当我们执行了MULTI命令,客户端就会开始事务,后续执行的命令就会被暂时缓存,等待事务的开启
EXEC命令是执行事务,在执行MULTI命令后所执行的命令都会在EXEC命令执行之后进行顺序执行并返回结果
WATCH命令则是监控事务中键的功能,通过WATCH命令可以监控我们操作的键,当我们操作的键被其它客户端修改,那么我们事务就不能执行成功
WATCH命令实现
当执行修改命令时,如果LPUSH、SET、SADD、DEL等命令时,如果操作的键是事务中的键,那么事务在执行时会执行失败并且返回一个空回复
另外事务的一些特殊情况如下:当我们执行multi命令之后执行的各种命令出错,那么最后执行EXEC命令时,Redis在2.6.5之前会直接报错,2.6.5之后则不会执行这些错误命令
如果执行MULTI命令之后执行的命令操作键的类型不对,那么最后通过EXEC执行事务时,事务会执行成功,但是错误的命令会报错
Redis执行事务的流程
开始事务(MULTI)
命令入队
执行事务(EXEC)、撤销事务(DISCARD)
10.分布式锁
Redis实现分布式锁可以通过setNX方法来实现,setNX方法是一个Redis添加缓存的方法,方法流程为,缓存的key和value请求到方法中,方法中调用SetParams的方法nx以及px方法,nx方法用于判断缓存中是否存在该key,px方法用于设置key的过期时间,当超过过期时间,那么这个key就会失效,当我们要删除这个key时通过lua脚本来实现,具体实现是通过lua脚本查询要删除的key跟缓存中的key是否相等以及value是否跟缓存中相等,如果相等则执行删除。