今天要和大家分享的是我们训练营内部整理的字节抖音电商的一面面经。我已经把所有的问题和答案都整理好了,希望对大家有帮助:
之前提到加了布隆过滤器后,代码变得十分复杂,具体讲讲?
点赞数据Redis Zset + MQ异步落库,具体讲讲?
点赞相关的数据库表是什么样的?如果消费者失败怎么办?重复消费怎么解决的?
看到写了压测接口,怎么压测的?
项目中通过CompletableFuture并发调用下游服务,具体讲讲?
如果我要用到前两个异步调用的结果,怎么办?
MySQL索引分类
SQL语句执行流程
慢SQL的原因都可能发生在哪个流程上
事务隔离级别以及分别有什么问题
谈谈对四大日志的理解
手搓:用快排思想实现快速查找第k大的数
面经详解
之前提到加了布隆过滤器后,代码变得十分复杂,具体讲讲?
-
正确答案:布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否属于某个集合。它可能返回假阳性(False Positive),但不会返回假阴性(False Negative)。在实际应用中,比如缓存穿透防护、网页爬虫去重、数据库查询优化等场景,布隆过滤器非常有用。
-
解答思路: 当你在项目中引入布隆过滤器时,代码复杂度增加的原因主要体现在以下几个方面:
- 设计阶段:需要合理选择哈希函数的数量和位数组的大小,这涉及到数学计算和性能权衡。
- 实现阶段:需要手动管理底层的位数组、哈希函数的选择与组合,以及内存分配。
- 调试阶段:由于其“误判”特性,在出现问题时难以直接定位,需要日志记录、测试用例覆盖等手段辅助分析。
- 维护阶段:如果数据规模变化较大,可能需要重新调整参数甚至更换实现方式。
-
深度知识讲解:
布隆过滤器的核心原理是使用一个长度为 m 的位数组(bit array),初始值全为0,并使用 k 个独立的哈希函数将元素映射到位数组中的 k 个位置上。当插入一个元素时,k 个哈希函数分别计算出 k 个位置,并将这些位置置为1。当查询一个元素是否存在时,同样使用 k 个哈希函数得到 k 个位置,若所有位置都为1,则认为该元素可能存在;只要有一个位置为0,则肯定不存在。
优点:
- 空间效率极高,适合大数据量下的存在性检测。
- 插入和查询的时间复杂度均为 O(k),常数时间。
缺点:
- 存在误判(False Positive)的概率。
- 不支持删除操作(除非使用计数布隆过滤器)。
- 哈希函数的设计对性能影响很大。
点赞数据Redis Zset + MQ异步落库,具体讲讲?
-
正确答案:
在处理点赞数据时,使用 Redis 的 ZSet(有序集合)可以高效地记录用户对内容的点赞行为,并支持快速统计和排序。同时,为了防止直接写入数据库造成性能瓶颈,通常会结合消息队列(MQ)进行异步落库操作,保证系统的高并发与最终一致性。 -
解答思路:
- 使用 Redis 的 ZSet 存储点赞关系,键为内容ID,值为用户ID,分数可设为点赞时间戳。
- 每次点赞或取消点赞,只需在 ZSet 中添加或删除对应的用户ID。
- 同时将点赞事件发送到消息队列中,由后台消费者异步消费并持久化到数据库。
- 数据库定期合并更新,避免频繁写入影响性能。
这种方案兼顾了高性能、高可用性和数据一致性。
-
深度知识讲解:
一、Redis ZSet 原理与优势
Redis 的 ZSet 是一种带分值的 Set,底层使用跳表(SkipList)+ HashTable 实现。
- 跳表(SkipList) :用于维护元素的有序性,支持 O(log n) 时间复杂度的插入、删除和查找。
- 哈希表:用于存储成员到分值的映射,实现 O(1) 时间复杂度的成员存在性判断。
ZSet 特别适合以下场景:
- 排行榜系统(如点赞数、积分排名)
- 点赞/收藏等社交行为记录
- 需要按分值排序的场景(如按时间、权重)
点赞相关的数据库表是什么样的?如果消费者失败怎么办?重复消费怎么解决的?
-
点赞表结构设计:
- 首先考虑点赞的基本信息:谁点了哪个内容。
- 是否支持取消点赞?因此引入status字段。
- 可能有多个类型的点赞对象(帖子、评论等),所以引入target_type。
- 建立索引:user_id + target_id + target_type组合索引以提高查询效率。
-
消费者失败的处理:
- 消息队列中消费失败时,需根据业务场景决定是否重试。
- 一般会设置最大重试次数(如3次),超过则进入死信队列(DLQ)人工处理。
- 在重试过程中,要确保操作是幂等的,避免数据不一致。
-
解决重复消费问题:
- 引入唯一标识符(如消息ID或业务ID组合)作为幂等判断依据。
- 使用Redis缓存已处理的消息ID,设置TTL与消息生命周期一致。
- 或者在数据库中使用唯一约束(如联合唯一索引)来防止重复插入。
-
深度知识讲解:
-
点赞系统底层实现原理:
- 点赞本质上是一种行为日志,属于社交系统中最常见的交互行为之一。
- 为了应对高并发场景,往往采用异步写入方式(如通过Kafka/RabbitMQ)解耦生产者和消费者。
- 同时为提升读性能,可能引入缓存(如Redis)存储当前点赞数,并定时落库。
-
幂等性设计:
-
幂等性是指同一个请求无论执行多少次,结果都是一样的。
-
实现方式可以是:
- 数据库层面:利用唯一索引,插入时若冲突则忽略。
- 缓存层面:使用Redis记录已处理的消息ID,如 setnx 或 redis stream 中的 consumer group。
- 逻辑层面:每次处理前查询是否已经处理过该条数据。
-
-
消息队列消费失败处理策略:
- 重试机制:自动重试,配合指数退避算法减少对系统的冲击。
- 死信队列(DLQ):将多次失败的消息转移到专门的队列中供后续人工处理。
- 日志记录:记录失败原因,便于排查。
-
扩展:分布式点赞计数更新优化:
- 直接更新数据库计数字段在高并发下容易造成锁竞争。
- 更优方案是使用Redis计数器,如 hash 结构保存每个帖子的点赞数。
- 定期异步将Redis中的计数同步到数据库,减少数据库压力。
-
看到写了压测接口,怎么压测的?
-
正确答案:压测接口通常通过模拟高并发请求来测试系统在高压环境下的性能表现,常见的压测工具包括JMeter、Locust、wrk等。压测时需要关注吞吐量(TPS/QPS)、响应时间、错误率、资源使用情况等指标。
-
解答思路:
- 确定压测目标:例如测试某个API的最大承载能力、是否存在性能瓶颈。
- 使用压测工具配置测试场景:设置并发用户数、请求频率、持续时间等参数。
- 执行压测并收集数据:记录系统的响应时间、吞吐量、失败请求数等。
- 分析结果:识别性能瓶颈(如数据库慢查询、线程阻塞、网络延迟等)。
- 调整系统配置或优化代码后再次压测,验证改进效果。
-
深度知识讲解:
压测的底层原理与核心知识点
-
并发模型:压测工具一般基于多线程或多协程模型来模拟并发请求。例如,JMeter使用Java线程,而Locust基于Python的gevent协程实现。
-
网络通信:压测过程中涉及HTTP/HTTPS协议栈的完整交互,包括DNS解析、TCP握手、发送请求、等待响应、关闭连接等过程。这些步骤都会影响最终的响应时间。
-
负载类型:
- 固定并发:保持一定数量的并发用户持续请求。
- 阶梯增长:逐步增加并发用户数,观察系统在不同压力下的表现。
- 混合场景:模拟真实业务逻辑,组合多个接口调用。
-
性能指标:
- TPS(Transactions Per Second):每秒事务数。
- QPS(Queries Per Second):每秒查询数。
- RT(Response Time):平均响应时间。
- 错误率:请求失败的比例。
- 吞吐量:单位时间内系统处理的请求数量。
-
分布式压测:当单机无法产生足够压力时,可以使用分布式架构部署压测节点,由一个主控节点统一调度。
-
监控体系:压测过程中应配合监控系统(如Prometheus + Grafana)实时查看服务器CPU、内存、磁盘IO、网络带宽、GC频率等资源消耗情况。
-
服务端优化点:
- 数据库索引优化
- 连接池大小调整(如Druid、HikariCP)
- 缓存机制(如Redis)
- 异步处理(如消息队列)
- JVM参数调优
-
项目中通过CompletableFuture并发调用下游服务,具体讲讲?如果我要用到前两个异步调用的结果,怎么办?
- 正确答案:CompletableFuture 是 Java 8 引入的用于简化异步编程的类,支持链式调用、组合多个 Future、异常处理等。在项目中通过它并发调用下游服务时,可以使用
supplyAsync或runAsync启动异步任务,并通过thenApply、thenCompose、thenCombine等方法组合结果。
如果需要使用前两个异步调用的结果,可以通过 thenCombine() 方法将两个 CompletableFuture 的结果合并处理。
- 解答思路:
- 首先明确 CompletableFuture 的基本使用方式,包括异步执行任务、任务之间的依赖关系。
- 对于并发调用多个服务的情况,使用
CompletableFuture.supplyAsync()分别发起异步请求。 - 如果后续操作依赖前两个任务的结果,则使用
thenCombine()来等待两个任务完成并合并它们的结果。 - 还可以结合
allOf()或anyOf()控制多个 Future 的执行顺序或超时机制。
- 深度知识讲解:
1. CompletableFuture 基本原理
CompletableFuture 实现了 Future 和 CompletionStage 接口,提供了更强大的功能来处理异步任务。与传统的 Future 相比,它支持:
- 链式调用(如 thenApply, thenAccept, thenRun)
- 组合多个 Future(如 thenCombine, thenCompose)
- 异常处理(exceptionally, handle)
- 手动完成任务(complete)
2. 核心线程池模型
默认情况下,CompletableFuture 使用 ForkJoinPool.commonPool() 作为其线程池来执行异步任务。但在生产环境中建议自定义线程池以避免资源争用和更好地控制并发行为。
3. thenCombine 使用详解
thenCombine() 方法允许你将两个独立的 CompletableFuture 的结果进行合并处理,返回一个新的 CompletableFuture。它的函数签名如下:
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
其中:
other是另一个 CompletableFutre 的结果。fn是一个 BiFunction,接受两个结果并返回合并后的值。
4. 与其他组合方法的区别
| 方法名 | 用途说明 |
|---|---|
| thenApply | 对当前 Future 的结果进行转换 |
| thenAccept | 消费当前 Future 的结果,不返回新值 |
| thenRun | 在当前 Future 完成后执行一个无参数的任务 |
| thenCompose | 将当前 Future 的结果作为输入,生成新的 Future |
| thenCombine | 合并两个 Future 的结果 |
| allOf | 等待所有 Future 完成 |
| anyOf | 只要有一个 Future 完成就继续执行 |
MySQL索引分类
-
正确答案:MySQL索引主要分为以下几类:主键索引(PRIMARY KEY)、唯一索引(UNIQUE)、普通索引(INDEX)、全文索引(FULLTEXT)、组合索引(Composite Index)、空间索引(Spatial Index,仅适用于MyISAM存储引擎)。此外,在InnoDB中还支持聚集索引(Clustered Index)和辅助索引(Secondary Index)两种结构。
-
解答思路:
- 首先明确索引的基本作用是提高查询效率。
- 根据索引的特点和应用场景进行分类,比如唯一性约束、多字段联合索引、支持全文检索等。
- 结合底层数据结构(如B+树、哈希索引)分析不同索引的实现机制和适用场景。
- 强调索引的使用原则,如最左前缀匹配、避免冗余索引、聚簇索引与主键设计的关系等。
SQL语句执行流程
-
正确答案:SQL语句的执行流程主要包括以下几个阶段:解析(Parsing)、重写(Rewriting)、优化(Optimization)、执行(Execution)和结果返回(Result Return)。每个阶段都由数据库管理系统(如MySQL、PostgreSQL等)内部模块完成。
-
解答思路: SQL语句从用户输入到最终执行并返回结果,需要经历多个处理步骤。我们可以按照顺序理解这些步骤:
-
连接建立与身份验证:首先客户端通过网络连接到数据库服务器,并进行身份认证。
-
查询缓存(可选) :如果启用了查询缓存,数据库会先检查是否有相同的查询已经执行过,若有缓存结果则直接返回,跳过后续流程。
-
解析(Parsing) :
- 对SQL语句进行语法分析(Syntax Analysis),判断是否符合SQL语法规范。
- 构建解析树(Parse Tree)或抽象语法树(Abstract Syntax Tree, AST)。
-
预处理/重写(Preprocessing/Rewriting) :
- 检查表名、列名是否存在。
- 权限校验(如用户是否有权限访问该表)。
- 查询重写(如视图展开、子查询展开、谓词下推等)。
-
查询优化(Query Optimization) :
- 生成多个可能的执行计划。
- 基于统计信息选择最优执行路径(如使用索引还是全表扫描、JOIN顺序等)。
- 优化器分为基于规则(Rule-Based Optimizer, RBO)和基于代价(Cost-Based Optimizer, CBO)两种类型。
-
执行引擎(Execution Engine) :
- 根据优化后的执行计划调用存储引擎接口读取数据。
- 执行聚合、排序、过滤、JOIN等操作。
-
结果返回(Result Return) :
- 将执行结果格式化后返回给客户端。
- 如果启用查询缓存,也会将结果缓存起来以备下次使用。
-
-
深度知识讲解:
1. 解析阶段详解
数据库接收到SQL语句后,首先进行的是词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。例如在MySQL中,是由
sql/sql_lex.cc和sql/sql_yacc.yy文件中的代码实现的。- 词法分析:将字符串分解成一个个token(如SELECT、FROM、WHERE、标识符、常量等)。
- 语法分析:根据SQL语法构造一棵结构化的语法树(AST)。
2. 查询优化原理
查询优化是SQL执行中最复杂的部分,其核心在于如何高效地获取所需数据。
- 逻辑优化:包括谓词下推、视图合并、子查询展开、常量传播等。
- 物理优化:决定访问路径(如是否使用索引)、JOIN顺序、JOIN方式(嵌套循环、哈希JOIN、归并JOIN)等。
优化器会利用表的统计信息(如行数、列的唯一值数量、分布情况等)来估算不同执行计划的代价(cost),选择代价最低的执行计划。
3. 存储引擎交互
在执行阶段,SQL执行引擎会调用存储引擎的API来访问数据。例如在MySQL中,InnoDB作为默认存储引擎,提供了行级锁、事务支持等功能。
- SELECT语句会触发对B+树索引的查找。
- UPDATE语句会修改记录,并记录到Redo Log和Undo Log中。
- JOIN操作可能会使用不同的算法,如NLJ(Nested Loop Join)、BNL(Block Nested Loop)、Hash Join等。
4. 锁机制与事务隔离级别
SQL执行过程中还涉及到锁的获取与释放。例如在可重复读(RR)隔离级别下,InnoDB使用间隙锁(Gap Lock)防止幻读。
不同的事务隔离级别会影响并发性能和一致性保证。
5. 缓存机制
- 查询缓存:虽然某些数据库(如MySQL 8.0)已移除,但在早期版本中用于加速相同查询的重复执行。
- 缓冲池(Buffer Pool) :用于缓存磁盘上的数据页,减少I/O开销。
慢SQL的原因都可能发生在哪个流程上
-
正确答案:慢SQL可能发生在多个流程中,包括查询解析、执行计划生成、数据检索、连接操作、排序与聚合、事务处理、锁竞争、网络传输等。常见的原因包括索引缺失、查询语句不规范、表结构设计不合理、数据库配置不当、硬件资源瓶颈等。
-
解答思路:
-
首先明确SQL执行的基本流程:客户端发送请求 -> 查询解析 -> 查询重写 -> 执行计划生成(优化器)-> 执行引擎 -> 数据读取/写入 -> 返回结果。
-
在每个阶段都可能发生性能问题:
-
查询解析阶段:语法错误或复杂表达式可能导致解析时间变长。
-
执行计划生成阶段:优化器选择不佳的执行计划(如全表扫描而非使用索引)。
-
执行阶段:
- 表扫描方式不合适(如未使用索引)
- 大量数据排序或分组
- 多表连接效率低(如笛卡尔积)
- 锁等待或死锁
-
事务和并发控制阶段:事务过长导致行锁阻塞其他查询。
-
数据存储层:磁盘IO性能差、数据碎片化严重。
-
网络传输阶段:返回大量数据导致带宽占用高。
-
-
结合具体场景分析是哪个环节导致了性能下降,并提出优化建议。
-
-
深度知识讲解:
SQL执行流程中的性能瓶颈点详解
1. 查询解析阶段
当SQL语句过于复杂或存在嵌套子查询、正则表达式、动态SQL拼接等问题时,解析时间会增加。此外,如果SQL中频繁使用函数对字段进行转换(如 WHERE DATE_FORMAT(create_time, '%Y-%m') = '2024-03'),也可能影响优化器的判断。
2. 执行计划生成阶段
这是影响SQL性能的关键阶段。优化器根据统计信息(如索引选择率、数据分布)选择执行路径。若统计信息不准或索引缺失,可能导致如下问题:
- 全表扫描代替索引扫描
- 不合理的Join顺序
- 使用临时表或文件排序
可以通过 EXPLAIN 或 EXPLAIN ANALYZE 查看执行计划。
3. 数据检索阶段
- 索引缺失:没有合适的索引会导致全表扫描。
- 回表查询过多:使用二级索引后仍需回主键索引查数据,尤其在范围查询时效率低。
- 覆盖索引未使用:本可以只用索引完成查询,却依然访问表数据。
4. Join 操作
- 大表Join小表:若驱动表过大,可能导致大量磁盘IO。
- Join类型选择不当:如Hash Join vs Nested Loop Join vs Merge Join。
- Join条件无索引:连接字段没有索引,导致全表遍历。
5. 排序与聚合
- 使用
ORDER BY、GROUP BY、DISTINCT等操作时,若无法使用索引,则会触发文件排序(filesort),消耗大量内存或临时磁盘空间。 - 聚合函数(如
COUNT(*),SUM())在大数据集上效率低。
6. 事务与锁竞争
- 长事务持有锁时间过长,造成其他SQL等待。
- 死锁检测机制导致事务回滚。
- 行锁升级为表锁(如InnoDB在某些条件下会退化为表锁)。
7. 数据库配置与硬件瓶颈
- 内存不足导致频繁换页。
- 磁盘IO性能差,尤其是随机读写。
- 并发连接数过高,超出数据库承载能力。
- 缓冲池(Buffer Pool)配置太小,缓存命中率低。
8. 网络传输
- 返回大量数据给客户端,造成网络拥塞。
- 客户端未限制返回行数(如忘记加
LIMIT)。
事务隔离级别以及分别有什么问题
正确答案:事务隔离级别是数据库管理系统中用于控制事务并发执行时数据可见性和一致性的机制。SQL标准定义了四种隔离级别,分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。每种隔离级别解决了不同的并发问题,但也可能带来性能上的代价。
-
解答思路:
- 先明确事务的四个特性(ACID):原子性、一致性、隔离性、持久性。
- 隔离性由隔离级别控制,不同级别解决不同的并发问题(脏读、不可重复读、幻读、更新丢失等)。
- 每个隔离级别对应着不同的锁机制和并发控制策略。
- 根据实际业务需求选择合适的隔离级别,在数据一致性和系统性能之间做权衡。
-
深度知识讲解:
四种隔离级别及其能防止的问题:
隔离级别 脏读 不可重复读 幻读 更新丢失 使用的技术 Read Uncommitted ✗ ✗ ✗ ✗ 无锁或共享锁 Read Committed ✓ ✗ ✗ ✗ 行级锁、语句级锁 Repeatable Read ✓ ✓ ✗/✓(取决于实现) ✓ 行级锁 + 范围锁 Serializable ✓ ✓ ✓ ✓ 表级锁 - 脏读(Dirty Read) :一个事务读取了另一个事务尚未提交的数据。
- 不可重复读(Non-repeatable Read) :在一个事务内多次读取同一行数据,由于其他事务对该行进行了修改并提交,导致前后读取结果不一致。
- 幻读(Phantom Read) :在一个事务内两次查询某个范围内的记录,由于其他事务插入或删除了记录并提交,导致第二次查询结果发生变化。
- 更新丢失(Lost Update) :两个事务同时更新同一数据,其中一个事务的更新被另一个覆盖。
谈谈对四大日志的理解
-
正确答案:在软件开发中,"四大日志"通常指的是 Java 领域中最常见的四种日志框架或门面:JUL(Java Util Logging)、Log4j、Logback 和 SLF4J。它们各自有不同的特点和适用场景,开发者可以根据项目需求选择合适的日志系统。
-
解答思路:
- 先明确“四大日志”指的是哪些组件。
- 分别介绍每个日志框架的基本功能、使用方式和优缺点。
- 比较它们之间的区别与联系。
- 讲解在实际开发中如何选择和整合这些日志系统。
- 最后可补充一些底层实现原理,如日志门面设计模式、适配机制等。
-
深度知识讲解:
1. JUL(java.util.logging)
-
是 JDK 自带的日志模块,位于 java.util.logging 包下。
-
特点:无需引入第三方库即可使用;配置较为复杂,性能不如 Log4j 或 Logback。
-
架构组成:
- Logger:负责记录日志的对象。
- Level:日志级别(SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST)。
- Handler:处理日志输出(如 ConsoleHandler、FileHandler)。
- Formatter:格式化日志内容(如 SimpleFormatter、XMLFormatter)。- 正确答案:在软件开发中,"四大日志"通常指的是 Java 领域中最常见的四种日志框架或门面:JUL(Java Util Logging)、Log4j、Logback 和 SLF4J。它们各自有不同的特点和适用场景,开发者可以根据项目需求选择合适的日志系统。
-
-
解答思路:
- 先明确“四大日志”指的是哪些组件。
- 分别介绍每个日志框架的基本功能、使用方式和优缺点。
- 比较它们之间的区别与联系。
- 讲解在实际开发中如何选择和整合这些日志系统。
- 最后可补充一些底层实现原理,如日志门面设计模式、适配机制等。
-
深度知识讲解:
1. JUL(java.util.logging)
-
是 JDK 自带的日志模块,位于 java.util.logging 包下。
-
特点:无需引入第三方库即可使用;配置较为复杂,性能不如 Log4j 或 Logback。
-
架构组成:
- Logger:负责记录日志的对象。
- Level:日志级别(SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST)。
- Handler:处理日志输出(如 ConsoleHandler、FileHandler)。
- Formatter:格式化日志内容(如 SimpleFormatter、XMLFormatter)。
-
用快排思想实现快速查找第k大的数
import java.util.Random;
public class QuickSelect {
// 主方法入口
public static int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k); // 第k大对应升序索引[n-k]
}
private static int quickSelect(int[] nums, int left, int right, int targetIndex) {
if (left == right) return nums[left]; // 终止条件
int pivotIndex = randomizedPartition(nums, left, right);
if (pivotIndex == targetIndex) {
return nums[pivotIndex];
} else if (pivotIndex < targetIndex) { // 目标在右区间
return quickSelect(nums, pivotIndex + 1, right, targetIndex);
} else { // 目标在左区间
return quickSelect(nums, left, pivotIndex - 1, targetIndex);
}
}
// 随机分区函数(优化性能)
private static int randomizedPartition(int[] nums, int left, int right) {
Random random = new Random();
int randomIndex = left + random.nextInt(right - left + 1); // 随机选基准
swap(nums, randomIndex, right); // 基准移至末尾
return partition(nums, left, right);
}
// 分区逻辑(左大右小)
private static int partition(int[] nums, int left, int right) {
int pivot = nums[right];
int i = left; // 记录大于基准的边界
for (int j = left; j < right; j++) {
if (nums[j] > pivot) { // 找第k大需保持左大右小
swap(nums, i, j);
i++;
}
}
swap(nums, i, right); // 基准归位
return i;
}
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 测试用例
public static void main(String[] args) {
int[] arr = {3, 2, 1, 5, 6, 4};
System.out.println(findKthLargest(arr, 2)); // 输出5
}
}
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:掘金面试群。