面经分享:第一次后端面试复盘

前言

今天也是有幸第一次约到了面试,下面进行分享。

这篇文章主要分为三部分:自我介绍的组织方式、面试中被问到的问题、面试后的复盘感悟。其中问题部分我按照主题重新整理了一下,并补上了面试后复盘出的参考回答。

自我介绍

要求 3 分钟自我介绍,我大致按照下面的顺序讲了一轮:

  1. 目标岗位
  2. 奖项经历
  3. 技术栈
  4. 项目经历
  5. 开源经历
  6. 简短总结

整体还是有点考验记忆的,不过这些内容都是自己做过的,所幸也没有卡词。等自我介绍结束后,我也差不多适应了面试氛围,刚好进入问答环节。

提问

顺序可能不太一样,我将同类型的问题放在一块了,实际面试时是混着来的。一共有三个面试官,第一位主要问缓存相关的问题,第二位主要问事务相关问题以及一些实际生产问题,第三位则是问一些 Java 基础相关的问题。

网络基础

HTTP 和 HTTPS 的区别

这一问直接炸了。因为我们大三才学计网,虽然自己在上学期自学过一点,但到现在早已经没印象了,只知道 HTTPS 的安全性高一些,索性直接爆了。

HTTP 是明文传输协议,默认端口是 80;HTTPS 可以理解为 HTTP + TLS/SSL,默认端口是 443

主要区别有几点:

  • 安全性不同:HTTP 明文传输,容易被窃听、篡改;HTTPS 通过 TLS 加密传输。
  • 身份认证不同:HTTPS 依赖 CA 证书,可以验证服务端身份,降低中间人攻击风险。
  • 完整性保护不同:HTTPS 可以校验数据是否被篡改。
  • 性能成本不同:HTTPS 需要额外的握手和加解密过程,但现在硬件和协议优化后,这个成本通常可以接受。

如果面试时继续追问 HTTPS 的过程,可以从“客户端发起请求、服务端返回证书、客户端校验证书、协商会话密钥、后续使用对称加密通信”这个流程展开。

缓存与 Redis

什么是缓存穿透、缓存击穿和缓存雪崩?

这三个问题都和缓存失效有关,但原因不一样。

问题 典型场景 核心原因 常见解决方案
缓存穿透 查询一个根本不存在的数据 缓存和数据库都没有该数据,请求直接打到数据库 缓存空值、布隆过滤器、参数校验
缓存击穿 某个热点 key 突然过期 大量请求同时访问同一个失效热点 key 互斥锁、逻辑过期、热点 key 永不过期
缓存雪崩 大量 key 同时过期 大面积缓存同时失效,请求集中打到数据库 过期时间加随机值、缓存预热、多级缓存、限流降级

简单记忆就是:穿透是查不存在,击穿是热点 key 失效,雪崩是大量 key 一起失效。

商城中大量伪造商品 ID、热门商品突然失效、大量商品突然失效分别是什么问题?

这道题其实是把上面的概念放到业务场景里问。

  1. 大量伪造商品 ID 被发送到服务器:属于缓存穿透。因为这些商品 ID 可能根本不存在,缓存查不到,数据库也查不到。

    • 可以在入口做参数校验。
    • 可以用布隆过滤器提前判断商品 ID 是否可能存在。
    • 对不存在的数据缓存空值,并设置较短过期时间。
  2. 热门商品突然失效:属于缓存击穿。热门商品访问量很大,一旦缓存失效,请求会瞬间打到数据库。

    • 可以给热点商品设置逻辑过期。
    • 可以用互斥锁保证只有一个线程回源重建缓存。
    • 对极热数据可以考虑不过期,而是后台异步刷新。
  3. 大量商品突然失效:属于缓存雪崩。大量 key 同一时间失效,数据库压力会瞬间升高。

    • 给缓存过期时间加随机值,避免同一时间集中过期。
    • 做缓存预热。
    • 增加限流、降级和熔断保护。
    • Redis 高可用部署,避免单点故障。

商品详情这种数据如何构建缓存?

商品详情页通常读多写少,很适合缓存,但也要考虑一致性问题。

一种比较常见的方式是 Cache-Aside 思想。刚好我在项目中写到过类似逻辑,所以面试时就直接拉过来说,顺便往项目经历上展开了一下:

  1. 用户请求商品详情。
  2. 先查 Redis。
  3. Redis 命中则直接返回。
  4. Redis 未命中则查数据库。
  5. 数据库查到后写入 Redis,并设置过期时间。
  6. 如果商品被修改,先更新数据库,再删除缓存。

这里更推荐“更新数据库后删除缓存”,而不是直接更新缓存。因为商品详情可能由多个表拼装而来,直接更新缓存容易漏字段或写入脏数据。删除缓存后,下次查询再重新构建,逻辑会更简单。

