数据库知识常用面试题

发布时间:2023-09-14 22:45
最后更新:2023-09-14 22:45
所属分类:
面试题集锦

这篇文章中所列出的面试题可以供所有需要完成数据库设计与操作的职位使用。

专题系列文章:

  1. 关于面试题集锦的使用
  2. IT系列职位通用常用面试题
  3. 基本IT知识常用面试题
  4. Go语言常用面试题
  5. Java语言常用面试题
  6. Python语言常用面试题
  7. 数据库知识常用面试题
  8. 前端技术栈常用面试题

什么是缓存雪崩?

缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩溃。

可用解决思路:

  • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
  • 事中:本地ehcache缓存 + hystrix限流与降级,避免数据库崩溃。
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存。

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。例如:某个黑客故意制造缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

可用解决思路:

  1. 做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
  2. 缓存无效 key :如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求key,会导致 redis 中缓存大量无效的 key 。这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
  3. 布隆过滤器:布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
扩展问题:什么是布隆过滤器?

【精通级别】如何保证缓存与数据库双写时的数据一致性?

方案一

如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。

缺点:串行化之后,就会导致系统的吞吐量会大幅度的降低,需要用比正常情况下多几倍的机器去支撑线上的一个请求。

方案二

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

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

很多时候,比较复杂一些的缓存场景,缓存不单单是数据库中直接取出来的值。并且更新缓存的代价有时候是很高的。删除缓存,而不是更新缓存,就是一个惰性计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。

扩展问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。

方案三

上亿流量高并发场景下,缓存会出现这个问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。数据库和缓存再一次变得不一致。

更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个 jvm 内部队列中。

一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。

该解决方案,最大的风险点在于,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。

描述一下你熟悉的关系数据库范式有哪些,有什么作用?

在进行数据库的设计时,所遵循的一些规范,只要按照设计规范进行设计,就能设计出没有数据冗余和数据维护异常的数据库结构。

数据库的设计的规范有很多,通常来说我们在设是数据库时只要达到其中一些规范就可以了,这些规范又称之为数据库的三范式,一共有三条,也存在着其他范式,我们只要做到满足前三个范式的要求,就能设陈出符合我们的数据库了。

  1. 数据库设计的第一范式(最基本),基本上所有数据库的范式都是符合第一范式的,符合第一范式的表具有以下几个特点:数据库表中的所有字段都只具有单一属性,单一属性的列是由基本的数据类型(整型,浮点型,字符型等)所构成的设计出来的表都是简单的二维表。
  2. 数据库设计的第二范式(是在第一范式的基础上设计的),要求一个表中只具有一个业务主键,也就是说符合第二范式的表中不能存在非主键列对只对部分主键的依赖关系。
  3. 数据库设计的第三范式,指每一个非主属性既不部分依赖与也不传递依赖于业务主键,也就是第二范式的基础上消除了非主属性对主键的传递依赖。

什么是事务?其四大特性是什么?

事务是逻辑上的一组操作,要么都执行,要么都不执行。

其四大特性(ACID)为:

  1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用。
  2. 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
  3. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
  4. 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

Redis 相关

Redis 中常用的都用哪些功能?能够实现哪些场景中的操作?

  1. 键值存储:Redis 使用键值对的方式存储数据,可以通过键来获取对应的值。
  2. 缓存:Redis 常用作缓存数据库,可以将热门的数据存储在内存中,提高读取速度。
  3. 发布/订阅:Redis 支持发布/订阅模式,可以用于实现实时消息传递、事件通知等功能。
  4. 排序集合:Redis 的有序集合可以存储带有分数的成员,并按照分数进行排序。
  5. 列表:Redis 的列表可以用来存储有序的元素集合,并支持插入、删除、查询等操作。
  6. 哈希表:Redis 的哈希表可以存储字段和值的映射关系,并支持添加、删除、查询等操作。
  7. 队列:Redis 的队列可以实现先进先出(FIFO)的数据结构,支持入队和出队操作。
  8. 事务:Redis 支持事务操作,可以将一系列操作作为一个原子操作执行。
  9. 持久化:Redis 可以将数据持久化到硬盘上,以便在重启后可以重新加载数据。

Redis 持久化有几种方式?

RDB(Redis Database)持久化:RDB 持久化是将 Redis 的数据以快照的形式保存到硬盘上。它可以在指定的时间间隔内自动创建快照,也可以通过手动触发创建快照。RDB 持久化适合用于备份和恢复数据,以及在指定时间点创建数据快照。

AOF(Append-Only File)持久化:AOF 持久化是将 Redis 的写操作以日志的形式追加到文件中。这个文件可以用来重建数据库,恢复数据。AOF 持久化可以在每个写操作后立即执行,也可以在指定的时间间隔内执行。AOF 持久化适合用于数据的持久性和恢复性要求较高的场景。

