Java 大厂一面模拟:从本地缓存到分布式事务的连环追问

一面定位

如果你来参加大厂 Java 一面,今天这场模拟面试将聚焦于本地缓存设计、分布式事务一致性、Spring 事务传播、MySQL 锁机制与 JVM 内存模型的联动考察。候选人画像为 2 年 Java 后端经验,参与过订单、支付或会员类系统设计。面试时长约 30 分钟,节奏紧凑,问题层层递进,重点考察从"知道"到"会用"再到"能兜底"的完整链路。

面试官风格参考牛客常见大厂一面:先问基础概念,再追问原理与边界,最后落到项目场景中的取舍与落地。整场面试不堆题库,但必须体现"拷打感"------即一个问题引出多个关联点,连续追问不留喘息。


主问题部分

1. 你在项目中用过哪些本地缓存?为什么不用 Redis?

参考回答: 我用过 Caffeine 做本地缓存,比如缓存商品基础信息、用户权限数据等读多写少的场景。选择本地缓存是因为这些数据变更频率低,且对延迟敏感,本地缓存可以避免网络开销,提升接口响应速度。而 Redis 虽然也能缓存,但每次访问都要走网络,QPS 高时对 Redis 压力大,也容易成为瓶颈。

2. Caffeine 的缓存淘汰策略有哪些?你项目中用的是哪一种?

参考回答: Caffeine 支持三种淘汰策略:基于大小的(Size-based)、基于时间的(Time-based)、基于引用的(Reference-based)。我项目中主要用基于大小的 maximumSize 配合 expireAfterWrite,比如设置最大 1000 条,写入后 5 分钟过期。这样既能控制内存占用,又能保证数据基本新鲜。

3. 本地缓存和 Redis 缓存如何配合使用?有没有遇到过一致性问题?

参考回答: 我们采用"本地缓存 + Redis 二级缓存"架构。先查本地,没有再查 Redis,都没有才查 DB。更新时先更新 DB,再删除 Redis,最后通过消息通知各节点删除本地缓存。遇到过一致性问题:某次 DB 更新后,Redis 删除成功,但本地缓存因消息丢失未清理,导致读到旧数据。后来加了本地缓存的 TTL 兜底,并记录日志用于排查。

4. 你说更新 DB 后删 Redis,那如果删 Redis 失败了怎么办?

参考回答: 这是个经典问题。我们目前的做法是:DB 更新成功后,异步发送一条"缓存删除"消息到 Kafka,由消费者执行 Redis 删除。如果删除失败,会重试 3 次,仍失败则告警人工介入。同时,Redis 本身也设置了过期时间作为最终兜底。

5. 如果多个服务实例同时更新同一数据,如何保证缓存一致性?

参考回答: 我们通过分布式锁(Redisson)保证同一时间只有一个实例能执行更新操作。更新流程是:获取锁 -> 更新 DB -> 删除 Redis -> 释放锁。其他实例在锁释放后,下次读取时会重新加载最新数据。

6. 你提到 Redisson,它底层是怎么实现分布式锁的?

参考回答: Redisson 基于 Redis 的 SET resource_name random_value NX PX timeout 命令实现。NX 表示只有 key 不存在时才设置,PX 设置过期时间防止死锁。释放锁时用 Lua 脚本判断 value 是否匹配,只有匹配才删除,避免误删其他线程的锁。

7. 如果 Redis 主从切换,Redisson 的锁会丢吗?

参考回答: 会的。Redis 主从异步复制,如果主节点加锁后还没同步到从节点就挂了,新主节点没有这个锁,会导致锁丢失。Redisson 官方推荐使用 RedLock 算法(多实例部署)来降低风险,但我们项目目前还是单 Redis 实例,靠业务层幂等和补偿机制兜底。

8. 你们的事务是怎么管理的?Spring 的 @Transactional 用过吗?

参考回答: 用过。我们大部分业务用 @Transactional 声明式事务,默认传播行为是 REQUIRED。跨服务调用时,会用 Seata 做分布式事务,比如下单扣库存 + 创建订单 + 扣余额这种多库操作。

