Redis面试总结

常考题目:

Redis支持的数据类型(必考)

zset跳表的数据结构(必考)

Redis的数据过期策略(必考)

Redis的持久化机制(必考)

 

Redis为什么能这么快

完全基于内存,绝大部分请求时纯粹的内存操作,执行效率高

数据结构简单,对数据操作也简单

采用单线程,单线程也能处理高并发请求,想多核也可以启动多实例

使用多路I/O复用模型,即非阻塞IO(Reids会根据不同的操作系统选择不同的底层实现)

 

 Redis 适合的场景

1. 缓存:减轻 MySQL 的查询压力,提升系统性能;

2. 排行榜:利用 Redis 的 SortSet(有序集合)实现;

3. 计算器/限速器:利用 Redis 中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等。限速器比较典型的使用场景是限制某个用户访问某个API的频率,比如抢购时,防止用户疯狂点击带来不必要的压力;

4. 好友关系:利用集合的一些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;

5. 消息队列:除了 Redis 自身的发布/订阅模式,我们也可以利用 List 来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压力,完全可以用 List 来完成异步解耦;

6. Session 共享:Session 是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用 Redis 保存 Session 后,无论用户落在那台机器上都能够获取到对应的 Session 信息。

 

Redis如何实现单线程的
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

 

 

Redis中的数据类型:

数据结构

数据类型存储的值

说明

使用场景

STRING(字符串-常用)

可以是保存字符串、整数和浮点数

可以对字符串进行操作,比如增加字符串或者求子串;如果是整数或者浮点数,可以实现计算,比如自增等

缓存、计数器、共享 Session、限速

LIST(列表)

它是一个链表,每一个节点都包含一个字符串

Redis支持从链表两端插入或者弹出节点,或者通过偏移对它进行裁剪;还可以读取一个或者多个节点,根据条件删除或者查找节点等

阻塞队列

SET(集合)

是一个收集器,但是是无须的,里面的每一个元素都是一个字符串,而且是独一无二,各不相同

可以新增,读取,删除单个元素,检查一个元素是否在集合中,计算它和其他集合的交集、并集和差集等;随机从集合中读取元素

关系处理

HASH(哈希散列表-常用)

类似Java中的Map,是一个键值对应的无序列表

可以增,删,查,改单个键值对,可以获取所有的键值对

 

ZSET(有序集合)

可以包含字符串,整数,浮点数,分值(score),元素的排序是根据分值的大小决定的

可以增删改查元素,根据分值的范围或者成员来获取对应的元素

排行榜

用于计数的HyperLogLog,用于支持存储地理位置信息的Geo

 

 Z-set内部数据结构实现:

zset内部使用skiplist跳表实现,实现简单,插入、删除、查找的复杂度均为O(logN)。

指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
插入的层数是随机的(重要特性)

 

悲观锁(Pessimistic Lock):每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block,直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock) :每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号(version)等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁策略:提交版本必须大于记录当前版本才能执行更新。

 

如何理解Redis中的乐观锁

当一个线程去执行某些业务逻辑,但是这些业务逻辑操作的数据可能被其他线程共享了,这样会引发多线程中数据不一致的情况。为了克服这个问题,首先,在线程开始时,读取这些多线程共享的数据,并将其保存到当前进程的副本中,我们称为旧值(old value),watch命令就是这样一个功能。然后,开启线程业务逻辑,由multi命令提供这一功能。在执行更新前,比较当前线程副本保存的旧值和当前线程共享的值是否一致,如果不一致,那么该数据已经被其他数据操作过,此次更新失败。为了保持一致,线程就不去更新任何值,而将事务回滚;否则就认为它没有被其他线程操作过,执行对应的业务逻辑,exec命令就是执行“类似”这样的一个功能。

 

如何保证缓存与数据库的双写一致性?

/*

如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做:读请求和写请求串行化,串到一个内存队列里去。

串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

更新的时候,先更新数据库,然后再删除缓存

*/

 

如何解决缓存雪崩

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。

把每个Key的失效时间都加个随机值,避免数据在同一时间大面积失效

事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

 

如何解决缓存击穿

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。或者是查询一个一定不存在的值,也需要直接请求数据库。

解决方式:

1 可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

2 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

 

如何解决缓存穿透

访问缓存和数据库都不存在的值

增加校验

布隆过滤器

 

 

 

分布式锁需要解决的问题:互斥性、安全性、死锁、容错

 

 

如何使用Redis实现异步队列

使用List作为队列,RPUSH生产消息,LPOP消费消息

  缺点:不管队列里有没有值,都会直接消费

  弥补:可以通过在应用层引入Sleep机制去调用LPOP重试,或使用BLPOP

更好的:pub/sub 主题订阅者模式

  订阅者可以订阅任意数量的频道

  缺点:消息的发布是无状态的,无法保证可达

 

Redis如何做持久化

RDB(快照)持久化:保存某个时间点的全量数据快照

  BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程

AOF(Append-Only-File)持久化:保存写状态

  记录下除了查询以外的所有变更数据库状态的指令

以append的形式追加保存到AOF文件中(增量)

RDB优点:全量数据快照,文件小,恢复快   缺点:无法保存最近一次快照之后的数据

AOF优点:可读性高,适合保存增量数据,数据不易丢失  缺点:文件体积大,恢复时间长

RDB-AOF混合持久化方式

 

键的过期时间

Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。

对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。

Redis 常见性能问题和解决方案?

1. Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件。如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;

2. 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内;

3. 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…

原文地址:https://www.cnblogs.com/flyuz/p/11275650.html