MySQL + Java 常规八股(2 w字 + 不定期更新)
Java 集合
基础八股
HashMap 你了解多少呢?(典中典八股)
它是一个基于哈希表的数据结构,用于存储键值对(key-value),核心呢是将键的哈希值映射到数组索引位置,然后通过 数组+链表 (在 java 8 以及之后是 数组 + 链表 + 红黑树) 来处理哈希冲突。我们可以围绕着:定位,冲突,扩容三个方面来进行展开。
首先是定位:HashMap 使用键的 hashCode() 方法计算哈希值,并通过 indexFor 方法(JDK 1.7 及之后版本移除了这个方法,直接使用 (n-1)&hash) 确定元素在数组中的存储位置。这个哈希值是经过一定的扰动处理的,可以防止哈希值分布不均,从而减少冲突。
然后是扩容:为了平衡查询速度和空间开销,HashMap 的默认初始容量为 16,负载因子为 0.75,也就是说,当存储的元素数量超过 16 * 0.75 = 12 个时,HashMap 会触发扩容操作,容量 x2 并重新分配元素位置,这种扩容较为影响性能。
最后一个是冲突:从 Java8 开始,为了优化当多个元素映射到同一个哈希桶时的查找性能,当链表的长度大于等于 8 且数组大小大于等于 64 的时候,链表会转变为红黑树(是一种自平衡二叉搜索树,查找复杂度降到O(log n)),如果树中元素的数量低于 6,红黑树会退化成链表,以减少不必要的树操作开销。
然后里面还有一个点就是,在多线程环境下,头插法可能导致链表形成环,特别是在并发扩容时(rehashing),当多个线程同时执行 put() 操作时,如果线程 A 正在进行头插,线程 B 也在同一时刻操作链表,可能导致链表结构出现环路,从而引发死循环,所以在 JDK8 后,头插法改为了尾插法。
HashMap 扩容机制
当扩容发生时,HashMap 并不是简单地将元素复制到新数组中。而是通过每个元素的哈希值和新的数组容量重新计算索引位置,因此元素的存储位置会发生变化,并且 Java8 以后的扩容不需要每个节点重新取模算下标,因为元素的新位置只与高位有关,通过和老数组上的&计算是否为0 就能判断新下标的位置,因此链表中的元素可能只需要部分移动,这一优化减少了扩容时候的计算开销。
HashMap 在 Java 扩容时采用 2 的 n 次方倍
主要是为了提高哈希值的分布均匀性和哈希计算的效率,HashMap 通过 (n-1)&hash 来计算元素存储的索引位置,这种位运算只有在数组容量是 2 的 n 次方时才能确保索引均匀分布。位运算的效率高于取模运算(hash % n),提高了索引计算的速度。
正常情况下,如果基于哈希码来计算数组下标,我们想到的都是 % (取余)计算。例如数组长度为 5,那么哈希码 %5 得到的值就是对应的数组下标。但相比于位运算而言,效率比较低,所以推荐用位运算,而要满足
i = (n - 1) & hash这个公式,n 的大小就必须是 2 的 n 次幂。即:当 b 等于 2 的 n 次幂时,a % b操作等于a & ( b - 1)
HashMap 的默认负载因子是 0.75 是时间复杂度和空间复杂度之间取得的一个合理的平衡
在某些特定场景下,可以根据业务需求调整 HashMap 的负载因子。高并发读取场景:可以降低负载因子(如 0.5),以减少哈希冲突,提高读取性能。内存受限场景:可以提高负载因子(如 0.85 或更高),以减少扩容次数和内存消耗,但可能会降低写入和查询的性能。
负载因子是
HashMap中的一个参数,用来衡量HashMap的满载程度,公式为负载因子 = 实际存储的元素数量 / 容量
ArrayList 的扩容机制你了解过吗?
ArrayList 中的元素数量超过当前容量的时候,会触发扩容机制。默认的容量是 10,发生扩容时,会创建一个新数组,新数组的容量为原来的数组容量的 1.5 倍,然后通过 Arrays.copyOf() 将老数组的元素复制到新数组中,ArrayList 没有负载因子的概念。
初始容量:1.7 和 1.8 的区别默认容量都是 10.
1.7:是在调用构造函数的时候,就开辟了空间。 1.8:是在调用 add 方法的时候,开辟空间。节约内存,只有真正使用的时候,才创建数组。
介绍一下 Java 的 CAS(Compare-And-Swap) 操作
它是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。优点是:原子性和无锁并发,缺点呢就是:ABA问题、自旋开销、和单变量限制。对于 ABA 问题的解决方案呢,用版本号或者时间戳解决。做任何一个值的修改,都加一个版本号,在进行 CAS 操作的时候,除了比较内存中的实际值与期望值外,还比较版本号。版本号相同就修改,否则失败重试,java 里面的 AtomicStampedReference,就使用版本号解决了 ABA 问题,里面是维护了一个 int 类型的 stamp。。在 Java 中,可以使用 并发包中的 Atomic 类(如 AtomicInteger、AtomicLong 等),这些类封装了 CAS 操作,提供了线程安全的原子操作。还有一个比较关键的问题就是 CAS 的总线风暴 了:lock 指令会将缓存通过总线刷到主存,同时通过总线通知其他的 CPU 核更新自己的缓存,如果 CAS 操作频繁,那么总线上的通信流量就会过大,从而导致总线风暴。

