本文答案整理为原创,转载请说明出处
文章目录
- redis 的底层数据结构有哪些
- redis 中的 SDS 和 C 语言中的字符串有什么区别,优点是什么
- redis 中的字典是如何实现的,如何解决冲突和扩容
- redis 的跳表的使用场景是什么,可以实现一下吗
- redis 缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效
- redis 的淘汰策略,来写一下 LRU 吧
- redis 的持久化方式,RDB 和 AOF 分别的使用场景
- redis 如何处理事务
- redis 为什么那么快?
- redis 是单线程为什么还那么快?
- redis 的操作为什么是原子性的,如何保证原子性
- redis 集群用过哪些方案,分别怎么做。讲一下一致性哈希
- redis 什么情况下会出现性能问题,有什么处理办法?
- 有没有使用过 redis 的分布式锁,有什么优缺点
- 说一下 redis 的内存模型
- 说一下 redis 和 memcache 的区别
- 你用 redis 做过什么?
- 你用过哪些非关系型数据库,都有什么特点,使用场景分别是什么
- Mongodb 相对于 Mysql 有哪些优势,底层索引使用的数据结构是什么,为什么要使用这个
- Mongodb 中的分片是什么意思
题目来源: 2020 字节跳动 数据库面试题汇总
题目列表
- redis 的底层数据结构有哪些
- redis 中的 SDS 和 C 语言中的字符串有什么区别,优点是什么
- redis 中的字典是如何实现的,如何解决冲突和扩容
- redis 的跳表的使用场景是什么,可以实现一下吗
- redis 缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效 (常问)
- redis 的淘汰策略,来写一下 LRU 吧
- redis 的持久化方式,RDB 和 AOF 分别的使用场景
- redis 如何处理事务
- redis 为什么那么快?
- redis 是单线程为什么还那么快?
- redis 的操作为什么是原子性的,如何保证原子性
- redis 集群用过哪些方案,分别怎么做。讲一下一致性哈希
- redis 什么情况下会出现性能问题,有什么处理办法?
- 有没有使用过 redis 的分布式锁,有什么优缺点
- 说一下 redis 的内存模型
- 说一下 redis 和 memcache 的区别
- 你用 redis 做过什么?(这里尽量不要讲只做过缓存,可以说一下队列,排行榜/计数器,发布/订阅)
- 你用过哪些非关系型数据库,都有什么特点,使用场景分别是什么(体现你技术广度的时刻到了,尽可能多说,* 但是不会的不要说,防止被问死)
- Mongodb 相对于 Mysql 有哪些优势,底层索引使用的数据结构是什么,为什么要使用这个
- Mongodb 中的分片是什么意思
redis 的底层数据结构有哪些
- 字符串
- 哈希
- 链表
- set 集合
- zset 有序集合
redis 中的 SDS 和 C 语言中的字符串有什么区别,优点是什么
SDS比C语言的字符串多了一个SDSHDR表头。里面存放free(空闲空间)、len(已用空间)、buf(缓冲区)
优点:
- 获取字符串长度更快。C语言获取字符串长度需要遍历整个字符串,时间复杂度为O(N),SDS的表头
len
成员存放了已使用空间,获取字符串长度复杂度为O(1) - 杜绝缓冲区溢出。C语言字符串本身不记录自身长度和空闲空间,容易造成缓冲区溢出。SDS表头
free
成员存放了空闲空间,拼接字符串前会先通过free
字段检测剩余空间是否能满足,如果空间不够就会扩容 - 减少内存分配次数。C语言对字符串进行增长或缩短操作,都需要重新分配内存。SDS使用了空间预分配和惰性空间释放策略,减少了内存分配次数
- 二进制安全。C语言字符串遇0则止,会对文件进行截断。SDS判断字符串是否结尾的依据是表头的
len
成员,不会改变二进制文件。
redis 中的字典是如何实现的,如何解决冲突和扩容
根据键值对的键计算哈希值和索引值,然后根据索引值,将包含键值对的哈希节点放到哈希数组的指定索引上
如何解决冲突:
redis采用链地址法解决键冲突。每个哈希节点有一个next指针,多个哈希节点通过next指针构成一个单向链表。总是将最新的节点添加到表头
扩容:
redis的扩容通过rehash(重新散列)
实现,为字典ht[1]
分配空间,ht[1]
的大小为第一个大于等于ht[0].used * 2
的 2n
redis 的跳表的使用场景是什么,可以实现一下吗
使用场景:有序列表。如实时统计分数排行榜
java实现:
// 跳表中存储的是正整数,并且存储的数据是不重复的
public class SkipList {
private static final int MAX_LEVEL = 16; // 结点的个数
private int levelCount = 1; // 索引的层级数
private Node head = new Node(); // 头结点
private Random random = new Random();
// 查找操作
public Node find(int value){
Node p = head;
for(int i = levelCount - 1; i >= 0; --i){
while(p.next[i] != null && p.next[i].data < value){
p = p.next[i];
}
}
if(p.next[0] != null && p.next[0].data == value){
return p.next[0]; // 找到,则返回原始链表中的结点
}else{
return null;
}
}
// 插入操作
public void insert(int value){
int level = randomLevel();
Node newNode = new Node();
newNode.data = value;
newNode.maxLevel = level; // 通过随机函数改变索引层的结点布置
Node update[] = new Node[level];
for(int i = 0; i < level; ++i){
update[i] = head;
}
Node p = head;
for(int i = level - 1; i >= 0; --i){
while(p.next[i] != null && p.next[i].data < value){
p = p.next[i];
}
update[i] = p;
}
for(int i = 0; i < level; ++i){
newNode.next[i] = update[i].next[i];
update[i].next[i] = newNode;
}
if(levelCount < level){
levelCount = level;
}
}
// 删除操作
public void delete(int value){
Node[] update = new Node[levelCount];
Node p = head;
for(int i = levelCount - 1; i >= 0; --i){
while(p.next[i] != null && p.next[i].data < value){
p = p.next[i];
}
update[i] = p;
}
if(p.next[0] != null && p.next[0].data == value){
for(int i = levelCount - 1; i >= 0; --i){
if(update[i].next[i] != null && update[i].next[i].data == value){
update[i].next[i] = update[i].next[i].next[i];
}
}
}
}
// 随机函数
private int randomLevel(){
int level = 1;
for(int i = 1; i < MAX_LEVEL; ++i){
if(random.nextInt() % 2 == 1){
level++;
}
}
return level;
}
// Node内部类
public class Node{
private int data = -1;
private Node next[] = new Node[MAX_LEVEL];
private int maxLevel = 0;
// 重写toString方法
@Override
public String toString(){
StringBuilder builder = new StringBuilder();
builder.append("{data:");
builder.append(data);
builder.append("; leves: ");
builder.append(maxLevel);
builder.append(" }");
return builder.toString();
}
}
// 显示跳表中的结点
public void display(){
Node p = head;
while(p.next[0] != null){
System.out.println(p.next[0] + " ");
p = p.next[0];
}
System.out.println();
}
}
参考文章:数据结构与算法——跳表
redis 缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效
- 缓存穿透:在缓存层和数据库层,都没有找到数据。解决方案一:把空对象缓存起来,并设置合理的过期时间。解决方案二:使用布隆过滤器
- 缓存击穿:缓存中的数据在某个时刻批量过期,导致大部分的请求都会直接落在数据库上。解决方案一:针对不同类型的数据,设置不同的过期时间。解决方案二:使用分布式锁。
- 缓存雪崩:缓存在某一时刻集中失效,或者缓存系统出现故障,所有的并发流量会直接到达数据库。解决方案一:保证redis的高可用,使用集群部署。解决方案二:限流降级
- 热点数据集中失效 :类似于缓存击穿。热点数据同时失效,造成都去查数据库。解决方案:利用集群部署,保证redis的高可用。
redis 的淘汰策略,来写一下 LRU 吧
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候,如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- allkeys-random:加入键的时候,如果过限,从所有key随机删除
- volatile-random:加入键的时候,如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
实现:
public class LRU<K,V> {
private static final float hashLoadFactory = 0.75f;
private LinkedHashMap<K,V> map;
private int cacheSize;
public LRU(int cacheSize) {
this.cacheSize = cacheSize;
int capacity = (int)Math.ceil(cacheSize / hashLoadFactory) + 1;
map = new LinkedHashMap<K,V>(capacity, hashLoadFactory, true){
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > LRU.this.cacheSize;
}
};
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
public synchronized void clear() {
map.clear();
}
public synchronized int usedSize() {
return map.size();
}
public void print() {
for (Map.Entry<K, V> entry : map.entrySet()) {
System.out.print(entry.getValue() + "--");
}
System.out.println();
}
}
redis 的持久化方式,RDB 和 AOF 分别的使用场景
持久化方式
RDB
:将某一时刻的内存快照,以二进制的方式写入磁盘AOF
:将redis的操作日志以追加的方式写入文件
使用场景
- 如果追求备份的速度,可以忽略部分数据丢失,推荐使用
RDB
- 如果追求数据的完整性,可以接受牺牲备份的速度,推荐使用
AOF
redis 如何处理事务
redis使用MULTI、EXEC、DISCARD、WATCH
四个事务命令
使用MULTI
开启事务,客户端可以向服务器发送任意多个命令,这些命令不会立马执行,而是被放到一个队列中,当调用EXEC
命令时,所有队列中的命令才会被执行。如果在执行事务的过程发生异常,而没有执行EXEC
,那么事务中的所有命令都不会被执行。至于在执行EXEC
命令之后发生了错误,及时事务中某个命令发生了错误,其他事务也会正常执行,没有回滚操作
通过调用DISCARD
,客户端可以清空事务队列,并放弃执行事务
WATCH
命令可以为redis提供CAS
行为
redis 为什么那么快?
- 采用多路复用IO阻塞机制
- 数据结构简单,操作节省时间
- 运行在内存中
redis 是单线程为什么还那么快?
- 采用了非阻塞IO多路复用机制
- 单线程操作,避免了频繁的上下文切换
- 运行在内存中
redis 的操作为什么是原子性的,如何保证原子性
因为redis是单线程的
对于redis来说,执行get
、set
以及eval
等api,都是一个一个的任务,这些任务都会由redis的线程去负责执行,任务要么执行成功,要么执行失败
redis 集群用过哪些方案,分别怎么做。讲一下一致性哈希
集群方案
- 主从模式。只需要在配置文件加上
slaveof 192.169.x.x 6379
,指明master的ip和端口号 - 哨兵模式。给几台哨兵服务添加不同的端口,配置主服务器的ip和端口,并且加上权值,使用Redis命令启动哨兵,
./redis-sentinel sentinel-26380.conf
- cluster集群模式。配置集群地址,开启cluster,
cluster-enable yes
,集群配置文件:cluster-config-file nodes-7000.conf
,这个可以不修改,集群超时时间:cluster-node-timeout 15000
,槽是否全覆盖:cluster-require-full-coverage no
,默认是yes,后台运行:daemonize yes
,输出日志:logfile "./redis.log"
,监听端口:port 7000
,拷贝同样的配置文件,修改端口
redis 什么情况下会出现性能问题,有什么处理办法?
- master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
- Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
有没有使用过 redis 的分布式锁,有什么优缺点
优点:
- redis性能很好
- redis对命令的支持较好,实现起来比较方便
缺点:
- redis分布式锁,需要自己不断尝试去获取锁,比较消耗性能
- redis获取锁的那个客户端挂了,只能等待超时时间后才能释放锁
说一下 redis 的内存模型
- 数据
- redis主进程本身运行需要占用的内存。如代码、常量池等
- 缓冲内存。
- 内存碎片。redis在分配、回收物理内存过程中产生
说一下 redis 和 memcache 的区别
- memcache仅支持简单的key-value数据结构,redis支持字符串、哈希、链表、set集合,有序set集合
- redis不是所有的数据都存储在内存,会将一些很久没用的value交换到磁盘
- redis采用的单核,memcache可以采用多核,在存储小数据的时候,redis性能更好,而在大数据的时候,memcache性能更好
- redis集群可以采用一主多从,一主一从。memcache集群只能采用一主多从
- redis支持数据恢复。memcache不支持
你用 redis 做过什么?
- 缓存
- 商品销量排行榜
- 消息队列
你用过哪些非关系型数据库,都有什么特点,使用场景分别是什么
- redis:可以通过key来添加、查询或者删除数据,鉴于使用主键访问,所以会获得不错的性能及扩展性。适用于储存用户信息,比如会话、配置文件、参数、购物车等等。
- MongoDB:将数据以文档的形式储存。适用于日志、分析
- HBase:将数据储存在列族中,一个列族存储经常被一起查询的相关数据。适用于日志、博客平台
参考文章[NoSQL 数据库的使用场景](https://www.cnblogs.com/dyllove98/p/3220329.html)
Mongodb 相对于 Mysql 有哪些优势,底层索引使用的数据结构是什么,为什么要使用这个
- Mongod的访问速度优于MySQL
- Mongodb获取数据的方式比MySQL快捷
- Mongodb的表的扩展、删除、维护简单,不依赖于SQL
- Mongodb对开发更友好,拿到的数据都是json格式
- Mongodb会将系统内存作为缓存
- Mongodb自带分布式文件系统,可以很方便部署在服务器集群上
Mongodb索引数据结构为B-Tree
为什么要使用这个数据结构,参考文章:mongo数据库索引原理
Mongodb 中的分片是什么意思
MongoDB分片集群将数据分布在一个或多个分片上。每个分片部署成一个MongoDB副本集,该副本集保存了集群整体数据的一部分。因为每个分片都是一个副本集,所以他们拥有自己的复制机制,能够自动进行故障转移。你可以直接连接单个分片,就像连接单独的副本集一样。但是,如果连接的副本集是分片集群的一部分,那么只能看到部分数据。
参考文章:MongoDB实战-分片概念和原理