数据库面试题汇总--非关系型数据库

348 阅读9分钟

这是我参与更文挑战的第26天,活动详情查看: 活动链接

1、redis 的底层数据结构有哪些

(1)字符串

(2)哈希

(3)链表

(4)set集合

(5)zset有序集合

2、redis 中的 SDS 和 C 语言中的字符串有什么区别,优点是什么

SDS(Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示。

SDS比C语言的字符串多了一个SDSHDR表头,存放了free(空闲空间)、len(已用空间)、buf(缓冲区)

优点:

(1)获取字符串长度更快。C语言获取字符串需要遍历整个字符串,时间复杂度为O(N),SDS的表头len已经存放了已使用空间,获取字符串长度只需要O(1)。

(2)杜绝缓冲区溢出。C语言字符串本身不记录字符串长度和已使用空间,容易造成缓冲区溢出。SDS表头free成员存放了空闲空间,拼接字符串前会先通过free判断剩余空间是否足够,如果不够就会进行扩容。

(3)减少内存分配次数。C语言对字符串进行增长或缩短操作,都要重新分配内存。SDS使用空间预分配和惰性空间释放策略,减少了内存分配的次数。

(4)二进制安全。C语言字符串遇0则止,会对文件进行截断。SDS判断字符串是否结尾的依据是表头的len成员,不会改变二进制文件。

SDS:redisbook.readthedocs.io/en/latest/i…

3、redis 中的字典是如何实现的,如何解决冲突和扩容

根据键值对的键计算哈希值和索引值,然后根据索引值,将包含键值对的哈希节点存放在哈希数组的指定索引上。

解决冲突:redis采用链地址法解决冲突,每个哈希节点有一个next指针,多个哈希节点通过next指针构成单向链表,总是将最新的节点添加到表头。

扩容:redis的扩容通过rehash(重新散列)实现,为字典ht[1]分配空间,ht[1]的大小为第一个大于等于ht[0].used * 2 的 2^n。

4、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();
	}	
}

数据结构与算法——跳表:www.jianshu.com/p/fef9817cc…

5、redis 缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效 (常问)

(1)缓存穿透:在缓存层和数据库层,都没有找到数据。解决方案1:把空数据存储起来,并设置过期时间。解决方案2:使用布隆过滤器。

(2)缓存击穿:缓存中的数据在某个时刻大批量过期,导致大部分的请求都会直接落在数据库上。解决方案1:针对不同类型的数据,设置不同的过期时间。解决方案2:使用分布式锁。

(3)缓存雪崩:缓存在某一时刻集中失效,或者缓存系统出现故障,所有的并发流量会直接到达数据库。解决方案1:保证redis的高可用,使用集群部署。解决方案2:降流限级。

(4)热点数据集中失效:类似于缓存击穿。热点数据同时失效,造成都去查数据库。解决方案:利用集群部署,保证redis的高可用。

6、redis 的淘汰策略,来写一下 LRU 吧

(1)noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键

(2)allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键

(3)volatile-lru:加入键的时候,如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键

(4)allkeys-random:加入键的时候,如果过限,从所有key随机删除

(5)volatile-random:加入键的时候,如果过限,从过期键的集合中随机驱逐

(6)volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键

(7)volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键

(8)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();
  }
}
7、redis 的持久化方式,RDB 和 AOF 分别的使用场景

持久化方式

(1)RDB:将某一时刻的内存快照,以二进制的方式写入磁盘

(2)AOF:将redis的操作日志以追加的方式写入文件

使用场景

(1)如果追求备份的速度,可以忽略部分数据丢失,推荐使用RDB

(2)如果追求数据的完整性,可以接受牺牲备份的速度,推荐使用AOF

8、redis 如何处理事务

redis使用MULTI、EXEC、DISCARD、WATCH四个事务命令

使用MULTI开启事务,客户端可以向服务器发送任意多个命令,这些命令不会立马执行,而是被放到一个队列中,当调用EXEC命令时,所有队列中的命令才会被执行。如果在执行事务的过程发生异常,而没有执行EXEC,那么事务中的所有命令都不会被执行。至于在执行EXEC命令之后发生了错误,及时事务中某个命令发生了错误,其他事务也会正常执行,没有回滚操作

通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务