Java 中的 ConcurrentHashMap 了解过吗?
可以从它的发展历程(JDK 1.7和1.8的区别)、实现原理(数据结构、锁机制、CAS、synchronized)、以及为什么它能支持高并发等方面进行回答。
首先是它的发展历程:JDk 1.7 采用的是分段锁,即每个 Segment 是独立的,可以并发访问不同的 Segment,默认是 16 个 Segment,所以最多有 16 个线程可以并发执行。但是 JDK 1.8 移除了 Segment ,锁的粒度变得更加细化,锁只在链表或红黑树的节点级别上进行,通过 CAS 进行插入操作,只有在更新链表或红黑树时才使用 synchronized,并且只锁住链表或数的头节点,进一步减少了锁的竞争,并发度大大增加,并且 JDK 1.7 ConcurrentHashMap 只使用了 数组+链表 的结构,而 JDK 1.8 和 HashMap 一样引入了红黑树。并且 JDK1.8中的 size()方法采用分段计数再求和的方式,在并发环境下可能不是精确值。
然后它们两个版本之间的扩容机制也有差异,JDK1.7 中的扩容是当 Segment 内的 HashMap 达到扩容阈值时,单独为该 Segment 进行扩容,而不会影响其他的 Segment,并且扩容过程也是:每个 Segment 维护自己的负载因子,当 Segment 中的元素数量超过阈值时,该 Segment 的 HashMap 会扩容,整体的 ConcurrentHashMap 并不是一次性全部扩容。
JDK 1.8 中的扩容:是全局扩容,因为这个版本的 ConcurrentHashMap 取消了 Segment,变成了一个全局的数组,因此当其中任意位置的元素超过阈值时,整个 ConcurrentHashMap 的数组都会被扩容,并且通过 CAS操作,能确保线程安全,所以可以采用多线程同时帮助完成渐进式扩容。
还有一点就是 Java 的 ConcurrentHashMap 不支持 key 或 value 为 null,因为可以避免歧义 + 简化实现。多线程环境下,get(key) 方法如果返回 null,不知道这个 null 是代表 key 不存在或者是值本来就是 null,如果允许 null,代码里面就需要频繁的去判断 null 到底是代表 key 不存在或者是值本来就是 null。
Java 中的 HashMap 和 Hashtable 有什么异同呢
Hashtable 作为遗留类,并不推荐使用。HashMap 和 Hashtable 都实现了 Map 接口,用于存储键值对,但它们有几个关键区别。
首先,线程安全性是根本区别。Hashtable 是线程安全的,其方法都使用了synchronized同步,但这导致在多线程下性能较差。而 HashMap 是非线程安全的,性能更高。其次,对 Null 的支持不同。HashMap允许一个 null 键和多个 null 值,而 Hashtable 不允许,会抛出空指针异常。再者,继承体系和性能细节也不同。HashMap 继承自 AbstractMap,而 Hashtable 继承自陈旧的Dictionary 类。在计算索引时,HashMap 使用高效的位运算,而 Hashtable 使用取模运算。在现代 Java 开发中,单线程用 HashMap,多线程高并发场景强烈推荐使用 ConcurrentHashMap 来替代 Hashtable,因为 ConcurrentHashMap 实现了更细粒度的锁机制,性能远超 Hashtable。
ConcurrentHashMap 和 Hashtable 的区别是什么
主要是在线程安全性的实现方式上有所不同,从而导致了它们在性能上的差距。Hashtable 使用的是单一的锁机制(全表锁),及对整个哈希表进行同步,所有的操作都必须通过同一个 synchronized ,无法做到并发访问。在 Java8 中,concurrentHashMap 采用了 CAS + synchronized 的方式进行线程安全控制。CAS 用于无锁的写入操作,如果某个 Node 节点为空,则通过 CAS 将数据插入节点,如果不为空,则会退化到 synchronized,使用 synchronized 锁定冲突的头结点,这种锁的粒度更细,仅仅锁住特定的冲突结点,因此并发性能较好。
LinkedList 与 ArrayList 的区别是什么,它们是线程安全的还是不安全的?
ArrayList 和 LinkedList 最根本的区别在于底层的数据结构,动态数组让 ArrayList 随机访问极快但插入删除慢,而双向链表让 LinkedList 插入删除快但随机访问慢。它们都是线程不安全的,在多线程环境下必须通过外部同步或使用线程安全容器来保证数据一致性。

扫盲八股
使用 HashMap 时,可以提升性能的小技巧
一句话概括吧:合理分配 capacity,适当调整负载因子,确保 hashcode 均匀分布。
需要保留元素的插入顺序,则可以使用
LinkedHashMap替换 HashMap, 它相对于HashMap维护了一个链表,记录元素的插入顺序。需要保留有序键值对的,使用
TreeMap如果是线程安全场景,则可以使用
ConcurrentHashMap
Java 中有哪些集合类,请简单介绍
集合类的话主要是分为两大类的:Collection 接口和 Map 接口,前者是存储对象的集合类,后者存储的是键值对(key - value)


List 接口:ArrayList: 基于动态数组,查询速度快,插入、删除慢。LinkedList: 基于双向链表,插入,删除快,查询速度慢,Vector:线程安全的动态数组,类似于 ArrayList,但开销比较大。
Set 接口:HashSet: 基于哈希表,元素无序,不允许重复。LinkedHashSet:基于链表和哈希表,维护插入顺序,不允许重复。TreeSet:基于红黑树,元素有序,不允许重复。
Queue 接口:PriorityQueue:基于优先级堆,元素按照自然顺序或指定比较器排序。LinkedList:可以作为队列使用,支持 FIFO(先进先出) 操作。
Map 接口:HashMap 基于哈希表,键值对无序,不允许键重复。LinkedHashMap:基于链表和哈希表,维护插入顺序,不允许键重复。TreeMap:基于红黑树,键值对有序,不允许键重复。Hashtable:线程安全的哈希表,不允许键或值为 null。ConcurrentHashMap:线程安全的哈希表,适合高并发环境,不允许键或值为 null。
Java 的 CopyOnWriteArrayList 和 Collections.synchronizedList 有什么异同点呢
CopyOnWriteArrayList: 是一个线程安全的 List 实现,特性就是写时复制,每次对 List 的修改操作都会复制创建一个新的底层数组。读操作不需要加锁,写操作需要加锁。
Collections.synchronizedList:是一个包装方法,可以将任何 List 转换为线程安全的版本,它会对每个访问方法进行同步(加 synchronized 锁),从而保证线程安全。

CopyOnWriteArrayList以写时复制的无锁读换取高并发读性能,适用于读多写极少的场景(如事件分发);Collections.synchronizedList通过方法级同步锁实现简单通用,但锁竞争会限制扩展性,且迭代需手动同步。两者本质是读性能 vs 写开销和弱一致性 vs 强一致性的权衡。补充建议:在写频繁场景,可考虑
ConcurrentLinkedQueue(无界)或LinkedBlockingQueue(有界),它们提供更细粒度的并发控制。
数组和链表在 Java 中的区别是什么
可以从四个方面来作答:存储结构,访问速度,操作方面,和使用场景方面。还可以延展到空间局部性:如果一个存储器的位置被引用,那么将来它附近的位置也会被引用。根据这个原理,就会有预读功能,像 CPU 缓存就会读连续的内存,这样一来如果你本就要遍历数组的,那么你后面的数据就已经被上一次读取前面数据的时候,一块被加载了,这样就是 CPU "亲和性" 低。
什么是 ConcurrentModificationException 错误
Fail-fast 是一种程序设计理念,指的是在程序设计执行过程中,如果遇到错误或异常状态,系统会立即停止或抛出异常,而不是继续执行下去。
通过这种机制,程序可以在问题发生的初期阶段迅速暴露出潜在的错误,避免在后续的操作中引发更严重的问题或导致数据不一致。
什么是 Java 的 SPI(Service Provider Interface) 机制
简单来说就是:调用方调用接口实现服务是 API,实现方通过接口实现提供服务时 SPI。

Java 中的阻塞队列了解过吗?
了解过一点,但是实际开发上没有用到过。就是 BlockingQueue ?,它继承自 Queue ,一般都用来作为生产者-消费者 模型中。


MySQL
基础八股
在 MySQL 中你了解哪些索引类型呢?(暖场八股)
可以从三个角度来回答:首先是 数据结构的角度 来看:
有 B+ 树索引:通过树形结构存储数据,适用于范围查询(如 between) 和精确查询 (如 =),支持有序数据的快速查找、排序和聚合操作,是 MySQL 默认的索引类型,常用于 InnoDB 和 MyISAM 引擎。
哈希索引:基于哈希表的结构,适用于等值查询(如 =),查询速度非常快,但不支持范围查询 (如 >、<),哈希索引不存储数据的数据,常用于 Memory 引擎。
倒排索引(即全文索引 Full-Text):用于全文搜索,将全文分词,通过存储词与文档的映射,支持模糊匹配和关键字搜索。特别适合用于大文本字段,如 text 类型的列,用于查找包含特定词语的记录。
R-树索引(多维空间树):没听说过。
从索引性质 的角度来看,可以分为,普通索引(二级索引、辅助索引),主键索引,联合索引,唯一索引,全文索引,空间索引。
从常见的基于 InnoDB B+树索引角度来看:聚簇索引 (Clustered Index) 和 非聚簇索引 (Non-clustered Index)。
聚簇索引:InnoDB 中主键索引就是聚簇索引,它基于主键排序存储,之所以叫聚簇索引是因为索引的叶子节点存储完整数据行数据
非聚簇索引:指的是 InnoDB 中非主键索引的索引,这个索引的叶子节点仅保持索引字段和主键值 如果要查询完整的数据行中的数据,需要再从聚簇索引即主键索引中通过主键查询,一个表可以有多个非聚簇索引。
# 主键索引:表中的每一行数据都有唯一的主键。每个表只能有一个主键索引,且主键值不能为 `null`,InnoDB 中主键索引是聚簇索引结构实现的 create table users (id int not null auto_increment,primary key(id) ); # 唯一索引:保证索引列中的值是唯一的,可以有效防止重复数据的插入。唯一索引允许 null 值,但一个列中可以有多个 null create unique index idx_username on users(username); # 普通索引:一般指非主键索引且非唯一索引 create index idx_username on users(username); # 全文索引:用于全文搜索,支持对长文本字段(如 `text` 类型) 进行关键字查找 create fulltext index idex_content on articles(content); # 联合索引:多个列组成的索引,适用于多列的查询条件,能够提高包含多个条件的查询的性能。联合索引中的列是按照指定顺序排列的 # 目录页里面放的也是最小索引以及最小索引所在的页号 # 数据页里面会存储两个索引的值以及主键 create index idex_username_email on users(username, email); # 哈希索引 create index idx_username_email on users(username, email) using hash; # 空间索引(很少见的索引类型) create spatial index idx_location on places(location);
MySQL 中使用索引一定有效吗?如何排查索引效果
索引失效的场景有很多: 包括使用了联合索引却不符合最左前缀、在索引中使用了运算??,like 的随意使用 %like%,or 的随意使用(select * from user where name = 'cong' or age = 18 其中 name 有索引,age 没有索引),和不同的参数也会导致索引失效,因为最终是否上索引是根据 MySQL 成本计算决定的,评估 CPU 和 I/O 成本最终选择用辅助索引还是全表扫描。
排除索引效果的话,一般情况下使用 explain 命令即可,通过在查询前加上 explain,它可以查看 MySQL 选择的执行计划,了解是否使用了索引、使用了哪个索引、估算的行数等信息。主要观察 Explain 结果的以下几个方面:
首先是 type(访问类型),这个属性显示了查询使用的访问方法,例如 ALL、index、range 等,当查询使用索引时,这个属性通常会显示为 index 或 range,表示查询使用了索引访问。如果这个值是 all,则表示查询执行了全表扫描,没有使用索引。然后是 key(使用的索引),这个属性显示了查询使用的索引,如果查询使用了索引,则会显示索引的名称。如果这个值是 null 则表示查询没有使用索引。最后是 rows(扫描的行数),这个属性显示了查询扫描的行数,需要评估下扫描量。
到底什么才叫用了索引呢?
利用主键索引快速查找
利用二级索引快速查找
全扫描二级索引进行查找
所以在索引能覆盖返回值的时候,一般都会选择二级索引来查找。
详细描述一条 SQL 语句在 MySQL 中的执行过程(典中典八股)
首先会经过连接器进行权限校验,如果权限校验账号密码不通过,则报错。然后会查询缓存,如果缓存中有,则直接返回,缓存是以 sql 语句作为 key,value 就是查询结果。8.0 这个功能被砍了,因为命中率极低。然后分析器进行词法分析和语法分析,判断语句是什么类型,比如 select 是查询语句;看有没有语法错误,有的话就报错。然后到达优化器,对 sql 语句进行优化,例如选择索引,多表连接查询,调整表的连接顺序等,最后到达执行器,调用存储引擎层提供的接口执行 sql 语句。

MySQL 的查询优化器如何选择执行计划
MySQL 会生成多个执行计划,选择一个成本最低的执行计划,它的成本包括:IO 成本 + cpu 成本。所谓 IO 成本就是将数据从磁盘加载到内存的成本。加载的时候是以页为单位,一页的成本记为 1,一页大小是 16 kb,所以 IO 成本是 数据大小 / 16 kb 。然后是 cpu 成本:数据被加载到内存之后,需要被比较,排序等,这些操作会消耗 cpu。一行数据的成本记为 0.2,所以 CPU 成本是 扫描行数 * 0.2 ,总成本就是 数据大小 / 16 kb + 扫描行数 * 0.2。
然后呢查询优化器还会自己进行优化,例如:在预处理阶段计算常量表达式,以简化查询,或者将子查询转换为连接查询,或者将子查询展开 (unroll),或者将子查询转换为连接查询(子查询优化)。还会进行表连接优化,简单来说就是,优化器会评估不同的表连接顺序,并选择成本最低的连接顺序。简单来说优化器会选择行数更少的表优先进行连接,以减少中间结果集的大小。例如:

MySQL 中如何进行 SQL 调优(典中典)
通过 set global slow_query_log = on 即可以开启慢查询日志,并可以设置慢 SQL 的阈值,然后再使用 explain 语句对 sql 进行优化嘛。一般可以从以下几个角度来进行优化:比如说加个缓存,用来提升查询的效率。通过业务来优化,比如少展示一些无关的字段,减少多表查询的情况,将列表查询替换成分页分批查询等。然后优化一下索引的设计,尽量让查询可以走索引,避免回表的发生,减少一次查询和随机 I/O,并且注意一下索引失效的情况。
MySQL 索引的最左前缀匹配原则是什么
最左匹配原则就是:当使用联合索引的时候,查询条件必须从索引的左侧开始匹配,查询条件必须包含联合索引的第一个列,然后是第二个列...... 这和它的底层原理有关系,因为 联合索引在 B+ 树中的排列方式遵循 "从左到右" 的顺序。然后在 Mysql8.0 后呢,官方又对其做了一个优化:将缺失的左边的索引值查出来,如果数据量很少,则拼凑上左边的索引,使得 sql 符合最左匹配原则,就能走索引了,并且除了这个关键点以外,还有很多的局限性,所以导致这个优化的场景很有限。
MySQL 的索引下推了解过吗?应用在联合索引上的(数据的有序性)
索引下推就是针对联合索引的,它是在存储引擎层根据索引条件先进行过滤,然后再进行回表,来减少回表次数。举个例子吧:a,b 两个字段组成了联合索引, where a = 1 and b like %xx,先根据 a = 1 查询出来数据,然后根据 b like %xx 在存储引擎层先过滤,最后再进行回表。再讲讲失效的情况吧:使用了聚簇索引,使用了函数或者表达式,使用了子查询(可能会导致索引下推失效),还得注意一下 MySQL 的版本,它是 MySQL 5.6 及之后的版本支持, InnoDB 和 MyISAM 这两个存储引擎都生效。
MySQL 的存储引擎你了解多少呢?说三个你较为了解的即可
MySQL 整体上分为 Server 层和存储引擎层,Server 层负责的部分是连接器、查询缓存、解析器、优化器、执行器,存储引擎层数据的读取和存储。存储引擎就像是一个插件,MySQL 可以根据业务场景使用不同的存储引擎,目前 MySQL 支持 InnoDB、MyISAM、Memory、CSV 等多个存储引擎。
然后 MySQL 比较常见的存储引擎主要有三个:InnoDB、MyISAM、Memory。首先是 InnoDB 引擎,它是 MySQL 默认的存储引擎,支持事务和行级锁,具有事务提交、回滚和奔溃恢复功能。其次是 MyISAM 引擎,虽然这个我没有用过,但是我在学习的时候有了解过,它是不支持事务和行级锁的**,而且由于只支持表锁,锁的粒度比较大,更新性能比较差,我认为它比较适合读多写少的情况。**最后是 Memory 引擎,这个我了解的不多,大概知道它是将数据存储在内存中的,所以数据的读写还是比较快的,但是数据不具备持久性,我觉得适用于临时存储数据的场景,并且现在存储在内存中,已经有了更加常用的 Redis ,所以这个使用还是比较少的。
请详细描述 MySQL 的 B+ 树中查询数据的全过程(典中典)
从根节点开始,比较数据键值与索引中存在的键值,确定数据落在哪个区间,从上往下找,最终找到要查询的记录所在的叶子节点,叶子节点里面有 page directory,用二分定位到数据所在的组(槽号),然后在分组内遍历查找。页目录的结构是:将数据记录进行了分组,每个槽相当于代表一组,它指向了每一组的最后那个元素(因为有序,所以指向的是主键最大的)。页目录查找步骤:查找的时候,先根据二分找槽位,也就是找记录所在的分组,槽位记录了这个分组最后一个元素,知道了最后一个元素的地址,只需要知道这个分组的第一个元素,然后从第一个元素遍历到最后一个元素,就能找到查找的元素。那怎么知道第一个元素呢?从上一个槽位找,上一个槽位是指向上一个分组的最后一个元素的,这个元素的下一个元素不就是第一个元素?元素之间又是单链表连接的,就很容易找到。槽位是相邻的空间,每个槽位是 2 个字节,所以也很容易根据当前槽位找到上一个槽位。

