b站一面,大概率挂

在字节KPI面,摄像头都不开之后,就对面试没什么期待了,但b站还是中规中矩的,只是我准备的不充分。

面试时间定在下午2点,时长一个小时。

面试提问

简单自我介绍,项目中细节追问,深挖了底层的技术实现,全程感觉都是在挖底层实现的细节,奈何我底层研究的不多。下面是进行记录+ai回答的标准答案,希望自己能记住吧。

1、项目中用到了@preAuthorize注解,底层原理是什么,为什么能够拦截请求进行验证。

@PreAuthorize是Spring Security提供的基于表达式的方法级安全控制机制。它的工作原理是基于Spring AOP,通过创建代理对象在方法调用前进行拦截。

核心处理流程是:当调用被@PreAuthorize注解的方法时,调用会被MethodSecurityInterceptor拦截,它会提取注解中的SpEL表达式,在特殊的评估上下文(MethodSecurityExpressionRoot)中执行这个表达式。

这个上下文提供了访问当前认证信息、方法参数和安全工具方法的能力。表达式执行结果为true时,允许方法调用;为false时,抛出AccessDeniedException。

2、mybatis中防止SQL注入的原理是什么?#{}和${}区别是什么

MyBatis 提供了两种参数占位符:

  • #{} 占位符(推荐 ✅)
  • ${} 占位符(危险 ❌,可能导致注入)

#{} 的原理

使用 #{} 时,MyBatis 会将参数传给 JDBC 的 PreparedStatement,内部使用 ? 占位符,并且通过 setXxx() 方法(如 setStringsetInt)安全绑定参数。

例子:

<select id="findUser" resultType="User">
    SELECT * FROM user WHERE name = #{name}
</select>

执行过程大概是:

SELECT * FROM user WHERE name = ?

然后 JDBC 做参数绑定:

preparedStatement.setString(1, "Jack");

因为参数不会直接拼接到 SQL 字符串里,所以即便输入恶意 SQL 片段,也会被当成普通字符串,无法执行注入。

${} 的原理

${}字符串替换,MyBatis 会把参数直接拼接到 SQL 中,效果类似:

SELECT * FROM user WHERE name = 'Jack'

如果传入 ' OR '1'='1,就会产生注入风险。

因此 ${} 一般只用于 动态拼接表名、字段名 之类不能用 ? 占位的场景,而且要做好白名单校验。

3、MySQL中什么是事务,ACID四个特性分别是由什么底层能力来实现的?

1️⃣ 什么是事务

在 MySQL(尤其是 InnoDB 引擎)中,事务 (Transaction) 是数据库作为一个执行单元的操作集合,要么全部执行成功,要么全部执行失败(回滚),保证数据一致性。

2️⃣ 事务的 ACID 四大特性

事务有 ACID 四个特性:

🔹 1. 原子性(Atomicity)

  • 定义:事务中的所有操作要么全部完成,要么全部不完成。
  • 底层实现:通过 Undo Log(回滚日志) 实现。每次执行更新操作前,InnoDB 会在 Undo Log 中记录一条回滚记录。如果事务失败或回滚,可以通过 Undo Log 将数据恢复到执行前的状态。

🔹 2. 一致性(Consistency)

  • 定义:事务执行前后,数据库必须保持一致性约束。比如转账,转账前后总金额不变。
  • 底层实现:由 原子性 + 隔离性 + 持久性 共同保证,同时依赖 数据库约束(外键、唯一性约束、触发器等)。事务保证操作的正确性。约束保证数据不会出现非法状态。

🔹 3. 隔离性(Isolation)

  • 定义:多个事务并发执行时,一个事务的中间状态对其他事务不可见。
  • 底层实现:锁机制(行锁、表锁、意向锁):保证并发访问时的正确性。MVCC(多版本并发控制):通过 Undo Log + 隐藏字段(trx_id、roll_pointer) 生成数据快照,实现读已提交、可重复读等隔离级别。

🔹 4. 持久性(Durability)

  • 定义:事务提交后,数据必须持久化,即使系统宕机也不会丢失。
  • 底层实现:通过 Redo Log(重做日志) 实现。InnoDB 在写数据时,先把修改记录写入 Redo Log(顺序写,性能高),再异步刷新到磁盘数据页。如果宕机,重启时可通过 Redo Log 恢复已提交事务的数据。

