Redis绝对是当下非常火热的一个NoSql数据库,在之前的博客中,我已经讲到了springboot集成redis的使用,今天在这里分享下Redis常问面试题

1.为什么使用Redis?

在项目中使用Redis,主要考虑两个角度:性能和并发。如果只是为了分布式锁这些其他功能,还有其他中间件 Zookpeer 等代替,并非一定要使用 Redis。

性能:

如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

特别是在秒杀系统,在同一时间,几乎所有人都在点,都在下单,执行的是同一操作——向数据库查数据。

根据交互效果的不同,响应时间没有固定标准。在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。

并发:

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。

2.Redis为什么快?

Redis的速度非常的快,单机的Redis就可以支持每秒10几万的并发,相对于mysql来说,性能是mysql的几十倍。速度快的原因主要有:

  1. 完全基于内存操作
  2. C语言实现,优化过的数据结构,基于几种基础的数据结果,Redis做了大量的优化,性能极高
  3. 使用单线程,无上下文的切换成本(redis的单线程是指网络请求模块使用了一个线程,所以不需考虑并发安全性。但是对于需要依赖多个操作的复合操作来说,还是需要锁的,而且有可能是分布式锁)
  4. 基于非阻塞的IO多路复用机制

对于非阻塞的多路IO复用机制,可以深入的去了解下,简单的说就是实现一个线程监控多个IO流,及时响应请求,举个栗子:

小明在 A 城开了一家快餐店店,负责同城快餐服务。小明因为资金限制,雇佣了一批配送员,然后小明发现资金不够了,只够买一辆车送快递。

经营方式一

客户每下一份订单,小明就让一个配送员盯着,然后让人开车去送。慢慢的小曲就发现了这种经营方式存在下述问题:

  • 时间都花在了抢车上了,大部分配送员都处在闲置状态,抢到车才能去送。
  • 随着下单的增多,配送员也越来越多,小明发现快递店里越来越挤,没办法雇佣新的配送员了。
  • 配送员之间的协调很花时间。

综合上述缺点,小明痛定思痛,提出了经营方式二。

经营方式二

小明只雇佣一个配送员。当客户下单,小明按送达地点标注好,依次放在一个地方。最后,让配送员依次开着车去送,送好了就回来拿下一个。上述两种经营方式对比,很明显第二种效率更高。

在上述比喻中:

  • 每个配送员→每个线程
  • 每个订单→每个 Socket(I/O 流)
  • 订单的送达地点→Socket 的不同状态
  • 客户送餐请求→来自客户端的请求
  • 明曲的经营方式→服务端运行的代码
  • 一辆车→CPU 的核数

于是有了如下结论:

  • 经营方式一就是传统的并发模型,每个 I/O 流(订单)都有一个新的线程(配送员)管理。
  • 经营方式二就是 I/O 多路复用。只有单个线程(一个配送员),通过跟踪每个 I/O 流的状态(每个配送员的送达地点),来管理多个 I/O 流。

类比到真实的线程模型:

Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。

目前支持I/O多路复用的系统调用有selectpselectpollepoll等函数。I/O多路复用就是通过一种机制一个进程可以监视多个描述符,一旦某个描述符读就绪或者写就绪,其能够通知应用程序进行相应的读写操作。

多路I/O复用机制与多进程和多线程技术相比系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

常见函数的特点如下:

  1. select函数:
    • 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
    • 有最大监听连接数1024个的限制
    • 如果任何一个sock(I/O stream)出现了数据,select没有返回具体是哪个返回了数据,需要采用轮询的方式去遍历获取
    • 线程不安全(当你在一个线程中已经监听该socket,另一个线程想要将该socket关闭,则结果会不可预知)
  2. poll函数:
    • 去掉了1024的限制(使用链表搞定)
    • 不再修改传入的参数数组
    • 依然是线程不安全的
  1. epoll函数

    • epoll不仅返回socket组里面数据,还可以确定具体哪个socket有数据

    • 线程不安全

Redis支持哪些数据类型,应用场景有哪些?

redis支持五种数据类型作为其Value,redis的Key都是字符串类型的。

redis没有直接使⽤C语⾔传统的字符串表示,⽽是⾃⼰实现的叫做简单动态字符串SDS的

抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串⻓度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重分配次数。

  • string:Rediz中字符串Value最大可为512M。可以用来做一些计数功能的缓存(也是实际工作中最常见的)
  • list:简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或尾部(右边),其底层实现是一个链表。可以实现一个简单的消息推送功能,做基于redis的分页功能等
  • set:是一个字符串类型的无序集合。可以用来进行全局去重等。
  • sorted set:是一个字符串类型的有序集合,给每一个元素一个固定的分数score来保存顺序。可以用来做排行榜应用或者进行范围查找等
  • hash:键值对集合,是一个字符串类型的key和value的映射表,也就是说其存储的value是一个键值对(key—value)。可以用来存放一些具体特定结构的信息。