简单讲讲 MySQL 中建立索引时需要注意哪些事项?
第一呢:不能够盲目建立索引,因为 索引并不是越多越好,索引会占用空间,并且每次修改的时候可能都需要维护索引的数据,消耗资源。第二呢:当数据库的修改频率远大于查询频率时,应该好好考虑是否需要建立索引,因为建立索引会减慢修改的效率,如果很少的查询较多的修改,则得不偿失。第三呢:对经常在 order by、group by、distinct 后面的字段建立索引,这些操作通常需要对结果进行排序、分组或去重,而索引可以帮助加快这些操作的数据。第四呢:对于需要频繁作为条件查询的字段应该建立索引,在 where 关键字后经常查询的字段,建立索引能提高查询的效率,如果有多个条件经常一起查询,则可以考虑联合索引,减少索引数量 。第五呢:对于一些长字段不应该建立索引:比如 text、longtext 这种类型字段不应该建立索引。因为占据的内存大,扫描的时候大量加载至内存中还耗时,使得提升的性能可能不明显,甚至可能还会降低整体的性能,因为别的缓存可能因它被剔除内存,下次查询还需要从磁盘中获取。第六呢:对于字段的值有大量重复的不要建立索引,比如说:性别字段,在这种重复比例很大的数据行中,建立索引也不能提高检索速度。但也有例外:定时任务的场景,大部分场景都是成功,少部分任务状态是失败的,这时候通过失败状态去查询任务,实际上能过滤大部分成功的任务,效率还是可以的。
为什么 MySQL 选择使用 B+ 树作为索引结构?
首先 B+ 树特别适合范围查询,因为叶子节点通过链表链接,从根节点定位到叶子节点查找到范围的起点之后,只需要顺序扫描链表即可遍历后续的数据,非常高效。然后呢:B+ 树不像红黑树,数据越多树的高度增长就越快,因为它是多叉树,非叶子节点仅保存主键或索引值和页面指针,使得每一页能容纳更多的记录,因此内存中就能存放更多索引,容易命中缓存,使得查询磁盘的 I/O 次数减少。 最后呢:B+ 树是一种自平衡树,每个叶子节点到根节点的路径长度相同,B+ 树在插入和删除节点时会进行分裂和合并操作,以保持树的平衡,但它又会有一定的冗余节点,使得删除的时候树结构的变化下,更高效。并且查找、插入、删除等操作的时间复杂度为O(log n),能够保证在大数据量的情况下也能有较快的响应时间。

数据库的三大范式有了解过吗?
第一范式(1NF) 就是规范化:它的目的是确保数据表的每一列都是单一值,消除重复的列,从而保证数据的原子性。
第二范式(2NF) 就是消除部分依赖:目的就是消除非主键字段对主键部分依赖,从而避免数据冗余和更新异常。
第三范式(3NF)就是消除传递依赖:目的是消除非主键字段对主键的传递依赖,从而进一步减少数据冗余和更新异常。

MySQL 中的 Log Buffer 是什么呢?它有什么作用
log buffer 是一块缓冲区,是用来暂时存放 redo log的,批量的将 redo log 从内存写入磁盘,以减少 io, 提高性能。 write ahead logging: 先写日志,然后再写磁盘。
redo log 刷盘策略分为两大类:
第一是,事务提交时进行刷盘:innodb_flush_log_at_trx_commit 参数控制提交事务的时候,如何将 log buffer 中的数据刷到磁盘中。值为 0 的时候,提交事务的时候不会进行刷盘,需要等待后台线程每隔 1s,将 log buffer 刷新到系统缓冲,再调用 fync 同步到磁盘中,性能最好,但是可能会丢失 1s 数据。当值为 1 并且提交事务的时候,将 log buffer 刷到系统缓存,可以不调用系统缓冲由操作系统来决定是什么时候将数据同步到磁盘中,或者自己调用 fync 同步到磁盘(性能最差,但是能保证数据不丢失)。
第二是:后台线程定期进行刷盘
InnoDB 有一个后台线程,每隔一秒就会将 redo log buffer 中的数据刷到系统缓存中,并调用 fync, 同步磁盘。不管 innodb_flush_log_at_trx_commit 设置成什么值,都会有这个后台线程在默默的刷盘操作。checkpoint 机制:redo log 采用的是日志循环存储的方式,会导致后写入额 redo 日志覆盖掉前边写的 redo 日志,所以提出了 checkpoint 机制,如果 write pos 追上 checkpoint, 则表示日志文件组满了,这个时候就不能再写入新的 redo log 记录,MySQL 得停下来,清空其中得一些记录,把 checkpoint 推进一下。