9. @Transactional 在什么情况下会失效?

参考回答: 常见失效场景有:方法不是 public、异常被捕获未抛出、同类内方法调用(AOP 代理不生效)、数据库引擎不支持事务(如 MyISAM)。我们项目里踩过同类内调用的坑,后来改成通过代理对象调用解决。

10. 你提到 Seata,它的事务模式有哪些?你们用的是哪一种?

参考回答: Seata 支持 AT、TCC、Saga、XA 模式。我们用的是 AT 模式,因为它对代码侵入小,自动反向 SQL 回滚。但 AT 模式要求所有参与事务的表必须有主键,否则无法生成回滚日志。

11. 如果 AT 模式回滚失败,你们怎么处理?

参考回答: Seata 会记录 undo_log,如果回滚失败,会进入"悬挂事务"状态,由定时任务扫描重试。我们还在业务层做了补偿接口,比如订单创建失败后,手动调用库存回滚接口。

12. MySQL 的间隙锁你了解吗?什么场景下会触发?

参考回答: 间隙锁是 InnoDB 在可重复读隔离级别下,为了防止幻读而加在索引记录之间的锁。比如执行 SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE,会锁住 (20,30) 这个区间,其他事务不能插入该范围内的数据。

13. 间隙锁会导致死锁吗?你们遇到过吗?

参考回答: 会的。比如两个事务分别插入不同数据,但落在同一个间隙,互相等待对方释放间隙锁,就会死锁。我们遇到过,后来通过缩小查询范围、避免大范围 FOR UPDATE 来减少触发概率。

14. JVM 的堆内存结构是怎样的?你项目中 GC 调优过吗?

参考回答: 堆分为新生代(Eden + Survivor)和老年代。我们项目用 G1 GC,堆大小 4G,新生代初始占比 30%。之前 Full GC 频繁,后来发现是缓存对象太多,调整了 Caffeine 的 maximumSize,并加了堆外内存监控。

15. 你说用 G1,它的 Mixed GC 是怎么工作的?

参考回答: Mixed GC 是 G1 特有的,它会同时回收新生代和部分老年代区域(根据回收价值排序)。触发条件是堆占用达到 InitiatingHeapOccupancyPercent(默认 45%)。我们设置成 60%,避免过早触发。


追问部分

面试官开始深入"本地缓存 + 分布式事务"的联动场景

追问 1:如果本地缓存未失效,但 DB 已被另一个服务更新,你们怎么感知?

候选人回答: 我们靠消息通知。更新服务发一条"数据变更"消息,各服务监听后清理本地缓存。

面试官追问: 消息丢了怎么办?有没有做过压测验证消息可靠性?

候选人回答: 消息丢了就只能靠 TTL 兜底。压测时模拟过消息丢失,发现本地缓存最长可能脏 5 分钟,业务上可接受。

面试官追问: 如果业务要求强一致,比如金融场景,这种方案还适用吗?

候选人回答: 不适用。强一致场景应该放弃本地缓存,或者用更短的 TTL + 主动刷新机制,甚至直接读 DB。

追问 2:Seata AT 模式的反向 SQL 是怎么生成的?

候选人回答: Seata 会在执行 SQL 前记录 before image,执行后记录 after image,回滚时用 before image 生成反向 SQL。

面试官追问: 如果更新的是 JSON 字段,before image 能准确回滚吗?

候选人回答: 不能完全准确。如果只更新 JSON 中某个字段,Seata 会记录整个 JSON 的旧值,回滚时覆盖整个字段,可能丢失其他并发更新的内容。

面试官追问: 那你们怎么解决?

候选人回答: 我们避免在事务中更新 JSON 字段,或者拆成多个字段,或者改用 TCC 模式手动控制回滚逻辑。

追问 3:G1 的 Region 大小怎么确定?你们设置过吗?

候选人回答: Region 大小由堆大小自动决定,范围 1MB~32MB。我们没有手动设置,默认就行。

面试官追问: 如果对象很大(比如 10MB),G1 怎么处理?

