清能互联

本文最后更新于:2025年10月14日 晚上

清能互联–后端Java面试

面试信息:

  • 时间:2024年7月18日 16:00
  • 形式:线上面试
  • 时长:47分钟

逻辑过期策略 + 缓存击穿优化

逻辑过期策略解决缓存击穿问题,这个可以说一下吗

这个策略是用来优化 Redis 中热点数据的缓存击穿问题的。

我们没有直接使用 Redis 的过期机制(即 TTL),而是把一个逻辑过期时间戳写入缓存对象里。每次读缓存时,先判断当前时间是否超过这个时间戳。

如果没过期,就直接返回旧数据;如果过期了,我们使用一个简单的锁机制(例如互斥标记或 Redis 分布式锁)确保只有一个线程去查询数据库并刷新缓存,其他线程继续返回旧值,不会都打到数据库。

这种方式可以保证系统的高可用性,同时避免缓存集中失效带来的数据库压力

数据库设计

问题:你们数据库层面做了什么设计?

在数据库设计层面,我们前期根据业务模型梳理了核心实体,完成了如用户表、订单表、商品(咖啡豆)表、农庄表等结构设计。

以订单表为例,我们设置了自增主键 id,订单编号设置了唯一索引 order_number,方便根据订单号精准查找。同时为 user_id 字段添加普通索引,加速用户订单的筛选。

此外,我们也根据查询场景设计了联合索引,比如 (user_id, status) 用于用户订单分页查询时减少回表,提高查询效率。整体设计兼顾了数据一致性、扩展性和性能优化。

问题:MySQL 最左匹配原则

它是针对 联合索引 的使用规则,比如我创建了一个联合索引 (a, b, c),那它在查询时只有从 最左边字段 a 开始连续匹配,索引才会生效。

举例来说,where a = 1 或者 where a = 1 and b = 2 都能命中索引,但如果是 where b = 2 and c = 3,因为跳过了最左边的 a,就不会命中索引了。

所以我们在设计联合索引的时候,会根据实际查询的 where 条件顺序,来安排字段顺序,尽量满足最左匹配。

问题:覆盖索引

  • 查询只用到了索引中的字段,无需回表,性能更高;

  • 如:索引 (user_id, status, create_time),若只查这 3 个字段,可直接从索引中返回数据。

压力测试

问题:你们做过压力测试吗?

项目是有部署上线的,我们使用的是学校分配的远程服务器,部署在 Ubuntu 环境下,提供了公网访问入口。我们当时的用户量较小,主要面向演示与测试使用

在压测方面,我们没有做很正式的压力测试,但做过一定的并发访问模拟,比如使用 Postman 脚本测试了一些接口在并发访问下的响应时间。当时的目标不是商业上线,所以系统承压量并不大。

Redis

问题:你们用到Redis哪些数据结构

在平台项目中,我们使用 Redis 作为缓存组件,主要用到了 StringHash 两种结构:

  • String 用于缓存热点数据,比如用户信息、农庄简介等,搭配逻辑过期策略避免缓存击穿;
  • Hash 用于管理结构化数据,比如商品库存,用字段名表示商品 ID,字段值表示剩余库存,便于原子性更新;

库存有过期时间?

对于库存数据,一般是作为持久数据,不设置过期时间,直接从数据库同步,避免商品丢失;

而像商品详情、农庄信息等更新频率低但访问频繁的数据,我们使用逻辑过期策略:设置一个过期时间戳字段,缓存不会物理删除,而是由后台线程检查是否“逻辑过期”,来控制刷新节奏,从而避免瞬时大量请求打到数据库。

消息队列

问题:你们 MQ 主要用来做什么的?

  • 异步下单;

  • 服务解耦;

  • 高并发下的削峰;

追问:下单链路中,MQ 解决了什么问题?你们怎么用的?

  1. 用户发起下单请求;

  2. 后端先通过 Redis + Lua 脚本 做库存预扣减,保证高并发下库存不超卖;

  3. 若库存充足,将订单信息封装成消息发送到 MQ 队列中

  4. 后台消费者监听这个队列,从队列中拿到消息后,进行后续处理:

     - 将订单写入 MySQL;

     - 扣减数据库中实际库存;

     - 更新订单状态;

     - 触发通知、物流等后续操作;

追问:放入 MQ 中,进入数据库的操作,这块具体可以再细化一下。细化说一下

订单放入MQ之后,有一个消费者服务监听这个订单队列。这个服务拿到消息后会:

  1. 再次做幂等检验,根据订单号检查数据库中是否已经存在该订单,防止重复消费
  2. 然后写入MySQL创建订单记录
  3. 同时扣减数据库的真实内存
  4. 最后,手动ack消息,表示消息成功

