面试面经|Java面试Redis面试题

序言

凡事预则立,不预则废。能读到这里的人,我相信都是这个世界上的“有心人”,还是那句老话:上天不负有心人!我相信你的每一步努力,都会收获意想不到的回报。

1、Redis 为何这么快?

1)基于内存;

2)单线程减少上下文切换,同时保证原子性;

3)IO多路复用;

4)高级数据结构(如 SDS、Hash以及跳表等)。

2、为何使用单线程?

官方答案
因为 Redis 是基于内存的操作,CPU 不会成为 Redis 的瓶颈,而最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

详细原因
1)不需要各种锁的性能消耗

Redis 的数据结构并不全是简单的 Key-Value,还有 List,Hash 等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

2)单线程多进程集群方案

单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。

所以单线程、多进程的集群不失为一个时髦的解决方案。

3、缓存三大问题以及解决方案?

缓存穿透:查询数据不存在
1)缓存空值

2)key 值校验,如布隆筛选器 ref 分布式布隆过滤器(Bloom Filter)详解(初版)

缓存击穿:缓存过期,伴随大量对该 key 的请求
1)互斥锁

2)热点数据永不过期

3)熔断降级

缓存雪崩:同一时间大批量的 key 过期
1)热点数据不过期

2)随机分散过期时间

4、先删后写还是先写后删?

先删缓存后写 DB
产生脏数据的概率较大(若出现脏数据,则意味着再不更新的情况下,查询得到的数据均为旧的数据)。

比如两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

先写 DB 再删缓存
产生脏数据的概率较小,但是会出现一致性的问题;若更新操作的时候,同时进行查询操作并命中,则查询得到的数据是旧的数据。但是不会影响后面的查询。

比如一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据。

解决方案

1)缓存设置过期时间,实现最终一致性;

2)使用 Cannel 等中间件监听 binlog 进行异步更新;

3)通过 2PC 或 Paxos 协议保证一致性。

5、如何保证 Redis 的高并发?

Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。

ref 如何保证Redis的高并发

6、Redis 如何保证原子性?

答案很简单,因为 Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。

但我们业务中常常有先 get 后 set 的业务常见,在并发下会导致数据不一致的情况。

如何解决

1)使用 incr、decr、setnx 等原子操作;

2)客户端加锁;

3)使用 Lua 脚本实现 CAS 操作。

7、有哪些应用场景?

Redis 在互联网产品中使用的场景实在是太多太多,这里分别对 Redis 几种数据类型做了整理:

1)String:缓存、限流、分布式锁、计数器、分布式 Session 等。

2)Hash:用户信息、用户主页访问量、组合查询等。

3)List:简单队列、关注列表时间轴。

4)Set:赞、踩、标签等。

5)ZSet:排行榜、好友关系链表。

8、Redis 有哪些数据结构?

  • 字符串 String
  • 字典 Hash
  • 列表 List
  • 集合 Set
  • 有序集合 Zset

9、String 类型的底层实现?

为了将性能优化到极致,Redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。以最常用的 String 为例,其底层实现就可以分为 int、embstr 以及 raw 这三种类型。这些特定的底层实现在 Redis 中被称为编码(Encoding),可以通过 OBJECT ENCODING [key] 命令查看。

Redis 中所有的 key 都是字符串,这些字符串是通过一个名为简单动态字符串(SDS) 的抽象数据类型实现的。

struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}


10、说说 SDS 带来的好处?

我们知道 Redis 是使用 C 语言写的,那么相比使用 C 语言中的字符串(即以空字符 \0 结尾的字符数组),自己实现一个 SDS 的好处是什么?

1)常数复杂度获取字符串长度

由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。

2)杜绝缓冲区溢出

3)减少修改字符串的内存重新分配次数

4)二进制安全

5)兼容部分 C 字符串函数

一般来说,SDS 除了保存数据库中的字符串值以外,还可以作为缓冲区(Buffer):包括 AOF 模块中的 AOF 缓冲区以及客户端状态中的输入缓冲区。

ref Redis详解(四)------ redis的底层数据结构

11、Redis 实现的链表有哪些特性?

链表是一种常用的数据结构,C 语言内部是没有内置这种数据结构的实现,所以 Redis 自己构建了链表的实现。

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
     //节点值复制函数
     void (*free) (void *ptr);
     //节点值释放函数
     void (*free) (void *ptr);
     //节点值对比函数
     int (*match) (void *ptr,void *key);
}list;

typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}listNode



1)双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 O(1)。

2)无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。

3)带长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。

4)多态:链表节点使用指针来保存节点值,可以保存各种不同类型的值。

5、Redis 是如何实现字典的?
字典又称为符号表或者关联数组、或映射(Map),是一种用于保存键值对的抽象数据结构。

typedef struct dictht{
     //哈希表数组
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩码,用于计算索引值
     //总是等于 size-1
     unsigned long sizemask;
     //该哈希表已有节点的数量
     unsigned long used;
 
}dictht



哈希算法
Redis计算哈希值和索引值方法如下:

# 1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
# 2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;


当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用 MurmurHash2 算法来计算键的哈希值。这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

ref哈希算法

扩容和收缩
当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤如下:

1)如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used * 2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间

你可能感兴趣的:(数据库,Java面试,Java,java,面试,redis)