候选人回答: 大对象会直接进入 Humongous Region,如果连续 Region 不够,会触发 Full GC。我们遇到过,后来优化了缓存对象结构,避免大对象。


面试点评

本场面试主要考察候选人在缓存架构、分布式事务、数据库锁、JVM GC四个核心模块的实战能力。重点不是背答案,而是看是否能将技术点串联成完整解决方案。

候选人容易卡壳的点:

  • 本地缓存与 Redis 的协同机制(尤其消息丢失场景)
  • 分布式事务回滚失败的处理(Seata 悬挂事务)
  • 间隙锁与死锁的关联分析
  • G1 大对象处理机制

大厂一面更看重"边界意识"和"兜底能力",比如是否考虑过消息丢失、回滚失败、缓存雪崩等极端情况。


技术补丁包

  1. Caffeine 本地缓存 原理:基于 W-TinyLFU 算法,结合频率与时效性淘汰策略。 设计动机:解决高并发下 Redis 网络开销问题,提升读性能。 边界条件:缓存一致性依赖消息通知或 TTL 兜底,强一致场景慎用。 落地建议:配合 Kafka 消息清理缓存,设置合理 maximumSize 和 expireAfterWrite。

  2. 二级缓存架构(本地 + Redis) 原理:先查本地,再查 Redis,最后查 DB;更新时 DB -> Redis -> 本地。 设计动机:平衡性能与一致性,减少 Redis 压力。 边界条件:消息丢失导致本地缓存脏数据,需 TTL 兜底。 落地建议:使用 RocketMQ 事务消息或本地消息表保障删除通知可靠性。

  3. Redisson 分布式锁 原理:基于 Redis SET NX PX + Lua 脚本释放。 设计动机:实现跨 JVM 的互斥访问。 边界条件:主从切换导致锁丢失,RedLock 可缓解但非绝对安全。 落地建议:锁 value 用 UUID,释放时校验;业务层加幂等防重复执行。

  4. Seata AT 模式 原理:通过代理数据源记录 before/after image,自动生成反向 SQL。 设计动机:实现无侵入的分布式事务。 边界条件:要求表有主键;JSON 字段回滚可能覆盖并发更新。 落地建议:避免在事务中更新复杂 JSON;回滚失败时启用补偿机制。

  5. Spring @Transactional 失效场景 原理:基于 AOP 代理,同类内调用不走代理。 设计动机:简化事务管理。 边界条件:非 public 方法、异常被捕获、同类内调用均失效。 落地建议:通过 ApplicationContext.getBean() 获取代理对象调用。

  6. MySQL 间隙锁 原理:在可重复读隔离级别下,锁住索引记录之间的间隙。 设计动机:防止幻读。 边界条件:大范围查询易触发,可能导致死锁。 落地建议:避免大范围 FOR UPDATE;缩小查询条件范围。

  7. G1 GC Mixed GC 原理:同时回收新生代和部分老年代 Region,按回收价值排序。 设计动机:平衡吞吐与延迟。 边界条件:IHOP 设置过低易频繁触发。 落地建议:根据堆大小调整 IHOP,监控 Mixed GC 频率。

  8. 大对象与 Humongous Region 原理:超过 Region 一半大小的对象直接进入 Humongous Region。 设计动机:避免复制开销。 边界条件:连续 Region 不足时触发 Full GC。 落地建议:避免缓存大对象;优化数据结构。

  9. 缓存一致性兜底策略 原理:通过 TTL 强制过期 + 消息通知 + 日志监控。 设计动机:保障最终一致性。 边界条件:TTL 内数据可能脏。 落地建议:设置合理 TTL;记录缓存更新日志用于排查。

  10. 消息可靠性保障 原理:事务消息 + 本地消息表 + 重试 + 告警。 设计动机:确保缓存清理指令必达。 边界条件:网络分区或消费者宕机。 落地建议:使用 RocketMQ 事务消息;消费者实现幂等。

  11. 分布式事务补偿机制 原理:事务失败时调用反向接口回滚。 设计动机:弥补 AT 模式回滚失败。 边界条件:补偿接口需幂等。 落地建议:设计通用补偿接口;记录事务日志用于对账。

  12. 死锁排查与预防 原理:通过 SHOW ENGINE INNODB STATUS 查看死锁日志。 设计动机:快速定位问题。 边界条件:间隙锁与插入意向锁冲突。 落地建议:统一加锁顺序;避免长事务。

  13. JVM 堆外内存监控 原理:通过 NMT 或 JMX 监控 Direct Buffer、Metaspace 等。 设计动机:避免 OOM 无法排查。 边界条件:堆外内存不受 GC 管理。 落地建议:启用 NMT;限制 Netty Direct Buffer 大小。

  14. 缓存穿透与空值缓存 原理:对查询不到的数据缓存空值。 设计动机:防止恶意查询压垮 DB。 边界条件:空值缓存过期时间不宜过长。 落地建议:空值缓存 5-10 分钟;配合布隆过滤器。

  15. 事务传播行为 REQUIRED 原理:当前有事务则加入,没有则新建。 设计动机:保证业务操作在同一个事务中。 边界条件:嵌套事务回滚影响外层。 落地建议:明确事务边界;避免长事务。

  16. RedLock 算法 原理:在多个独立 Redis 实例上依次加锁,多数成功才算成功。 设计动机:提高锁的可用性。 边界条件:时钟漂移可能导致锁提前释放。 落地建议:仅在强一致场景使用;配合 fencing token。

  17. Undo Log 与回滚机制 原理:记录数据修改前的状态,用于回滚。 设计动机:支持事务回滚。 边界条件:回滚失败时需人工介入。 落地建议:定期清理 undo_log;监控回滚失败率。

  18. G1 Region 大小自动调整 原理:根据堆大小自动选择 1MB~32MB。 设计动机:适应不同堆规模。 边界条件:手动设置不当影响 GC 效率。 落地建议:一般无需手动设置;监控 Region 分布。

  19. 缓存雪崩防护 原理:随机化 TTL,避免同时过期。 设计动机:防止大量缓存同时失效压垮 DB。 边界条件:随机范围不宜过大。 落地建议:TTL 加随机值(如 5~10 分钟)。

  20. 分布式事务最终一致性 原理:通过消息 + 补偿实现最终一致。 设计动机:避免强一致带来的性能损耗。 边界条件:补偿延迟。 落地建议:设计对账系统;监控不一致率。