MySQL 的 Change Buffer 是什么,它又有什么作用呢?
它是 MySQL InnoDB 存储引擎中的一个机制,用于暂存对二级索引的插入与更新操作的变更,而不立即执行这些操作,随后,当 InnoDB 进行合适的条件时,会将这些变更写入到二级索引中。
当前二级索引不在buffer pool 中时,那么 innodb 会把更新操作缓存到 change buffer 中,当下次访问到这条数据后,会把索引页加载到 buffer pool中,并且应用上 change buffer 里面的变更,这样就确保了数据的一致性。
从 MySQL 获取数据,是从磁盘读取的吗(buffer pool)
并不总是从磁盘读取,mysql 8.0 之前有查询缓存,会先去查询缓存里面找,如果查询缓存里面有的话,直接返回。8.0 的时候移除了查询缓存,因为命中率低,它是以 sql 作为 key,sql 语句要相同,而且表不能发生任何变化才能命中。还有个 buffer pool,里面存储了一个一个的数据页,mysql 会从 buffer pool 里面找,如果找到的话就会返回。buffer pool 是什么呢?一块内存空间,在访问某条数据的时候,mysql 会以页为单位将包含这条数据的数据页,加载到 buffer pool 中,每页大小是 16 kb, 符合空间局部性原理,以后对该数据的修改以及访问都在 buffer pool 上。buffer pool 内存淘汰:变型的 LRU,buffer pool 分为年轻代和老年代, 默认的比例是 5:3,当将新的页加入到 buffer pool 中的时候,会将这个数据也放在老年代的头部,如果 1s 内该数据页再被访问的话,不会被放入新生代,只有 1s 之后被再次访问才会被加入到新生代。
为什么这个设计?为了解决预读失效以及大批量热点数据被淘汰的问题。innoDB 在读取某个数据页的时候,认为后面的数据也会被使用,就会连续读取多个页面,但是这只是一种判断,这些页面也有可能不被使用,如果直接放到新生代,会淘汰新生代的热点数据,1s 机制:相当于有个时间窗口,过来这个窗口之后,被再次访问的数据才会被当成是热点数据,才放入新生代,避免错误的淘汰大量的热点数据。
你们生产环境的 MySQL 中使用了什么事务隔离级别?为什么选择那个呢
MySQL 中默认事务隔离级别是可重复读,有些项目为了提高并发,以及降低死锁概率,改成了读已提交,因为可重复读有间隙锁和临建锁,锁定的范围更大,并发性会差一点。
半一致性读:执行 update 语句的时候,会扫描表中的行,当扫描到当前行,如果发现当前行已经被锁定了,那么就会执行半一致读,得到当前行数据的最新版本(即使有事务更新了当前行数据,但是没有提交事务,也能读到最新版本的数据),判断是否和 where 语句的条件匹配,如果匹配,则当前的 update 也需要对当前加锁,因为已经被锁定了,所以需要等待;如果不匹配,则当前的 update 不需要对当前行加锁。
半一致读+读已提交隔离级别,能够进一步提高 sql 并发。
MySQL 中有哪些锁类型了解过吗,发生了死锁又该如何解决呢
我主要了解的锁类型有:行级锁,表记锁,共享锁,排它锁,间隙锁,临建锁。
S 锁,也被称为是共享锁,事务在读取记录的时候获取 S 锁,它允许多个事务同时获取 S 锁且相互之间并不会冲突
X 锁,也被称为独占锁(排它锁),事务在修改记录的时候获取 X 锁,且只允许一个事务获取 x 锁,其他事务需要阻塞等待。
假设业务上真的用到了表锁,那么表锁和行锁之间肯定会冲突,当 InnoDB 加表锁的时候,如何判断表里面是否已经有行锁了?所以就引入了一个意向锁的东西。
LS 共享意向锁
LX 独享意向锁
间隙锁:唯一的目的就是防止其他事务插入数据到间隙中
解决方案
手动 kill 掉被阻塞的事务以及其线程 ID,或者 MySQL 自带死锁检测机制(innodb_deadlock_detect),当检测到死锁时,就会自动回滚事务中持有最少资源的那个并且也有等待超时参数(innodb_lock_wait_timeout),当获取锁的等待时间超过阈值的时候,就释放锁进行回滚。
show engine innodb status; select * from information_schema.innode_locks select * from information_schema.innode_waits kill <thread_id>
将大事务拆分成多个小事务快速释放锁,可降低死锁产生的概率和避免冲突
适当的调整锁的顺序。
MySQL 中的 MVCC 是什么
它就是一种并发控制机制,允许多个事务同时读取和写入数据库并且无需互相等待,从而提高数据库的并发性能。
在 MVCC 中,数据库为每个事务创建一个数据快照,每当数据被修改时,MySQL 不会立即覆盖原有数据,而是生成新版本的记录,每个记录都保留了对应的版本号或时间戳。多版本之间串联起来形成了一条版本链,这样不同时刻启动的事务可以无锁地获得不同版本的数据,此时读写操作不会阻塞。

Undo Log
实际上 MVCC 所谓的多版本不是真的存储了多个版本的数据,只是借助 undolog 记录每次写操作的反向操作,所以索引上对应的记录只会有一个版本,即最新版本,只不过可以根据 undolog 中的记录反向操作得到数据的历史版本,所以看起来是多个版本。

没错,之前 insert 产生的 undolog 没了,insert 的事务提交了之后对应的 undolog 就回收了,因为不可能有别的事务会访问比这还要早的版本了。
update 产生的 undolog 不会马上删除,因为可能有别的事务需要访问之前的版本,所以不能删。
readView
creator_trx_id 当前事务 ID
m_ids,生成 readView 时还活跃的事务 ID 集合,也就是已经启动但是还未提交的事务 ID 列表
min_trx_id,当前活跃 ID 之中之后的最小值
max_trx_id, 生成 readView 时 InnoDB 将分配下一个事务的 ID 的值(事务 DI 是递增分配的,越往后面申请的事务 ID 越大)

MySQL 是怎么来实现事务的呢
它主要是通过:锁、redo log、undo log、mvcc 来进行实现事务的。
MySQL 利用锁(行锁、间隙锁等等) 机制,使用数据并发修改的控制,满足事务的隔离性。Redo Log(重做日志),它会记录事务对数据库的所有修改,当 MySQL 发生宕机或崩溃时,通过重放 redolog 就可以恢复数据,用来满足事务的持久性。Undo Log(回滚日志):它会记录事务的反向操作,简单地说就是保存数据的历史版本,用于事务的回滚,使得事务执行失败之后可以恢复之前的样子,实现原子性。 MVCC(多版本控制),满足了非锁定读的需求,提高了并发度,实现了读已提交和可重复读两种隔离级别,实现了事务的隔离性。
扫盲八股
MySQL 的覆盖索引了解过吗?
覆盖索引是指二级索引包含了要查询的所有数据,不需要回表,就能够得到所有要查询的数据,因为不需要加载所有的数据到内存中,所以也减少了随机 IO,提高了查询速度,节约了内存。
关于 MySQL 的 explain 语句进行 sql 分析的属性详解?
首先是 type 类型很重要:system 表示查询的表只有一行(系统表)。这是一个特殊的情况,并不常见,all(性能最差):表示 MySQL 需要扫描表中的所有行,即全表扫描,通常出现在没有索引(索引失效)的查询条件中。range:表示 MySQL 会扫描表的一部分,而不是全部行。范围扫描通常出现在使用索引的范围查询中(如 BETWEEN、>、<、>=、<=)。index:表示 MySQL 扫描索引中的所有行,即使索引列的值覆盖索引,也需要整个索引。ref:MySQL 使用非唯一索引扫描来查找行,查询条件使用的索引是非唯一的(如普通索引)。

MySQL 的回表了解过吗?
回表是指在使用二级索引(非聚簇索引) 作为条件进行查询时,由于二级索引中只存储了索引字段的值和对应的主键值,无法得到其他数据,如果要查询数据行中的其他数据,需要根据主键去聚簇索引查找实际的数据行,这个过程被称为回表。
回表其实不仅仅只是多查一次,还会带来随机 I/O,这是因为通过 id 去主键索引查询的时候,id 肯定是不连续的。还有一个操作系统常识就是:顺序 I/O 查询快,而随机 I/O 慢,所以回表的效率比较低。
Mysql InnoDB 引擎中的的聚簇索引和非聚簇索引有什么区别呢?
这就是数据库中的两种索引类型,它们在组织和存储数据时有不同的方式。聚簇索引的话,它的非叶子节点上存储的是索引字段的值,叶子节点存储的是这条记录的整行数据,且每个表只能有一个聚簇索引,通常是主键索引,比较适合范围查询和排序。 非聚簇索引的话:非叶子节点上存储的都是索引值,叶子节点存储的是数据行的主键和对应的索引列,且一个表可以有多个非聚簇索引(也被称为非主键索引、辅助索引、二级索引)。