4、索引是什么,底层是由什么数据结构实现的?在哪些场景会失效?

1️⃣ 什么是索引?

👉 索引(Index)就像书本的目录

  • 你要查「某个词」时,不用从头翻到尾,而是先看目录,快速定位页码。
  • 在数据库中,索引就是一种 加速查询 的数据结构,可以极大提高数据检索速度。

常见的索引类型:

  • 主键索引(聚簇索引):数据按主键存储在 B+Tree 的叶子节点中。
  • 普通索引(非聚簇索引):叶子节点存储的是主键值,通过主键再去表里查数据(二次回表)。
  • 唯一索引:不允许重复值。
  • 组合索引:多个字段联合组成的索引。

2️⃣ 索引的底层数据结构

MySQL(InnoDB 引擎)索引主要用 B+ 树 实现。

🔹 为什么用 B+ 树?

  • 二叉树:树高太高,大表需要很多次 IO。
  • B 树:叶子节点和非叶子节点都存数据,范围查询效率低。
  • B+ 树:所有数据都存在叶子节点,叶子节点形成一个有序链表 → 适合范围查询。非叶子节点只存键(目录),层级少,查询时磁盘 IO 次数更少。一个节点能存很多索引值(因为页大小固定 16KB)。

3️⃣ 索引失效的常见场景

即使建了索引,以下情况也可能失效,导致全表扫描:

  1. 对索引字段做计算或函数操作❌
  2. LIKE 以 % 开头
  3. OR 条件混用,部分字段无索引。【SELECT * FROM user WHERE id = 1 OR age = 20;】如果 id 有索引但 age 没索引,就会全表扫描。
  4. 组合索引未遵守最左前缀原则。假设组合索引 (a, b, c),查询必须从 a 开始,如果只查 b, c,索引可能失效。
  5. 数据分布不均(区分度低)比如 gender 只有 男/女,即使建索引,优化器可能判断全表扫描更快。
  6. 隐式类型转换这里 MySQL 会隐式转换成字符串,可能导致索引失效。

5、SQL调优有哪些思路?

1️⃣ SQL 语句层面的优化

就像写作文,结构合理,老师(优化器)才能快速理解。

  • 避免 SELECT *👉 只取需要的字段,减少网络传输和 IO。
-- ❌
SELECT * FROM user;
-- ✅
SELECT id, name, age FROM user;
  • 减少子查询,尽量用 JOIN
-- ❌ 子查询嵌套,执行效率低
SELECT name FROM user WHERE id IN (SELECT user_id FROM order);
-- ✅ 用 JOIN
SELECT u.name FROM user u JOIN order o ON u.id = o.user_id;
  • LIMIT 分页优化大偏移量分页会慢:✅ 优化:基于索引或 ID 过滤
SELECT * FROM order LIMIT 100000, 10; -- 会扫描前 100000 行❌
SELECT * FROM order WHERE id > 100000 LIMIT 10; -- ✅
  • 避免在 WHERE 条件里对字段做函数/运算(会导致索引失效)
-- ❌
SELECT * FROM user WHERE YEAR(create_time) = 2024;
-- ✅
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

2️⃣ 索引层面的优化

索引是 SQL 调优的核心,就像高速公路 vs 土路。

  • 建立合适的索引

高频查询的字段 → 建单列索引

多条件查询 → 建联合索引(遵守最左前缀原则)

唯一值字段(如手机号、身份证) → 建唯一索引

  • 覆盖索引(避免回表,提高查询速度)
-- 如果 user(id, name, age) 有联合索引
SELECT id, name FROM user WHERE age = 20;
-- ✅ 可以直接从索引中获取数据,不用回表

    避免索引失效的场景

  • LIKE 以 % 开头
  • 对索引字段做运算/函数
  • 隐式类型转换
  • OR 条件里有未建索引字段
  • 数据区分度过低(如性别字段)

3️⃣ 数据库层面的优化

