没有自我介绍环节
1. 写了多少算法题?
2. 手撕,力扣25. K 个一组翻转链表变题
3. 了解instanceof吗?
instanseof是Java的二元运算符,用于测试一个对象是否是一个特定类(或其子类、实现接口)的实例。他与class.isInstanse()方法的功能类似。如果对象是null,会直接返回false。
4. DTO是Integer,VO是int,VO直接dto.getId()有没有问题
可能会出现NPE,也就是著名的NullPointerException。因为Integer是引用类型,有可能为null,给int类型的字段赋值的时候会发生自动拆箱,抛出NullPointerException。
5. 怎么把对象交给IoC管理(就想到了@Bean,面试官有没有不用注解的)
- 注解
@Component:加到类上,包括@Controller、@Service、@Repository、@Configuration
- XML
applicationContext.xml中通过标签定义Bean。
- 配置类注册
- 使用
@Configuration配合@Bean,@Bean标记方法
- 使用
- 使用
FactoryBean接口 - 使用
BeanDefinitionRegistryPostProcessor- 通过编程方式动态注册Bean定义
6. 数据库ACID的I是怎么实现的
- 锁:行锁、间隙锁、临键锁
- MVCC:隐藏字段
事务ID和回滚指针,ReadView决定可见性
当前读走锁,快照读走MVCC
7. 给个数据表,查找第九万条数据,怎么查
- 有自增主键且连续:
SELECT * FROM t WHERE id >= 90000 ORDER BY id LIMIT 1; - 有自增主键:
select * from page where id >=(select id from page order by id limit 89999, 1) order by id limit 1; - 通用:
SELECT * FROM t LIMIT 89999,1;
8. select怎么使用排他锁
1 | SELECT ... FOR UPDATE |
9. select * 查询普通索引的过程
- 在普通索引的B+树上找到满足条件的索引项。
- 从索引项中获取主键值。
- 如果整个记录就包含主键和普通索引的字段,也就是索引覆盖,就可以直接返回结果了,否则会发生回表
- 回表:根据主键值,回到主键索引(聚簇索引)的B+树上,查找完整的行记录。
10. InnoDB索引的数据结构
B+树
11. 是否了解聚簇索引
聚簇索引就是主键索引,叶子节点是数据页,存放了完整的行记录。
12. 聚簇索引B+树的树高
通常为 2 - 4 层,一页是 16KB。
- 非叶子节点:一个主键(BigInt)8B + 一个指针 6B,一页可以存放 1170 个索引
- 叶子节点:假设一行1KB,一页可以放16条记录
两层B+树可以存 1170 * 16 = 18720条
三层B+树可以存 1170 * 1170 * 16 约为 2000w
四层B+树可以存 1170 * 1170 * 1170 * 16 = 250亿
13. in和exists的区别
IN用于检查左边的表达式是否存在于右边的列表或子查询的结果集中。如果存在,则返回TRUE,否则返回FALSE。
语法结构:
1 | SELECT column_name(s) |
EXISTS用于判断子查询是否至少能返回一行数据。它不关心子查询返回什么数据,只关心是否有结果。如果子查询有结果,则返回TRUE,否则返回FALSE。
语法结构:
1 | SELECT column_name(s) |
- 性能差异:在很多情况下,EXISTS 的性能优于 IN,特别是当子查询的表很大时。这是因为EXISTS 一旦找到匹配项就会立即停止查询,而IN可能会扫描整个子查询结果集。
- 使用场景:如果子查询结果集较小且不频繁变动,IN 可能更直观易懂。而当子查询涉及外部查询的每一行判断,并且子查询的效率较高时,EXISTS 更为合适。
- NULL值处理:IN 能够正确处理子查询中包含NULL值的情况,而EXISTS 不受子查询结果中NULL值的影响,因为它关注的是行的存在性,而不是具体值。
- 大表驱动小表用 exists,小表驱动大表用 in;MySQL 优化器 8.0 后趋于自动转换。
14. update语句在数据库引擎层的执行过程
- 从
Buffer Pool中找到数据页,如果不在则从磁盘加载。 - 写
undo log,用于事务回滚和MVCC。 - 修改
Buffer Pool中的数据页(此时为脏页)。 - 写
redo log(prepare状态),用于崩溃恢复。 - 写
binlog,用于主从复制和数据恢复。 - 提交事务,将
redo log置为commit状态。 - 后续由后台线程将脏页刷回磁盘。
刷盘时机:
redo log日志即将覆盖buffer pool内存不足或者脏页比例过高,按照LRU刷盘- 后台线程定时刷新
- 服务器空闲时期
- 数据库正常关闭
15. 索引失效的场景
- 对索引列进行函数计算、表达式计算、类型转换。
- 使用!=、<>、NOT IN、IS NOT NULL,优化器看“过滤比例”,大于阈值就全表。
- 以通配符开头的LIKE查询,如’%abc’。
- 复合索引不满足最左前缀原则。
- 在索引列上使用OR,如果OR的每个条件列都有索引可能会走union,否则容易全表扫描。
- 数据分布极度不均匀,优化器认为全表扫描更快。
16. 如何看select有没有使用索引,使用什么索引
使用EXPLAIN命令。看key字段显示使用了哪个索引,rows字段表示扫描行数,type字段表示连接类型。
17. explain结果的type字段是什么
type字段:表示访问/连接类型,从好到坏常见的有:
null > system > const > eq_ref > ref > range > index > ALL
- null:查询语句没有表
- system:访问系统表
- const:通过主键或唯一索引一次就找到。
- ref:使用普通索引。
- index:用了索引,但还是遍历了全索引树。
- ALL:全表扫描,需要优化。
18. 手写一个简单的Result类,用于封装统一返回结果
1 | public class Result<T> { |
19. 自己有没有建过表,项目中最熟悉哪张表?
20. 手写建表语句
1 | CREATE TABLE t_order ( |
21. 怎么快速给上万个Controller加出参入参的日志
使用Spring AOP,可以定义一个切面,拦截所有Controller方法的调用,并记录入参和出参。
- 连接点:程序执行过程中能够拦截到的任何一个点,在Spring AOP中指一次方法的执行
- 切点:大量连接点的表达式筛选
- 通知:拦截到方法后,代码在何时机被执行
- **
@Before**:前置通知 - **
@Around**:环绕通知 - **
@AfterReturning**:返回通知 - **
@AfterThrowing**:异常通知 - **
@After**:最终通知 - 仅需记录/校验入参 →
@Before - 仅关心成功结果 →
@AfterReturning - 仅关心异常 →
@AfterThrowing - 必须同时拿到入参、返回值、异常,或需要改参/改返/重试 →
@Around - 只做资源清理 →
@After
- **
- 切面:切点+通知打包为一个类
- 织入:把切面代码混合到原有的业务代码,使用动态代理
22. 了解AOP吗
面向切面编程,将横切关注点(如日志、事务、安全)与核心业务逻辑分离。核心概念有:切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)、织入(Weaving)。
23. 缓存穿透和缓存击穿
- 穿透:查询一个不存在的数据,缓存和数据库都没有,导致每次请求都打到数据库。解决方案:布隆过滤器、缓存空值。
- 击穿:一个热点key过期瞬间,大量请求击穿到DB。解决方案:互斥锁(Redis SETNX)、永不过期(逻辑过期)。
- 雪崩:大量 key 在同一时间集中过期,导致缓存瞬间失效,大量请求全部打到数据库。解决方案:随机过期、多级缓存。
24. 非核心业务场景怎么解决缓存穿透
缓存空对象,并设置一个较短的过期时间(如1-5分钟),防止数据后续被真实写入后仍返回空值。
这里的空对象并非是null,可以是使用包装对象,给业务对象创建包装类,增加标志位。
25. 有一个key的QPS是20万,瓶颈在哪,如何解决
这里的核心瓶颈就是单台Redis服务器的性能瓶颈:CPU(单线程)> 网络瓶颈 > 内存瓶颈
解决方案:
- 本地缓存:使用Guava Cache 或者 Caffeine,将最热的数据存在JVM
- Redis集群分片:将这个热点Key分散到不同的Redis节点上
26. 有个评论系统,要求相同的用户短时间内无法发送相同的信息,假如有用户持续发送相同的几万字的文本,如何防止
后端:
- 对评论使用SHA-256或者MD5求哈希值
- 以用户ID为key,在Redis中维护一个Set
- 如果评论已经存在,就拒绝发送,否则就将评论的哈希值存入redis,根据业务需要设置过期时间
27. redis的key的维度,是用户维度还是业务维度
这题是刚刚题的追问,用户维度,同上
28. 数据库和缓存的一致性(这里答了延迟双删,追问,答监听binlog,说生产不用,继续追问)
- 延迟双删:先删缓存,再更新数据库,延迟后再删缓存
- 先更新数据库,然后监听binlog->异步消息->删缓存
- 先更新数据库,然后发送mq消息,mq接收到消息后,异步删除缓存,删除失败可重试
先更新数据库,再删除缓存的问题就是:如果A线程更新数据库的时候,缓存恰好过期了,B线程进入数据库读取数据。
这样会产生短暂的不一致:A更新完数据库,还未更新缓存。
如果B在A删除缓存后,又写入了缓存,又会产生短暂的不一致。但是一般读操作比写操作快,发生概率很低。
但是,如果更新数据库成功,但是删除缓存失败了,就会导致不一致,就需要用mq或者监听binlog的方式重试。
29. redis宕机了怎么办(答了个有日志,面试官说生产不用,不知道这里是不是答RDB)
- 持久化:RDB快照 + AOF日志
- 高可用:主从复制 + 哨兵模式