再深入一点:

聚簇索引:最底层的叶子节点是一个数据页,里面存储的是主键以及整个记录,各个记录之间单链表连接,按主键有序排列,每个数据页有页号;然后倒数第二层抽出来目录页(存储目录项的数据页),里面存放的是一个个目录项,目录项存放的是下面每个数据页的最小主键以及该主键所在的页号,几个数据页为一组,存放在一个目录页里面;数据页比较多的时候,会形成多个目录页,最上层根据下层的目录页,生成一个总的目录页。
非聚簇索引: 最底层不会存储完整的记录,只会记录索引列以及该索引列对应的主键值,目录页记录的是最小索引列的值以及页号,还会记录主键值。
非聚簇索引为什么要记录主键值?确保目录项的唯一性。例如现在一个页面有两个目录项,索引列的值都是 1,但是页号分别是 4 和 5,如果只记录索引的值,现在来了一个插入的数据,索引列也是 1,这个数据应该插入到 4 页还是 5 页上?不知道,所以把主键加进来了,主键肯定是不一样的,当二级索引一样的时候,就比较主键的值,通过主键找位置。
介绍一下数据库的脏读、不可重复读和幻读(第二次查询比第一次多了数据)
首先介绍一下数据库的脏读(Dirty Read):就是一个事务读取到另一个事务未提交的数据,假如未提交的事务回滚了,那么不就出现了数据不一致的问题吗?然后介绍一下不可重复读(Non-repeatable Read):就是在同一个事务中,读取同一数据两次,但由于其他事务的提交,读取的结果不同,举个例子就是 事务 A 读取了一行数据,事务 B 修改并提交了这行数据,导致事务 A 再次读取时得到不同的值。最后介绍一下幻读:在同一事务中,执行相同的查询操作,返回的结果集由于其他事务的插入而发生变化。例如:事务 A 查询符合某条件的记录,事务 B 插入了新记录并提交,导致事务 A 再次查询时看到不同的记录数量。
MySQL 中的事务隔离级别有哪些呢?
我了解到的事务隔离级别大概有四个:读未提交(READ UNCOMMITTED):这是最低的隔离级别,一个事务可以看到另一个事务尚未提交的数据,这可能会导致脏读问题。读已提交(READ COMMITTED):一个事务只能看到已经提交的其他事务所做的修改,可能会引发不可重复读问题。可重复读(REPEATABLE READ):确保在一个事务中的多个查询返回的结果是一致的,可以避免不可重复读问题,但是可能会产生幻读问题。串行化(SERIALIZABLE):最高的隔离级别,是指每个 SQL 事务在下一个 SQL 事务开始之前完成其全部操作,它可以避免所有并发问题,但是会大大降低并发性能。所以选择什么样的隔离级别,需要自己在并发性与数据一致性之间做好平衡,一般的话互联网大厂会选择读已提交隔离级别。
SQL 中的执行顺序是什么
select column1, column2from table AS t1join table as t2on t1.columnA = t2.columnB where constraint = 1 group by column having constraint order by column (ASC/DESC) limit count;
查询的执行首先从 from 子句开始,确定数据的来源(表、视图、连接等),然后 SQL 引擎会执行连接操作(JOIN),将多张表的数据结合起来,接下来,SQL 引擎会对来自 from 和 join 的数据进行过滤,保留符合条件的行,where 子句执行的是行级别的过滤。然后 SQL 引擎会按照 group by 子句中的字段进行分组操作,将数据分为若干组,接着 having 子句执行时会对已经分组的数据进行过滤,保留符合条件的组,与 where 子句不同,HAVING 是用于过滤分组后的数据,在经过上述操作后,SQL 引擎会选择需要的列并进行返回,这个阶段是实际返回查询结果的地方,紧接着,SQL 引擎会按照 ORDER BY 子句中指定的列对结果进行排序,最后 LIMIT 子句限制查询结果,只返回指定数量的行。
MySQL 在设计表 (建表) 时需要注意什么
在满足业务需求的情况下,需要额外考虑表结构的高效性、扩展性以及维护性。首先得考虑表的范式用于消除数据冗余,提高数据一致性,或是适当冗余以提升查询性能。然后再考虑选择的数据类型是否合适。然后再考虑主键与唯一约束是否应该添加。然后再考虑表的外键:外键约束会影响性能,所以一般现在互联网公司都不使用外键。最后考虑数据库的索引设计,因为它很重要,所以放在后面再进行考虑(参考构建索引需要注意的事项),此外还应该避免不必要的 NULL 值,可以适当减少额外的开销,和简化业务逻辑。
MySQL 三层 B+ 树能存多少数据
默认是在 MySQL 的 InnoDB 引擎中,先介绍一下相关的参数吧。
每个节点页大小为 16KB(即 16384 字节)。
假设每个数据记录的主键和数据大小为 1KB(一般会比这个小,但这里取整方便计算)。
每个内部节点(非叶子节点)存储的是指向子节点的指针和索引键。
我们可以这么计算:叶子节点,第三层为叶子节点,每个叶子节点可以存储 16 条数据记录(16KB / 1KB);第二层,假设每个指针为 6 个 byte 和索引键位 8 byte,那么中间可以指向(16*1024/(6+8) == 1170)个叶子节点,第一层同理,可以指向 1170 个中间节点。所以三层 B+ 树的总数大概为 1170 * 1170 *16 条,也就是千万级的数据记录。
MySQL 插入一条 SQL 语句,redo log 记录的是什么呢
它记录的是物理日志,记录 "某页(Page) 某位置的数据被修改为某值" 。它不记录逻辑操作(如 "插入一行"),而是直接记录对页的变更,所以在插入操作中,redo log 记录的是事务在数据页上的修改数据页的插入点、记录的偏移量和插入的实际数据并更新页目录、页头等元数据。
它的具体执行流程如下:首先呢数据先写入内存中的缓冲池,而不是直接写入磁盘,同时生成一条 redo log,记录插入对数据页的物理修改细节,最后日志先行的设计让 redo log 先被写入磁盘上的 redo log 文件。
Page: 100,Offset: 50, Length: 20, Insert: "New Record Data"
MySQL 事务的二阶段提交是什么
MySQL 事务的两阶段提交是确保 binlog 和 redolog 一致性的关键机制,旨在防止主备数据库不一致。它的过程如下:首先是 Prepare 阶段,SQL 成功执行并生成 redolog,处于 prepare 状态,然后是 BinLog 持久化,它先通过 write() 将 binlog 内存日志数据写入文件缓冲区,再用 fsync() 将其从文件缓冲区永久写入磁盘,最后是 Commit 阶段:在执行引擎内部执行事务操作,更新 redolog。为什么需要两个阶段的提交,若不采用两个阶段提交,可能出现两种数据不一致情况。一是先写 redo log 成功但 binlog 未写,系统崩溃重启后,主备同步会缺变更记录;二是先写 binlog 成功但 redo log 未写,重启后崩溃恢复无操作,但主备同步会将新值同步到备库,导致主备数据不一致。
两阶段提交保证一致性的方式
情况一:一阶段提交后奔溃(redo log 处于 prepare 状态),崩溃恢复时直接回滚事务,主备均未执行该事务。
情况二:一阶段成功且写完 binlog 后崩溃,检查 binlog 中事务是否存在且完整,若存在且完整则提交事务,否则回滚。
情况三:redo log 处于 commit 状态时崩溃,重启后处理同情况二。
如何判断 binlog 和 redolog 一致的方法
当 MySQL 写完 redolog 并标记为 prepare 状态时,会在 redolog 中记录全局唯一标识事务的 XID,设置 sync_binlog = 1 后,写 redolog 完成第一阶段,MySQL 会将对应 binlog 刷新到磁盘,binlog 结束位置也有 XID,当两者 XID 一致时,MySQL 认为 binlog 和 redolog 逻辑上一致。

