模拟面
本文最后更新于:2025年10月14日 晚上
模拟面试
自我评价
最好对着摄像头说
尽量说一点简历上没有的内容
可以说一下为什么做这个项目,可以介绍兴趣爱好、成绩
你用时长3分钟左右,尽量压缩在2分钟左右,说明白即可,可以多练习
面试官您好,我22年进入北京工业大学修读软件工程专业,将于26年上半年本科毕业。
非常感谢有机参加贵公司的面试,我也对贵司的技术氛围和这次成长学习的机会十分珍惜。
简历上展示了一些我在本科阶段参与的研究和项目实践,我做两点补充:
第一,我是一个喜欢提前设计的人。在项目开发中,我习惯在动手前先做整体功能的梳理,把需求拆解清晰,选定合理的技术栈,明确模块职责,设计好接口结构。这种Coding after Design,特别是在团队合作的项目中尤为重要,减少返工,也让后续迭代更顺利,也方便后期维护和拓展。
第二,我十分珍惜这次实习机会,抱着学习+贡献的心态。希望通过这段时间,真正参与到真实的开发流程中,再实战中提升能力,也为团队做出自己的价值。
项目拷打
什么是JWT?Json Web Token,还有什么可替代的吗?Cookie和Session了解过吗?
都是存用户的登陆状态
Cookie 储存用户浏览器,容易被用户篡改
Session 储存在服务器,分布式部署的时候,会出现问题
JWT 无状态 ,基于Token的认证机制。不依靠客户端和服务器,主要有三个部分 :
- Header(存一些加密算法的类型 SHA-2)
- payload(存用户的信息,用户名,用户ID,角色)
- signture(签名,防伪的作用)
JWT的主要作用:
用户登录成功后,服务端生成一个加密的 JWT,其中包含了一些用户身份的信息(比如用户 ID、角色、过期时间等),然后返回给客户端;
客户端将这个 Token 存储(通常是放在
localStorage
或cookie
中);之后客户端在每次请求时都会携带这个 Token;
服务端通过验证 Token 的签名和有效期,完成用户的身份认证。
ThreadLocal是什么?会产生什么问题呢?
ThreadLocal
是 Java 提供的一种线程隔离机制,它允许我们为每个线程创建一个独立的变量副本。
每一个线程单独的区域,可以存放这个线程的信息,可以从线程里面保留用户的信息进行逻辑的判断。
可能导致内存泄漏。
缓存击穿?
某个热点数据突然失败,大量请求直接打到数据库,绕过缓存,导致了数据库的压力,
加锁,第一个发现缓存没了,代码进行加锁,让后面的请求等着,等待第一个线程把缓存加载进来,释放锁,后面正常请求缓存对热点数据,不设置过期时间,添加时间标记,发现过期,一个线程去更新缓存,其他的线程,返回旧的缓存信息,虽然会导致一些数据的不一致问题,但是时间很短,几乎无影响。
为什么要用Lua脚本呢?
解决原子性,一般都涉及多个redis操作,分两步:查库存剩余量 -> 在缓存中扣减库存,目的是保证业务的正确性。
你用MQ干什么了?
服务的解耦、流量的消峰、异步处理
异步下单(削峰)处理订单
在演唱会管理系统中,用户下单购票是一个高并发场景,涉及:
- 库存扣减
- 订单创建
- 状态更新
- 支付超时处理等
如果这些操作全部同步完成,不仅响应慢,而且容易造成系统阻塞。
因此我们将下单请求写入 MQ 队列,后台消费者异步处理:
- 用户点击“提交订单”后,我们只做一些轻量级校验和库存预减(Redis/Lua 脚本),然后将订单信息发送到 RabbitMQ;
- 消费者服务监听消息,异步完成订单入库、状态管理等后续操作;
- 这样极大减轻了核心业务接口的压力,实现了“削峰+解耦”。
延迟双删是如何解决数据库和缓存一致性的?
更新数据库时,如果没有正确处理缓存可能出现缓存脏数据。
延迟双删:删除两次缓存 + 延迟一段时间,确保最终一致
为什么用redis做分布式锁
为了避免在 高并发场景下多个线程同时重建缓存,造成数据库压力或脏数据,我们用分布式锁控制只有一个线程可以重建缓存。
🔐 加锁:使用
SET key value NX EX
原子命令🔓 解锁:用 Lua 脚本保证原子性
项目中的难点是什么?
技术上的难点
高并发下的库存扣减和数据的一致性。在我做的两个项目中,都有类似电商类的下单模块,演唱会购买/咖啡豆购买。我们要处理“商品库存扣减 + 异步下单”流程,这个环节是整个系统的高并发瓶颈点。
难点在于:
- 高并发下防止超卖
多个用户同时请求一个商品,很容易出现库存被扣成负数的情况;
我们使用 Redis 缓存库存,并通过 Lua 脚本实现库存扣减逻辑的原子性,确保“一人一单”。
- 保证缓存一致性
- 下单成功后要更新数据库 + 更新 Redis;
- 我们采用了延迟双删策略来确保缓存与数据库最终一致;
- 同时结合分布式锁 + 逻辑过期的方式解决缓存击穿问题。
协作上的难点
前后端联调期间的接口不一致
我们是前后端分离架构,使用 RESTful API,在联调时遇到几个问题:
- 前端理解与接口定义有出入
- 比如分页参数、错误码设计、权限控制等;
- 我们统一使用 Postman + Apifox 管理接口文档,并制定接口响应标准结构。
- 多角色权限逻辑复杂
- 平台支持管理员、农场主、普通用户三类角色,权限边界需要严格控制;
- 我基于 Spring Security + JWT 实现了基于角色的访问控制(RBAC),并通过拦截器 + 注解做了统一管理。
可持续开发平台管理系统
- JWT + Spring Security
📌 Q: Spring Security 的过滤器链是如何工作的?JWT 是在哪个环节被解析的?
答:
每次请求进来都会经过 Spring Security 的一系列校验流程。
我们在系统里加了一个专门的过滤器,放在用户请求刚进入的时候,它会从请求头里把 JWT token 拿出来,做一次校验和解析,确认用户身份是否合法。
如果合法,就把用户的信息保存下来,整个请求链路就能识别“是谁在访问”。
📌 Q: 如果想给某些接口开放匿名访问,怎么配置?permitAll() 和 anonymous() 有什么区别?
答:
我们可以在权限控制里配置允许某些接口“任何人都能访问”,比如登录、注册,这种用 permitAll()
就行。
anonymous()
是只允许未登录用户访问,比如注册接口不希望登录用户重复调用。
大多数场景下我们用的是 permitAll()
,更灵活一些。
📌 Q: JWT 一旦过期是不是就要重新登录?你怎么处理 token 续签?
答:
JWT 过期后默认是不能用了,所以我们设计了“刷新 token”机制:
登录时给前端两个 token,一个短期用来访问接口,一个长期用来换新的;
如果短 token 过期了,前端可以用长期的 token 换一个新的,而不需要重新登录,体验更好。
✅ 2. Redis 缓存机制
📌 Q: 你提到逻辑过期、缓存击穿,那你有考虑缓存穿透和雪崩吗?
答:
是的,我们都有考虑:
- 缓存穿透:对数据库查不到的数据,我们也会缓存一个空值,防止反复打数据库;
- 缓存雪崩:我们会给不同缓存设置不同的过期时间,加一点随机数,避免同一时间全部失效。
📌 Q: 为什么采用“延迟双删”?和消息队列配合缓存更新时怎么保证顺序?
答:
延迟双删是为了防止并发读写冲突。我们会先删缓存,再更新数据库,然后延迟一小段时间再删一次缓存,避免脏数据被读到。
配合消息队列时,我们也是先处理数据库,再更新缓存,确保顺序一致。
📌 Q: Redis 为什么要加锁而不是乐观锁?
答:
我们加锁是为了防止多个线程同时更新缓存或查询数据库,比如热点数据过期时。
Redis 加锁简单高效,能保证同一时刻只有一个线程重建缓存。
乐观锁适合数据库更新,不太适合缓存控制。
✅ 3. 高并发库存处理
📌 Q: Lua 脚本里具体做了哪些操作?考虑过超卖与重复下单吗?
答:
Lua 脚本的好处是操作是一次性原子执行的,我们在脚本里做了两件事:
一是检查库存是否大于 0,二是扣减库存并标记用户已经下过单。
这样就能防止超卖和重复下单问题。
📌 Q: 为什么用 Redis 做库存,而不是数据库?
答:
Redis 是内存数据库,性能更高,特别适合高并发的场景。
数据库在高并发下更容易锁表,响应慢,所以我们用 Redis 先预扣库存,再通过消息队列异步落库。
📌 Q: 消息队列异步下单,那如果消息投递失败怎么办?用了哪些保证机制?
答:
我们开启了消息确认机制:
- 生产端会确认消息有没有发成功,失败就重发;
- 消费端处理完后才手动确认,防止消息丢失或乱序;
- 还加了幂等控制,比如同一个订单不能重复入库。
✅ 4. RabbitMQ 的可靠性
📌 Q: 如何实现消息的可靠投递和可靠消费?
答:
生产端我们开启了确认机制,确保消息成功送出去;
消费端是手动确认的,逻辑处理完后再发确认信号;
中间如果失败,会触发重试机制,保证消息不丢。
📌 Q: 延迟队列是怎么实现的?为什么不用线程轮询?
答:
我们用的是 RabbitMQ 的“延迟队列 + 死信队列”方案:
先发一个有过期时间的消息,过期后自动进死信队列,再由消费者处理,比如取消订单。
这种方式性能高、占资源少,比自己轮询数据库更稳、更准时。
✅ 5. 业务建模
📌 Q: 农场、订单、用户等实体是怎么设计的?有没有考虑冗余字段?
答:
我们把用户、农场、商品都拆成独立表,用 ID 做关联。
订单里保存了商品的快照信息,比如商品名、单价,这是冗余字段,主要是防止将来商品改名后订单信息变了,保证历史一致性。
📌 Q: 如何处理订单状态变化?状态流转怎么设计?
答:
我们用枚举来表示订单状态,比如:待支付、已支付、已取消。
每次状态变更都记录时间,控制只能按照正确流程流转。
比如“待支付”超过 15 分钟就自动变成“已取消”,我们通过延迟队列实现这个过程。
演唱会管理系统
✅ 1. 分布式锁
📌 Q: 你用的 Redis 分布式锁具体实现方式是什么?是 SETNX 还是 Redisson?
答:
我们用的是基于 SETNX
的简单实现,也就是 Redis 提供的 set key value NX EX
命令,一次性就能加锁并设置过期时间,避免死锁。
Redisson 是更完整的封装,但我们的项目场景比较轻,所以用原生命令就足够了。
📌 Q: 如何保证锁的可重入?如何处理锁续期?
答:
我们没做可重入,因为请求间不共享线程,一般不需要重复加同一把锁。
锁续期也没有使用专门机制,而是设置了合理的过期时间(比如 10 秒),并确保业务处理时间远小于它。如果要实现续期,可以用后台线程定期延长锁时间,Redisson 是自带这个功能的。
📌 Q: 你说用了 ThreadLocal,它和分布式锁什么关系?释放锁时为什么要校验 token?
答:
ThreadLocal 主要是用来保存当前线程的用户信息,比如登录用户,和锁没有直接关系。
而释放锁时校验 token(或 UUID)是为了防止误删别人的锁,因为 Redis 是共享资源,只允许加锁的人自己解锁。所以我们在加锁时写入唯一值,解锁时用 Lua 脚本判断是否一致,再执行删除。
✅ 2. 设计模式
📌 Q: 策略模式在项目中是怎么落地的?用了哪些接口与实现类?
答:
我们在做“票务定价和促销策略”时用到了策略模式。
定义一个定价策略接口,比如 PricingStrategy
,然后实现了多种策略类,如 NormalPriceStrategy
、DiscountPriceStrategy
,根据演唱会活动类型动态选择。
这样代码扩展性很好,不用写一堆 if-else。
📌 Q: 有没有考虑工厂模式来配合策略注册?
答:
有的,我们用了一个简单的工厂类,根据类型返回对应的策略实例,相当于“策略注册表”。
后面如果需要支持更复杂的场景,比如运行时配置不同策略,也可以引入配置中心配合这个工厂来动态管理。
📌 Q: 项目中还有没有用到其他设计模式?
答:
我们还用了模板方法模式,比如统一处理订单状态变更时,定义一个公共的流程骨架,不同业务只需要实现具体的细节逻辑。
另外,订单处理流程中的消息发送和回调也部分用到了观察者模式的思想(通过 MQ 解耦)。
✅ 3. Redis 延迟双删 + 分布式锁
📌 Q: Redis 分布式锁 + 延迟双删会不会有一致性问题?
答:
如果加锁 + 双删都写对了,一般不会有问题。但如果忘记解锁或者第二次删除失败,确实可能出现缓存不一致。
所以我们也会考虑用异步重试机制,或者定期检查缓存一致性,特别是对重要数据。
📌 Q: 延迟多久删?为什么这么设置?有没有考虑删除失败的补偿机制?
答:
我们一般延迟 500 毫秒左右,目的是等数据库更新完之后,确保没有并发读请求再写回旧值。
如果删除失败,可以通过定时任务或消息重试机制补偿,这块后期可以做自动检测或报警。
✅ 4. Docker 使用
📌 Q: 你在项目中提到 Docker 部署,具体是怎么操作的?
答:
我们把 Spring Boot 项目打成 jar 包,用 Dockerfile 构建镜像,然后用 docker run
运行容器。
整个流程包括打包、构建镜像、运行镜像,我在本地和测试服务器上都部署过。
📌 Q: Spring Boot 项目如何打包成镜像?Dockerfile 如何编写?
答:
我们使用的是一个很简单的 Dockerfile,大概如下:
1 |
|
先用 Maven 打包出 jar,再用 Docker 构建镜像:
1 |
|
📌 Q: Redis、RabbitMQ 是如何部署的?用了 docker-compose 吗?
答:
是的,我们用 docker-compose.yml
一次性部署了 Redis、RabbitMQ 和 MySQL,方便本地调试和部署统一管理。
配置好端口、挂载、密码等之后,运行 docker-compose up
就能一起启动所有服务,非常方便。
八股问题
Java基础部分
ArrayList和LinkedList的区别是什么,什么情况下使用?
ArrayList 基于动态数组实现的;连续内存,查询很快 更改很慢
LinkedList 是基于双向链表实现的;内存不连续,更改很快,查询很慢
接口和抽象类的区别是什么
设计动机上不同。
接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为定义接口。
抽象类的设计是自下而上的。我们写了很多类,发现他们之间有共性,有很多代码可以复用。
接口没有构造方法;抽象类有构造方法。
接口支持多继承;抽象类只能但集成。
接口不能实现方法,
jdk1.8之后 接口是可以写方法的
String、StringBuffer和StringBuilder分别是什么?区别在哪里
String
- 不可变类,字符一旦创建,其内容无法更改。
- 适合字符串内容不会频繁变动的场景
StringBuffer
可变类,可以进行字符串增删查改
是线程安全的、append()
StringBuilder
- 可变类
- 非线程安全,但是性能比StringBuffer好
- 适合单线程下,需要大量修改字符串的场景
== 和 equals()比较的区别是什么
==
比较两个引用是否指向同一个对象(内存地址)。如果是基本数据类型,则比较他们的值equals()
比较两个对象的内容是否相等,通常需要重写比较逻辑。hashcode()和equals()有什么关联?
如果两个对象根据equals()相等,他们的hashCode()必须相同。
但是反过来不要求成立。
重写equal不重写hashcode会导致一个set中被放进去两个相同元素,导致业务逻辑问题
Hashmap了解过吗?说一下
基于哈希表的数据结构,用于储存key + value。哈希冲突的处理, java1.8 之前:是 数组+链表,1.8之后 是数组 + 链表 + 红黑树
Hashmap 默认大小是16,阈值(负载因子是0.75)12,两倍扩容:16 -> 32
计算机网络部分
TCP和UDP区别是什么
连接:TCP 可靠传输 (拥塞控制 三次握手 保证消息的顺序性);UDP 不可靠传输 适用于实时的系统,比如说 游戏
服务对象: TCP是一对一两点服务;UDP支持一对多,多对多。
拥塞控制、流量控制: TCP有拥塞控制和流量控制机制。UDP没有,即使网络十分拥堵,也不会影响UDP的发送速率。
HTTP和HTTPS的区别是什么
端口:80 / 443
SEO:HTTPS的网站会靠前
安全性:HTTP是明文传输,存在安全风险。HTTPS报文加密传输
MySQL
MySQL的索引是干什么的?为什么要用?如何优化?
索引类似书的目录,用来加快查询效率
索引用在数据量很大的表的时候为了快速查找数据才使用的。避免全表扫描,提升系统性能。
优化:对频繁查找的字段添加索引,大量重复的字段要避免使用索引,比如说性别。
MySQL的回表是什么
MySQL 查询时先通过索引找到主键,然后再回到主表根据主键取出完整的数据行。
SELECT name, age FROM student WHERE name= 123;
对student表的name进行建立索引,但是没有对age建立索引。
额外的查询,造成了IO的浪费
MySQL的事务隔离级别都有什么
并发问题 场景示例 脏读 你读到了别人还没提交的临时数据 不可重复读 你前后查询一条数据,发现值变了 幻读 你两次 SELECT COUNT(*)
,发现数量变了(有人插入了新数据)Read Uncommitted(读未提交)
- 最不安全,可以读到别人未提交的数据,也就是“脏读”。
🔸 Read Committed(读已提交)
- 只能读到别人已经提交的数据;
- 解决了脏读,但可能不可重复读(前后查询同一行结果变了);
- Oracle 默认使用这个级别。
🔸 Repeatable Read(可重复读) ✅ MySQL 默认
- 在一个事务中多次读取相同行结果一样,防止不可重复读;
- 但可能出现幻读(比如查订单数突然变多);
- MySQL 用 MVCC(多版本并发控制) 技术解决幻读,实际很少发生。
🔸 Serializable(可串行化)
- 最安全,所有事务串行执行,防止所有并发问题;
- 但性能最差,容易产生锁竞争和阻塞。
Spring
常用注解说一下
@Autowired
:装配bean.@Component
:标记一个类作为Spring的bean.@Configuration
:标记一个类作为Spring的配置类.@Service
:标记一个类作为服务层组件@Repository
:标记一个类作为数据访问层组件Body和Param的区别 控制访问不是springboot的 是springsecurty的
RequestBody :接受Json数据自动映射为java对象;
RequestParam:读取URL参数信息,liule.com/id=1
IOC AOP?
IoC 控制反转:交由Spring来管理Bean,直接注入就可以,方便开发
AOP 切面编程:能够讲那些与业务无关,却为业务模块锁共同调用的逻辑封装起来,以减少系统的重复代码。
基于反射,可以对代码进行额外的操作,如记录日志等信息
Redis
是干什么的?平常怎么用的?
高性能的缓存中间件。数据储存在内存中,读写非常快,用于加速访问。
平常用于缓存热点数据,存放临时数据。
数据结构说一下
常用的就四个:String、Hash、List、Set(ZSet(有序),set(无序))
GEO(计算地理位置)、Stream(消息队列)
Redis为什么这么快?
基于内存、高效的数据结构、内部单线程,避免切换线程造成的额外开销
缓存穿透、击穿、雪崩
穿透:直接绕过缓存,访问数据库,id = -1 (缓存为null)
雪崩:多级缓存、分布式部署,设置不同的过期时间
消息队列
你为什么要用消息队列?
消息队列是使用队列通信的组件,是一种异步通信机制.
目的:解耦服务;异步处理;流量消锋
如何处理重复消息
为什么出现重复
尽管开启确认机制,仍然可能因网络波动,ack丢失等原因,导致生产者重发或消费者重新处理,从而产生重复消息。
幂等:在处理可以把订单号作为唯一标识,加到redis里面,后面的请求直接查redis。
反问
一定要问,一定!!!
常问问题:
- 组的业务是什么?多少人?
- 入职需要准备什么
- 后面有没有流程了
- 对实习生有哪些培养机制
- 工作节奏如何