Redis

112 阅读19分钟

Redis

redis是一个高性能的键值对存储数据库,也是一个基于内存的数据结构存储系统,同时也支持持久化数据存储

使用场景: 缓存: 穿透,击穿,雪崩; 双写一致,持久化; 数据过期,淘汰策略 分布式锁: setnx, redisson

其他面试题: 集群:主从,哨兵,集群; 事务; Redis为什么快

缓存穿透

就是查询缓存和数据库中都不存在的数据,每次查询的时候都会透过缓存直接去查库,当用户或者黑客对该不存在的数据进行不断地查询或攻击,会对数据库造成的压力很大,甚至可能宕机

解决方案:

1.非法请求的限制: 在API入口处判断请求参数是否合理,是否含有非法值,请求字段是否存在,如果判断是恶意请求直接返回错误,避免进一步访问缓存和数据库

2.缓存空值或者默认值: 如果Redis和数据库中都不存在该数据,就缓存一个空值.并设置过期时间

3.使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

布隆过滤器:它是由二进制向量(或位数组)和一系列随机映射函数(哈希函数)组成. 相比于list,map,等数据结构,它占用空间更少并且效率更高,但是缺点是返回的结果是概率性的,可能会有误报,并且数据不易删除,添加到集合中的元素越多,误报的可能行就越大

原理: 当一个元素加入布隆过滤器的时候,首先会使用布隆过滤器中哈希函数对元素进行计算,得到哈希值,根据得到不同的哈希值,在位数组中把对应下标的值置为1

当我们需要判断一个元素是否存在于布隆过滤器的时候,先对给定元素再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为1,都为1存在, 有一个不为1,就不存在

缓存击穿

就是缓存中的某个热点数据过期了,正好这个时候有大量的请求访问了该热点数据, 就无法从缓存中获取,会直接访问数据库(通常是缓存中的那份数据已经过期),这样可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,数据库可能宕机

缓存中就不存在,数据库中存在; 缓存中的key过期

解决方案:

1.互斥锁: 锁的对象就是key,这样当大量查询同一个key的请求并发进来时,只能有一个请求获取到锁,然后获取到锁的线程查询数据库,然后将结果放入到缓存中,然后释放锁,其他处于锁等待的请求即可继续执行,这时缓存中已经有了数据,所以直接从缓存中获取到数据返回,并不会查询数据库

线程1: 查询缓存,未命中---->添加互斥锁(分布式锁)---->查询数据库重建缓存数据----->写入缓存---->释放锁

线程2: 查询缓存,未命中---->获取锁失败(线程1正在进行缓存重建)----->休眠后不断重试---->缓存命中

2.设置热点数据(key)永不过期, 也可以正常给key设置过期时间, 不过要在后台同时启动一个定时任务定时去更新这个缓存

缓存雪崩

是指在同一时段大量的缓存key同时失效(设置了相同的过期时间)或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力

解决方案:

1.给不同的key设置不同的(1-5分钟)随机值

2.利用Redis集群提高服务的可用性 (哨兵,集群)

3.给缓存业务添加降级限流策略(nginx或 spring cloud gateway)

4.给业务添加多级缓存(已经缓存可以Guava或Caffeine,二级设置Redis)

双写一致

当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

读操作: 缓存命中,直接返回; 未命中查询数据库,写入缓存,设置超时时间

写操作: 延迟双删

删除缓存-------->修改数据库-----超时---->删除缓存

持久化

为什么要持久化?

Redis是内存数据库,宕机后数据会消失, 所以提供持久化机制,将存储在内存中的数据保存到 磁盘上

RDB: 是Redis默认的存储方式, 它是通过快照将某一时刻的数据保存到磁盘中

两种方式: save 同步,阻塞 bgsave 异步 非阻塞

数据快照. 就是把内存中的所有数据都记录到磁盘中,当Redis故障重启后, 从磁盘读取快照文件, 恢复数据

Redis内部有触发RDB的机制,可以再redis.config文件中找到