组提交
MySQL 的组提交(Group Commit) 是提升数据库性能与事务处理效率的优化技术,主要用于优化 redo log 的写入过程。它通过将多个事务的 redo log 刷盘操作合并为一次磁盘同步操作,从而减少 fsync 的调用次数,提高 MySQL 在高并发环境下的事务提交效率。

有了组提交后的二阶段提交
在引入组提交后,MySQL 事务的两阶段提交过程发生变化,主要体现在日志刷盘环节:
write 和 fsync 操作:write 操作将数据写入文件缓冲区,数据暂存于内存;fsync 用于将文件修改强制持久化到磁盘,常与 write 配合确保数据落盘。
两阶段提交变化:由于组提交,日志刷盘过程中的 fsync 步骤被延迟,需要等待一个组内多个事务都处于 Prepare 阶段后,才进行一次组提交,将日志统一持久化到磁盘。

了解过数据库的逻辑外键吗?
它是一种在应用程序层面上管理和维护数据完整性的方法,主要是利用应用程序代码来保证引用的完整性。
为什么不推荐使用数据库的外键

数据库外键性能问题盘点
第一个问题就是:关联外键越多锁定的数据也就越多,锁的数据越多除了性能问题,还可能会带来死锁的问题。第二个问题就是除了插入、删除、更新相关外键,数据库都需要去检查数据的完整性,这就产生了性能开销。第三个问题就是,在高并发数据量大的情况下,一个修改会产生意料之外的级联更新使得数据库压力过大,导致系统其他操作数据库的请求阻塞,很可能会导致数据库系统的崩溃。
逻辑删除中的唯一性问题你了解过吗?
假如用户报名参加某个店铺活动,并且活动记录表以userId+shopId 作为唯一索引,后续用户如果又不参加了,则逻辑删除 is_deleted 字段 标记为 1,但是用户又突然再次想参加了,这时由于表中实际上还存在上一次的记录,所以会出现唯一索引冲突,导致业务无法正常执行下去。
常见的有两种解决方案:复用一条记录+日志表,将userId+shopId+is_deleted 作为唯一索引且 is_deleted 可以存储时间戳,或者主键。
数据库的性能优化方法有哪些呢
首先是 sql 优化:包括开启慢查询日志,找出慢 sql, explain 分析。然后是库表设计方面的优化:冷热数据分离(用的多的字段单独成表),增加中间表(联合查询的数据,单独组成一张表,省去联合查询),冗余字段并且减少连表查询,合理的选择正确的数据类型,分库分表。再然后是硬件方面包括加内存啊,加 cpu,磁盘用 ssd。还有不怎么用的一点是:数据库需要不断的维护:包括定期备份,定期清理,重建索引等等,再最后实在是没办法优化了可以考虑用缓冲:包括本地缓冲啊,redis 分布式缓冲啊等等。

如何实现数据库的不停服迁移
首先可以先关注量级,如果是几十万的数据其实直接用代码进行迁移,然后简单核对下就结束了,如果数据量大那么才需要好好的设计方案。然后假如是不停服的数据迁移需要考虑在线数据的插入和修改,从而保证数据的一致性。最后迁移完成后还需要注意回滚,因为一旦发生问题需要及时切换回老库,防止对业务产生影响。
双写方案
可以先讲一遍迁移的流程:首先将云上数据库(新库) 作为自建数据库(旧库)的从库,进行数据同步(或者可以利用云上的能力,比如阿里云的 DTS),然后开始改造业务代码,数据写入修改不仅要写入旧库,同时也要写成新库,这就是所谓的双写,但是双写需要加开关,即通过修改配置实时打开双写和关闭双写,接着在业务低峰期,确保数据同步完全一致的时候(即主从不延迟,这个都是有对应的监控的),关闭同步,同时打开双写开关,此时业务代码读取的还是旧数据库。然后再进行数据核对,数据量很大的场景只能抽象调查(可以利用定时任务写代码进行抽样核对,一旦不一致就告警和记录)。最后如果确认数据一致,此时可以进行灰度切流,比如 1% 的用户切到新读的数据库(比如今天访问前 1% 的用户或者根据用户 ID 或其他业务字段),如果发现没有问题,则可以逐步增加开放的比例,比如 5% -> 20% -> 50% -> 100%。,最后继续保留双写,跑个几天(或者更久),确保新库确实没问题了,此时关闭双写,只写新库,这时候迁移就完成了。
flink-cdc 方案
除了主从同步,代码双写的方案,也可以采用第三方工具,例如 flink-cdc 等工具来进行数据的同步,它的有点方便,且支持异构 (比如 mysql 同步到 pg、es 等等) 的数据源。

像 flink-cdc 执行先同步全量历史数据,再无缝切到同步增量数据,上图中的蓝色小块就是新增的插入数据,会追加到实时一致性快照中,像上图中黄色小块是更新的数据,则会在已有的历史数据里面做更新。
了解过 Write-Ahead Logging(WAL) 技术吗?它的优点是什么?MySQL 中是否用到了 WAL ?
WAL 技术呢是一种数据库的事务日志管理技术,它确保了在修改真正的数据之前,先将修改记录写入到日志。这使得即使系统崩溃了,通过日志也能再次恢复数据,这保证了数据的持久性和一致性。
它的核心思想就是先写日志,再写数据,并且它的大致流程如下:当一个事务开始的时候,所有对数据库的修改都会先记录到一个日志文件中,然后可以用于恢复相关的数据,最后当日志记录被安全地写入磁盘后,才会将这些修改应用到数据库文件中。
Redo Log
在 MySQL InnoDB 存储引擎中,重做日志(Redo Log) 就是 WAL 的实现,用于保证事务的持久性和奔溃恢复的能力。它的工作机制如下:
当一个事务开始时,所有对数据库的修改首先记录到重做日志缓冲区中,在重做日志缓冲区的数据会周期性地刷新到磁盘上地重做日志文件(ib_logfile0 和 ib_logfile1),当事务提交时,InnoDB 确保重做日志已经写入磁盘,然后将数据页地修改写入数据文件,最后当 mysql 系统崩溃时,InnoDB 会在启动时通过重做日志重新应用所有未完成的事务,以恢复数据库到一致性的状态。
MySQL 中 exists 与 in 的区别有了解过吗?
最主要的区别还是它们的执行顺序不太一样。
exists 会先执行外层的查询语句,然后拿到外层得到的所有行数据,每一行都去执行一遍子查询,看子查询的结果里面有没有匹配的行,如果有的话,则加入最终要返回的结果集合,然后继续搞下一行。in 的话会先执行子查询语句,返回一个结果集,然后执行外层的查询语句,看外层得到的所有行有没有在子查询的结果集中的,有的话就加入最终要返回的结果集。
exists 适合于子查询结果集比较大的时候,in 则适合于子查询结果集合比较小的时候,但它们具体呢还是需要通过 explain 进行分析。
什么情况下,不推荐为数据库建立索引
第一种情况就是:数据量很小的表(如几百条记录的时候),建立索引它并不会显著的提高查询性能,反而可能会增加管理的复杂性。第二种情况就是:对于插入、更新和删除操作更新频繁的表,索引会导致额外的维护开销,因为每次数据变更时都需要更新索引,这会影响性能。第三情况:执行的 select 数量超过一定的比例,此时二级索引可能不会显著提升性能,因为它需要大量的回表查询,开销也比较大,所以数据库最终可能会选择走全表扫描。第四种情况就是:低频查询的列,因为它的查询频率比较低,所以建立索引的成本和维护负担可能会超过带来的性能提升。最后一种情况就是:长文本字段(非常长的 varchar 或 json, blob 和 text 类型,这些类型的列通常包括大量的数据),