需要注意的点:

  • 缓存 key 要设计清楚,比如 product:detail:{id}
  • 过期时间不要完全相同,可以加随机值。
  • 热点商品可以做预热或逻辑过期。
  • 对不存在的商品可以缓存空值,防止穿透。
  • 修改商品信息时要保证数据库和缓存的最终一致性。

秒杀系统

如果让你设计一个商城秒杀系统,你会怎么做?从数据库设计层面讲一下。

秒杀系统的核心问题是:高并发下不能超卖,系统不能被瞬间打崩。

但当时还是比较有压力,没能仔细思考,只说了一下可以用加锁、事务、分布式缓存、多服务器并发来在一定程度上缓解这个问题。

从数据库设计角度,可以拆成几张核心表:

作用
product 普通商品信息
seckill_activity 秒杀活动信息,如开始时间、结束时间、状态
seckill_product 秒杀商品信息,如商品 ID、秒杀价、秒杀库存
seckill_order 秒杀订单,记录用户、商品、活动、订单状态

关键约束:

  • seckill_order 中可以对 (user_id, activity_id, product_id) 建唯一索引,防止同一用户重复抢购。
  • 库存扣减要用原子条件更新,例如:
1
2
3
UPDATE seckill_product
SET stock = stock - 1
WHERE id = #{id} AND stock > 0;

如果影响行数为 1,说明扣减成功;如果为 0,说明库存不足。

实际系统里不会让所有请求都直接打数据库。一般会把库存提前加载到 Redis,用 Redis 先做预扣减,再通过消息队列异步创建订单,数据库作为最终落库。

从事前、事中、事后的层面讲一下秒杀系统

这道题更像是在考系统设计思路。

事前:削峰和预热

这里我回答的是用 Spring 的 @Scheduled 注解做定时任务,在活动开始前定期把数据加载到缓存中。

  • 活动开始前把商品、库存、活动信息预热到 Redis。
  • 对接口做防刷设计,比如验证码、登录校验、限流、隐藏秒杀地址。
  • 静态资源走 CDN,减少应用服务器压力。

事中:限流和异步化

这里还是往并发上去说。

  • 网关层或接口层限流,超过系统承载能力的请求直接拒绝。
  • Redis 原子扣减库存,避免大量请求进入数据库。
  • 用消息队列异步下单,削平瞬时流量。
  • 对用户维度做幂等控制,防止重复下单。

事后:补偿和对账

这一步没什么思路,就说了说打日志和哨兵节点。复盘后发现,Redis 哨兵更偏向高可用保障,不是秒杀“事后补偿”的核心答案。这里更应该围绕补偿、对账和问题追踪来讲。

  • 消费消息失败时要有重试和死信队列。
  • 定时校验订单、库存、支付状态是否一致。
  • 对超时未支付订单进行关闭并回补库存。
  • 记录完整日志,方便排查问题。

数据库与事务

你认为什么是接口?

我当时第一反应是从前后端分离项目里的 API 接口去答:接口就是前端调用后端能力的入口,通过接口完成业务功能,比较常见的设计风格是 RESTful API。

复盘后发现,这道题最好先区分语境:

  1. Java 语法层面的接口

    • interface 是一种抽象规范,只定义能力,不关心具体实现。
    • 实现类通过 implements 实现接口。
    • 它常用于解耦、面向接口编程和多态。
  2. 系统设计层面的接口

    • 接口是系统对外暴露能力的契约。
    • 对前端来说,后端接口通常表现为 HTTP API。
    • 一个好的接口需要定义清楚请求路径、请求方法、参数、响应结构、错误码、鉴权方式和幂等规则。

如果面试官没有限定上下文,可以先说:“接口这个词有两层含义,一个是 Java 里的 interface,一个是系统对外暴露的 API,我分别说一下。”这样回答会更稳。

SQL 注入在事前、事中、事后如何防护

我当时回答了两部分:一是对前端的数据进行预处理,二是通过白名单防止 SSRF(服务端请求伪造)。这里其实答偏了,SSRF 和 SQL 注入不是同一类问题。

SQL 注入的本质是:用户输入被当成 SQL 语句的一部分执行了。比如把用户输入直接拼接进 SQL,就可能让攻击者改变原本的查询逻辑。

可以按事前、事中、事后三个阶段来回答:

事前:开发阶段避免注入点

  • 使用预编译语句和参数绑定,比如 PreparedStatement
  • MyBatis 中优先使用 #{},避免把用户输入直接放进 ${}
  • 不手写字符串拼接 SQL,尤其是登录、搜索、排序、筛选这类接口。
  • 对动态表名、排序字段这种无法参数化的位置做白名单校验。
  • 数据库账号最小权限,不给业务账号过高权限。