般情况下,可以认为redis的支持的数据类型有上述五种,其底层数据结构包括:简单动态字符串,链表,字典,跳表,整数集合以及压缩列表。

这篇文章讲了很多用redis实现的业务模型:

Redis五种数据结构和使用场景

为什么Redis6.0之后又改用多线程?

redis使用多线程并非是完全摒弃单线程,redis还是使⽤单线程模型来处理客户端的请求,只是使⽤多线程来处理数据的读写和协议解析,执⾏命令还是使⽤单线程。

这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

热key是什么?热key问题怎么解决?

热key就是突然有几十万的请求去访问redis上的某个特定的key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机引发雪崩。

针对热key的解决方案:

  1. 提前把热key打散到不同的服务器上,降低压力
  2. 加入二级缓存,提前加载热key数据到内存中,如果redis宕机,走内存查询

缓存击穿、缓存穿透和缓存雪崩三连问

缓存击穿

缓存击穿指的是单个key并发访问过高,过期时导致所有请求直接打到db上,这个和热key的问题比较类似,只是说的点在于key过期导致请求打到db上

解决方案:

  1. 加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后⾯的请求就可以从缓存中拿到数据了。当然加锁会有点慢,但好比宕机强。
  1. 将过期时间组合写在value中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。

缓存穿透

缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在⼀样。

针对这个问题,加⼀层布隆过滤器。布隆过滤器的原理是在你存⼊数据的时候,会通过散列函数将它映射为⼀个位数组中的K个点,同时把他们置为1。

这样当⽤户再次来查询A,⽽A在布隆过滤器值为0,直接返回,就不会产⽣击穿请求打到DB了。

显然,使⽤布隆过滤器之后会有⼀个问题就是误判,因为它本身是⼀个数组,可能会有多个值落到同⼀个位置,那么理论上来说只要我们的数组⻓度够⻓,误判的概率就会越低,这种问题就根据实际情况来就好了。

对于布隆过滤器,下面这篇文章讲的很清楚:

布隆过滤器

缓存雪崩

当某⼀时刻发⽣⼤规模的缓存失效的情况,⽐如你的缓存服务宕机了,会有⼤量的请求进来直接打到DB上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太⼀样的是,他是指⼤规模的缓存都过期失效了。

针对缓存雪崩的几个解决方案:

  1. 针对不同key设置不同的过期时间,避免同时过期
  2. 限流,如果redis宕机,可以限流,避免同时刻大量请求打崩db
  3. 二级缓存,同热key的方案

Redis的过期策略

惰性删除

惰性删除指的是当我们查询key的时候才对key进⾏检测,如果已经达到过期时间,则删除。显然,他有⼀个缺点就是如果这些过期的key没有被访问,那么他就⼀直⽆法被删除,⽽且⼀直占⽤内存。

定期删除

定期删除指的是redis每隔一段时间对数据库做一次检查,删除里面过期的key。由于不可能对所有key去做轮询删除,所以redis会每次随机取一些key去做检查和删除

定期+惰性删除都没删除过期的key怎么办?

假设redis每次定期随机查询key的时候没有删掉,这些key也没做查询,就会导致key一直保存在redis里面无法被删除,这时候就会走到redis的内存淘汰机制。

  1. volatile-lru:从已设置过期的key中,移除最近最少时候的key进行淘汰
  2. volatike-ttl:从已设置过期的key中,移除将要过期的key
  3. volatile-random:从已设置过期时间的key随机选择key淘汰
  4. allkeys-lru:从key中选择最近最少使用的进行淘汰
  5. allkeys-rand:从key中随机选择key进行淘汰
  6. noeviction:当内存达到阈值的时候,新写入操作报错

redis的持久化有哪些?有什么区别?

redis持久化分为RDB和AOF两种

RDB(快照方式 snapshotting) 全量持久化

RDB持久化可以手动执行也可以根据配置定期执行,它的作用是将某个时间点上的数据库状态保存到RDB⽂件中,RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。

可以通过SAVE或者BGSAVE来生成RDB文件

SAVE命令会阻塞redis进程,直到RDB文件生成完毕,在进程阻塞期间,redis不能处理任何命令请求,这显然是不合适的。

BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请求,不会阻塞进程。

在恢复大数据集时候,RDB相对于AOF要快

AOF(append only file) 增量持久化

AOF和RDB不同,AOF是通过保存redis服务器所执行的写命令来记录数据库状态的。