像是高速公路的“基础设施建设”。

  • 读写分离主库负责写,从库负责读,提高并发能力。
  • 分库分表单表数据量过大(>千万级),查询慢时,可以做垂直/水平拆分。
  • 缓存(Redis)高频访问、实时性要求不高的数据可以先查缓存,减少数据库压力。
  • SQL 执行计划分析(EXPLAIN)通过 EXPLAIN 查看 SQL 走没走索引,是否全表扫描,关键字段:type、rows、Extra。
  • 适当的表设计字段选择合适的数据类型(如 INT 比 VARCHAR 高效)。避免过多的 JOIN,大表 JOIN 大表性能差。

6、哪些因素会导致线程不安全?如何解决?

1️⃣ 什么是线程不安全?

👉 线程不安全就是多个线程并发访问共享数据时,可能导致数据错误或状态异常。

2️⃣ 导致线程不安全的常见因素

🔹 1. 多线程对共享变量的写操作

  • 如果多个线程同时修改一个变量,没有同步机制,就可能产生错误。
  • 例子:多线程调用 increment() 时,count++ 实际包含三步(读、加一、写),可能导致丢失更新。
class Counter {
    private int count = 0;
    public void increment() {
        count++; // 非原子操作
    }
    public int getCount() {
        return count;
    }
}

多线程调用 increment() 时,count++ 实际包含三步(读、加一、写),可能导致丢失更新。

    🔹 2. 内存可见性问题

    • 一个线程修改了变量值,但另一个线程读到的还是旧值。
    • 因为 CPU 有缓存,线程可能读的是自己缓存的数据。
    • 比喻:就像两个人拿着同一本书的复印件,A 改了原件,但 B 手里的复印件没变。

    🔹 3. 指令重排序

  • JVM 和 CPU 为了优化性能,会对指令执行顺序做调整。
  • 如果没有同步机制,可能导致线程看到的执行顺序与代码不一致。
  • 常见场景:单例模式双重检查锁(DCL),如果没有 volatile,可能导致对象还没初始化就被使用。

3️⃣ 如何解决线程不安全?

✅ 1. 加锁机制

  • synchronized:保证代码块/方法在同一时刻只能有一个线程进入。
  • ReentrantLock:更灵活的锁,可以实现公平锁、超时锁。

✅ 2. 使用原子类(CAS)或线程安全类

  • AtomicInteger、AtomicBoolean 等,底层用 CAS(Compare And Swap) 保证原子性,性能比锁更高。
  • concurrentHashMap、CopyOnWriteArrayList、等类内部有锁机制,考研保证原子性。

✅ 3. 使用 volatile 保证可见性

  • 适用于“一个线程写,多个线程读”的场景。
  • 比如 停止线程:

✅ 5. 无锁并发编程(减少共享)

  • 尽量减少线程间共享数据,采用 线程本地变量(ThreadLocal)。
  • 例子:每个线程都有自己独立的副本,不会互相干扰。

7、JVM是什么?类加载的顺序是怎样的?出现类加载冲突常是由什么原因导致的?

1. JVM 是什么

JVM(Java Virtual Machine,Java 虚拟机)是 Java 程序的运行环境,它屏蔽了底层操作系统和硬件的差异,使得 Java 可以做到“一次编写,到处运行”。

主要职责:

  • 类加载(Class Loading):加载 .class 字节码文件。
  • 字节码执行(Execution Engine):将字节码解释或 JIT 编译为机器码。
  • 内存管理(Memory Management):包括堆、方法区、栈、本地方法栈、程序计数器等。
  • 垃圾回收(GC):自动管理对象生命周期。

2.类加载顺序

类加载顺序为“加盐准析出”即 加载→验证→准备→解析→初始化

1、加载:JVM根据类的权限定义命找到.class文件生成二进制字节流,将该类的字段、方法、父类存入方法区生成运行时数据结构,在堆区生成代表该类的.class对象。

2、验证:对于文件格式、字节码、是否继承final等方面进行验证。

3、准备:对于静态变量分配内存,并且赋初始值。

4、解析:将类中的符号引用改为直接引用,指向具体的地址。

5、初始化:对于代码中涉及的初始化值进行赋值,