另外 Redis 中还可以使用上述两种模式的混合模式进行持久化。

如何使用 Redis 作为一个消息队列使用?

消息队列可以利用 Redis 提供的列表数据结构来实现。

  1. 生产者将信息推入队列:生产者可以使用 Redis 的 LPUSH 命令将信息推入队列的左侧,即队列的头部。
  2. 消费者从队列中获取信息:消费者可以使用 Redis 的 BRPOP 命令从队列的右侧(即队列的尾部)阻塞地获取信息。如果队列为空,BRPOP 将会一直阻塞,直到有新的信息推入队列。

通过以上方式,Redis 可以实现一个简单的信息队列。生产者将信息推入队列的头部,而消费者从队列的尾部获取信息。这样可以确保先进先出(FIFO)的顺序。

如何使用 Redis 实现分布式锁?这种分布式锁有哪些缺陷?

要基于Redis实现分布式锁功能,可以使用 Redis 的 SETNX 命令来实现。SETNX 命令可以将一个键的值设置为指定的字符串,但只有当该键不存在时才会设置成功,否则不会设置。以下是一种常见的实现方式:

  1. 获取锁:当一个进程需要获取锁时,它可以使用 SETNX 命令将一个特定的键设置为一个唯一的标识符,表示该进程已经获取了锁。如果 SETNX 命令返回 1,表示获取锁成功;如果返回 0,表示锁已经被其他进程持有,获取锁失败。
  2. 释放锁:当一个进程完成任务后,它可以使用 DEL 命令来删除该键,释放锁。只有持有锁的进程才能释放锁,其他进程不能删除该键。

基于 Redis 实现的分布式锁可能会存在以下缺陷:

  1. 单点故障:如果 Redis 服务器发生故障,可能会导致整个分布式锁系统不可用。为了解决这个问题,可以使用 Redis 的主从复制或者集群来提高可用性。
  2. 锁失效问题:如果一个进程获取了锁但在执行任务期间发生了故障,导致锁没有被释放,其他进程将无法获取到该锁。为了解决这个问题,可以设置锁的超时时间,并使用一个定时任务来监控锁的状态,如果锁超时未释放,可以由其他进程获取锁。
  3. 死锁问题:如果一个进程在执行任务期间出现异常或发生死锁,导致锁一直没有被释放,其他进程将无法获取到该锁。为了解决这个问题,可以使用锁的自动续期机制,即在获取锁时设置一个定时任务来定期更新锁的过期时间。
  4. 性能问题:由于 Redis 是一个内存数据库,如果锁的粒度过细或锁的数量过多,可能会导致 Redis 的内存消耗增加,从而影响性能。为了解决这个问题,可以通过合理地设计锁的粒度,减少锁的数量,或者使用分布式锁的优化方案,如 Redlock、Redisson 等。

Redis 如何完成内存优化?

  1. 使用合适的数据结构:根据实际需求选择合适的数据结构,如使用Hash数据结构来存储字段和值的映射关系,使用Set数据结构来存储唯一的元素集合等。合理选择数据结构可以减少内存的使用。
  2. 使用压缩:Redis 提供了对字符串数据的压缩功能。可以使用压缩算法对存储的数据进行压缩,减少内存的使用。
  3. 设置合理的过期时间:对于不再使用的数据,可以设置合理的过期时间,使其自动过期并释放内存空间。
  4. 分片和分区:如果数据量较大,可以将数据进行分片或分区,将数据分散存储在多个 Redis 实例中,从而减少单个实例的内存使用。
  5. 使用持久化方式:可以将 Redis 的数据持久化到硬盘上,以便在重启后可以重新加载数据,减少内存的使用。
  6. 使用内存淘汰策略:Redis 提供了多种内存淘汰策略,如 LRU(最近最少使用)、LFU(最不经常使用)等。可以根据实际需求选择合适的策略,使得内存中存储的数据更加高效。
  7. 升级到新版本:Redis 的新版本通常会对内存使用进行优化,可以考虑升级到新版本以获得更好的性能和内存使用效率。

Redis 中有哪些淘汰策略?

  1. LRU(Least Recently Used):最近最少使用策略。根据键的最近使用时间来淘汰数据,即最长时间没有被访问的数据将被淘汰。
  2. LFU(Least Frequently Used):最不经常使用策略。根据键的访问频率来淘汰数据,即访问次数最少的数据将被淘汰。
  3. Random:随机淘汰策略。随机选择要淘汰的数据。
  4. TTL(Time To Live):过期时间淘汰策略。根据键的过期时间来淘汰数据,即已经过期的数据将被淘汰。
  5. Clock(近似LRU):近似 LRU 策略。使用一个时钟算法来判断数据的访问时间,根据访问时间和引用位来判断数据是否被淘汰。

索引标签
面试题
数据库