功能

问题:你主要实现了哪一块的逻辑

商品上架

追问:如何实现分页

我主要是:

  • 在 MyBatis 层写了对应的 SELECT ... LIMIT SQL;
  • 分页逻辑是前端传 pageNumpageSize 两个参数;
  • 后端用 offset = (pageNum - 1) * pageSize 来计算偏移量;
  • 然后返回总记录数和当前页数据。

实际开发中我们也用过 MyBatis 的 PageHelper 插件,它可以自动帮我们处理分页逻辑,减少重复代码。

追问:商品上架后放到缓存中吗

在写入数据库的同时,也将商品信息同步写入Redis缓存中。

  • 上架时,我们先写入数据库,然后将该商品加入缓存结构。用Hash存储商品详情,以List缓存某个农庄下的商品ID列表;

  • 为保持缓存数据库一致性,我们用延迟双删策略;上架前先删除商品缓存,写库成功后,再延迟一段时间做二次删除,确保缓存更新。

权限认证

无追问

八股

Java

问题:你们用的Java几

Java8

问题:JDK8的特性有哪些,你能说以下吗?

  1. 元空间代替永久代:元空间是分配在直接内存中,解决永久代存在的内存不足、GC效率低问题
  2. Lambda表达式:简洁的方式表示函数示接口
  3. StreamAPI:更方便处理集合操作,过滤、排序、分组等
  4. 新增接口默认实现方法
  5. 新增时间API
  6. 新增Optional类:提供一中优雅的方式解决可能出现的空指针问题

数据结构

你用了哪些数据类型

  • int用于库存、数量
  • double:价格
  • boolean:状态开关
  • String:文本信息
  • List:展示商品列表
  • Set:用于去重,用户收藏等
  • Map:下单时存<商品ID, 数量>
  • Date:处理下单时间

基础数据类型和他们的长度

  • byte(8)
  • short(16)
  • int(32)
  • long(64)
  • float(32)
  • double(64)
  • char(16)
  • boolean(1)

包装类型有用过吗

  • Integer
    • 在集合中必须用包装类型,集合不能存基本数据类型
    • 比如订单模块,统计商品购买的数量,分页的参数都是Integer
  • BigDecimal
    • 在处理金额的时候用过,因为double在浮点运算时有精度问题。

方法重载和重写有了解吗

**方法重载(Overloading): ** 在同一个类中,允许有多个同名方法,只要他们的参数列表不同,与返回值无关。

**方法重写(Overriding): ** 子类在继承父类时,可以重写父类的某个方法,从而为该方法提供新的实现。

Java集合

你的项目中用了哪些集合

  • List:用于存储用户下单时购买的多个商品
  • Map:用于key-value映射场景。
  • Set:用于去重,例如用户的收藏夹

你用的是哪种Map

主要使用了 HashMap,因为它查询效率较高,时间复杂度基本是 O(1)。

在某些需要保持插入顺序的场景(比如维护用户添加商品的顺序),也使用过 LinkedHashMap

HashMap有序吗

HashMap无序的。它的底层使用数组 + 链表/红黑树结构存储数据,键值对的插入顺序和遍历顺序无关

HashMap 是线程安全吗?

HashMap 不是线程安全的。如果多个线程同时访问并修改同一个 HashMap,可能会导致数据不一致或死循环。

其他问题

你平时怎么去学习的

  1. 项目实践中学习
  2. 体系化学习

项目中肯定会遇到问题,一般是通过什么方式去解决这个问题?

  1. 先自己定位问题:通过日志分析问题出错的位置和报错信息。
  2. 查阅资料:官方文档,CSDN,StackOverflow
  3. AI辅助

你看书或者看资料里有没有一个系统地去想了解某个内容的时候去学?

你在发明专利或管理平台项目中,印象最深的一个点,不管是技术、管理这些方面是什么

在管理平台项目中,我印象最深的是作为团队主要负责人协调整体开发流程的经历。我们这个项目是从立项到部署上线全流程自己主导完成的,期间我承担了需求分析、任务拆解、接口文档撰写与后端架构设计等核心工作。
项目初期,成员对业务流程理解不一,我就主动整理了用户流程图,并召开了几次小型讨论会,把需求细化并同步到所有人。后来在接口联调阶段,我又编写了2/3以上的接口文档,统一了参数规范和错误码设计,有效避免了前后端的反复沟通。
整个过程中,我深刻体会到项目推进中“沟通效率、节奏控制和责任分配”的重要性,也意识到作为小组负责人不仅要写代码,更要让团队协作高效、有序。这段经历提升了我对项目管理和团队协同的整体把控能力。