AOF通过追加、写入、同步三个步骤来实现持久化机制

  1. 当AOF持久化处于激活状态,服务器执行写命令之后,写命令将会被追加append到aof_buf缓冲区的末尾
  2. 当服务器每结束一个事件循环之前,就会调用flushAppendOnlyFile函数决定是否要将aof_buf的内容保存到AOF文件中,可以通过配置appendfsync来决定
1
2
3
always ##aof_buf内容写入并同步到AOF文件
everysec ##aof_buf中内容写入到AOF文件,如果上次同步AOF文件距离现在超过1秒,则再次对AOF文件进行同步
no ##将aof_buf内容写入到AOF文件,但是不会对AOF文件进行同步,同步时间由操作系统决定

如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失一次事件循环的写命令),但是性能较差,而everysec模式只不过可能丢失1秒钟的数据,而no模式的效率和everysec相仿,但是会丢失上次同步AOF文件之后的所有写命令数据。

怎么实现Redis的高可用?

主从架构

主从模式是最简单的实现高可用的方案,核心就是主从同步。主从同步的原理如下:

  1. slave发送sync命令到master
  2. master收到sync之后,执行bgsave,生成RDB全量文件
  3. master把slave的写命令记录到缓存
  4. bgsave执行完毕之后,发送RDB到slave、slave执行
  5. master发送缓存中的写命令到slave,slave执行

这里从机发送的命令是sync,但在redis2.8版本之后就已经使用psync替代sync,因为sync非常消耗系统资源,psync效率更高。

哨兵

基于主从方案的确实很明显,假设master宕机,那么就不能写入数据,slave也就失去了作用,整个架构就不可用了,除非手动切换,主要原因就是没有自动故障转移机制。而哨兵(sentinel)的功能比单纯的主从架构全面的多,它具备自动故障转移、集群监控、消息通知等功能。

哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,自动将某个slave提升位master,然后由新的master继续接收命令。整个过程如下:

  1. 初始化sentinel,将普通的redis代码替换成sentinel专业代码
  2. 初始化master字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
  3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
  4. 每隔10秒向master发送info命令,获取master和它下⾯所有slave的当前信息
  5. 当发现master有新的slave之后,sentinel和新的slave同样建⽴两个连接,同时每个10秒发送info命令,更新master信息
  6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回⽆效回复,将会被标记为下线状态
  7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
  8. 领头sentinel从已下线的的master所有slave中挑选⼀个,将其转换为master
  9. 让所有的slave改为从新的master复制数据将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新master的从服务器

sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是否已经下线,这种⽅式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。

Redis事务机制

隔离性:redis是单进程的程序,保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。所以redis的事务支持隔离性

redis会将一个事务中的所有命令序列化,然后按顺序执行。redis不可能在一个事务的执行过程中插入执行另一个客户端发出的请求。可以保证Redis将这些命令作为一个单独的隔离操作执行。

  1. 服务端收到客户端请求,事务以MULTI开始
  2. 如果客户端正处于事务状态,则会把事务放⼊队列同时返回给客户端QUEUED,反之则直接执行这个命令
  3. 当收到客户端EXEC命令时,WATCH命令监视整个事务中的key是否有被修改,如果有则返回空回复到客户端表示失败,否则redis会遍历整个事务队列,执⾏队列中保存的所有命令,最后返回结果给客户端

WATCH的机制本身是⼀个CAS的机制,被监视的key会被保存到⼀个链表中,如果某个key被修改,那么REDIS_DIRTY_CAS标志将会被打开,这时服务器会拒绝执⾏事务。

解释下:

MULTI:标记一个事务块的开始

EXEC:执行所有事务块内的命令

DISCARD:取消事务,放弃执行事务块的所有命令

UNWATCH:取消WATCH命令对所有key的监视

WATCH key [key…] :监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断

需要注意的是redis的事务不支持回滚操作,redis以 MULTI 开始一个事务,然后将多个命令入队到事务中,最后由 EXEC 命令触发事务,一并执行事务中的所有命令。只有当被调用的redis命令有语法错误时,这条命令才会执行失败,或者对某个键执行不符合其数据类型的操作,但是应该在将命令入队列的时候就应该并且能够发现这些问题,所以redis的事务不支持进行回滚操作。

redis缓存与数据库一致性问题

不一致原因

不管是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况
因为写和读是并发的,没法保证顺序,如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

优化思路:

双删+超时

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。这样最差的情况是在超时时间内存在不一致,当然这种情况极其少见,可能的原因就是服务宕机。此种情况可以满足绝大多数需求。
当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定时间,比如500毫秒,这样毫无疑问又增加了写请求的耗时。

异步淘汰缓存

通过读取binlog的方式,异步删除缓存

好处:业务代码侵入性低,将缓存与数据库不一致的时间尽可能缩小。