3. 类加载器(ClassLoader)机制

  • 启动类加载器(Bootstrap)加载 JDK 核心类(rt.jar)。
  • 扩展类加载器(Extension)加载 JDK 扩展目录下的类。
  • 应用类加载器(App)加载用户自定义的类(classpath 下的类)。
  • 自定义类加载器程序员可实现 ClassLoader,打破默认的加载逻辑。

👉 采用 双亲委派模型

类加载器在加载类时,会先把请求交给父类加载器,如果父类加载失败才会自己尝试加载,防止核心类被篡改。

4. 类加载冲突的原因

类加载冲突常见于:

  1. 多版本 JAR 包冲突(最常见),比如项目依赖了两个版本的 log4j,不同版本中的同一个类可能冲突。
  2. 不同类加载器加载了相同的类,JVM 认为“类的唯一性 = 类的全限定名 + 加载它的类加载器”。即使两个类的字节码完全相同,如果由不同的类加载器加载,JVM 也认为它们是不同的类。这就是 ClassCastException 的根本原因之一。
  3. 打破双亲委派,有些框架(如 Tomcat、OSGi、Spring Boot)为了模块化或插件化,使用自定义类加载器,不同模块之间可能出现冲突。

类加载器在加载类时,会先把请求交给父类加载器,如果父类加载失败才会自己尝试加载,防止核心类被篡改。

8、创建线程池的参数有哪些?分别是什么含义?随着任务数量增加,线程池内的线程数目是怎么变化的?阻塞队列情况如何变化?

1、线程池参数

public ThreadPoolExecutor(
    int corePoolSize,          // 核心线程数
    int maximumPoolSize,       // 最大线程数
    long keepAliveTime,        // 空闲线程存活时间
    TimeUnit unit,             // 存活时间单位
    BlockingQueue<Runnable> workQueue, // 任务阻塞队列
    ThreadFactory threadFactory,       // 线程工厂(一般用于命名线程)
    RejectedExecutionHandler handler   // 拒绝策略
)

线程池是 Java 并发编程中的重要工具,它通过复用线程来减少线程创建和销毁的开销,从而提高系统的性能和稳定性。在 Java 中,线程池的核心实现类是 ThreadPoolExecutor,它提供了七个重要的参数来配置线程池的行为。接下来我会详细讲述这七大参数的定义、作用以及它们如何影响线程池的工作机制。

第一个是核心线程数(corePoolSize),它是指线程池中始终保持存活的线程数量,即使这些线程处于空闲状态。

当提交一个新任务时,如果当前线程数小于核心线程数,线程池会优先创建新线程来处理任务,而不是将任务放入队列。例如,设置 corePoolSize=5 表示线程池会始终维护至少 5 个线程。

第二个是最大线程数(maximumPoolSize),它是指线程池中允许的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,线程池会继续创建新线程来处理任务。如果线程数已经达到最大值,则任务会被拒绝。例如,设置 maximumPoolSize=10 表示线程池最多可以创建 10 个线程。

第三个是线程空闲时间(keepAliveTime),它是指非核心线程在空闲状态下保持存活的时间。当线程池中的线程数超过核心线程数时,多余的空闲线程会在指定的空闲时间后被回收。例如,设置 keepAliveTime=60 表示非核心线程在空闲 60 秒后会被销毁。

第四个是时间单位(unit),它用于指定线程空闲时间的计量单位。常见的单位包括 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。例如,unit=TimeUnit.SECONDS 表示空闲时间以秒为单位。

第五个是任务队列(workQueue),它是一个阻塞队列,用于存放等待执行的任务。当线程池中的线程数达到核心线程数时,新提交的任务会被放入任务队列中等待执行。常见的队列类型包括:

ArrayBlockingQueue:有界队列,适用于控制资源使用。

LinkedBlockingQueue:无界队列,适用于任务量较大的场景。

SynchronousQueue:不存储任务的队列,适用于直接传递任务给线程的场景。

第六个是线程工厂(threadFactory),它用于创建线程池中的线程。通过自定义线程工厂,可以为线程设置名称、优先级或其他属性,便于调试和管理。例如,使用 Executors.defaultThreadFactory() 创建默认线程工厂。

第七个是拒绝策略(handler),它用于处理当线程池无法接受新任务时的情况(例如线程数达到最大值且任务队列已满)。常见的拒绝策略包括:

AbortPolicy:抛出异常,拒绝任务。

CallerRunsPolicy:由调用线程执行任务。

DiscardPolicy:直接丢弃任务。

DiscardOldestPolicy:丢弃队列中最旧的任务,并尝试重新提交新任务。

2. 线程池中线程数和队列的变化过程

假设不断提交任务,线程池的行为如下(重点!):

  1. 任务数 ≤ corePoolSize直接创建新线程执行任务(直到线程数达到 corePoolSize)。此时 阻塞队列为空。
  2. 任务数 > corePoolSize,队列未满不会再创建新线程,而是把任务放入 阻塞队列 等待执行。
  3. 任务数 > corePoolSize 且 队列已满创建新的线程执行任务,直到线程数达到 maximumPoolSize。
  4. 任务数 > maximumPoolSize 且 队列已满执行 拒绝策略。

9、对于多线程任务,假设有线程池,核心线程数目为100,每个task用1秒处理完成,请问持续20秒,每秒提交1个task,线程池内线程的数目是如何变化的?

分析:

  1. 第 1 秒提交第一个任务,线程池还没有线程。线程池会创建一个新线程执行任务。活跃线程数 = 1。
  2. 第 2 秒提交第 2 个任务,前一个任务已执行完毕。线程池会复用刚才的线程(空闲线程不会销毁,因为线程池会保留核心线程)。活跃线程数 = 1。
  3. 第 3 秒 ~ 第 20 秒每秒只提交 1 个任务,速率是 1/s。每个线程处理时间 = 1s,能刚好跟上任务速率。所以始终只需要 1 个线程 就能处理所有任务。

结论:

  • 在 整个 20 秒内,线程池最多只会创建 1 个线程,并反复复用它。
  • 虽然 corePoolSize = 100,但由于任务速率太低,线程池不会预先创建 100 个线程(默认情况下,核心线程是懒加载的,即有任务才创建)。
  • 因此:线程池线程数变化:从 0 → 1 → 一直保持 1。队列变化:一直为空,因为线程能实时消费任务。

关键点总结

  • 线程池不会一下子创建 corePoolSize 个线程,而是根据需要逐个创建(除非你调用 prestartAllCoreThreads() 提前启动)。
  • 线程数目取决于 任务提交速率 和 任务执行时长。
  • 在这个例子里,因为 提交速率 ≤ 单线程消费速率,所以线程池只需要 1 个线程即可。

10、泛泛而谈的问题

给你一个需求,你会怎么去实现?

如何学习新的知识点?

看书学习和看视频学习有什么差异?

对于论坛经验贴如何辨识正确性?

你喜欢看源码吗?

算法题

对于一个链表,就地反转其中的第m~n个节点

思路很简单,找到第m个节点的前驱和第n个节点的后继,用头部插入法进行逆转。

总结

自己对于底层的业务逻辑还是太生疏了,应该认真钻研,而不是仓促去面试,之前的面试错过了一个好面试官,当前这个也没把握住,还是慢一点比较好。

同时找实习的同学陆陆续续有了新的offer,自己留在原地难免落魄,毕竟每个人的状态基础是不一样的,不应该和别人去比较。

我看到双9✌学历优秀,但被导师困在原地,

我看到科大前舍友速通企鹅,但入职2个月项目组被裁,需要再找实习,

我看到研一就在美团的舍友,但9月想换新厂没约面,(现在已快手oc)

我看到2段大厂的学长,但秋招无offer……

人人都有自己的剧本,都有各种意难平,比昨天好一点就行,不必强行催促自己,

毕竟在学校的日子,过一天少一天。

全部评论
点赞 回复 分享
发布于 09-19 20:42 上海

相关推荐