save 900 1 # 900秒内至少有一个key被修改,则执行bgsave

RDB的执行原理:

​ bgsave开始时会fork主进程得到子进程, 子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

AOF: 是通过将redis服务器所执行的写命令保存到AOF文件中来实现持久化的,但是文件越来越大就需要重写AOF,新的AOF文件不会有冗余命令,所有就大大减小了AOF的体积

默认是关闭的,开启的话要将配置文件中的appendonly no改成yes

数据过期策略

惰性删除: 只会在取出key的时候才会对数据进行过期检查,如果过期,直接删掉,反之返回key

优点: 对CPU友好,只会在使用该key时才会进行过期检查, 对于很多用不到的key不用浪费时间及进行过期检查

缺点: 对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

定期删除: 就是每隔一段时间,我们就对一些key进行检查,抽取一批key删除里面过期的key(从数据库中取出一定数量的随机key)

定期清理有两种模式: SLOW模式和FAST模式

SLOW模式是定时任务.执行频率默认是10Hz(每秒执行10次,每个执行周期是100ms),每次不超过25ms, 可以通过配置文件来调整这个次数

FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,有效释放过期键占用的内存

缺点:难以确定删除操作执行的时长和频率

redis的过期删除策略: 惰性删除 + 定期删除两种策略进行配合使用

但是,仅仅通过给key设置过期时间还是有问题的,,因为还是可能定期删除和惰性删除会漏掉很多的过期key的情况,导致大量的key堆积在内存里,会使内存溢出, 解决:Redis内存淘汰机制

数据淘汰策略

当Redis中的内存不够用时,此时再向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删掉

一种有8中数据淘汰策略,lfu是4.0版本后增加的

noeviction: 不淘汰任何key,当时内存满时不允许写入新数据,否则直接报错

volatile-ttl: 对设置了TTL(过期时间)的key,比较key的剩余TTL值,TTL越小越先被淘汰

allkeys-random: 对全体key,随机进行淘汰

volatile-random: 对设置了TTL的key,随机进行淘汰

allkeys-lru:对全体key,基于LRU算法进行淘汰( LRU: 最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高)

volatile-lru: 对设置了TTL的key, 基于LRU算法进行淘汰

allkeys-lfu:对全体key,基于LFU算法进行淘汰( LFU: 最少频率使用,统计每个key的访问频率,值越小淘汰优先级越高)

volatile-lfu: 对设置了TTL的key, 基于LFU算法进行淘汰

分布式锁

为什么要使用分布式锁?

在多线程环境中, 如果多个线程同时访问共享资源,会发生数据竞争,可能会导致脏数据或者系统问题.威胁到系统运行

Redis实现分布式锁主要利用setnx命令

获取锁 SET lock value NX EX 10(EX时过期时间) 释放锁 DEL lock

SETNX命令加锁过程: 线程1------->执行SETNX命令--------->判断对应的key(锁)是否存在--------->存在则加锁,否则获取锁失败

基于Lua脚本释放锁的过程: 线程进来------->通过Lua脚本执行-------->判断key(锁)对应的value是否相等------>相等则执行DEL命令释放锁,否则释放锁失败

为了防止误删,使用Lua脚本通过key对应的value(唯一值)来判断, 选用Lua脚本是为了保证解锁操作的原子性

但是这种方式会有一个问题,就是在释放锁之前服务挂掉了,可能会导致锁无法被释放,造成共享资源无法再被其他线程/进程访问 解决方法: 给这个key(也就是锁),设置一个过期时间

SET 锁名 uniqueValue(唯一标识) EX 3(过期时间) NX(只有当锁对应的key值不存在的时候才能SET成功) 这个地方要保证设置的锁和过期时间是一个原子操作,不然可能会出现锁无法释放的问题

但是这种解决方案还会有漏洞,如果操作共享资源的时间大于过期时间就会出现锁提前过期的问题,就是锁过期了,任务还没执行完