事中:运行阶段拦截异常请求

  • 对明显异常的参数做校验,比如超长输入、非法枚举值。
  • 对高风险接口做限流,避免被批量探测。
  • 可以接入 WAF 或网关规则,拦截典型注入 payload。
  • 记录关键 SQL 异常和异常请求来源,方便追踪。

事后:发现问题后的修复和复盘

  • 先下线或封禁异常入口,避免继续扩大影响。
  • 排查日志,确认被访问的数据范围。
  • 修复注入点,补充参数化查询和白名单校验。
  • 轮换可能泄露的账号密码或密钥。
  • 补充安全测试用例,避免同类问题再次出现。

这里还要注意一点:前端校验只能提升用户体验,不能作为安全边界。真正的防护必须放在后端和数据库访问层。

索引是什么?

这里回答上了,并往Explain性能分析上发散了一下,但把占用性能忘了,也是比较遗憾。

索引可以理解为数据库为了提高查询效率而维护的一种数据结构。没有索引时,数据库可能需要全表扫描;有索引后,可以通过索引快速定位到目标数据。

MySQL InnoDB 中常见的索引结构是 B+ 树。它的特点是:

  • 非叶子节点主要存索引值,用来导航。
  • 叶子节点存放完整数据或主键值。
  • 叶子节点之间有链表,适合范围查询。

索引不是越多越好。它会占用存储空间,并且在 INSERTUPDATEDELETE 时需要维护索引结构,所以会影响写入性能。

什么是事务?

这就是很简单的概念了,还可以往 Spring 的 AOP 特性上发散一下。

事务是一组数据库操作的逻辑单元,要么全部成功,要么全部失败。它的经典特性是 ACID

  • A 原子性:事务中的操作要么全部成功,要么全部回滚。
  • C 一致性:事务执行前后,数据要满足业务约束。
  • I 隔离性:并发事务之间不能互相随意干扰。
  • D 持久性:事务提交后,数据修改要持久保存。

比如转账场景中,A 扣钱和 B 加钱必须放在一个事务里,否则就可能出现一边成功、一边失败的问题。

支付模块分为下订单、扣费、加积分,怎么确保一致性?

这一问我当时只想到了开事务,但没有继续往分布式场景展开。现在复盘来看,应该先判断这三个动作是不是在同一个服务、同一个数据库里。

如果这三个操作都在同一个服务、同一个数据库里,可以直接使用本地事务,把下订单、扣费、加积分放到同一个事务中。

但实际业务里,订单、支付、积分往往是不同服务,这时就变成了分布式事务问题。常见方案有:

  1. 本地事务 + 可靠消息

    • 订单和消息写入同一个本地事务。
    • 事务提交后,通过消息队列通知积分服务。
    • 积分服务消费消息时要保证幂等。
  2. TCC

    • Try 阶段预留资源。
    • Confirm 阶段确认提交。
    • Cancel 阶段释放资源。
    • 一致性更强,但业务改造成本更高。
  3. 最终一致性

    • 允许短时间不一致。
    • 通过消息重试、补偿任务、对账任务保证最终一致。

面试回答时可以先说明:强一致性成本高,实际业务里通常会结合可靠消息、幂等、重试和补偿来保证最终一致性。

下订单后保留 30 分钟,未支付恢复库存,怎么实现?各有什么优缺点?

这个问题本质是延迟任务。我现场只想到了 Redis 过期监听 和 轮询,还是缺乏开发经验。

方案 做法 优点 缺点
定时任务轮询 定时扫描超时未支付订单 实现简单,容易理解 不够实时,扫描压力大
Redis 过期监听 给订单 key 设置 30 分钟过期,监听过期事件 实现不复杂,实时性较好 Redis 过期事件不保证绝对可靠
延迟队列 下单后发送 30 分钟后投递的延迟消息 可靠性较好,适合生产 依赖 MQ,系统复杂度更高
时间轮 用时间轮管理大量延迟任务 性能好,适合大规模定时任务 实现和维护成本更高

生产上更推荐 延迟队列 + 幂等关闭订单

  1. 创建订单时锁定库存,并发送一条 30 分钟后触发的延迟消息。
  2. 消息到期后检查订单状态。
  3. 如果仍未支付,则关闭订单并恢复库存。
  4. 如果已经支付,则不做处理。

这里必须注意幂等。因为消息可能重复投递,关闭订单和恢复库存都不能重复执行。

生产故障排查

如果一台服务器的内存意外跑到了 100%,你会怎么排查?

这一问我在掘金上看到有人发过帖,可惜只是走马观花看了一下,没能回答上,比较遗憾。

可以按照“先止血、再定位、最后复盘”的思路回答。