WATCH命令可以为redis提供CAS行为

9、redis 为什么那么快?

(1)采用多路复用IO阻塞机制

(2)数据结构简单,操作节省时间

(3)运行在内存中

10、redis 是单线程为什么还那么快?

(1)采用了非阻塞IO多路复用机制

(2)单线程操作,避免了频繁的上下文切换

(3)运行在内存中

11、redis 的操作为什么是原子性的,如何保证原子性

因为redis是单线程的

对于redis来说,执行getset以及eval等api,都是一个一个的任务,这些任务都会由redis的线程去负责执行,任务要么执行成功,要么执行失败

12、redis 集群用过哪些方案,分别怎么做。讲一下一致性哈希

集群方案

(1)主从模式。只需要在配置文件加上slaveof 192.169.x.x 6379,指明master的ip和端口号

(2)哨兵模式。给几台哨兵服务添加不同的端口,配置主服务器的ip和端口,并且加上权值,使用Redis命令启动哨兵,./redis-sentinel sentinel-26380.conf

(3)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,拷贝同样的配置文件,修改端口

13、redis 什么情况下会出现性能问题,有什么处理办法?

(1)master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。

(2)Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

(3)Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

(4)Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内

14、有没有使用过 redis 的分布式锁,有什么优缺点

优点:

(1)redis性能很好

(2)redis对命令的支持较好,实现起来比较方便

缺点:

(1)redis分布式锁,需要自己不断尝试去获取锁,比较消耗性能

(2)redis获取锁的那个客户端挂了,只能等待超时时间后才能释放锁

15、说一下 redis 的内存模型

(1)数据

(2)redis主进程本身运行需要占用的内存。如代码、常量池等

(3)缓冲内存。

(4)内存碎片。redis在分配、回收物理内存过程中产生

16、说一下 redis 和 memcache 的区别

(1)memcache仅支持简单的key-value数据结构,redis支持字符串、哈希、链表、set集合,有序set集合

(2)redis不是所有的数据都存储在内存,会将一些很久没用的value交换到磁盘

(3)redis采用的单核,memcache可以采用多核,在存储小数据的时候,redis性能更好,而在大数据的时候,memcache性能更好

(4)redis集群可以采用一主多从,一主一从。memcache集群只能采用一主多从

(5)redis支持数据恢复。memcache不支持

17、你用 redis 做过什么?(这里尽量不要讲只做过缓存,可以说一下队列,排行榜/计数器,发布/订阅)

(1)缓存

(2)商品销量排行榜

(3)消息队列

18、你用过哪些非关系型数据库,都有什么特点,使用场景分别是什么(体现你技术广- 度的时刻到了,尽可能多说,但是不会的不要说,防止被问死)

(1)redis:可以通过key来添加、查询或者删除数据,鉴于使用主键访问,所以会获得不错的性能及扩展性。适用于储存用户信息,比如会话、配置文件、参数、购物车等等。

(2)MongoDB:将数据以文档的形式储存。适用于日志、分析

(3)HBase:将数据储存在列族中,一个列族存储经常被一起查询的相关数据。适用于日志、博客平台

19、Mongodb 相对于 Mysql 有哪些优势,底层索引使用的数据结构是什么,为什么要使用这个

(1)Mongod的访问速度优于MySQL

(2)Mongodb获取数据的方式比MySQL快捷

(3)Mongodb的表的扩展、删除、维护简单,不依赖于SQL

(4)Mongodb对开发更友好,拿到的数据都是json格式

(5)Mongodb会将系统内存作为缓存

(6)Mongodb自带分布式文件系统,可以很方便部署在服务器集群上

Mongodb索引数据结构为B-Tree

为什么要使用这个数据结构,参考文章:blog.csdn.net/u012939368/…

20、Mongodb 中的分片是什么意思

MongoDB分片集群将数据分布在一个或多个分片上。每个分片部署成一个MongoDB副本集,该副本集保存了集群整体数据的一部分。因为每个分片都是一个副本集,所以他们拥有自己的复制机制,能够自动进行故障转移。你可以直接连接单个分片,就像连接单独的副本集一样。但是,如果连接的副本集是分片集群的一部分,那么只能看到部分数据。

参考文章:blog.csdn.net/wanght89/ar…