2025 届秋招常见八股文汇总——数据库

一条 SQL 语句是如何执行的

  1. 连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。
  2. 查询缓存(MySQL 8.0 中已弃用):MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。
  3. 分析器:MySQL 需要识别出 SQL 语句中的字符串分别是什么,代表什么。
  4. 优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
  5. 执行器:MySQL 通过分析器知道了要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。

事务的四大特性

事务的四大特性通常被称为 ACID 特性。

  1. 原子性:确保事务的所有操作要么全部执行成功,要么全部失败回滚,不存在部分成功的情况。
  2. 一致性:事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。
  3. 隔离性:多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。
  4. 持久性:一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。

数据库的事务隔离级别有哪些

  1. 读未提交(Read Uncommitted):
    • 允许一个事务读取另一个事务尚未提交的数据修改。
    • 最低的隔离级别,存在脏读、不可重复读和幻读的问题。
  2. 读已提交(Read Committed):
    • 一个事务只能读取已经提交的数据。其他事务的修改在该事务提交之后才可见。
    • 解决了脏读问题,但仍可能出现不可重复读和幻读。
  3. 可重复读(Repeatable Read):
    • 事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。
    • 解决了不可重复读问题,但仍可能出现幻读。
  4. 串行化(Serializable):
    • 最高的隔离级别,确保事务之间的并发执行效果与串行执行的效果相同,即不会出现脏读、不可重复读和幻读。

MySQL 为什么使用 B+ 树来作索引

B+ 树是一个 B 树的变种,提供了高效的数据检索、插入、删除和范围查询性能。

  • 单点查询:B 树进行单个索引查询时,最快可以在 O(1) 的时间代价内就查到。从平均时间代价来看,会比 B+ 树稍快一些。但是 B 树的查询波动会比较大,因为每个节点既存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,所以数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+ 树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更 “矮胖”,查询底层节点的磁盘 I/O 次数会更少。
  • 插入和删除效率:B+ 树有大量的冗余节点,删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,删除非常快。B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。B 树没有冗余节点,删除节点的时候非常复杂,可能涉及复杂的树的变形。
  • 范围查询:B+ 树所有叶子节点间有一个链表进行连接,而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。存在大量范围检索的场景,适合使用 B+ 树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如 nosql 的 MongoDB。

说一下索引失效的场景

索引失效意味着查询操作不能有效利用索引进行数据检索,从而导致性能下降,下面一些场景会发生索引失效。

  1. 使用 OR 条件:当使用 OR 连接多个条件,并且每个条件用到不同的索引列时,索引可能不会被使用。
  2. 使用非等值查询:当使用 != 或 <> 操作符时,索引可能不会被使用,特别是当非等值条件在 WHERE 子句的开始部分时。
  3. 对列进行类型转换: 如果在查询中对列进行类型转换,例如将字符列转换为数字或日期,索引可能会失效。
  4. 使用 LIKE 语句:以通配符 % 开头的 LIKE 查询会导致索引失效。
  5. 函数或表达式:在列上使用函数或表达式作为查询条件,通常会导致索引失效。
  6. 表连接中的列类型不匹配:如果在连接操作中涉及的两个表的列类型不匹配,索引可能会失效。例如,一个表的列是整数,另一个表的列是字符,连接时可能会导致索引失效。

什么是慢查询?原因有哪些?可以怎么优化?

数据库查询的执行时间超过指定的超时时间时,就被称为慢查询。

原因:

  • 查询语句比较复杂:查询涉及多个表,包含复杂的连接和子查询,可能导致执行时间较长。
  • 查询数据量大:当查询的数据量庞大时,即使查询本身并不复杂,也可能导致较长的执行时间。
  • 缺少索引:如果查询的表没有合适的索引,需要遍历整张表才能找到结果,查询速度较慢。
  • 数据库设计不合理:数据库表设计庞大,查询时可能需要较多时间。
  • 并发冲突:当多个查询同时访问相同的资源时,可能发生并发冲突,导致查询变慢。
  • 硬件资源不足:如果 MySQL 服务器上同时运行了太多的查询,会导致服务器负载过高,从而导致查询变慢

优化:

  • 运行语句,找到慢查询的 sql。
  • 查询区分度最高的字段。
  • explain:显示 mysql 如何使用索引来处理 select 语句以及连接表,可以帮助选择更好的索引、写出更优化的查询语句。
  • order by limit 形式的 sql 语句,让排序的表优先查。
  • 考虑建立索引原则。
  • 考虑分库分表、主从。

undo log、redo log、binlog 有什么用

  • undo log 是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
  • redo log 是物理日志,记录了某个数据页做了什么修改,每当执行一个事务就会产生一条或者多条物理日志。
  • binlog(归档日志)是 Server 层生成的日志,主要用于数据备份和主从复制。

