数据库面试题汇总--非关系型数据库
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:https://redisbook.readthedocs.io/en/latest/internal-datastruct/sds.html
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();
}
}
数据结构与算法——跳表:https://www.jianshu.com/p/fef9817cc943
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来说,执行get
、set
以及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
为什么要使用这个数据结构,参考文章:https://blog.csdn.net/u012939368/article/details/76696673
20、Mongodb 中的分片是什么意思
MongoDB分片集群将数据分布在一个或多个分片上。每个分片部署成一个MongoDB副本集,该副本集保存了集群整体数据的一部分。因为每个分片都是一个副本集,所以他们拥有自己的复制机制,能够自动进行故障转移。你可以直接连接单个分片,就像连接单独的副本集一样。但是,如果连接的副本集是分片集群的一部分,那么只能看到部分数据。
参考文章:https://blog.csdn.net/wanght89/article/details/77842336