昨天 13:23
安徽大学 Java
两个傻逼公司喜欢玩弄候选人的时间,美团hr面后说两天内给通知,结果后面忘了这回事自动挂掉了,和暑期实习如出一辙,都是终面笑嘻嘻的聊然后反手给你挂了。但是我没想到的是秋招这个hr是真的敢说,直接贴脸开大就是说现在简历库里随手抓一个简历就是985本硕,三四段大厂实习,你觉得你的优势是什么,不要你觉得,你怎么证明?我证明nm😅。面试的时候自己在那低估什么24年这一批人质量都不大行,25年的刚来不知道,得找几个有活力的能活跃气氛的硕士,当时他就直接无视我自言自语了很久,那个时候我就感觉应届生面对这些公司真的就没有尊严可言,看的不是能力合不合格,而是相比其他人合不合适。字节更是重量级,第一个部门所有面试通过,但是因为主管突然注意到我一月份的面经太差直接撤销流程,我nm😅你tm之前没发现啊。然后换第二个部门走到终面直接就是不感兴趣走个过场,一直走神不想听你说话,最后反问有什么建议直接蹬鼻子上脸说就这么点时间哪够我了解你的没有建议。最后换第三个部门走到二面聊的一切正常感觉就是必过的,然后反手又给我挂了。tm的字节整整浪费了我一个月的时间,我还是那句话,既然你觉得我学历不行,经历不垂直还是人格没有魅力等等一切因素,就不要发起面试浪费时间。剩余的也是一样的,京东,快手,科大讯飞二面挂,为什么这三个要单独说,因为一面都是面试官聊的特别好推进快,二面被老登摆脸色,摆着一副特别拽的脸色故意问你一堆八股,然后问你实习就压力你说根本没有内容,最后反问tm面试官就回答两三个字,只想快点结束,估计面评也被恶意的打的特别差,因为后续刷新一个月都没反应最后阿里全系简历挂,腾讯测评挂,好像秋招已经宣判死刑了。回望这两个月,好像自己并没有做错什么,但是感觉活的非常卑微。总计面了四十多场0offer,八股项目实习已经背的滚瓜烂熟,但是每场面试还是会担惊受怕,因为感觉自己怎么精炼回答内容都不是满意的答案,自己就像谄媚一般想尽心思怎么顺着面试官的心意现在已经彻底麻木,不愿去面试,不愿去背八股,最近两周的负反馈已经把我给压垮。我只想用一句话来形容秋招,那便是“人为刀俎,我为鱼肉”,所谓的双向选择就是扯淡,现如今应届生有什么资本去站在公司的对立面
一只末影酱:狠狠点了哥们,有些老登纯纯就是吃了互联网红利
如何判断面试是否凉了
点赞 评论 收藏
分享
09-17 18:41
门头沟学院 Java
📍面试公司:小红书🕐面试时间:9.11&nbsp;三天后挂💻面试岗位:后端开发❓面试问题:1.自我介绍2.实习拷打7.场景题:直播平台需要实时统计每个直播间的在线人数(观众进入/离开直播间时需要实时更新),并支持查询指定直播间的当前在线人数。要求系统在高并发场景下(如热门主播直播间同时在线100万人(高点直播间进房QPS=15W,离房QPS=5W),同时在线直播间10W)保证数据准确性和性能。设计思路。8.你说使用AtomicLong去进行加减,本实例的数据加减完之后存放在哪里?9.这样子的话单机的话应该是没什么问题的,如果是分布式场景多台机器应该怎么办呢?10.你说引入消息队列,这是在哪一个环节进行的?是用户进房/离房的时候发消息吗?11.用Redis的话你说用哈希去存,哈希存的是什么东西?12.假设你说的没问题,现在同时有10万个直播间,你用一个Redis的哈希结构来存,会不会有问题?用什么方法可以解决这个问题呢?13.在你的实习过程中,用户的权限应该也是落到Redis上的,是把所有用户都放到一个Redis的哈希结构吗?当时是怎么弄的?14.你说要把直播间做一个分片,根据直播间ID去做分片再落到不同的Redis,那为什么不直接把直播间ID作为Key呢?15.用Redis的哈希结构意义是什么?16.你说通过ID哈希取模再分片的方式,如果开始每一片的Redis&nbsp;key存了1000个用户ID,后期用户增多到100万,扩容怎么做呢?重新物理哈希吗?17.一个用户存一个Key的方式可行吗?18.针对10个分片,每个分片上1万个直播间,会不会存在什么性能问题?19.对redis集群有什么影响?20.如果有过载的情况,我们要怎么处理?21.手撕:跳跃游戏
查看17道真题和解析
点赞 评论 收藏
分享
评论
6
8
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务