解决方案:我们可以使用redission,它自带续期机制,它里边专门提供了一个用来监控和续期锁的Watch dog(看门狗), 如果操作共享资源的线程还未执行完成的话,Watch dog会不断的延长锁的过期时间,进而保证锁不会因为超时而被释放,直到锁释放完成后才会解除这个续期 默认30秒,如果自定义,则不会生效自动续期机制

可重入锁实现: 就是在一个线程中可以多次获取同一把锁

Redis集群方案

主从复制: 是指将一台Redis服务器的数据,同步数据到多台Redis服务器上,主从服务器之间采用的是读写分离的方式

单节点Redis的并发能力是有上线的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离

一般都是一主多从, 主节点负责写数据,从节点负责读数据

主从同步数据的流程:

全量同步: 一般用于初次复制场景. 首先从节点请求主节点同步数据,建立连接,协商同步(携带replication id, offset:复制的进度-1), 主节点判断是否是第一次请求,如果是就与从节点同步版本信息, 然后主节点执行bgsave,生产rdb文件,把文件发送给从节点去执行,从节点会先把当前数据清空,然后加载rdb文件,但是在这期间的写操作命令并没有记录到刚刚生成的RDB文件中,这时主从数据就会不一致,所以为了保证主从一致性,主服务器会将生成RDB文件期间,发送RDB文件给从服务器期间和从服务器加载RDB文件期间中收到的写操作命令,记录到缓存区中(一个日志文件),然后主服务器把生成之后的命令日志文件发送给从节点进行同步,这样主从服务器的数据就一致了;

2.8之后的增量同步: 用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连接上主节点后,如果条件允许,主节点会补发丢失的数据给从节点, 补发的数据远小于全量数据, 可以有效避免全量复制的过高开销

当从节点服务重启之后,数据就不一致了, 所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值 ,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

2.8之后哨兵模式

它的作用是实现主从节点自动故障转移,它会检测主节点是否存活,如果发现主节点挂了,它就会重新选举一个从节点切换为主节点,并且把主节点的相关信息通知给从节点和客户端(它也是由多个节点组成的集群 一般3台)

作用:

监控: Sentinel 哨兵会不断检查master和slave是否正常工作

自动故障恢复: 如果master宕机了,哨兵会选择一个从节点当作新的主节点继续工作, 当故障实例恢复后也以新的master为主,保证了集群的高可用性

通知: 哨兵可以将故障转移的结果发送给客户端,主节点换了, 哨兵就会通知给客户端新的主节点

服务状态监控

哨兵基于心跳机制检测服务状态,每隔1秒向集群的每个实例发送ping命令:

主观下线: 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

客观下线:若超过指定数量的哨兵都认为该实例主观下线,则该实例客观下线. quorum值最好超过哨兵实例数量的一半

哨兵选主规则:

首先判断主与从节点断开时间长短,如超过指定值就排除该从节点

选择slave-priority从节点优先级最高的,越小优先级越高

如果slave-priority一样,则判断slave节点的offset值,越大优先级越高

最后判断slave节点的运行id大小,越小优先级越高

脑裂

是由于主节点和从节点和哨兵处于不同的网络分区,哨兵没有能够心跳感知到主节点,所以会通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂一样,这样会导致客户端还在老的主节点那里写入数据,新数据无法同步数据到从节点,当网络恢复后, 哨兵会将老的主节点降为从节点,这时再从新的主节点同步数据,就会导致数据丢失

解决: 修改redis配置,可以设置最少从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,避免数据丢失

如何判断主节点真的故障了?

哨兵每隔1秒会给所有主从节点发送ping命令, 当主从节点收到ping命令后,会发送一个响应命令给哨兵,这样就可以判断他们是否在正常运行

Redis哨兵集群是通过什么方式组建的?

哨兵实例之间可以相互发现,主要使用了Redis pub/sub机制(发布/订阅机制) 主库有一个名叫sentinel:hello频道,不同哨兵之间就是通过它来相互通信的,哨兵1把自己的ip和端口发布到该频道上,哨兵2,3就可以直接获取了,然后建立网络连接

