Redis是一个基于C语言开发的开源数据库,与传统数据库不同的是,Redis的数据是存储在内存中的,因此读写速度非常快,广泛应用于缓存,消息队列等方向。Redis 提供了多种数据类型来支持不同的业务场景比如–> 除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、内存淘汰机制、过期删除机制等等。
5种基本数据结构: String(字符串),List(列表),Set(集合),Hash(散列),Zset(有序集合)
3种特殊数据结构: HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)
Redis和MySQL的区别:
- MySQL属于关系型数据库,使用表格存储数据,Redis属于非关系型数据库,使用键值对存储数据
- MySQL将数据持久化存储在硬盘上,Redis数据存储在内存中,因此具有非常高的读写性能
- MySQL作为关系型数据库,拥有强大的SQL查询功能,能够进行复杂关系查询
- MySQL提供了严格的事务支持,能确保数据的一致性和完整性;MySQL还支持索引用于提高查询性能
- Redis中支持多种数据结构,每种数据结构都有相应的操作命令,方便进行存储和操作
- Redis支持分布式架构,可以通过分片来将数据分布在多个节点上,提高系统的可扩展性和容错性
- Redis的发布/订阅功能可以用来实现消息队列,用于在不同的应用程序或服务之间传递消息
Redis和MySQL两者的使用场景:
Redis是一个内存数据结构存储,常用于需要高速读写操作、缓存或作为消息代理的场景。主要用于实时分析、排行榜、发布/订阅消息系统、实时通信
MySQL是一个关系数据库管理系统,适用于需要存储大量数据、复杂查询和事务处理的场景。主要用于电子商务网站、客户关系管理系统、内容管理系统
要回答这种问题,首先是结合业务进行回答的,有的业务的一致性要求比较高,有些业务的一致性是允许延迟一致性的。双写一致性的概念就是修改数据库中的数据,相应缓存中的数据也要被修改。缓存和数据库的数据要保持一致。
Redis数据备份文件,也就是数据快照,就是将内存中的所有数据都记录在磁盘中,当Redis故障重启后,从磁盘中读取快照文件,恢复数据。我们可以手动使用save或者bgsave来触发RDB机制,也可以在redis.conf配置文件中去配置自动触发机制(每隔多少秒触发一次bgsave)。
执行原理:bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件。
但是redis主进程是没有办法直接操作物理内存的,操作系统给每一个进程分配了一个虚拟内存,主进程可以操作虚拟内存,操作系统创建了一个页表用于维护虚拟地址和物理地址之间的映射关系,主进程去操作虚拟内存,虚拟内存基于页表关联到真正物理内存存储数据的地址,这样就能实现对物理内存数据的读写操作,内部主进程fork(克隆)一个子进程子进程将主进程的页表数据进行拷贝,子进程拥有了和主进程相同的映射关系,那么子进程在操作自己的虚拟内存时,因为映射关系和主进程一样,最终能读到相同的物理内存区域,这样就实现了子进程和主进程内存空间的共享,然后子进程将读到的数据写入到rdb文件中替换旧的rdb文件。
此时如果主进程对数据进行操作,而子进程在读数据,这时候可能会出现脏数据,这个情况下fork的时候采用了一种copy-on-write的技术,就是在fork的时候物理内存中的数据是read-only的,就是只读模式,用户进程只能读不能写,此时如果主进程有写操作过来,会先去拷贝一份数据,在拷贝的数据中进行写操作,主进程的读操作也是在拷贝的数据中。
RDB创建快照会阻塞线程吗?
在Redis中创建了两个命令来生成RDB快照文件:
save: 同步保存操作,会阻塞 Redis 主线程
bgsave: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项
AOF(日志)持久化,它会将所有写操作追加到一个日志文件中,以日志的形式来记录每个写操作,这个日志文件可以用来重建数据库,只许追加文件不许改写文件,Redis启动之初就会读取该文件重新构建数据,也就是Redis重启的话就是根据日志文件的内容将写指令从前往后执行一次以完成数据的恢复工作,AOF持久化机制的优点是可靠性高,缺点是文件比较大,恢复速度比RDB慢。
缓存穿透:key对应的数据在Redis中并不存在,每次的请求key从缓存获取不到,请求就会传到数据库,数据库也没有,当请求量达到一定程度就回压垮数据库。
解决方法: 1. 将这个空对象设置到缓存里面,下次请求的话直接从缓存里面拿,这种情况一般将空对象设置一个较短过期时间,2. 对参数进行校验,不合法参数进行拦截
缓存击穿:某个key对应数据库中存在,但是Redis缓存在某个时间节点过期,此时有大量请求发送过来,发现缓存过期,就会从后端数据库加载到缓存,这时候大量并发可能会将数据库压垮。
解决方法; 1. 热点数据设置永不过期 2. 加锁,当多个线程去查询数据库的这条数据时,我们可以在第一条查询语句加互斥锁,这样当其他线程拿不到锁就进行等待,当第一个线程查询到数据时,然后将数据返回到Reids缓存起来,后面线程进来发现有缓存,直接从缓存处拿取数据
缓存雪崩:高并发情况下,大量的缓存失效,或者缓存层出现故障,于是所有的请求都会到达数据库,数据库的调用量暴增,造成数据库宕机
解决办法: 1. 随机设置key失效时间,避免大量的key集体失效 2. 若是集群部署,可将热点数据均分在不同的Redis库中可能避免key全部失效 3. 不设置过期时间 4. 跑定时任务,在缓存失效前刷新新的缓存
总结:雪崩就是大面积的key缓存失效,穿透是Redis里不存在这个缓存key,击穿是Redis某个热点key突然失效,最终的受害者都是数据库,对于Redis宕机,请求数据全部去数据库这种情况,我们可以有以下思路
事发前: 实现Redis的高可用(主从架构+Sentinel(哨兵)),尽量避免Redis挂掉这种情况
事发中: 玩意Redis真的挂了,我们可以设置本地缓存(ehcache)+限流,尽量避免数据库被压垮(起码保证给服务正常运行)
事发后: Redis持久化,重启后从磁盘上加载数据,快速恢复缓存数据
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。 Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
Redis中的key过期策略是用于设置key的生存时间,并在时间到期后自动将过期的key删除。常见的有:
定时删除:当key的生存时间到期时,Redis会自动删除该key。这种策略可以保证资源的及时释放,但是在大量key同时过期时,会阻塞服务器,导致服务器暂时停止服务。
惰性/懒汉式删除:只有当客户端访问一个已过期的key时,Redis才会删除这个key。这种策略CPU开销小,但是如果过期key不被访问,那么这些key会一直存在于内存中,占用内存空间。
定期删除:Redis每隔一段时间,会随机测试一些key,删除其中已过期的key。这种策略是定时删除和惰性删除的折中方案,通过调整删除操作执行的频率和时长,可以在控制CPU开销和内存占用之间找到一个平衡。
当Redis内存满了之后,如果此时还有新的key要添加进Redis中,这时候就会使用数据一种规则将内存中的数据删除掉,这种数据的删除规则称为内存淘汰策略。
在Redis中支持8种不同的策略来选择要删除的key:
- noeviction:不淘汰任何key,但是内存满的时候不允许写入新的数据,默认是这种。
- volatile-ttl:对设置了TTL的key,比较key剩余的TTL的值,越小的月线被淘汰。
- 在所有数据中随机进行淘汰。
- 在设置了TTL的数据中进行随机淘汰。
- 对使用时间最晚的数据进行淘汰。
- 在设置了TTL的数据中将使用时间最晚的数据进行淘汰。
- 对使用频率最低的数据进行淘汰。
- 在设置了TTL的数据中将使用频率最低的数据进行淘汰。
首先使用本地锁是只能解决单个JVM中的线程安全问题的,当使用反向代理进行负载均衡进行集群的情况下本地锁就不能解决线程安全问题,这种情况下就要使用分布式锁。
主要有三种集群的方案:主从复制(主从集群)、哨兵模式、分片集群。单节点的Redis并发能力是有限的,需要进一步提高Redis的并发能力就需要搭建集群。
主从同步有一个明显的缺点,就是保证不了Redis的高可用,比如主机节点宕机之后就丧失了redis的高可用。这个时候Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障修复。
由于网络问题,主节点master和哨兵都处于不同的分区,哨兵只能去检测从节点,这时候哨兵会在从节点中重新选主一个master,这时候会出现两个主节点,客户端的写入在原本的master,这时候如果网络恢复,原本的主节点会清除RDB,重新加载新选主的master的RDB,出现数据不一致问题。
min-replicas-to-write 1
:表示最少的slave节点为1。min-replicas-max-log 5
:表示数据复制和同步的延迟不能超过5秒。主从和哨兵只能解决高可用,高并发读的问题,但是海量数据的存储和高并发写的问题并没有解决。
首先Redis是基于内存进行操作的,执行速度非常快;其次Redis采用了单线程,避免了不必要的上下文切换和竞争条件,多线程还要考虑线程安全问题。最后Redis使用了I/O多路复用模型和非阻塞IO。
Redis是基于内存的,所以它的性能的瓶颈是网络延迟,而不是执行速度,I/O多路复用模型主要就是为了实现高效的网络请求。
在计算机操作系统中,有用户空间和内核空间,用户空间的权限较低,不能直接调用系统资源,而内核空间的权限高,可以直接调用系统资源,当用户空间需要使用系统资源时,会先将用户数据写入内核空间,然后内核空间使用系统资源进行处理,Linux为了提高IO效率,在用户空间和内核空间中都加入了缓冲区,写数据时,会先从用户缓冲区写入内核缓冲区,然后内核缓冲区再写入设备中;在读数据的时候,设备会读取内核缓冲区的数据,内核缓冲区从用户缓冲区中复制数据。
在上述操作中,影响IO的主要有两个因素:
这时候有三种常见的I/O模型:
BIO:在获取数据(recvfrom操作)的时候,如果内核缓冲区没有数据,此时就处于阻塞状态,当数据到达内核缓冲区的时候,代表数据就绪,此时需要将内核缓冲区的数据复制到用户缓冲区,在这个过程中用户进程依然处于阻塞状态,当复制结束后,用户进程解除阻塞,然后处理数据。
NIO:非阻塞IO的recvfrom操作后会立即返回结果而不是阻塞进程,当用户进程尝试从内核缓冲区读取数据时,此时数据未到达内核缓冲区,就返回异常给用户进程,用户进程拿到error后再次尝试,直到数据就绪然后复制数据。
这种方式的效率不是很高,当用户进程在等待过程中虽然没有阻塞,但是用户进程在等待数据过程中会不断询问内核有没有请求完成,会导致CPU空转,也会导致CPU使用率暴增。
IO多路复用:利用单个线程监控多个Socket并在某个Socket可读/可写时得到通知,从而避免无效等待,充分利用了CPU资源。
流程:用户进程调用select,指定要监听的Socket集合,内核监听对应的多个Socket,任何一个Socket就绪就返回readable,此过程中用户进程阻塞;然后用户进程依次遍历找到就虚的Socket,一次调用recvfrom读取数据,内存缓冲区将数据拷贝到用户缓冲区,用户进程处理数据。
在select监听过程中有提供select、poll、epoll来进行监听,使用select和poll进行监听时都只能收到通知,但是不知道那个Socket就绪,之能通过遍历来确定;在epoll中,只要有就绪就会采用回调机制直接通知select,然后直接将就虚的Socket写入用户缓冲区进行处理。
Redis的网络模型就是使用的IO多路复用模型结合事件处理器来应对多个Socket请求,其中有连接应答处理器、命令回复处理器、命令请求处理器。在Redis6.0之后,为了提高性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令转换使用了多线程,增加了命令转换速度,在命令执行时,依旧使用的是单线程。
主要是从内存占用、对范围查找的支持、实现难易程度这三方面总结的原因:
从内存占用上来比较,跳表比平衡树更灵活一些。 平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
在做范围查找的时候,跳表比平衡树操作要简单。 在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
从算法实现难度上来比较,跳表比平衡树要简单得多。 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。