第一步:确认现象

  • tophtop 看整体内存、CPU 情况。
  • free -h 看内存和 swap 使用情况。
  • ps aux --sort=-%mem | head 找出最占内存的进程。

第二步:定位进程

如果是 Java 进程,可以继续看:

  • jps -l 找 Java 进程。
  • jstat -gcutil <pid> 1000 观察 GC 情况。
  • jmap -histo:live <pid> 看对象数量和大小。
  • 必要时用 jmap -dump 导出堆快照,再用 MAT、VisualVM 等工具分析。

第三步:结合日志和业务判断

  • 最近是否上线了新版本。
  • 是否有大流量请求或异常接口。
  • 是否有大对象缓存、集合无限增长、线程池堆积。
  • 是否存在文件读取、批量查询一次性加载过多数据的问题。

第四步:临时止血

  • 如果服务已不可用,可以先扩容、限流或重启异常实例。
  • 如果是某个接口导致的,可以临时下线入口或降级。
  • 止血后再保留现场信息,避免重启后证据丢失。

Java 基础

OOP 特点

OOP 主要有四个特点:

  • 封装:把属性和行为封装到对象内部,对外暴露必要接口。
  • 继承:子类复用父类的属性和方法。
  • 多态:同一个接口或父类引用,在运行时可以表现出不同实现。
  • 抽象:提取共性,隐藏不必要的细节。

面试里可以重点讲封装、多态和抽象。继承虽然常见,但实际开发中过度继承也容易导致类层级复杂。也可以刚好引出 Java 为什么没有多继承:避免菱形继承问题。

类和对象的区别

类是模板,对象是类的实例。

比如 User 是一个类,它定义了用户应该有哪些属性和行为;而 new User() 创建出来的某一个具体用户,就是对象。

换句话说,类描述“是什么”,对象表示“具体是哪一个”。

构造函数有哪几种?

在 Java 里常见的说法有:

  • 无参构造函数:没有参数,用于创建默认对象。
  • 有参构造函数:通过参数初始化对象属性。
  • 默认构造函数:如果类里没有显式定义任何构造函数,编译器会自动生成一个无参构造函数。

需要注意的是:如果已经手写了有参构造函数,Java 就不会再自动生成无参构造函数。这时如果框架或代码需要无参构造,就要自己补上。

这一问我当时答得比较窄,只答了 Java 里最常见的构造函数。复盘后可以再补一层:构造对象不一定只靠构造函数,很多框架和设计模式会把对象创建过程封装起来。

从 Java 基础角度看,构造函数主要看是否有参数、是否由编译器默认生成;从工程实践角度看,对象还可能通过工厂方法、Builder、反射、反序列化、Spring 容器等方式创建。面试时如果能区分“构造函数”和“对象创建方式”,回答会更完整。

你怎么看待 new 一个对象?

我理解的 new 不只是“创建一个对象”这么简单,它背后至少包含几个步骤:

  1. 在堆内存中为对象分配空间。
  2. 对对象字段进行零值初始化。
  3. 执行构造方法中的初始化逻辑。
  4. 返回对象引用,让栈上的变量可以指向这个对象。

从 JVM 层面看,对象创建还可能涉及类加载检查、内存分配方式、对象头、引用指向等内容。面试时如果能从“语法层面、内存层面、JVM 层面”逐层回答,会比只说 new 是创建对象更完整。

反问环节

我的问题是:我这次面试还有什么能改进的地方?

对方回答(大致)是:“作为一个大二学生,能回答上大部分问题已经很好了,但底层方面还是比较匮乏。如果想要做一个好的后端工程师,还是应该向 JVM 内存、Spring 原理、汇编等方面加深学习。”

感悟

整体上大概回答了 2/3,剩下的能编就编,要么就往自己的项目或熟悉的领域去扯,实在不了解的部分就直接 gg 投降。

平时刷博客还是有用的。服务器跑满内存的示例我刚好在掘金看到有人发过,虽然具体记不太清了,但还是勉强扯了一下。

说实话没想到对方会问到这些,因为大家都是大二的,虽然对方也表示了没期望有人能全部回答上。我原本以为大致 10 分钟就能结束战斗,结果硬生生聊了三十几分钟。

这次面试给我的最大提醒是:项目经历只能证明“做过”,但面试会继续追问“为什么这么做、底层怎么实现、出问题怎么排查”。后面还是要把 JVM、Spring、Redis、MySQL、Linux 排查这些内容系统补一遍,不能只停留在会用 API 的层面。

总体而言,第一次面试还是受益匪浅,面试官也很友善,在知道我没有学习计网后就直接换了题型。三位面试官也都在我思考问题时耐心等待,也没有压力面,可以说体验很好。

出于隐私原因,这里就不放公司名称了。