分片集群(Cluster)

主从和哨兵可以解决高可用,高并发的问题,但是依然有两个问题没有解决:

海量数据存储问题 和 高并发写的问题 就使用到了分片集群

Redis的分片集群有什么作用?

可以解决数据海量存储问题和高并发写的问题, 集群中有多个master,每个master保存不同的数据,并且还可以给每个master设置多个slave节点, 可以解决高并发读的问题, 同时每个master之间通过ping检测彼此健康状态,类似于哨兵模式。当客户端请求可以访问集群任意节点,最终都会被转到正确节点

集群中有多个master,每个master保存不同的数据,可以解决数据海量存储的问题和高并发写的问题

每个master都可以有多个slave节点, 可以解决高并发读的问题

master之间通过ping检测彼此之间的健康状态

客户端请求可以访问集群任意节点,最终都会被转到正节点

Redis分片集群中数据是怎么存储(写)和读取的?

嗯~~ Redis分片集群它引入了哈希槽的概念,有16384个哈希槽,将16384个插槽分配到不同的实例中。根据key的有效部分通过CRC16计算出每个key的hash值,然后对16384进行取模(66666%16284)运算来决定放到那个槽(有效部分:如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身作为有效部分)

其他面试题

Redis是单线程的,但是为什么还那么快呢?

Redis是纯内存操作,所以执行速度非常快

采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题

使用I/O多路复用模型,非阻塞IO

能解释一下I/O多路复用模型吗?

I/O多路复用模型主要就是解决了高效的网络请求, 因为Redis虽然它的执行速度很快,但是它的性能瓶颈是网络延迟而不是执行速度

Linux系统中一个进程使用的内存情况划分两部分: 用户空间和内核空间

用户空间只能执行受限的命令,而且不能直接调用系统资源必须通过内核提供的接口来访问

内核空间可以执行特权命令,调用一切系统资源

Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区

写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备

读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

阻塞IO

就是两个阶段都必须阻塞等待

阶段一

用户进程尝试读取数据------------>此时数据尚未到达,内核需要等待数据----------->此时用户进程处于阻塞状态

阶段二

数据到达并拷贝到内核缓冲区,代表已就绪---------->将内核数据拷贝到用户缓冲区-------->拷贝过程中,用户进程依然阻塞等待------->拷贝完成,用户进程才解除阻塞,处理数据

非阻塞IO

非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程

阶段一:

用户进程尝试读取数据---->此时数据尚未到达,内核需要等待数据----->返回异常个用户进程--------->用户进程拿到error后,再次尝试读取----->循环往复,直到数据就绪

阶段二:

将内核数据拷贝到用户缓冲区------->拷贝过程中,用户进程依然阻塞等待------->拷贝完成,用户进程解除阻塞,处理数据

非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态,虽然是非阻塞,但性能并没有得到提高,而且忙等机制会导致CPU空转,CPU使用率暴增

IO多路复用

是利用单线程来同时监听多个Socket,并在某个Socket可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源

阶段1:

用户进程调用select,指定要监听的Socket集合--------->内核监听对应的多个Socket--------->任意一个或多个socket手机数据就绪则返回readable------------>此过程中用户进程阻塞

阶段2:

用户进程找到就绪的socket------>依次调用recvfrom读取数据----->内核将数据拷贝到用户空间------->用户进程处理数据

不过监听Socket的方式,通知的方式有多种实现,常见的有: select poll epoll

差异select和poll只会通知用户进程有S从课堂就绪,但不确定具体是哪个Scoket,需要用户进程逐个遍历Scoket来确认

epoll则会在通知用户进程Scoket就绪的同时,把已就绪的Socket写入到用户空间

其中redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如:提供了连接应答处理器,命令回复处理器,命令请求处理器

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,最后在命令执行的时候,依然是单线程