MySQL 有哪些常见的面试题？
面试官：我看你简历上写了MySQL，对MySQL InnoDB引擎的索引了解吗？ 候选者：嗯啊，使用索引可以加快查询速度，其实上就是将无序的数据变成有序（有序就能加快检索速度） 候选者：在InnoDB引擎中，索引的底层数据结构是B+树 面试官：那为什么不使用红黑树或者B树呢？ 候选者：MySQL的数据是存储在硬盘的，在查询时一般是不能「一次性」把全部数据加载到内存中 候选者：红黑树是「二叉查找树」的变种，一个Node节点只能存储一个Key和一个Value 候选者：B和B+树跟红黑树不一样，它们算是「多路搜索树」，相较于「二叉搜索树」而言，一个Node节点可以存储的信息会更多，「多路搜索树」的高度会比「二叉搜索树」更低。 候选者：了解了区别之后，其实就很容易发现，在数据不能一次加载至内存的场景下，数据需要被检索出来，选择B或B+树的理由就很充分了（一个Node节点存储信息更多（相较于二叉搜索树），树的高度更低，树的高度影响检索的速度） 候选者：B+树相对于B树而言，它又有两种特性。 候选者：一、B+树非叶子节点不存储数据，在相同的数据量下，B+树更加矮壮。（这个应该不用多解释了，数据都存储在叶子节点上，非叶子节点的存储能存储更多的索引，所以整棵树就更加矮壮） 候选者：二、B+树叶子节点之间组成一个链表，方便于遍历查询（遍历操作在MySQL中比较常见）   候选者：我稍微解释一下吧，你可以脑补下画面 候选者：我们在MySQL InnoDB引擎下，每创建一个索引，相当于生成了一颗B+树。 候选者：如果该索引是「聚集(聚簇)索引」，那当前B+树的叶子节点存储着「主键和当前行的数据」 候选者：如果该索引是「非聚簇索引」，那当前B+树的叶子节点存储着「主键和当前索引列值」 候选者：比如写了一句sql：select * from user where id >=10，那只要定位到id为10的记录，然后在叶子节点之间通过遍历链表(叶子节点组成的链表)，即可找到往后的记录了。 候选者：由于B树是会在非叶子节点也存储数据，要遍历的时候可能就得跨层检索，相对麻烦些。 候选者：基于树的层级以及业务使用场景的特性，所以MySQL选择了B+树作为索引的底层数据结构。 候选者：对于哈希结构，其实InnoDB引擎是「自适应」哈希索引的（hash索引的创建由InnoDB存储引擎引擎自动优化创建，我们是干预不了） 面试官：嗯…那我了解了，顺便想问下，你知道什么叫做回表吗？ 候选者：所谓的回表其实就是，当我们使用索引查询数据时，检索出来的数据可能包含其他列，但走的索引树叶子节点只能查到当前列值以及主键ID，所以需要根据主键ID再去查一遍数据，得到SQL 所需的列 候选者：举个例子，我这边建了给订单号ID建了个索引，但我的SQL 是：select orderId,orderName from orderdetail where orderId = 123 候选者：SQL都订单ID索引，但在订单ID的索引树的叶子节点只有orderId和Id，而我们还想检索出orderName，所以MySQL 会拿到ID再去查出orderName给我们返回，这种操作就叫回表   候选者：想要避免回表，也可以使用覆盖索引（能使用就使用，因为避免了回表操作）。 候选者：所谓的覆盖索引，实际上就是你想要查出的列刚好在叶子节点上都存在，比如我建了orderId和orderName联合索引，刚好我需要查询也是orderId和orderName，这些数据都存在索引树的叶子节点上，就不需要回表操作了。 面试官：既然你也提到了联合索引，我想问下你了解最左匹配原则吗？ 候选者：嗯，说明这个概念，还是举例子比较容易说明 候选者：如有索引 (a,b,c,d)，查询条件 a=1 and b=2 and c>3 and d=4，则会在每个节点依次命中a、b、c，无法命中d 候选者：先匹配最左边的，索引只能用于查找key是否存在（相等），遇到范围查询 (>、<、between、like左匹配)等就不能进一步匹配了，后续退化为线性查找 候选者：这就是最左匹配原则   面试官：嗯嗯，我还想问下你们主键是怎么生成的？ 候选者：主键就自增的 面试官：那假设我不用MySQL自增的主键，你觉得会有什么问题呢？ 候选者：首先主键得保证它的唯一性和空间尽可能短吧，这两块是需要考虑的。 候选者：另外，由于索引的特性（有序），如果生成像uuid类似的主键，那插入的的性能是比自增的要差的 候选者：因为生成的uuid，在插入时有可能需要移动磁盘块（比如，块内的空间在当前时刻已经存储满了，但新生成的uuid需要插入已满的块内，就需要移动块的数据） 面试官：OK…   本文总结： 面试官：你是怎么理解InnoDB引擎中的事务的？ 候选者：在我的理解下，事务可以使「一组操作」要么全部成功，要么全部失败 候选者：事务其目的是为了「保证数据最终的一致性」。 候选者：举个例子，我给你发支付宝转了888块红包。那自然我的支付宝余额会扣减888块，你的支付宝余额会增加888块。 候选者：而事务就是保证我的余额扣减跟你的余额增添是同时成功或者同时失败的，这样这次转账就正常了   面试官：嗯，那你了解事务的几大特性吗？ 候选者：嗯，就是ACID嘛，分别是原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability）。 候选者：原子性指的是：当前事务的操作要么同时成功，要么同时失败。原子性由undo log日志来保证，因为undo log记载着数据修改前的信息。 候选者：比如我们要 insert 一条数据了，那undo log 会记录的一条对应的 delete 日志。我们要 update 一条记录时，那undo log会记录之前的「旧值」的update记录。 候选者：如果执行事务过程中出现异常的情况，那执行「回滚」。InnoDB引擎就是利用undo log记录下的数据，来将数据「恢复」到事务开始之前   候选者：一致性我稍稍往后讲，我先来说下隔离性 面试官：嗯… 候选者：隔离性指的是：在事务「并发」执行时，他们内部的操作不能互相干扰。如果多个事务可以同时操作一个数据，那么就会产生脏读、重复读、幻读的问题。 候选者：于是，事务与事务之间需要存在「一定」的隔离。在InnoDB引擎中，定义了四种隔离级别供我们使用： 候选者：分别是：read uncommit(读未提交)、read commit (读已提交)、repeatable read (可重复复读)、serializable (串行) 候选者：不同的隔离级别对事务之间的隔离性是不一样的（级别越高事务隔离性越好，但性能就越低），而隔离性是由MySQL的各种锁来实现的，只是它屏蔽了加锁的细节。   候选者：持久性指的就是：一旦提交了事务，它对数据库的改变就应该是永久性的。说白了就是，会将数据持久化在硬盘上。 候选者：而持久性由redo log 日志来保证，当我们要修改数据时，MySQL是先把这条记录所在的「页」找到，然后把该页加载到内存中，将对应记录进行修改。 候选者：为了防止内存修改完了，MySQL就挂掉了（如果内存改完，直接挂掉，那这次的修改相当于就丢失了）。 候选者：MySQL引入了redo log，内存写完了，然后会写一份redo log，这份redo log记载着这次在某个页上做了什么修改。 候选者：即便MySQL在中途挂了，我们还可以根据redo log来对数据进行恢复。 候选者：redo log 是顺序写的，写入速度很快。并且它记录的是物理修改（xxxx页做了xxx修改），文件的体积很小，恢复速度也很快。   候选者：回头再来讲一致性，「一致性」可以理解为我们使用事务的「目的」，而「隔离性」「原子性」「持久性」均是为了保障「一致性」的手段，保证一致性需要由应用程序代码来保证 候选者：比如，如果事务在发生的过程中，出现了异常情况，此时你就得回滚事务，而不是强行提交事务来导致数据不一致。   面试官：嗯，挺好的，讲了蛮多的 面试官：刚才你也提到了隔离性嘛，然后你说在MySQL中有四种隔离级别，能分别来介绍下吗？ 候选者：嗯，为了讲清楚隔离级别，我顺带来说下MySQL锁相关的知识吧。 候选者：在InnoDB引擎下，按锁的粒度分类，可以简单分为行锁和表锁。 候选者：行锁实际上是作用在索引之上的（索引上次已经说过了，这里就不赘述了）。当我们的SQL命中了索引，那锁住的就是命中条件内的索引节点（这种就是行锁），如果没有命中索引，那我们锁的就是整个索引树（表锁）。 候选者：简单来说就是：锁住的是整棵树还是某几个节点，完全取决于SQL条件是否有命中到对应的索引节点。 候选者：而行锁又可以简单分为读锁（共享锁、S锁）和写锁（排它锁、X锁）。 候选者：读锁是共享的，多个事务可以同时读取同一个资源，但不允许其他事务修改。写锁是排他的，写锁会阻塞其他的写锁和读锁。   候选者：我现在就再回到隔离级别上吧，就直接以例子来说明啦。 面试官：嗯… 候选者：首先来说下read uncommit(读未提交)。比如说：A向B转账，A执行了转账语句，但A还没有提交事务，B读取数据，发现自己账户钱变多了！B跟A说，我已经收到钱了。A回滚事务【rollback】，等B再查看账户的钱时，发现钱并没有多。 候选者：简单的定义就是：事务B读取到了事务A还没提交的数据，这种用专业术语来说叫做「脏读」。 候选者：对于锁的维度而言，其实就是在read uncommit隔离级别下，读不会加任何锁，而写会加排他锁。读什么锁都不加，这就让排他锁无法排它了。   候选者：而我们又知道，对于更新操作而言，InnoDB是肯定会加写锁的（数据库是不可能允许在同一时间，更新同一条记录的）。而读操作，如果不加任何锁，那就会造成上面的脏读。 候选者：脏读在生产环境下肯定是无法接受的，那如果读加锁的话，那意味着：当更新数据的时，就没办法读取了，这会极大地降低数据库性能。 候选者：在MySQL InnoDB引擎层面，又有新的解决方案（解决加锁后读写性能问题），叫做MVCC(Multi-Version Concurrency Control)多版本并发控制   候选者：在MVCC下，就可以做到读写不阻塞，且避免了类似脏读这样的问题。那MVCC是怎么做的呢？ 候选者：MVCC通过生成数据快照（Snapshot)，并用这个快照来提供一定级别（语句级或事务级）的一致性读取 候选者：回到事务隔离级别下，针对于 read commit (读已提交) 隔离级别，它生成的就是语句级快照，而针对于repeatable read (可重复读)，它生成的就是事务级的快照。   候选者：前面提到过read uncommit隔离级别下会产生脏读，而read commit (读已提交) 隔离级别解决了脏读。思想其实很简单：在读取的时候生成一个”版本号”，等到其他事务commit了之后，才会读取最新已commit的”版本号”数据。 候选者：比如说：事务A读取了记录(生成版本号)，事务B修改了记录(此时加了写锁)，事务A再读取的时候，是依据最新的版本号来读取的(当事务B执行commit了之后，会生成一个新的版本号)，如果事务B还没有commit，那事务A读取的还是之前版本号的数据。 候选者：通过「版本」的概念，这样就解决了脏读的问题，而「版本」其实就是对应快照的数据。 候选者：read commit (读已提交) 解决了脏读，但也会有其他并发的问题。「不可重复读」：一个事务读取到另外一个事务已经提交的数据，也就是说一个事务可以看到其他事务所做的修改。 候选者：不可重复读的例子：A查询数据库得到数据，B去修改数据库的数据，导致A多次查询数据库的结果都不一样【危害：A每次查询的结果都是受B的影响的】 候选者：了解MVCC基础之后，就很容易想到repeatable read (可重复复读)隔离级别是怎么避免不可重复读的问题了（前面也提到了）。 候选者：repeatable read (可重复复读)隔离级别是「事务级别」的快照！每次读取的都是「当前事务的版本」，即使当前数据被其他事务修改了(commit)，也只会读取当前事务版本的数据。   候选者：而repeatable read (可重复复读)隔离级别会存在幻读的问题，「幻读」指的是指在一个事务内读取到了别的事务插入的数据，导致前后读取不一致。 候选者：在InnoDB引擎下的的repeatable read (可重复复读)隔离级别下，快照读MVCC影响下，已经解决了幻读的问题（因为它是读历史版本的数据） 候选者：而如果是当前读（指的是 select * from table for update），则需要配合间隙锁来解决幻读的问题。 候选者：剩下的就是serializable (串行)隔离级别了，它的最高的隔离级别，相当于不允许事务的并发，事务与事务之间执行是串行的，它的效率最低，但同时也是最安全的。 面试官：嗯，可以的。我看你提到了MVCC了，不妨来说下他的原理？ 候选者：MVCC的主要是通过read view和undo log来实现的   候选者：undo log前面也提到了，它会记录修改数据之前的信息，事务中的原子性就是通过undo log来实现的。所以，有undo log可以帮我们找到「版本」的数据 候选者：而read view 实际上就是在查询时，InnoDB会生成一个read view，read view 有几个重要的字段，分别是：trx_ids（尚未提交commit的事务版本号集合），up_limit_id（下一次要生成的事务ID值），low_limit_id（尚未提交版本号的事务ID最小值）以及creator_trx_id（当前的事务版本号） 候选者：在每行数据有两列隐藏的字段，分别是DB_TRX_ID（记录着当前ID）以及DB_ROLL_PTR（指向上一个版本数据在undo log 里的位置指针） 候选者：铺垫到这了，很容易就发现，MVCC其实就是靠「比对版本」来实现读写不阻塞，而版本的数据存在于undo log中。 候选者：而针对于不同的隔离级别（read commit和repeatable read），无非就是read commit隔离级别下，每次都获取一个新的read view，repeatable read隔离级别则每次事务只获取一个read view 面试官：嗯，OK的。细节就不考究了，今天就到这里吧。 本文总结： 面试官：要不你来讲讲你们对MySQL是怎么调优的？ 候选者：哇，这命题很大阿…我认为，对于开发者而言，对MySQL的调优重点一般是在「开发规范」、「数据库索引」又或者说解决线上慢查询上。 候选者：而对于MySQL内部的参数调优，由专业的DBA来搞。 面试官：扯了这么多，你就是想表达你不会MySQL参数调优，对吧 候选者：草，被发现了。 面试官：那你来聊聊你们平时开发的规范和索引这块，平时是怎么样的吧。 候选者：嗯，首先，我们在生产环境下，创建数据库表，都是在工单系统下完成的（那就自然需要DBA审批）。如果在创建表时检测到没有创建索引，那就会直接提示warning（：   候选者：理论上来说，如果表有一定的数据量，那就应该要创建对应的索引。从数据库查询数据需要注意的地方还是蛮多的，其中很多都是平时积累来的。比如说： 候选者：1. 是否能使用「覆盖索引」，减少「回表」所消耗的时间。意味着，我们在select 的时候，一定要指明对应的列，而不是select * 候选者：2. 考虑是否组建「联合索引」，如果组建「联合索引」，尽量将区分度最高的放在最左边，并且需要考虑「最左匹配原则」 候选者：3.对索引进行函数操作或者表达式计算会导致索引失效 候选者：4.利用子查询优化超多分页场景。比如 limit offset , n 在MySQL是获取 offset + n的记录，再返回n条。而利用子查询则是查出n条，通过ID检索对应的记录出来，提高查询效率。 面试官：嗯… 候选者：5.通过explain命令来查看SQL的执行计划，看看自己写的SQL是否走了索引，走了什么索引。通过show profile 来查看SQL对系统资源的损耗情况（不过一般还是比较少用到的） 候选者：6.在开启事务后，在事务内尽可能只操作数据库，并有意识地减少锁的持有时间（比如在事务内需要插入&&修改数据，那可以先插入后修改。因为修改是更新操作，会加行锁。如果先更新，那并发下可能会导致多个事务的请求等待行锁释放）   面试官：嗯，你提到了事务，之前也讲过了事务的隔离级别嘛，那你线上用的是什么隔离级别？ 候选者：嗯，我们这边用的是Read Commit（读已提交），MySQL默认用的是Repeatable read（可重复读）。选用什么隔离级别，主要看应用场景嘛，因为隔离级别越低，事务并发性能越高。 候选者：（一般互联网公司都选择Read Commit作为主要的隔离级别） 候选者：像Repeatable read（可重复读）隔离级别，就有可能因为「间隙锁」导致的死锁问题。 候选者：但可能你已经知道，MySQL默认的隔离级别为Repeatable read。很大一部分原因是在最开始的时候，MySQL的binlog没有row模式，在read commit隔离级别下会存在「主从数据不一致」的问题 候选者：binlog记录了数据库表结构和表数据「变更」，比如update/delete/insert/truncate/create。在MySQL中，主从同步实际上就是应用了binlog来实现的（： 候选者：有了该历史原因，所以MySQL就将默认的隔离级别设置为Repeatable read   面试官：嗯，那我顺便想问下，你们遇到过类似的问题吗：即便走对了索引，线上查询还是慢。 候选者：嗯嗯，当然遇到过了 面试官：那你们是怎么做的？ 候选者：如果走对了索引，但查询还是慢，那一般来说就是表的数据量实在是太大了。 候选者：首先，考虑能不能把「旧的数据」给”删掉”，对于我们公司而言，我们都会把数据同步到Hive，说明已经离线存储了一份了。 候选者：那如果「旧的数据」已经没有查询的业务了，那最简单的办法肯定是”删掉”部分数据咯。数据量降低了，那自然，检索速度就快了… 面试官：嗯，但一般不会删的 候选者：没错，只有极少部分业务可以删掉数据（： 候选者：随后，就考虑另一种情况，能不能在查询之前，直接走一层缓存（Redis）。 候选者：而走缓存的话，又要看业务能不能忍受读取的「非真正实时」的数据（毕竟Redis和MySQL的数据一致性需要保证），如果查询条件相对复杂且多变的话（涉及各种group by 和sum），那走缓存也不是一种好的办法，维护起来就不方便了… 候选者：再看看是不是有「字符串」检索的场景导致查询低效，如果是的话，可以考虑把表的数据导入至Elasticsearch类的搜索引擎，后续的线上查询就直接走Elasticsearch了。 候选者：MySQL->Elasticsearch需要有对应的同步程序(一般就是监听MySQL的binlog，解析binlog后导入到Elasticsearch) 候选者：如果还不是的话，那考虑要不要根据查询条件的维度，做相对应的聚合表，线上的请求就查询聚合表的数据，不走原表。 候选者：比如，用户下单后，有一份订单明细，而订单明细表的量级太大。但在产品侧(前台)透出的查询功能是以「天」维度来展示的，那就可以将每个用户的每天数据聚合起来，在聚合表就是一个用户一天只有一条汇总后的数据。 候选者：查询走聚合后的表，那速度肯定杠杠的（聚合后的表数据量肯定比原始表要少很多） 候选者：思路大致的就是「以空间换时间」，相同的数据换别的地方也存储一份，提高查询效率   面试官：那我还想问下，除了读之外，写性能同样有瓶颈，怎么办？ 候选者：你说到这个，我就不困了。 候选者：如果在MySQL读写都有瓶颈，那首先看下目前MySQL的架构是怎么样的。 候选者：如果是单库的，那是不是可以考虑升级至主从架构，实现读写分离。 候选者：简单理解就是：主库接收写请求，从库接收读请求。从库的数据由主库发送的binlog进而更新，实现主从数据一致（在一般场景下，主从的数据是通过异步来保证最终一致性的）   面试官：嗯… 候选者：如果在主从架构下，读写仍存在瓶颈，那就要考虑是否要分库分表了 候选者：至少在我前公司的架构下，业务是区分的。流量有流量数据库，广告有广告的数据库，商品有商品的数据库。所以，我这里讲的分库分表的含义是：在原来的某个库的某个表进而拆分。 候选者：比如，现在我有一张业务订单表，这张订单表在广告库中，假定这张业务订单表已经有1亿数据量了，现在我要分库分表 候选者：那就会将这张表的数据分至多个广告库以及多张表中（： 候选者：分库分表的最明显的好处就是把请求进行均摊（本来单个库单个表有一亿的数据，那假设我分开8个库，那每个库1200+W的数据量，每个库下分8张表，那每张表就150W的数据量）。   面试官：你们是以什么来作为分库键的？ 候选者：按照我们这边的经验，一般来说是按照userId的（因为按照用户的维度查询比较多），如果要按照其他的维度进行查询，那还是参照上面的的思路（以空间换时间）。 面试官：那分库分表后的ID是怎么生成的？ 候选者：这就涉及到分布式ID生成的方式了，思路有很多。有借助MySQL自增的，有借助Redis自增的，有基于「雪花算法」自增的。具体使用哪种方式，那就看公司的技术栈了，一般使用Redis和基于「雪花算法」实现用得比较多。 候选者：至于为什么强调自增（还是跟索引是有序有关，前面已经讲过了，你应该还记得）   面试官：嗯，那如果我要分库分表了，迁移的过程是怎么样的呢 候选者：我们一般采取「双写」的方式来进行迁移，大致步骤就是： 候选者：一、增量的消息各自往新表和旧表写一份 候选者：二、将旧表的数据迁移至新库 候选者：三、迟早新表的数据都会追得上旧表（在某个节点上数据是同步的） 候选者：四、校验新表和老表的数据是否正常（主要看能不能对得上） 候选者：五、开启双读（一部分流量走新表，一部分流量走老表），相当于灰度上线的过程 候选者：六、读流量全部切新表，停止老表的写入 候选者：七、提前准备回滚机制，临时切换失败能恢复正常业务以及有修数据的相关程序。   面试官：嗯…今天就到这吧 本文总结： 线上不是给你炫技的地方，安稳才是硬道理。能用简单的方式去解决，不要用复杂的方式 我推荐一个拥有从零开始的文档的项目，既能用于毕设又可以在面试的时候大放异彩。 该项目业务极容易理解，代码结构还算是比较清晰，最可怕的是几乎每个方法和每个类都带有中文注释 拥有非常全的文档，作者从零搭建的过程一一都有记录，项目使用了蛮多的可靠和稳定的中间件的，包括并不限于SpringBoot、SpringDataJPA、MySQL、Docker、docker-compose、Kafka、Redis、Apollo、prometheus、Grafana、GrayLog、xxl-job等等。在使用每一个技术栈之前都讲述了为什么要使用，以及它的业务背景。我看过，他所说的场景是完全贴合线上环境的。 跟着README文档的部署使用姿势就能跑起来，一步一步debug挺有意思的，作者还搞了个前端后台管理系统就让整个系统变得更好理解了。并且在GitHub或者Gitee提的Issue几乎都会有回复，也非常乐于合并开发者们的pull request，会让人参与感贼强。 我相信在校、工作一年左右或常年做内网CRUD后台的同学去看看肯定会有所启发，作者会经常在群里回答该项目相关的问题和代码设计思路。 B站也在开始更新消息推送平台的视频哟！ 目前这个项目GitHub和Gitee加起来已经6Kstars了，我相信破万是迟早的事情。 嗯，没错。这个项目叫做austin，是我写的 消息推送平台-Austin就是奔着真实互联网线上项目去设计和实现的，将项目克隆下来把中间件换成目前公司在用的，完善下基础建设它就能成为线上项目 austin项目核心功能：统一的接口发送各种类型消息，对消息生命周期全链路追踪 项目出现意义：只要公司内有发送消息的需求，都应该要有类似austin的项目，对各类消息进行统一发送处理。这有利于对功能的收拢，以及提高业务需求开发的效率 austin项目核心流程：austin-api接收到发送消息请求，直接将请求进MQ。austin-handler消费MQ消息后由各类消息的Handler进行发送处理  项目Gitee链接： 项目GitHub链接： 《对线面试官》Java面试八股文 《对线面试官》是我一字字敲出来的，是我准备跳槽时按照自己的思路撰写而成。 它不会花长的篇幅讲述基础API相关的知识，会着重讲解重点高频率面试题，又或是实际工作中如何使用这项技术的它。模拟真实的面试场景，面试者实际的回答内容可能是怎么样的。 如果有看过的人，自然就懂，跟大部分的面试题是有区别的，这个系列也使我在知乎获近万赞，至少我认为它是有价值。等我下次跳槽，我绝对会回看我自己写的对线面试官系列。 为了增添趣味性会有图片版本（在其中穿插大量表情包或者梗），为了复习效率性会有纯文版本，纯文版本还有离线PDF电子书，离线PDF电子书有明亮版和暗黑版。 对线面试官 第一季 共40篇已结束 对线面试官 第二季 会有吗？会的 最近我把时间都花在写我的个人项目（消息推送平台），在项目上用到了很多的中间件。 这些中间件都值得拉出来做成面试题（事实上，当我作为面试官的时候也很喜欢问面试者在项目中相关的技术栈问题），我一直信奉着只要写在简历上的内容，肚子就得有墨水 在自学之路上，我已经把【基础重要的知识点】、【简历模板】、【思维导图】【对线面试官】等等全部整理成电子书，共有1263页！已经有8756个初学者都下载了！ 我把这些上传到网盘，你们有需要直接下载就好了。做到这份上了，不会还想白嫖吧？点赞和转发又不用钱。 链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom