文章目录
Redis概述
Redis是一个基于C语言开发的开源数据库,与传统数据库不同的是,Redis的数据是存储在内存中的,因此读写速度非常快,广泛应用于缓存,消息队列等方向。Redis 提供了多种数据类型来支持不同的业务场景比如–> 除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、内存淘汰机制、过期删除机制等等。
Redis基本数据类型
5种基本数据结构: String(字符串),List(列表),Set(集合),Hash(散列),Zset(有序集合)
3种特殊数据结构: HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)
Redis与MySQL的区别以及使用场景
Redis和MySQL的区别:
- MySQL属于关系型数据库,使用表格存储数据,Redis属于非关系型数据库,使用键值对存储数据
- MySQL将数据持久化存储在硬盘上,Redis数据存储在内存中,因此具有非常高的读写性能
- MySQL作为关系型数据库,拥有强大的SQL查询功能,能够进行复杂关系查询
- MySQL提供了严格的事务支持,能确保数据的一致性和完整性;MySQL还支持索引用于提高查询性能
- Redis中支持多种数据结构,每种数据结构都有相应的操作命令,方便进行存储和操作
- Redis支持分布式架构,可以通过分片来将数据分布在多个节点上,提高系统的可扩展性和容错性
- Redis的发布/订阅功能可以用来实现消息队列,用于在不同的应用程序或服务之间传递消息
Redis和MySQL两者的使用场景:
Redis是一个内存数据结构存储,常用于需要高速读写操作、缓存或作为消息代理的场景。主要用于实时分析、排行榜、发布/订阅消息系统、实时通信
MySQL是一个关系数据库管理系统,适用于需要存储大量数据、复杂查询和事务处理的场景。主要用于电子商务网站、客户关系管理系统、内容管理系统
如何保持双写一致性(缓存一致性)
要回答这种问题,首先是结合业务进行回答的,有的业务的一致性要求比较高,有些业务的一致性是允许延迟一致性的。双写一致性的概念就是修改数据库中的数据,相应缓存中的数据也要被修改。缓存和数据库的数据要保持一致。
1. 延迟双删
- redis缓存的一般流程就是
- 读操作的一般流程就是上图,写操作的话一般是使用延迟双删,删除缓存—>修改数据库–延时–>删除缓存
-
- 一般设计两个问题:1. 就是为什么先删除缓存在修改数据库(反转是否可以);2. 是为什么要删两次
- 无论是先删除缓存还是先修改数据库,都补课避免的会产生脏数据,先删除缓存是为了降低脏数据的出现,使用双删是防止脏数据,比如两个线程,线程a在删除缓存之后,线程b进行读操作,这时候线程b拿到的就是修改之前的数据,这时候线程a进行修改数据库和删除缓存操作,之后线程b将之前拿到的数据返回并存入redis,此时就产生了脏数据。
- 为什么要使用延时删除,一般情况下DB是主从模式的,是读写分离的,使用延时是主节点要将数据同步到从节点,极大控制了脏数据的风险,但延时不容易控制,也会有产生脏数据的风险。
2. 分布式锁(强一致性时使用)
- 在进行写操作的时候不允许其他线程,只有当一个线程操作完成之后其他线程才能进行读写,但是效率低。
- 但是一般放入缓存中的数据都是读多写少,加锁可以使用读写锁,也就是共享锁和排他锁。
-
- 共享锁(读锁readLock):共享读操作。
- 排他锁(写锁writeLock):阻塞其他线程进行读写操作。
3. 中间件
- 在实际业务开发中出现短暂的不一致是主流的,可以使用异步通知保证数据的一致性。
- 第一种是基于MQ的数据一致性
- 异步通知保证数据一致性
- 最终的一致性保证取决于MQ的可靠性。
- 第二种是基于阿里的中间件Canal的异步通知(基于MySQL主从同步来实现)
- 利用Canal这个中间件,不需要修改业务代码,只需要伪装一个MySQL的从节点,Canal通过读取binglog数据来更新缓存。
Redis持久化机制
RDB(redis database)
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(append only file)
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 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
key过期策略
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的数据中将使用频率最低的数据进行淘汰。
Redis分布式锁
首先使用本地锁是只能解决单个JVM中的线程安全问题的,当使用反向代理进行负载均衡进行集群的情况下本地锁就不能解决线程安全问题,这种情况下就要使用分布式锁。
Redis集群的方案主要有几种
主要有三种集群的方案:主从复制(主从集群)、哨兵模式、分片集群。单节点的Redis并发能力是有限的,需要进一步提高Redis的并发能力就需要搭建集群。
主从复制(主从集群)
- 要想提高单节点Redis的并发能力就要搭建主从集群,实现读写分离,主节点master负责写,多个从节点slave负责操作,但是当master节点写入数据时,一般涉及到slave的数据的同步问题。
- 主从同步原理:
- 主从全量同步:slave在执行replicaof命令建立连接,向master发起数据同步请求,master接收到请求之后会先判断是否第一次同步,是第一次同步的话返回master的版本信息,slave接收保存版本信息,接着master执行bgsave,生成RDB发送给slave,slave清空本地的RDB,然后加载接收的RDB,slave在接收RDB的过程中如果master有操作,此时有一个repl_baklog日志文件记录的时RDB期间进行的所有命令,之后将repl_baklog中的命令发送给slave,slave进行加载。在master判断的时候,每一个master都有唯一的replid(replication Id)用来判断从节点上的id是否一致。在repl_baklog记录同步时会有一个偏移量offset,用来记录repl_baklog中的数据量,当repl_baklog进行追加时,offset的数据量也会增加,一般用offset来判断master和salve中是否同步。
- 主从增量同步:主要是作用与slave重启或后期数据变化。首先当slave重启后,会向master发送replid和offset,然后master接收到之后通过replid判断是否第一次同步,不是第一次同步就直接返回continue,然后去repl_baklog获取offset后,将数据发送给slave进行同步。
哨兵模式
主从同步有一个明显的缺点,就是保证不了Redis的高可用,比如主机节点宕机之后就丧失了redis的高可用。这个时候Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障修复。
- 哨兵其实也是由多个Redis节点组成的一个集群,一般至少部署三台哨兵。
- 哨兵一般实现三个功能:监控、自动故障修复、通知。
- 监控:Sentinel会不断检查master和slave是否正常工作。
- 自动故障修复:如果master故障,会将一个slave提升为master,当故障恢复后也以新的master为主。
- 通知:Sentinel充当Redis的服务发现来源,当集群中发生故障转移时,会将最新的信息推送给Redis的客户端,客户端就会连接新的主节点,极大的保证了Redis的高可用。
- 监控的原理实际就是每隔一秒向每个实例发送一个ping命令,如果某个Sentinel发现某个实例未在规定的时间内响应,就认为该实例主管下线。若超过指定数量(quorum 最好超过哨兵数量的一半)的Sentinel都认为该实例主观下线,则该实例就是客观下线。如果判断这个实例客观下线了,就要进行master的重新选主。
- 重新选主的规则:
- 如果一个从节点与主节点的断开时间超过了指定值,则排除该节点。
- 判断从节点的slave-priority值(优先级),越小优先级越高。
- 如果优先级一样,则判断offset值,值越大优先级越高。
- 最后判断slave节点运行id的大小,越小优先级越高。
Redis集群(哨兵模式)下的脑裂问题
由于网络问题,主节点master和哨兵都处于不同的分区,哨兵只能去检测从节点,这时候哨兵会在从节点中重新选主一个master,这时候会出现两个主节点,客户端的写入在原本的master,这时候如果网络恢复,原本的主节点会清除RDB,重新加载新选主的master的RDB,出现数据不一致问题。
- 解决办法:在redis中又两个配置参数可以用来解决集群脑裂问题。
min-replicas-to-write 1
:表示最少的slave节点为1。min-replicas-max-log 5
:表示数据复制和同步的延迟不能超过5秒。- 只有达到以上两个要求,客户端才能成功向master中写入数据。
分片集群
主从和哨兵只能解决高可用,高并发读的问题,但是海量数据的存储和高并发写的问题并没有解决。
- 使用分片集群可以解决,分片集群的特点主要有:
- 一个分片集群中有多个master,每个master保存不同的数据。
- 每个master可以有多个slave节点。
- master之间通过ping检测彼此的健康状态。
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
- 在分片集群中的数据读写是在Redis分片集群中引入了hash槽,Redis集群中有16348个hash槽,每一个分片集群中会平均分配这个hash槽给master,每一个key通过CRC16校验后对16348取模来决定放置与哪一个槽,在读的时候也是如此,先将key通过CRC16校验,然后对16348取模来确定去哪一个槽中读取数据。
Redis是单线程的,为什么还是那么快
首先Redis是基于内存进行操作的,执行速度非常快;其次Redis采用了单线程,避免了不必要的上下文切换和竞争条件,多线程还要考虑线程安全问题。最后Redis使用了I/O多路复用模型和非阻塞IO。
解释I/O多路 复用模型
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之后,为了提高性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令转换使用了多线程,增加了命令转换速度,在命令执行时,依旧使用的是单线程。
Redis为什么使用跳表而不是用B+树?
主要是从内存占用、对范围查找的支持、实现难易程度这三方面总结的原因:
从内存占用上来比较,跳表比平衡树更灵活一些。 平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
在做范围查找的时候,跳表比平衡树操作要简单。 在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
从算法实现难度上来比较,跳表比平衡树要简单得多。 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。