MySQL 有哪些锁

  • 以锁粒度的维度划分
    • 全局锁:锁定数据库中的所有表。加上全局锁之后,整个数据库只能允许读,不允许做任何写操作。
    • 表级锁:每次操作锁住整张表。主要分为三类:
      • 表锁(分为表共享读锁 read lock、表独占写锁 write lock)。
      • 元数据锁(meta data lock,MDL):基于表的元数据加锁,加锁后整张表不允许其他事务操作。这里的元数据可以简单理解为一张表的表结构。
      • 意向锁(分为意向共享锁、意向排他锁):这个是 InnoDB 中为了支持多粒度的锁,为了兼容行锁、表锁而设计的,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。
    • 行级锁:每次操作锁住对应的行数据。主要分为三类:
      • 记录锁 / Record 锁:也就是行锁,一条记录和一行数据是同一个意思。防止其他事务对此行进行 update 和 delete,在 RC、RR 隔离级别下都支持。
      • 间隙锁 / Gap 锁:锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert,产生幻读。在 RR 隔离级别下都支持。
      • 临键锁 / Next-Key 锁:间隙锁的升级版,同时具备记录锁+间隙锁的功能,在 RR 隔离级别下支持。
  • 以互斥性的角度划分
    • 共享锁 / S 锁:不同事务之间不会相互排斥、可以同时获取的锁;
    • 排他锁 / X 锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁;
    • 共享排他锁 / SX 锁:MySQL 5.7 版本中新引入的锁,主要是解决 SMO 带来的问题。
  • 以操作类型的维度划分
    • 读锁:查询数据时使用的锁;
    • 写锁:执行插入、删除、修改、DDL 语句时使用的锁。
  • 以加锁方式的维度划分
    • 显示锁:编写 SQL 语句时,手动指定加锁的粒度;
    • 隐式锁:执行 SQL 语句时,根据隔离级别自动为SQL操作加锁。
  • 以思想的维度划分
    • 乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁;
    • 悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行。

Redis 的数据类型有哪些

Redis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)

  • 字符串 STRING:存储字符串数据,最基本的数据类型。
  • 哈希表 HASH:存储字段和值的映射,用于存储对象。
  • 列表 LIST:存储有序的字符串元素列表。
  • 集合 SET:存储唯一的字符串元素,无序。
  • 有序集合 ZSET:类似于集合,但每个元素都关联一个分数,可以按分数进行排序。

Redis 版本更新,又增加了几种数据类型,

  • BitMap:存储位的数据结构,可以用于处理一些位运算操作。
  • HyperLogLog:用于基数估算的数据结构,用于统计元素的唯一数量。
  • GEO:存储地理位置信息的数据结构。
  • Stream:专门为消息队列设计的数据类型。

Redis 是单线程的还是多线程的,为什么

Redis 在其传统的实现中是单线程的(网络请求模块使用单线程进行处理,其他模块仍用多个线程),这意味着它使用单个线程来处理所有的客户端请求。这样的设计选择有几个关键原因:

  1. 简化模型:单线程模型简化了并发控制,避免了复杂的多线程同步问题。
  2. 性能优化:由于大多数操作是内存中的,单线程避免了线程间切换和锁竞争的开销。
  3. 原子性保证:单线程执行确保了操作的原子性,简化了事务和持久化的实现。
  4. 顺序执行:单线程保证了请求的顺序执行。

Redis 的单线程模型并不意味着它在处理客户端请求时不高效。实际上,由于其操作主要在内存中进行,Redis 能够提供极高的吞吐量和低延迟的响应。

此外,Redis 6.0 引入了多线程的功能,用来处理网络 I/O 这部分,充分利用 CPU 资源,减少网络 I/O 阻塞带来的性能损耗。

Redis 持久化机制有哪些

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点。

介绍一下 Redis 缓存雪崩和缓存穿透,如何解决这些问题

  • 缓存雪崩是指在某个时间点,大量缓存同时失效,导致请求直接访问数据库或其他后端系统,增加了系统负载。对于缓存雪崩,可以通过合理设置缓存的过期时间,分散缓存失效时间点,或者采用永不过期的策略,再结合定期更新缓存。
  • 缓存击穿是指一个缓存中不存在但是数据库中存在的数据,当有大量并发请求查询这个缓存不存在的数据时,导致请求直接访问数据库,增加数据库的负载。典型的场景是当一个缓存中的数据过期或被清理,而此时有大量请求访问这个缓存中不存在的数据,导致大量请求直接访问底层存储系统。对于缓存击穿,可以采用互斥锁(例如分布式锁)或者在查询数据库前先检查缓存是否存在,如果不存在再允许查询数据库,并将查询结果写入缓存。
  • 缓存穿透是指查询一个在缓存和数据库都不存在的数据,这个数据始终无法被缓存,导致每次请求都直接访问数据库,增加数据库的负载。典型的情况是攻击者可能通过构造不存在的 key 大量访问缓存,导致对数据库的频繁查询。对于缓存穿透,可以采用布隆过滤器等手段来过滤掉恶意请求,或者在查询数据库前先进行参数的合法性校验。

如何保证数据库和缓存的一致性

Cache Aside

  • 原理:先从缓存中读取数据,如果没有就再去数据库里面读数据,然后把数据放回缓存中,如果缓存中可以找到数据就直接返回数据;更新数据的时候先把数据持久化到数据库,然后再让缓存失效。
  • 问题:假如有两个操作一个更新一个查询,第一个操作先更新数据库,还没来及删除数据库,查询操作可能拿到的就是旧的数据;更新操作马上让缓存失效了,所以后续的查询可以保证数据的一致性;还有的问题就是有一个是读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,也会造成脏数据。
  • 可行性:出现上述问题的概率其实非常低,需要同时达成读缓存时缓存失效并且有并发写的操作。数据库读写要比缓存慢得多,所以读操作在写操作之前进入数据库,并且在写操作之后更新,概率比较低。

Read/Write Through

  • 原理:Read/Write Through 原理是把更新数据库(Repository)的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
  • Read Through:就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside 策略是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对调用方是透明的。
  • Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己更新数据库(这是一个同步操作)。

Write Behind

  • 原理:在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。
  • 第二步失效问题:这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注