数据库中的游标了解过吗?
不太了解,只知道它的作用是允许逐行处理查询结果集,可以对结果集中的每一行进行复杂的操作,但是它会带来一定的性能问题,因为它需要在内存中维护结果集的状态。所以应尽量使用集合操作(如批量更新) 而不是逐行进行处理。
数据库中的视图有了解过吗?
它是一个虚拟表,并且它并不存储实际的数据,而是通过查询其他表的数据来生成的。它可以将复杂的查询封装成一个简单的视图,使得用户在查询数据时更加方便,同时通过视图可以限制用户访问特定的表和列,保护敏感数据。
数据库中该使用什么格式来存储 金额数据呢?
BigDecimal 则很适用于高精度金额场景,且非常的灵活,只不过相对于 long 的性能它会差很多,但是在大部分业务上我个人可以认为忽略这个问题,除非是一写特殊场景下需要极端的性能,所以一般情况下我更加推荐使用 BigDecimal 。
long 类型保存到分,使得它的小数位(厘)的数据并不好处理(它需要手动的进行处理,比较麻烦),并且在高精度的金额计算场景下并不适合,例如有一些第三方的支付系统是需要进行抽成的,例如千分之三,万分之一等等,这类的抽成要求精度比较高,long 类型无法胜任。
MySQL 中 auto_increment 达到最大值时会发生什么
在 MySQL 中,如果表定义的自增 ID 到达上限后,再申请下一个 ID,得到的值不变,因此会导致重复值的错误。
如果 InnoDB 表中没有配置主键,那么还有最大值的上限吗?
如果 InnoDB 表中不配置主键,那么默认的 InnoDB 会创建一个不可见的长度为 6 个字节的 row_id, 它在全局维护一个 dict_sys.row_id 值,所有需要用到 row_id 的表,每次插入一行数据,都会获取这个值,然后将其 +1, 这个值的范围是 0~2^48-1, 如果这个值达到上限后,又会从 0 开始,然后继续循环,如果插入的新数据的 row_id 在表中已存在,那么就会覆盖老的数据,不会产生有任何的报错。
说说 DATETIME 与 TIMESTAMP 类型的区别
首先呢 DATETIME: 是以字符串形式来进行存储的,范围为 1000-01-01 00:00:00 到 9999-12-31 23:59:59, DATETIME(0) = 5 字节, DATETIME(6) = 8 字节,老版本为(5.6.4 之前) 固定 8 字节。然后是 TIMESTAMP: 以 Unix 时间戳形式存储,范范围为 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC,TIMESTAMP(6) = 7 字节,老版本(5.6.4 之前) 固定 4 字节。
需要注意的点

默认值和自动更新
DATETIME:在 MySQL 5.6 版本之后,可以使用 DEFAULT 和 ON UPDATE 子句来指定自动初始化和更新行为,但不像 TIMESTAMP 那么直观。
TIMESTAMP:在 MySQL 5.6 及更高版本中,TIMESTAMP 列可以有默认的当前时间戳 CURRENT_TIMESTAMP,并且可以使用 ON UPDATE_TIMESTAMP 使其在行更新自动更新为当前时间戳,这使得 TIMESTAMP 非常适合记录行的创建和修改时间。
说说 DELETE、DROP 和 TRUNCATE 区别是什么
Delete:delete 操作会生成 binlog、redolog 和 undolog,本质上这个删除其实就是给数据打一个标记,并不实时删除,因此 delete 之后,空间的大小并不会变化多少。
Drop: 在 InnoDB 中,每张表数据内容和索引都存储在一个以 .ibd 后缀的文件中,drop 就是直接把这个文件给删除了! 还有一个 .frm 后缀的文件(这个文件包含的元数据和结构定义)删除了。默认创建的表会有独立表空间,把 innodb_file_per_table 的值改为 OFF 后,就会被放到共享表空间中,即统一的 ibdata1 文件中。
Truncate:会对整张表的数据进行删除,且不会记录回滚等日志,所以它无法被回滚。
MySQL 的 Doublewrite Buffer 是什么,有了解过吗
我们来说说它的工作原理吧:当 InnoDB 需要将脏页(dirty page,即已被修改但尚未写入磁盘的页) 写入磁盘时,首先将这些数据页写入到 Doublewrite Buffer 中,将数据页写入 Doublewrite Buffer 和落盘后,InnoDB 将这些数据页从 Doublewrite Buffer 写入到实际的数据文件中(如 .ibd 文件)。当数据页写入一半断电了,在崩溃恢复的时候,InnoDB 会检查 Doublewrite Buffer 中的数据页,如果在系统崩溃前数据页已经成功写入 Doublewrite Buffer,那么这些数据页是完整和一致的。
Doublewrite Buffer 通常在系统表空间文件(ibdata1) 中,分为两个 1MB 的区域,共 2MB,每个区域可存储 64 个 16 KB 的页。
什么是数据库的读写分离
读写分离就是读操作和写操作从以前的一台服务器上剥离开来,将主库压力分担一些到从库,本质上是因为访问量太大,主库的压力过大,单机数据库无法支撑并发读写,然后一般而言读的次数远远高于写,因此将读操作分发到从库上,这就是常见的读写分离。
读写分离还有个操作就是主库不建查询的索引,从库建查询的索引。因为索引是需要维护的,比如你插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入,修改也是一样的。所以将读操作分到从库了之后,可以在主库把查询要用的索引删除了,以减少写操作对主库的影响。
一般有两种做法:做法一是代码封装,做法二是使用中间件。
如何在 MySQL 中避免单点故障
一般都会使用主从架构 来避免单点故障,主数据库处理写操作,从数据库处理读操作,主数据库故障时可以切换到从数据库中,并且需要建立监控系统,实时监控数据库的健康状态,并在发生故障时及时告警。
主从架构
它就是主机和从机,从机和备机的区别在于它是对外提供服务的,一般而言主从就是读写分离,写请求指派到主机,读请求指派到从机。

主备架构
主备架构就是主机和备机,备机不对外提供服务,只是默默地在同步主机的数据,然后等某一天主机挂了之后,它取而代之。

主主架构
一般情况下都不会有主主架构。当同时有两个写请求达到分别打到两个主库同一张表的时候,则会同时创建一条记录,这条记录的 ID 是一样的,这样数据同步之后其中有一条数据就被覆盖了。
