数据库原理与设计
09 如何回答 MySQL 的索引原理与优化问题?
在互联网技术面试中,面试官除了会考察分布式、中间件等技术以外,还会考察数据库知识。无论你是程序员,还是架构师,都要掌握关系型数据库 MySQL 的原理与设计问题,从今天起,我就用 4 讲的时间带你打卡 MySQL 的面试内容。
今天这一讲,我们就先来看一看怎么回答 MySQL 的索引原理与优化问题。
案例背景
很多面试官考察候选人对“数据库知识”的掌握程度,会以“数据库的索引原理和优化方法”作为切入点。
假设面试官问你: 在电商平台的订单中心系统中,通常要根据商品类型、订单状态筛选出需要的订单,并按照订单创建的时间进行排序,那针对下面这条 SQL,你怎么通过索引来提高查询效率呢?
select * from order where status = 1 order by create_time asc
有的同学会认为,单独给 status 建立一个索引就可以了。
但是更优的方式是建立一个 status 和 create_time 组合索引,这是为了避免 MySQL 数据库发生文件排序。因为在查询时,你只能用到 status 的索引,但如果要对 create_time 排序,就要用文件排序 filesort,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort。
所以你要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序。
案例分析
通过这个案例,你可以发现“索引知识”的重要性,所以我一般也会拿索引知识来考察候选人,并扩展出 MySQL 索引原理与优化策略的一系列问题,比如:
- 数据库索引底层使用的是什么数据结构和算法呢?
- 为什么 MySQL InnoDB 选择 B+Tree 当默认的索引数据结构?
- 如何通过执行计划查看索引使用详情?
- 有哪些情况会导致索引失效?
- 平时有哪些常见的优化索引的方法?
- ……
总结起来就是如下几点:
- 理解 MySQL InnoDB 的索引原理;
- 掌握 B+Tree 相比于其他索引数据结构(如 B-Tree、二叉树,以及 Hash 表)的优势;
- 掌握 MySQL 执行计划的方法;
- 掌握导致索引失效的常见情况;
- 掌握实际工作中常用的建立高效索引的技巧(如前缀索引、建立覆盖索引等)。
如果你曾经被问到其中某一个问题,那你就有必要认真夯实 MySQL 索引及优化的内容了。
案例解答
MySQL InnoDB 的索引原理
从数据结构的角度来看, MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。我在表中总结了 MySQL 常见的存储引擎 InnoDB、MyISAM 和 Memory 分别支持的索引类型。(后两个存储引擎在实际工作和面试中很少提及,所以本讲我只讲 InnoDB) 。
索引类型
在实际应用中,InnoDB 是 MySQL 建表时默认的存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型。
在创建表时,InnoDB 存储引擎默认使用表的主键作为主键索引,该主键索引就是聚簇索引(Clustered Index),如果表没有定义主键,InnoDB 就自己产生一个隐藏的 6 个字节的主键 ID 值作为主键索引,而创建的主键索引默认使用的是 B+Tree 索引。
接下来我们通过一个简单的例子,说明一下 B+Tree 索引在存储数据中的具体实现,为的是让你理解通过 B+Tree 做索引的原理。
首先,我们创建一张商品表:
CREATE TABLE `product` (`id` int(11) NOT NULL,`product_no` varchar(20) DEFAULT NULL,`name` varchar(255) DEFAULT NULL,`price` decimal(10, 2) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
然后新增几行数据:
商品数据表
- 通过主键查询(主键索引)商品数据的过程
此时当我们使用主键索引查询商品 15 的时候,那么按照 B+Tree 索引原理,是如何找到对应数据的呢?
select * from product where id = 15
我们可以通过数据手动构建一个 B+Tree,它的每个节点包含 3 个子节点(B+Tree 每个节点允许有 M 个子节点,且 M>2),根节点中的数据值 1、18、36 分别是子节点(1,6,12),(18,24,30)和(36,41,52)中的最小值。
每一层父节点的数据值都会出现在下层子节点的数据值中,因此在叶子节点中,包括了所有的数据值信息,并且每一个叶子节点都指向下一个叶子节点,形成一个链表。如图所示:
主键索引查询过程
我们举例讲解一下 B+Tree 的查询流程,比如想要查找数据值 15,B+Tree 会自顶向下逐层进行查找:
- 将 15 与根节点的数据 (1,18,36) 比较,15 在 1 和 18 之间,所以根据 B+Tree的搜索逻辑,找到第二层的数据块 (1,6,12);
- 在第二层的数据块 (1,6,12) 中进行查找,因为 15 大于 12,所以找到第三层的数据块 (12,15,17);
- 在叶子节点的数据块 (12,15,17) 中进行查找,然后我们找到了数据值 15;
- 最终根据数据值 15 找到叶子节点中存储的数据。
整个过程一共进行了 3 次 I/O 操作,所以 B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率。
那么问题来了,如果你当前查询数据时候,不是通过主键 ID,而是用商品编码查询商品,那么查询过程又是怎样的呢?
- 通过非主键(辅助索引)查询商品数据的过程
如果你用商品编码查询商品(即使用辅助索引进行查询),会先检索辅助索引中的 B+Tree 的 商品编码,找到对应的叶子节点,获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫回表。
以上就是索引的实现原理。 掌握索引的原理是了解 MySQL 数据库的查询效率的基础,是每一个研发工程师都需要精通的知识点。
在面试时,面试官一般不会让你直接描述查询索引的过程,但是会通过考察你对索引优化方法的理解,来评估你对索引原理的掌握程度,比如为什么 MySQL InnoDB 选择 B+Tree 作为默认的索引数据结构?MySQL 常见的优化索引的方法有哪些?
所以接下来,我们就详细了解一下在面试中如何回答索引优化的问题。
B+Tree 索引的优势
如果你被问到“为什么 MySQL 会选择 B+Tree 当索引数据结构?”其实在考察你两个方面: B+Tree 的索引原理; B+Tree 索引相比于其他索引类型的优势。
我们刚刚已经讲了 B+Tree 的索引原理,现在就来回答一下 B+Tree 相比于其他常见索引结构,如 B 树、二叉树或 Hash 索引结构的优势在哪儿?
- B+Tree 相对于 B 树 索引结构的优势
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
- B+Tree 相对于二叉树索引结构的优势
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据(这里的查询参考上面 B+Tree 的聚簇索引的查询过程)。
而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
- B+Tree 相对于 Hash 表存储结构的优势
我们知道范围查询是 MySQL 中常见的场景,但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。
至此,你就知道“为什么 MySQL 会选择 B+Tree 来做索引”了。在回答时,你要着眼于 B+Tree 的优势,然后再引入索引原理的查询过程(掌握这些知识点,这个问题其实比较容易回答)。
接下来,我们进入下一个问题:在实际工作中如何查看索引的执行计划。
通过执行计划查看索引使用详情
我这里有一张存储商品信息的演示表 product:
CREATE TABLE `product` (`id` int(11) NOT NULL,`product_no` varchar(20) DEFAULT NULL,`name` varchar(255) DEFAULT NULL,`price` decimal(10, 2) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,KEY 'index_name' ('name').KEY 'index_id_name' ('id', 'name')
) CHARACTER SET = utf8 COLLATE = utf8_general_ci
表中包含了主键索引、name 字段上的普通索引,以及 id 和 name 两个字段的联合索引。现在我们来看一条简单查询语句的执行计划:
执行计划
对于执行计划,参数有 possible_keys 字段表示可能用到的索引,key 字段表示实际用的索引,key_len 表示索引的长度,rows 表示扫描的数据行数。
这其中需要你重点关注 type 字段, 表示数据扫描类型,也就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为(考虑到查询效率问题,全表扫描和全索引扫描要尽量避免):
- ALL(全表扫描);
- index(全索引扫描);
- range(索引范围扫描);
- ref(非唯一索引扫描);
- eq_ref(唯一索引扫描);
- const(结果只有一条的主键或唯一索引扫描)。
总的来说,执行计划是研发工程师分析索引详情必会的技能(很多大厂公司招聘 JD 上写着“SQL 语句调优” ),所以你在面试时也要知道执行计划核心参数的含义,如 type。在回答时,也要以重点参数为切入点,再扩展到其他参数,然后再说自己是怎么做 SQL 优化工作的。
索引失效的常见情况
在工作中,我们经常会碰到 SQL 语句不适用已有索引的情况,来看一个索引失效的例子:
这条带有 like 查询的 SQL 语句,没有用到 product 表中的 index_name 索引。
我们结合普通索引的 B+Tree 结构看一下索引失效的原因: 当 MySQL 优化器根据 name like ‘%路由器’ 这个条件,到索引 index_name 的 B+Tree 结构上进行查询评估时,发现当前节点的左右子节点上的值都有可能符合 ‘%路由器’ 这个条件,于是优化器判定当前索引需要扫描整个索引,并且还要回表查询,不如直接全表扫描。
当然,还有其他类似的索引失效的情况:
- 索引列上做了计算、函数、类型转换操作,这些情况下索引失效是因为查询过程需要扫描整个索引并回表,代价高于直接全表扫描;
- like 匹配使用了前缀匹配符 ‘%abc’;
- 字符串不加引号导致类型转换;
我给你的建议是, 如果 MySQL 查询优化器预估走索引的代价比全表扫描的代价还要大,则不走对应的索引,直接全表扫描,如果走索引比全表扫描代价小,则使用索引。
常见优化索引的方法
- 前缀索引优化
前缀索引就是用某个字段中,字符串的前几个字符建立索引,比如我们可以在订单表上对商品名称字段的前 5 个字符建立索引。使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
但是,前缀索引有一定的局限性,例如 order by 就无法使用前缀索引,无法把前缀索引用作覆盖索引。
- 覆盖索引优化
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+tree 的叶子节点上都能找得到的那些索引,从辅助索引中查询得到记录,而不需要通过聚簇索引查询获得。假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?
我们可以建立一个组合索引,即商品ID、名称、价格作为一个组合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。所以,使用覆盖索引的好处很明显,即不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
- 联合索引
联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。比如联合索引 (userpin, username),如果查询条件是 WHERE userpin=1 AND username=2,就可以匹配上联合索引;或者查询条件是 WHERE userpin=1,也能匹配上联合索引,但是如果查询条件是 WHERE username=2,就无法匹配上联合索引。
另外,建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
区分度就是某个字段 column 不同值的个数除以表的总行数,比如性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 uuid 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
总结
今天,我们讲了 MySQL 的索引原理,介绍了 InnoDB 为什么会采用 B+Tree 结构。因为 B+Tree 能够减少单次查询的磁盘访问次数,做到查询效率最大化。另外,我们还讲了如何查看 SQL 的执行计划,从而找到索引失效的问题,并有针对性的做索引优化。
最后,我总结一些你容易在面试中被问到的,索引的使用原则:
另外,你在了解索引优势的同时,也要了解索引存在的问题:索引会带来数据的写入延迟,引入额外的空间消耗;在海量数据下,想要通过索引提升查询效率也是有限的。
10 如何回答 MySQL 的事务隔离级别和锁的机制?
上一讲,我讲了 MySQL 的索引原理与优化问题,今天我带你继续学习 MySQL 的事务隔离级别和锁的机制,MySQL 的事务和锁是并发控制最基本的手段,在面试中,它们与 09 讲的索引一样,同样是 MySQL 重要的考察点。
案例背景
MySQL 的事务隔离级别(Isolation Level),是指:当多个线程操作数据库时,数据库要负责隔离操作,来保证各个线程在获取数据时的准确性。它分为四个不同的层次,按隔离水平高低排序,读未提交 < 读已提交 < 可重复度 < 串行化。
MySQL 隔离级别
- 读未提交(Read uncommitted):隔离级别最低、隔离度最弱,脏读、不可重复读、幻读三种现象都可能发生。所以它基本是理论上的存在,实际项目中没有人用,但性能最高。
- 读已提交(Read committed):它保证了事务不出现中间状态的数据,所有数据都是已提交且更新的,解决了脏读的问题。但读已提交级别依旧很低,它允许事务间可并发修改数据,所以不保证再次读取时能得到同样的数据,也就是还会存在不可重复读、幻读的可能。
- 可重复读(Repeatable reads):MySQL InnoDB 引擎的默认隔离级别,保证同一个事务中多次读取数据的一致性,解决脏读和不可重复读,但仍然存在幻读的可能。
- 可串行化(Serializable):选择“可串行化”意味着读取数据时,需要获取共享读锁;更新数据时,需要获取排他写锁;如果 SQL 使用 WHERE 语句,还会获取区间锁。换句话说,事务 A 操作数据库时,事务 B 只能排队等待,因此性能也最低。
至于数据库锁,分为悲观锁和乐观锁,“悲观锁”认为数据出现冲突的可能性很大,“乐观锁”认为数据出现冲突的可能性不大。那悲观锁和乐观锁在基于 MySQL 数据库的应用开发中,是如何实现的呢?
- 悲观锁一般利用 SELECT … FOR UPDATE 类似的语句,对数据加锁,避免其他事务意外修改数据。
- 乐观锁利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,实现版本判断。
案例分析
如果面试官想深挖候选人对数据库内部机制的掌握程度,切入点一般是 MySQL 的事务和锁机制。接下来,我就从初中级研发工程师的角度出发,从概念到实践,带你掌握“MySQL 事务和锁机制”的高频考点:
- 举例说明什么是脏读、不可重复度和幻读(三者虽然基础,但很多同学容易弄混)?
- MySQL 是怎么解决脏读、不可重复读,和幻读问题的?
- 你怎么理解死锁?
- ……
案例解答
怎么理解脏读、不可重复读和幻读?
脏读: 读到了未提交事务的数据。
事务并发时的“脏读”现象
假设有 A 和 B 两个事务,在并发情况下,事务 A 先开始读取商品数据表中的数据,然后再执行更新操作,如果此时事务 A 还没有提交更新操作,但恰好事务 B 开始,然后也需要读取商品数据,此时事务 B 查询得到的是刚才事务 A 更新后的数据。
如果接下来事务 A 触发了回滚,那么事务 B 刚才读到的数据就是过时的数据,这种现象就是脏读。
“脏读”面试关注点:
- 脏读对应的隔离级别是“读未提交”,只有该隔离级别才会出现脏读。
- 脏读的解决办法是升级事务隔离级别,比如“读已提交”。
不可重复读: 事务 A 先读取一条数据,然后执行逻辑的过程中,事务 B 更新了这条数据,事务 A 再读取时,发现数据不匹配,这个现象就是“不可重复读”。
事务并发时的“不可重复读”现象
“不可重复读”面试关注点:
- 简单理解是两次读取的数据中间被修改,对应的隔离级别是“读未提交”或“读已提交”。
- 不可重复读的解决办法就是升级事务隔离级别,比如“可重复度”。
幻读: 在一个事务内,同一条查询语句在不同时间段执行,得到不同的结果集。
事务并发时的“幻读”现象
事务 A 读了一次商品表,得到最后的 ID 是 3,事务 B 也同样读了一次,得到最后 ID 也是 3。接下来事务 A 先插入了一行,然后读了一下最新的 ID 是 4,刚好是前面 ID 3 加上 1,然后事务 B 也插入了一行,接着读了一下最新的 ID 发现是 5,而不是 3 加 1。
这时,你发现在使用 ID 做判断或做关键数据时,就会出现问题,这种现象就像是让事务 B 产生了幻觉一样,读取到了一个意想不到的数据,所以叫幻读。当然,不仅仅是新增,删除、修改数据也会发生类似的情况。
“幻读”面试关注点:
- 要想解决幻读不能升级事务隔离级别到“可串行化”,那样数据库也失去了并发处理能力。
- 行锁解决不了幻读,因为即使锁住所有记录,还是阻止不了插入新数据。
- 解决幻读的办法是锁住记录之间的“间隙”,为此 MySQL InnoDB 引入了新的锁,叫间隙锁(Gap Lock),所以在面试中,你也要掌握间隙锁,以及间隙锁与行锁结合的 next-key lock 锁。
怎么理解死锁
除了事务隔离级别,很多同学在面试时,经常会被面试官直奔主题地问:“谈谈你对死锁的理解”。要回答这样开放的问题,你就要在脑海中梳理出系统化的回答思路:死锁是如何产生的,如何避免死锁。
死锁一般发生在多线程(两个或两个以上)执行的过程中。因为争夺资源造成线程之间相互等待,这种情况就产生了死锁。我在 06 讲也提到了死锁,但是并没有讲它产生的原因以及怎么避免,所以接下来我们就来了解这部分内容。
线程死锁
比如你有资源 1 和 2,以及线程 A 和 B,当线程 A 在已经获取到资源 1 的情况下,期望获取线程 B 持有的资源 2。与此同时,线程 B 在已经获取到资源 2 的情况下,期望获取现场 A 持有的资源 1。
那么线程 A 和线程 B 就处理了相互等待的死锁状态,在没有外力干预的情况下,线程 A 和线程 B 就会一直处于相互等待的状态,从而不能处理其他的请求。
死锁产生的四个必要条件。
互斥条件
互斥: 多个线程不能同时使用一个资源。比如线程 A 已经持有的资源,不能再同时被线程 B 持有。如果线程 B 请求获取线程 A 已经占有的资源,那线程 B 只能等待这个资源被线程 A 释放。
持有并等待
持有并等待: 当线程 A 已经持有了资源 1,又提出申请资源 2,但是资源 2 已经被线程 C 占用,所以线程 A 就会处于等待状态,但它在等待资源 2 的同时并不会释放自己已经获取的资源 1。
不可剥夺条件
不可剥夺: 线程 A 获取到资源 1 之后,在自己使用完之前不能被其他线程(比如线程 B)抢占使用。如果线程 B 也想使用资源 1,只能在线程 A 使用完后,主动释放后再获取。
循环等待
循环等待: 发生死锁时,必然会存在一个线程,也就是资源的环形链。比如线程 A 已经获取了资源 1,但同时又请求获取资源 2。线程 B 已经获取了资源 2,但同时又请求获取资源 1,这就会形成一个线程和资源请求等待的环形图。
死锁只有同时满足互斥、持有并等待、不可剥夺、循环等待时才会发生。并发场景下一旦死锁,一般没有特别好的方法,很多时候只能重启应用。因此,最好是规避死锁,那么具体怎么做呢?答案是:至少破坏其中一个条件(互斥必须满足,你可以从其他三个条件出发)。
- 持有并等待:我们可以一次性申请所有的资源,这样就不存在等待了。
- 不可剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。
- 循环等待:可以靠按序申请资源来预防,也就是所谓的资源有序分配原则,让资源的申请和使用有线性顺序,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样的线性化操作就自然就不存在循环了。
总结
我们花了两讲的时间,把 MySQL 数据库面试中的高频问题熟悉了一遍,但是如果从数据库领域应用开发者角度出发,至少还需要掌握以下几部分内容。
- 数据库设计基础:掌握数据库设计中的基本范式,以及基础概念,例如表、视图、索引、外键、序列号生成器等,掌握数据库的数据类型的使用,清楚业务实体关系与数据库结构的映射。
- 数据库隔离级别:掌握 MySQL 四种事务隔离级别的基础知识,并进一步了解 MVCC、Locking 等机制对于处理的进阶问题的解决;还需要了解不同索引类型的使用,甚至是底层数据结构和算法等。
- SQL 优化:掌握基础的 SQL 调优技巧,至少要了解基本思路是怎样的,例如 SQL 怎样写才能更好利用索引、知道如何分析 SQL 执行计划等。
- 数据库架构设计:掌握针对高并发等特定场景中的解决方案,如读写分离、分库分表等。
当然在准备面试时我并不建议你找一堆书闷头苦读,还是要从实际工作中,从使用数据库出发,并结合实践,完善和深化自己的知识体系,今天的内容就讲到这里,我们下一讲见。
11 读多写少:MySQL 如何优化数据查询方案?
今天这一讲,我们将面试继续聚焦到 MySQL 上,看一看当面试官提及“在读多写少的网络环境下,MySQL 如何优化数据查询方案”时,你要从哪些角度出发回答问题。
案例背景
假设你目前在某电商平台就职,公司面临双 11 大促,投入了大量营销费用用于平台推广,这带来了巨大的流量,如果你是订单系统的技术负责人,要怎么应对突如其来的读写流量呢?
这是一个很典型的应用场景,我想很多研发同学会回答:通过 Redis 作为 MySQL 的缓存,然后当用户查看“订单中心”时,通过查询订单缓存,帮助 MySQL 抗住大部分的查询请求。
如果你也是这么想,说明没认真思考过问题。因为应用缓存的原则之一是保证缓存命中率足够高,不然很多请求会穿透缓存,最终打到数据库上。然而在“订单中心”这样的场景中,每个用户的订单都不同,除非全量缓存数据库订单信息(又会带来架构的复杂度),不然缓存的命中率依旧很低。
所以在这种场景下,缓存只能作为数据库的前置保护机制,但是还会有很多流量打到数据库上,并且随着用户订单不断增多,请求到 MySQL 上的读写流量会越来越多,当单台 MySQL 支撑不了大量的并发请求时,该怎么办?
案例分析
互联网大部分系统的访问流量是读多写少,读写请求量的差距可能达到几个数量级,就好比你在京东上的商品的浏览量肯定远大于你的下单量。
所以你要考虑优化数据库来抗住高查询请求,首先要做的就是区分读写流量区,这样才方便针对读流量做单独扩展,这个过程就是流量的“读写分离”。
读写分离是提升 MySQL 并发的首选方案,因为当单台 MySQL 无法满足要求时,就只能用多个具有相同数据的 MySQL 实例组成的集群来承担大量的读写请求。
MySQL 主从结构
MySQL 做读写分离的前提,是把 MySQL 集群拆分成“主 + 从”结构的数据集群,这样才能实现程序上的读写分离,并且 MySQL 集群的主库、从库的数据是通过主从复制实现同步的。
那么面试官会问你“MySQL 集群如何实现主从复制?” 换一种问法就是“当你提交一个事务到 MySQL 集群后,MySQL 都执行了哪些操作?”面试官往往会以该问题为切入点,挖掘你对 MySQL 集群主从复制原理的理解,然后再模拟一个业务场景,让你给出解决主从复制问题的架构设计方案。
所以,针对面试官的套路,你要做好以下的准备:
- 掌握读多写少场景下的架构设计思路,知道缓存不能解决所有问题,“读写分离”是提升系统并发能力的重要手段。
- 深入了解数据库的主从复制,掌握它的原理、问题,以及解决方案。
- 从实践出发,做到技术的认知抽象,从方法论层面来看设计。
案例解答
MySQL 主从复制的原理
无论是“MySQL 集群如何实现主从复制”还是“当你提交一个事务到 MySQL 集群后,MySQL 集群都执行了哪些操作?”面试官主要是问你:MySQL 的主从复制的过程是怎样的?
总的来讲,MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
为了方便你记忆,我把 MySQL 集群的主从复制过程梳理成 3 个阶段。
- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 回放 Binlog:回放 binlog,并更新存储数据。
主从复制过程
但在面试中你不能简单地只讲这几个阶段,要尽可能详细地说明主库和从库的数据同步过程,为的是让面试官感受到你技术的扎实程度(详细过程如下)。
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
一主多从
同时,在读流量比较大时,你可以部署多个从库共同承担读流量,这就是“一主多从”的部署方式,你在垂直电商项目中可以用该方式抵御较高的并发读流量。另外,从库也可以作为一个备库,以避免主库故障导致的数据丢失。
MySQL 一主多从
当然,一旦你提及“一主多从”,面试官很容易设陷阱问你:那大促流量大时,是不是只要多增加几台从库,就可以抗住大促的并发读请求了?
当然不是。
因为从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
其实,你从 MySQL 主从复制过程也能发现,MySQL 默认是异步模式:MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
而这时,面试官一般会追问你“**MySQL 主从复制还有哪些模型?”**主要有三种。
- 同步复制:事务线程要等待所有从库的复制成功响应。
- 异步复制:事务线程完全不等待从库的复制成功响应。
- 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。
这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
讲到这儿,你基本掌握了 MySQL 主从复制的原理,但如果面试官想挖掘你的架构设计能力,还会从架构设计上考察你怎么解决 MySQL 主从复制延迟的问题,比如问你“在系统设计上有哪些方案可以解决主从复制的延迟问题?”
从架构上解决主从复制延迟
我们来结合实际案例设计一个主从复制延迟的解决方案。
在电商平台,每次用户发布商品评论时,都会先调用评论审核,目的是对用户发布的商品评论进行如言论监控、图片鉴黄等操作。
评论在更新完主库后,商品发布模块会异步调用审核模块,并把评论 ID 传递给审核模块,然后再由评论审核模块用评论 ID 查询从库中获取到完整的评论信息。此时如果主从数据库存在延迟,在从库中就会获取不到评论信息,整个流程就会出现异常。
主从延迟影响评论读取的实时性
这是主从复制延迟导致的查询异常,解决思路有很多,我提供给你几个方案。
- 使用数据冗余
可以在异步调用审核模块时,不仅仅发送商品 ID,而是发送审核模块需要的所有评论信息,借此避免在从库中重新查询数据(这个方案简单易实现,推荐你选择)。但你要注意每次调用的参数大小,过大的消息会占用网络带宽和通信时间。
- 使用缓存解决
可以在写入数据主库的同时,把评论数据写到 Redis 缓存里,这样其他线程再获取评论信息时会优先查询缓存,也可以保证数据的一致性。
不过这种方式会带来缓存和数据库的一致性问题,比如两个线程同时更新数据,操作步骤如下:
线程 A 先更新数据库为 100,此时线程 B 把数据库和缓存中的数据都更新成了 200,然后线程 A 又把缓存更新为 100,这样数据库中的值 200 和缓存中的值 100 就不一致了,解决这个问题,你可以参考 06 讲。
总的来说,通过缓存解决 MySQL 主从复制延迟时,会出现数据库与缓存数据不一致的情况。虽然它和“使用数据冗余”的方案相比并不优雅,但我还是建议你在面试中做一下补充,这样可以引出更多的技术知识,展现自己与其他人的差异。
- 直接查询主库
该方案在使用时一定要谨慎,你要提前明确查询的数据量不大,不然会出现主库写请求锁行,影响读请求的执行,最终对主库造成比较大的压力。
当然了,面试官除了从架构上考察你对 MySQL 主从复制延迟的理解,还会问你一些扩展问题,比如:当 MySQL 做了主从分离后,对于数据库的使用方式就发生了变化,以前只需要使用一个数据库地址操作数据库,现在却要使用一个主库地址和多个从库地址,并且还要区分写入操作和查询操作,那从工程代码上设计,怎么实现主库和从库的数据访问呢?
实现主库和从库的数据库访问
一种简单的做法是:提前把所有数据源配置在工程中,每个数据源对应一个主库或者从库,然后改造代码,在代码逻辑中进行判断,将 SQL 语句发送给某一个指定的数据源来处理。
这个方案简单易实现,但 SQL 路由规则侵入代码逻辑,在复杂的工程中不利于代码的维护。
另一个做法是:独立部署的代理中间件,如 MyCat,这一类中间件部署在独立的服务器上,一般使用标准的 MySQL 通信协议,可以代理多个数据库。
该方案的优点是隔离底层数据库与上层应用的访问复杂度,比较适合有独立运维团队的公司选型;缺陷是所有的 SQL 语句都要跨两次网络传输,有一定的性能损耗,再就是运维中间件是一个专业且复杂的工作,需要一定的技术沉淀。
技术认知
以上就是你在应聘初中级工程师时需要掌握的内容,如果你应聘的是高级研发工程师,在回答问题时,还要尽可能地展示自己对 MySQL 数据复制的抽象能力。因为在网络分布式技术错综复杂的今天,如果你能将技术抽象成一个更高层次的理论体系,很容易在面试中脱颖而出。
以 Raft 协议为例,其内部是通过日志复制同步的方式来实现共识的,例如在领导者选举成功后,它就会开始接收客户端的请求,此时每一个客户端请求都将被解析成一条指令日志,然后并行地向其他节点发起通知,要求其他节点复制这个日志条目,并最终在各个节点中回放日志,实现共识。
我们抽象一下它的运作机制:
运作机制
如果客户端将要执行的命令发送给集群中的一台服务器,那么这台服务器就会以日志的方式记录这条命令,然后将命令发送给集群内其他的服务,并记录在其他服务器的日志文件中,注意,只要保证各个服务器上的日志是相同的,并且各服务器都能以相同的顺序执行相同的命令的话,那么集群中的每个节点的执行结果也都会是一样的。
这种数据共识的机制就叫复制状态机,目的是通过日志复制和回放的方式来实现集群中所有节点内的状态一致性。
其实 MySQL 中的主从复制,通过 binlog 操作日志来实现从主库到从库的数据复制的,就是应用了这种复制状态机的机制。所以这种方式不是 MySQL 特有的。
除了我上面提到的 Raft 协议以外,在 Redis Cluster 中也用到了 backlog 来实现主从节点的数据复制,其方式和 MySQL 一模一样。
可以这么说,几乎所有的存储系统或数据库,基本都用了这样一套方法来解决数据复制和备份恢复等问题。这一点你可以在学习中进一步体会。
总结
今天,我们先从一个案例出发,了解了在互联网流量读多写少的情况下,需要通过“读写分离”提升系统的并发能力,又因为“读写分离”的前提是做 “主+从”的数据集群架构,所以我们又讲了主从复制的原理,以及怎么解决主从复制带来的延迟。
总的来说,在面试中,回答 MySQL 实现读写分离问题的前提,是你要掌握这些内容(这是初中级研发工程师都需要了解并掌握的):MySQL 主从复制的原理、模式、存在的问题,怎么解决。
对于中高级研发工程师来说,不仅要掌握这些内容,还要展现出对技术的抽象能力,例如本讲中的复制状态机的原理和应用场景。
12 写多读少:MySQL 如何优化数据存储方案?
上一讲,我带你学习了在高并发读多写少的场景下,数据库的一种优化方案:读写分离。通过主从复制的技术把数据复制多份,读操作只读取从数据库中的数据,这样就增强了抵抗大量并发读请求的能力,提升了数据库的查询性能。这时,你的系统架构如下:
系统架构图
案例背景
假设在这样的背景下,面试官出了一道考题:
公司现有业务不断发展,流量剧增,交易数量突破了千万订单,但是订单数据还是单表存储,主从分离后,虽然减少了缓解读请求的压力,但随着写入压力增加,数据库的查询和写入性能都在下降,这时你要怎么设计架构?
这个问题可以归纳为:数据库写入请求量过大,导致系统出现性能与可用性问题。
要想解决该问题,你可以对存储数据做分片,常见的方式就是对数据库做“分库分表”,在实现上有三种策略:垂直拆分、水平拆分、垂直水平拆分。所以一些候选人通常会直接给出这样的回答“可以分库分表,比如垂直拆分、水平拆分、垂直水平拆分”。
这么回答真的可以吗?
案例分析
我在面试候选人时发现,大部分研发工程师都能把分库分表策略熟练地回答出来,因为这个技术是常识,那你可能会问了:既然这个技术很普遍,大家都知道,面试官为什么还要问呢?
虽然分库分表技术方案很常见,但是在面试中回答好并不简单。因为面试官不会单纯浮于表面问你“分库分表的思路”,而是会站在业务场景中,当数据出现写多读少时,考察你做分库分表的整体设计方案和技术实现的落地思路。一般会涉及这样几个问题:
- 什么场景该分库?什么场景该分表?
- 复杂的业务如何选择分片策略?
- 如何解决分片后的数据查询问题?
案例解答
如何确定分库还是分表?
针对“如何确定分库还是分表?”的问题,你要结合具体的场景。
- 何时分表
当数据量过大造成事务执行缓慢时,就要考虑分表,因为减少每次查询数据总量是解决数据查询缓慢的主要原因。你可能会问:“查询可以通过主从分离或缓存来解决,为什么还要分表?”但这里的查询是指事务中的查询和更新操作。
- 何时分库
为了应对高并发,一个数据库实例撑不住,即单库的性能无法满足高并发的要求,就把并发请求分散到多个实例中去(这种应对高并发的思路我之前也说过)。
总的来说,分库分表使用的场景不一样: 分表是因为数据量比较大,导致事务执行缓慢;分库是因为单库的性能无法满足要求。
如何选择分片策略?
在明确分库分表的场景后,面试官一般会追问“怎么进行分片?”换句话说就是按照什么分片策略对数据库进行分片?
- 垂直拆分
垂直拆分是根据数据的业务相关性进行拆分。比如一个数据库里面既存在商品数据,又存在订单数据,那么垂直拆分可以把商品数据放到商品库,把订单数据放到订单库。一般情况,垂直拆库常伴随着系统架构上的调整。
垂直拆分
比如在对做系统“微服务”改造时,将原本一个单体系统拆分成多个子系统,每个系统提供单独的服务,那么随着应用层面的拆分带来的也有数据层面的拆分,将一个主库的数据表,拆分到多个独立的子库中去。
对数据库进行垂直拆分最常规,优缺点也很明显。
垂直拆分可以把不同的业务数据进行隔离,让系统和数据更为“纯粹”,更有助于架构上的扩展。但它依然不能解决某一个业务的数据大量膨胀的问题,一旦系统中的某一个业务库的数据量剧增,比如商品系统接入了一个大客户的供应链,对于商品数据的存储需求量暴增,在这个时候,就要把数据拆分到多个数据库和数据表中,也就是对数据做水平拆分。
- 水平拆分
垂直拆分随架构改造而拆分,关注点在于业务领域,而水平拆分指的是把单一库表数据按照规则拆分到多个数据库和多个数据表中,比如把单表 1 亿的数据按 Hash 取模拆分到 10 个相同结构的表中,每个表 1 千万的数据。并且拆分出来的表,可以分别存放到不同的物理数据库中,关注点在于数据扩展。
水平拆分
拆分的规则就是哈希分片和范围分片(这部分内容你可以参考 04 讲中的内容,我就不赘述了)。但我要强调一下 Range 分片,因为 04 讲中有些同学对 Range 分片解决数据热点的问题有些误解。
- Range(范围分片)
是按照某一个字段的区间来拆分,最好理解的就是按照时间字段分片,比如可以把一个月的数据放入一张表中,这样在查询时就可以根据时间先定位数据存储在哪个表里面,再按照查询条件来查询。
但是按时间字段进行范围分片的场景并不多,因为会导致数据分布不均,毕竟不是每个月的销量都是平均的。所以常见的 Range 分片是按照字段类型,比如按照商品的所属品类进行分片。这样与 Hash 分片不同的是,Range 分片就可以加入对于业务的预估。
Range 分片
但是同样的,由于不同“商品品类”的业务热点不同,对于商品数据存储也会存在热点数据问题,这个时候处理的手段有两个。
1、垂直扩展
由于 Range 分片是按照业务特性进行的分片策略,所以可以对热点数据做垂直扩展,即提升单机处理能力。在业务发展突飞猛进的初期,建议使用“增强单机硬件性能”的方式提升系统处理能力,因为此阶段,公司的战略往往是发展业务抢时间,“增强单机硬件性能”是最快的方法。
2、分片元数据
单机性能总是有极限的,互联网分布式架构设计高并发终极解决方案还是水平扩展,所以结合业务的特性,就需要在 Range 的基础上引入“分片元数据”的概念:分片的规则记录在一张表里面,每次执行查询的时候,先去表里查一下要找的数据在哪个分片中。
这种方式的优点是灵活性高,并且分片规则可以随着业务发展随意改动。比如当某个分片已经是热点了,那就可以把这个分片再拆成几个分片,或者把这个分片的数据移到其他分片中去,然后修改一下分片元数据表,就可以在线完成数据的再分片了。
分片元数据
但你要注意,分片元数据本身需要做高可用(面试考察点可以参考 04 讲中的内容)。方案缺点是实现起来复杂,需要二次查询,需要保证分片元数据服务的高可用。不过分片元数据表可以通过缓存进行提速。
- 垂直水平拆分
垂直水平拆分,是综合垂直和水平拆分方式的一种混合方式,垂直拆分把不同类型的数据存储到不同库中,再结合水平拆分,使单表数据量保持在合理范围内,提升性能。
垂直水平拆分
如何解决数据查询问题?
分库分表引入的另外一个问题就是数据查询的问题(比较常见),比如面试官会问类似的问题:
在未分库分表之前,我们查询数据总数时,可以直接通过 SQL 的 count() 命令,现在数据分片到多个库表中,如何解决呢?
解题思路很多,你可以考虑其他的存储方案,比如聚合查询使用频繁时,可以将聚合查询的数据同步到 ES 中,或者将计数的数据单独存储在一张表里。如果是每日定时生成的统计类报表数据,也可以将数据同步到 HDFS 中,然后用一些大数据技术来生成报表。
技术认知
不夸张地说,MySQL 是每个后端开发人员都要精通的数据库,因为其开源、轻量级,且有着金融级别的事务保证,所以一直是互联网项目的标配。
但是随着近些年技术的发展,下一代存储技术上出现了 NewSQL ,我觉得未来它可能会取代 MySQL :
NewSQL 是新一代的分布式数据库,不但具备分布式存储系统的高性能、高可用,弹性扩容等能力,还兼容传统关系型数据库的 SQL 标准。并且,还提供了和传统关系型数据库不相上下的事务保证,是具备了支撑未来交易类业务能力的。
为了能体现你个人的技术视野,我希望你在面试的过程中,也谈一些与存储这个技术领域有关的内容,比如 NewSQL 的发展和相关开源产品,如 CockroachDB、TiDB。你可以在面试前熟悉一下 NewSQL 数据库的原理,然后以其和现有关系型的区别为切入点,和面试官讨论即可。
总结
总的来说,在面对数据库容量瓶颈和写请求并发量大时,你可以选择垂直分片和水平分片:垂直分片一般随着业务架构拆分来进行;水平分片通常按照 Hash(哈希分片)取模和 Range(范围分片)进行,并且,通常的形态是垂直拆分伴随着水平拆分,即先按照业务垂直拆分后,再根据数据量的多少决定水平分片。
Hash 分片在互联网中应用最为广泛,简单易实现,可以保证数据非常均匀地分布到多个分片,但其过滤掉了业务属性,不能根据业务特性进行调整。而 Range 分片却能预估业务,更高效地扫描数据记录(Hash 分片由于数据被打散,扫描操作的 I/O 开销更大)。除了 Hash 分片和 Range 分片,更为灵活的方式是基于分片元数据。
不过你要注意,这几种方式也会引入诸如聚合查询的问题,要想解决聚合查询,你可以让聚合查询记录存储在其他存储设备中(比如 ES、HDFS)。
最后,除了中规中矩地回答面试官提出的问题,我也希望你能展示自己的技术视野,选择 NewSQL 作为切入点。