小红书日常实习后端一面记录

没有自我介绍环节

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,面试官有没有不用注解的)

  1. 注解
    • @Component:加到类上,包括@Controller、@Service、@Repository、@Configuration
  2. XML
    • applicationContext.xml中通过标签定义Bean。
  3. 配置类注册
    • 使用@Configuration配合@Bean@Bean标记方法
  4. 使用 FactoryBean 接口
  5. 使用 BeanDefinitionRegistryPostProcessor
    • 通过编程方式动态注册Bean定义

6. 数据库ACID的I是怎么实现的

  • :行锁、间隙锁、临键锁
  • MVCC:隐藏字段事务ID回滚指针ReadView决定可见性

当前读走锁,快照读走MVCC

7. 给个数据表,查找第九万条数据,怎么查

  1. 有自增主键且连续:SELECT * FROM t WHERE id >= 90000 ORDER BY id LIMIT 1;
  2. 有自增主键:select * from page where id >=(select id from page order by id limit 89999, 1) order by id limit 1;
  3. 通用:SELECT * FROM t LIMIT 89999,1;

8. select怎么使用排他锁

1
SELECT ... FOR UPDATE

9. select * 查询普通索引的过程

  1. 在普通索引的B+树上找到满足条件的索引项。
  2. 从索引项中获取主键值。
  3. 如果整个记录就包含主键和普通索引的字段,也就是索引覆盖,就可以直接返回结果了,否则会发生回表
  4. 回表:根据主键值,回到主键索引(聚簇索引)的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
2
3
4
5
6
7
SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1, value2, ...);

SELECT column_name(s)
FROM table_name
WHERE column_name IN (SELECT column_name FROM another_table WHERE condition);
  • EXISTS 用于判断子查询是否至少能返回一行数据。它不关心子查询返回什么数据,只关心是否有结果。如果子查询有结果,则返回TRUE,否则返回FALSE。

语法结构:

1
2
3
SELECT column_name(s)
FROM table_name
WHERE EXISTS (SELECT column_name FROM another_table WHERE condition);
  • 性能差异:在很多情况下,EXISTS 的性能优于 IN,特别是当子查询的表很大时。这是因为EXISTS 一旦找到匹配项就会立即停止查询,而IN可能会扫描整个子查询结果集。
  • 使用场景:如果子查询结果集较小且不频繁变动,IN 可能更直观易懂。而当子查询涉及外部查询的每一行判断,并且子查询的效率较高时,EXISTS 更为合适。
  • NULL值处理:IN 能够正确处理子查询中包含NULL值的情况,而EXISTS 不受子查询结果中NULL值的影响,因为它关注的是行的存在性,而不是具体值。
  • 大表驱动小表用 exists,小表驱动大表用 in;MySQL 优化器 8.0 后趋于自动转换。

14. update语句在数据库引擎层的执行过程

  1. Buffer Pool中找到数据页,如果不在则从磁盘加载。
  2. undo log,用于事务回滚和MVCC。
  3. 修改Buffer Pool中的数据页(此时为脏页)。
  4. redo logprepare状态),用于崩溃恢复。
  5. binlog,用于主从复制和数据恢复。
  6. 提交事务,将redo log置为commit状态。
  7. 后续由后台线程将脏页刷回磁盘。

刷盘时机:

  1. redo log日志即将覆盖
  2. buffer pool内存不足或者脏页比例过高,按照LRU刷盘
  3. 后台线程定时刷新
  4. 服务器空闲时期
  5. 数据库正常关闭

15. 索引失效的场景

  1. 对索引列进行函数计算、表达式计算、类型转换。
  2. 使用!=、<>、NOT IN、IS NOT NULL,优化器看“过滤比例”,大于阈值就全表。
  3. 以通配符开头的LIKE查询,如’%abc’。
  4. 复合索引不满足最左前缀原则。
  5. 在索引列上使用OR,如果OR的每个条件列都有索引可能会走union,否则容易全表扫描。
  6. 数据分布极度不均匀,优化器认为全表扫描更快。

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Result<T> {
private int code;
private String msg;
private T data;

public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}

public static <T> Result<T> error( String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
}

19. 自己有没有建过表,项目中最熟悉哪张表?

20. 手写建表语句

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
version INT NOT NULL DEFAULT 1,
KEY idx_user (user_id)
);

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(单线程)> 网络瓶颈 > 内存瓶颈
解决方案

  1. 本地缓存:使用Guava Cache 或者 Caffeine,将最热的数据存在JVM
  2. Redis集群分片:将这个热点Key分散到不同的Redis节点上

26. 有个评论系统,要求相同的用户短时间内无法发送相同的信息,假如有用户持续发送相同的几万字的文本,如何防止

后端:

  1. 对评论使用SHA-256或者MD5求哈希值
  2. 以用户ID为key,在Redis中维护一个Set
  3. 如果评论已经存在,就拒绝发送,否则就将评论的哈希值存入redis,根据业务需要设置过期时间

27. redis的key的维度,是用户维度还是业务维度

这题是刚刚题的追问,用户维度,同上

28. 数据库和缓存的一致性(这里答了延迟双删,追问,答监听binlog,说生产不用,继续追问)

  1. 延迟双删:先删缓存,再更新数据库,延迟后再删缓存
  2. 先更新数据库,然后监听binlog->异步消息->删缓存
  3. 先更新数据库,然后发送mq消息,mq接收到消息后,异步删除缓存,删除失败可重试

先更新数据库,再删除缓存的问题就是:如果A线程更新数据库的时候,缓存恰好过期了,B线程进入数据库读取数据。
这样会产生短暂的不一致:A更新完数据库,还未更新缓存。
如果B在A删除缓存后,又写入了缓存,又会产生短暂的不一致。但是一般读操作比写操作快,发生概率很低。
但是,如果更新数据库成功,但是删除缓存失败了,就会导致不一致,就需要用mq或者监听binlog的方式重试。

29. redis宕机了怎么办(答了个有日志,面试官说生产不用,不知道这里是不是答RDB)

  1. 持久化:RDB快照 + AOF日志
  2. 高可用:主从复制 + 哨兵模式

30. 反问