简介:这是一份面向后端开发求职者与在校学生的 MySQL 面试知识点总结文档,围绕数据库原理与索引机制梳理高频考点,适合准备初中级后端岗位面试、需要系统复盘 MySQL 底层逻辑的读者。资源包共 1 个 docx 文件,约 40KB,以问答形式组织内容,便于按主题检索与背诵。文档覆盖关系型与非关系型数据库的区别、一条 SQL 语句从连接器到执行器的完整执行流程、索引的使用原因与哈希表/有序数组/搜索树三种底层数据结构、主键索引与二级索引的类型划分、MyISAM 与 InnoDB 实现 B+ 树索引的差异、InnoDB 选择 B+ 树的原因、普通索引与唯一索引的取舍、覆盖索引与索引下推、索引失效的常见操作,以及 change buffer、redo log 与 binlog 等日志机制。已有 251 人学习,内容偏重原理串联与面试追问应对,可帮助读者在短时间内建立从 SQL 执行到索引优化的完整知识框架。
1. MySQL面试题:为什么背了三百道还是挂在一道索引题上
很多人准备 MySQL 面试的方式是打开一份 PDF,从「什么是事务」背到「什么是 MVCC」,三百道题刷完,自我感觉良好。结果面试官一句「这条 SQL 为什么没走索引」,当场卡壳。问题不在于背得不够多,而在于背的是结论,不是判断过程。MySQL 面试题真正拉开差距的地方,从来不是「知不知道」,而是「给定一条 SQL、一张表结构、一个执行计划,你能不能说出它为什么慢、该怎么改」。这也是为什么同样刷题,有人能拿到 offer,有人连一面都过不了。
这篇内容面向的是正在准备后端/数据方向面试的工程师,也适合已经工作但想系统梳理 MySQL 知识的人。我不会给你一份「标准答案合集」,而是把 MySQL 面试题里最高频的几个考点——索引、事务、锁、执行计划、存储引擎——拆成「面试官到底想听什么」和「你该怎么答到点上」。每一块都配可复现的 SQL 和验证步骤,你在本地 MySQL 里跑一遍,比背十遍答案管用。热词里那些 mysql 安装教程、mysql 下载、mysql workbench 使用教程,属于前置操作,默认你已经有一个能连上的 MySQL 实例,版本 5.7 或 8.0 都行,差异我会在涉及的地方点出来。
先说一个反直觉的结论:MySQL 面试题里,索引相关的问题占了将近一半,但大部分候选人答不好,不是因为不懂 B+ 树,而是因为没建立「优化器视角」。面试官问「为什么没走索引」,他想听的不是「因为不满足最左前缀」,而是你能不能说清楚:优化器在什么条件下会放弃索引、回表成本怎么估算、覆盖索引为什么能救场。这些判断,光背概念是背不出来的,得动手看执行计划。
接下来的章节,我会按「面试高频考点 → 面试官真实意图 → 可复现验证 → 答法模板」这个顺序展开。中间会有一个专门的避坑章节,讲那些面试现场最容易翻车的细节。最后一章给你一套自检方法,让你在面试前能自己判断「我这个回答能不能过」。
2. 索引与执行计划:面试官问「为什么没走索引」时到底在考什么
2.1 从 EXPLAIN 的输出反推优化器的决策逻辑
面试里最高频的场景是这样的:面试官给一张表、一条 SQL,问你「这条 SQL 走索引了吗,为什么」。很多人第一反应是看 WHERE 条件有没有索引列,但真正该做的是看 EXPLAIN 的输出。EXPLAIN 的每一列都在告诉你优化器的判断,关键是你要会读。
先建一张典型的面试用表,模拟用户订单场景:
CREATE TABLE `t_order` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` BIGINT UNSIGNED NOT NULL, `order_no` VARCHAR(32) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这张表有两个二级索引:idx_user_status是联合索引,idx_created是单列索引。现在跑几条 SQL,看 EXPLAIN 输出:
-- 查询1:联合索引的最左列 EXPLAIN SELECT * FROM t_order WHERE user_id = 1001; -- 查询2:跳过最左列 EXPLAIN SELECT * FROM t_order WHERE status = 1; -- 查询3:联合索引两列都用上 EXPLAIN SELECT * FROM t_order WHERE user_id = 1001 AND status = 1; -- 查询4:范围查询打断后续列 EXPLAIN SELECT * FROM t_order WHERE user_id > 1001 AND status = 1;跑完你会看到,查询 1 和查询 3 的key列显示idx_user_status,查询 2 显示NULL(全表扫描),查询 4 虽然用了索引,但key_len比查询 3 短——因为user_id > 1001是范围条件,status列在索引里无法继续用于精确匹配。
这里的关键参数是key_len。它表示优化器实际使用的索引字节数。对于idx_user_status (user_id BIGINT, status TINYINT),在 utf8mb4 下,user_id占 8 字节(BIGINT),status占 1 字节(TINYINT),加上可空标记各 1 字节,如果两列都用于等值匹配,key_len大约是 8+1+1+1=11(具体值随版本和列定义略有差异)。如果只用了user_id,key_len会明显变小。面试时你能主动说出「看 key_len 判断联合索引用了几列」,比背「最左前缀」四个字有说服力得多。
type列同样重要。从好到差的顺序是system > const > eq_ref > ref > range > index > ALL。面试里如果看到type=ALL,基本可以判定全表扫描;看到type=index,说明走了索引但扫了整棵索引树,通常也不理想。rows列是优化器估算的扫描行数,filtered是过滤后剩余行的百分比,这两个值结合起来能判断优化器对这条 SQL 的成本预估。
提示:EXPLAIN 在 MySQL 8.0 里可以用
EXPLAIN FORMAT=JSON看到更详细的成本估算,包括每个候选索引的 cost 值。面试时如果被追问「优化器为什么选了这个索引」,能提到 cost 比较会加分。
2.2 覆盖索引、回表与索引下推:三个必须能画出来的概念
面试官问完「为什么没走索引」,紧接着往往会问「什么是回表」「覆盖索引为什么快」。这三个概念是连在一起的,你得能一条线讲清楚。
InnoDB 的二级索引叶子节点存的是索引列的值加主键值。当你通过二级索引找到主键后,还需要拿主键去聚簇索引里取完整行数据,这个过程就是回表。回表是一次额外的 B+ 树查找,成本不低。覆盖索引的意思是,查询需要的所有列都在二级索引里,不需要回表。
用刚才的表验证:
-- 需要回表:SELECT * 要取所有列 EXPLAIN SELECT * FROM t_order WHERE user_id = 1001 AND status = 1; -- 覆盖索引:只取索引里有的列 EXPLAIN SELECT user_id, status FROM t_order WHERE user_id = 1001 AND status = 1;看 EXPLAIN 的Extra列。第一条通常没有特殊标记,第二条会出现Using index,这就是覆盖索引的标志。面试时你说「Extra 里出现 Using index 说明是覆盖索引,不需要回表」,面试官就知道你是真跑过。
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化。在没有 ICP 之前,存储引擎通过索引找到主键后,把行数据返回给 Server 层,由 Server 层去过滤 WHERE 条件里索引无法覆盖的部分。有了 ICP,存储引擎在索引层面就先把能过滤的条件过滤掉,减少回表次数。
-- 假设查询:user_id = 1001 AND status > 0 AND amount > 500 -- idx_user_status 只能覆盖 user_id 和 status -- amount 的条件无法在索引里过滤 EXPLAIN SELECT * FROM t_order WHERE user_id = 1001 AND status > 0 AND amount > 500;如果Extra里出现Using index condition,说明 ICP 生效了。面试时被问到「索引下推解决了什么问题」,你可以说:它把部分 WHERE 条件下推到存储引擎层,在索引扫描阶段就过滤掉不满足条件的记录,减少回表次数。这个回答比「减少 IO」具体得多。
2.3 面试答法:从「是什么」升级到「怎么判断」
知道概念之后,面试答法要升级。面试官问「为什么没走索引」,一个能过的回答结构是这样的:
第一步,确认表结构和索引定义。说清楚有哪些索引、列顺序是什么。第二步,看 EXPLAIN 的key、type、key_len、rows、Extra五个关键列。第三步,判断是「没有可用索引」「有索引但优化器放弃」还是「用了索引但效率不高」。第四步,给出改写建议。
举个例子,面试官问:「SELECT * FROM t_order WHERE status = 1为什么没走索引?」你可以这样答:这张表有idx_user_status (user_id, status),但查询条件只有status,不满足最左前缀,优化器无法用这个索引定位。idx_created也和status无关。所以只能全表扫描。如果要优化,要么建一个status的单列索引,要么把查询改成带上user_id。但要注意,如果status的区分度很低(比如只有 0 和 1 两种值),建索引后优化器也可能因为选择性太差而放弃,这时候需要考虑用覆盖索引或者改写查询逻辑。
这个回答里包含了「索引结构判断」「最左前缀」「区分度」「优化器选择」四个层次,面试官能看出你不是背的。
3. 事务与锁:从「八股文」到能解释死锁现场
3.1 隔离级别与 MVCC:面试官想听的是「你见过什么现象」
事务隔离级别是 MySQL 面试题的必考项,但大部分人答的是教科书定义:读未提交、读已提交、可重复读、串行化,分别对应什么问题。面试官如果只问到这个层面,说明他在走流程;如果追问「你实际遇到过什么隔离级别导致的问题」,那才是拉开差距的地方。
先建一张实验表:
CREATE TABLE `t_account` ( `id` INT PRIMARY KEY, `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; INSERT INTO t_account (id, balance) VALUES (1, 1000.00);然后开两个会话,验证可重复读下的幻读现象。会话 A 和会话 B 分别执行:
-- 会话A SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM t_account WHERE id = 1; -- 看到 balance = 1000 -- 会话B UPDATE t_account SET balance = 2000 WHERE id = 1; COMMIT; -- 会话A再次查询 SELECT * FROM t_account WHERE id = 1; -- 仍然是 1000 COMMIT;在可重复读级别下,会话 A 两次查询看到的结果一致,这是 MVCC 通过 ReadView 实现的。但如果你在会话 A 里执行UPDATE t_account SET balance = balance + 100 WHERE id = 1,再查询,会发现 balance 变成了 2100——因为当前读看到的是最新版本。这个「快照读」和「当前读」的区别,是面试官判断你是否真理解 MVCC 的关键。
面试时被问到「可重复读怎么实现的」,不要只答「MVCC」,要说出:每行记录有隐藏的DB_TRX_ID和DB_ROLL_PTR,ReadView 记录了创建时刻活跃的事务 ID 列表,通过可见性判断决定读哪个版本。同时要提到,可重复读下 InnoDB 用间隙锁(Gap Lock)在很大程度上避免了幻读,但并不是完全避免——某些场景下仍然可能出现。
3.2 行锁、间隙锁与死锁:用两个会话复现一次
锁的问题是面试里最容易翻车的。面试官问「什么是间隙锁」,你答「锁住一个范围」,这不够。他要的是你能说清楚:间隙锁在什么隔离级别下生效、锁的是什么、什么时候会冲突。
复现一次死锁:
-- 会话A BEGIN; UPDATE t_account SET balance = balance - 100 WHERE id = 1; -- 不提交 -- 会话B BEGIN; UPDATE t_account SET balance = balance - 100 WHERE id = 2; -- 不提交 -- 会话A UPDATE t_account SET balance = balance - 100 WHERE id = 2; -- 阻塞 -- 会话B UPDATE t_account SET balance = balance - 100 WHERE id = 1; -- 死锁,MySQL 自动回滚其中一个事务执行完后用SHOW ENGINE INNODB STATUS查看死锁日志,能看到LATEST DETECTED DEADLOCK段落,里面记录了两个事务分别持有什么锁、等待什么锁。面试时如果你能说出「用 SHOW ENGINE INNODB STATUS 看死锁日志,重点看 TRANSACTION 段落的 lock_mode 和 waiting for this lock to be granted」,面试官会认为你有实战经验。
间隙锁的关键点:在可重复读级别下,UPDATE ... WHERE id > 5这样的范围条件会锁住间隙,防止其他事务在间隙中插入。但在读已提交级别下,间隙锁基本不生效。面试时被问到「间隙锁什么时候用」,你要能说出「可重复读 + 范围条件 + 当前读」这三个前提。
3.3 事务面试的答法:用「现象 → 原因 → 验证」三段式
事务和锁的问题,最好的答法是三段式:先说现象,再说原因,最后说怎么验证。比如面试官问「什么是幻读」,你可以这样答:
现象是:同一个事务里,两次执行同样的范围查询,第二次看到了第一次没有的行。原因是:在可重复读级别下,快照读通过 MVCC 避免了大部分幻读,但当前读(比如SELECT ... FOR UPDATE)仍然可能看到新插入的行,因为间隙锁只能锁住已有的间隙,无法阻止新间隙的产生。验证方法是:开两个会话,一个用SELECT ... FOR UPDATE做范围查询,另一个插入新行,观察是否阻塞。
这个答法比「幻读是指一个事务读取了另一个事务插入的数据」这种定义式回答强得多,因为它展示了你的判断过程。
4. 存储引擎与日志:InnoDB 的「黑匣子」怎么在面试里讲清楚
4.1 redo log、undo log、binlog 的分工与面试常见追问
InnoDB 的日志系统是面试里区分度很高的考点。很多人知道「redo log 保证持久性,undo log 保证原子性,binlog 用于主从复制」,但面试官追问「redo log 和 binlog 的两阶段提交怎么做的」「为什么需要两阶段提交」,就答不上来了。
redo log 是 InnoDB 引擎层的物理日志,记录的是「在某个数据页上做了什么修改」。它采用循环写的方式,空间固定。undo log 是逻辑日志,记录的是「反向操作」,比如插入对应删除、更新对应反向更新,用于事务回滚和 MVCC 的快照读。binlog 是 Server 层的逻辑日志,记录的是「执行了什么 SQL 或产生了什么行变更」,用于主从复制和数据恢复。
两阶段提交的过程:事务提交时,InnoDB 先写 redo log,标记为 prepare 状态;然后 Server 层写 binlog;最后 InnoDB 把 redo log 标记为 commit 状态。为什么需要两阶段?因为如果不这样做,比如先写 binlog 再写 redo log,写完 binlog 后崩溃,重启后 redo log 没有记录,但 binlog 已经写了,从库会执行这个事务而主库没有,导致主从不一致。两阶段提交保证了 redo log 和 binlog 的逻辑一致。
面试时被问到「redo log 和 binlog 有什么区别」,你可以从四个维度答:层级(引擎层 vs Server 层)、内容(物理 vs 逻辑)、写入方式(循环写 vs 追加写)、用途(崩溃恢复 vs 主从复制)。每个维度一句话,干净利落。
4.2 缓冲池与刷脏:面试官问「更新语句的执行流程」时怎么答
「一条 UPDATE 语句在 MySQL 里是怎么执行的」是高频题。完整的回答要覆盖:连接器 → 分析器 → 优化器 → 执行器 → 存储引擎 → 缓冲池 → redo log → binlog。
具体来说:执行器调用 InnoDB 接口,InnoDB 先在缓冲池里找对应的数据页。如果命中,直接修改缓冲池里的页;如果不命中,从磁盘读入缓冲池再修改。修改后的页变成脏页,同时写 redo log。事务提交时,redo log 按策略刷盘(innodb_flush_log_at_trx_commit控制),binlog 也写入。脏页的刷盘由后台线程异步完成,不阻塞事务提交。
这里的关键参数是innodb_flush_log_at_trx_commit。值为 1 时,每次事务提交都刷 redo log 到磁盘,最安全但性能最差;值为 0 时,每秒刷一次,崩溃可能丢一秒数据;值为 2 时,写入操作系统缓存,每秒刷盘。面试时被问到「怎么平衡性能和安全」,能说出这个参数的不同取值和取舍,比泛泛而谈「看业务需求」有说服力。
注意:MySQL 8.0 的 redo log 写入机制有变化,引入了无锁化设计,但面试时如果面试官没追问版本差异,按通用原理答即可,不要主动展开不确定的细节。
4.3 MyISAM 与 InnoDB 的对比:别只答「一个支持事务一个不支持」
MyISAM 和 InnoDB 的对比是基础题,但很多人只答「InnoDB 支持事务,MyISAM 不支持」。面试官如果追问「还有呢」,就没了。完整的对比至少包括:事务支持、锁粒度(表锁 vs 行锁)、外键、崩溃恢复、索引结构(都是 B+ 树但叶子节点存的内容不同)、全文索引(MyISAM 原生支持,InnoDB 5.6 后也支持)。
更重要的是,你要能说出「为什么现在默认用 InnoDB」。因为 InnoDB 支持事务和行锁,在并发场景下表现更好;支持崩溃恢复,数据安全性更高;支持外键,数据一致性更有保障。MyISAM 的优势在于某些只读场景下查询速度略快,但这个优势在现代硬件和 InnoDB 优化下已经不明显。
面试时如果被问到「什么场景下会用 MyISAM」,可以说:现在基本不用了,除非是只读的历史归档表,且对空间占用极其敏感。这个回答既展示了知识,也展示了工程判断。
5. 避坑与排查:面试现场最容易翻车的五个细节
5.1 坑一:把「最左前缀」当成万能答案
现象:面试官问「为什么没走索引」,候选人张口就答「因为不满足最左前缀」。面试官追问「那这条 SQL 满足最左前缀为什么也没走」,候选人卡住。
原因:最左前缀只是索引可用的必要条件,不是充分条件。优化器还会考虑区分度、回表成本、索引覆盖等因素。如果status列只有 0 和 1 两个值,即使满足最左前缀,优化器也可能因为选择性太差而放弃索引。
解决:答「没走索引」时,先确认索引是否存在、是否满足最左前缀,再看rows和filtered估算,最后考虑区分度和回表成本。如果区分度低,可以建议用覆盖索引或改写查询。
5.2 坑二:混淆「快照读」和「当前读」
现象:面试官问「可重复读下两次查询结果一样吗」,候选人答「一样」,面试官追问「那 UPDATE 之后呢」,候选人答不上来。
原因:没有区分快照读和当前读。普通的SELECT是快照读,走 MVCC;SELECT ... FOR UPDATE、UPDATE、DELETE是当前读,读最新版本。
解决:答事务隔离相关问题时,先明确「是快照读还是当前读」。快照读看 ReadView,当前读看锁。两者机制不同,不能混为一谈。
5.3 坑三:死锁日志看不懂就瞎猜
现象:面试官问「遇到过死锁吗,怎么排查的」,候选人说「看日志」,追问「日志里看什么」,答不上来。
原因:没有真正用SHOW ENGINE INNODB STATUS看过死锁日志。
解决:死锁日志的LATEST DETECTED DEADLOCK段落里,重点看两个TRANSACTION块,每个块里有lock_mode(锁类型)、waiting for this lock to be granted(等待的锁)、holds the lock(s)(持有的锁)。找到两个事务互相等待的锁,就能定位死锁原因。
5.4 坑四:把 binlog 格式当成无关紧要的细节
现象:面试官问「binlog 有几种格式」,候选人答「STATEMENT、ROW、MIXED」,追问「有什么区别、怎么选」,答不上来。
原因:只背了名字,没理解每种格式的适用场景。
解决:STATEMENT 记录 SQL 语句,日志量小但某些函数(如NOW())可能导致主从不一致;ROW 记录行变更,日志量大但一致性好;MIXED 由 MySQL 自动选择。现在默认推荐 ROW,因为一致性优先。面试时能说出「ROW 格式下 binlog 记录的是每行变更前后的值,所以从库重放时不会因为函数不确定性导致数据不一致」,就到位了。
5.5 坑五:忽略版本差异
现象:候选人按 MySQL 5.7 的知识回答,面试官用的是 8.0,某些行为已经变了。
原因:没有关注版本差异。
解决:几个关键差异要记住:MySQL 8.0 默认字符集从 latin1 变成 utf8mb4;8.0 引入了降序索引、窗口函数、CTE;8.0 的 redo log 写入机制有优化;8.0 移除了查询缓存。面试时如果不确定版本,可以说「这个行为在 5.7 和 8.0 下可能不同,5.7 是这样的,8.0 我理解有变化,具体以实际版本为准」。这样既展示了知识边界,也展示了严谨态度。
6. 面试前自检:用一套 SQL 验证你的 MySQL 知识是不是真的
最后一章给你一套自检方法。面试前花一个小时,在本地 MySQL 里把这套 SQL 跑一遍,能跑通并解释清楚每一步,基本就稳了。
先建一套完整的实验环境:
-- 建库建表 CREATE DATABASE IF NOT EXISTS interview_test DEFAULT CHARSET utf8mb4; USE interview_test; CREATE TABLE `t_user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `age` TINYINT NOT NULL DEFAULT 0, `city` VARCHAR(32) NOT NULL DEFAULT '', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_name_age` (`name`, `age`), KEY `idx_city` (`city`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试数据 INSERT INTO t_user (name, age, city) VALUES ('Alice', 25, 'Beijing'), ('Bob', 30, 'Shanghai'), ('Charlie', 35, 'Beijing'), ('David', 28, 'Shenzhen'), ('Eve', 32, 'Shanghai');然后按顺序跑下面这组验证 SQL,每一条都要能说出预期结果和原因:
-- 1. 验证最左前缀 EXPLAIN SELECT * FROM t_user WHERE name = 'Alice'; -- 走 idx_name_age EXPLAIN SELECT * FROM t_user WHERE age = 25; -- 不走 idx_name_age -- 2. 验证覆盖索引 EXPLAIN SELECT name, age FROM t_user WHERE name = 'Alice'; -- Extra: Using index EXPLAIN SELECT * FROM t_user WHERE name = 'Alice'; -- 需要回表 -- 3. 验证索引下推 EXPLAIN SELECT * FROM t_user WHERE name = 'Alice' AND age > 20; -- Extra: Using index condition -- 4. 验证排序是否走索引 EXPLAIN SELECT * FROM t_user ORDER BY name, age; -- 可能走索引排序 EXPLAIN SELECT * FROM t_user ORDER BY age; -- 不走索引排序 -- 5. 验证事务隔离 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM t_user WHERE id = 1; -- 在另一个会话里 UPDATE t_user SET age = 99 WHERE id = 1; COMMIT; SELECT * FROM t_user WHERE id = 1; -- 仍然是原值 COMMIT;跑完之后,对照下面这张表检查你的理解:
| 验证项 | 关键观察 | 面试答法要点 |
|---|---|---|
| 最左前缀 | key 列是否显示索引名 | 不满足最左前缀时优化器无法定位 |
| 覆盖索引 | Extra 是否出现 Using index | 覆盖索引避免回表,减少 IO |
| 索引下推 | Extra 是否出现 Using index condition | 条件下推到引擎层,减少回表次数 |
| 排序 | Extra 是否出现 Using filesort | 排序字段顺序与索引一致可避免 filesort |
| 事务隔离 | 两次查询结果是否一致 | 快照读走 MVCC,当前读走锁 |
这套自检的价值在于:它逼你把「背过的答案」变成「跑过的现象」。面试时你说「我在本地验证过,Extra 里出现 Using index 就是覆盖索引」,比说「我记得覆盖索引是……」可信度高一个量级。
我自己的习惯是,每次面试前把这张表里的 SQL 跑一遍,重点看 EXPLAIN 的key、type、Extra三列。有一次面试被问到「ORDER BY 什么时候会走索引」,我直接说「排序字段的顺序和索引列顺序一致,且排序方向一致时,Extra 里不会出现 Using filesort」,面试官点了点头。这个结论不是背的,是跑出来的。
希望帮到你。
本文还有配套的精品资源,点击获取