相关推荐
s1mple“”8 小时前
互联网大厂Java面试实录:谢飞机的AIGC求职之旅 - JVM并发编程到Spring Cloud微服务
spring boot·aigc·微服务架构·java面试·分布式系统·rag技术·redis数据库
sniper_fandc8 小时前
Spring Cloud系列—Seata分布式事务解决方案AT模式
spring cloud·seata
却话巴山夜雨时i13 小时前
互联网大厂Java面试:从Spring到微服务
spring cloud·微服务·oauth2·java面试·stream api
__土块__1 天前
大厂后端一面模拟:从线程安全到分布式缓存的连环追问
jvm·redis·mysql·spring·java面试·concurrenthashmap·大厂后端
却话巴山夜雨时i2 天前
互联网大厂Java面试实录:从Spring Boot到Kafka的技术问答
spring boot·redis·flink·kafka·java面试·rest api·互联网大厂
東雪木2 天前
Java学习——泛型基础:泛型的核心作用、泛型类 / 方法 / 接口的定义
java·学习·java面试
東雪木2 天前
Java学习——内部类(成员内部类、静态内部类、局部内部类、匿名内部类)的用法与底层实现
java·开发语言·学习·java面试
鬼先生_sir2 天前
SpringCloud Seata 四大模式(AT/TCC/SAGA/XA)全解析
seata·springcloud·分布式事务
却话巴山夜雨时i5 天前
互联网大厂Java面试实录:从Spring Boot到Kafka的场景应用深度解析
spring boot·kafka·prometheus·微服务架构·java面试·技术解析·互联网大厂