电商场景Java面试:从JVM到Kafka,谢飞机与严肃面试官的3轮交锋
“谢飞机,这是你的面试官,王总。王总在大厂带了八年后端,出了名的严肃。”HR小姐姐说完就关上了门。
谢飞机搓了搓手,咧嘴一笑:“王总好!我是谢飞机,写Java写了三年,主要搞电商。”
王总扶了扶眼镜,没有寒暄,直接翻开简历:“那我们开始吧。先聊点基础的。”
第一轮:项目热身与Java核心
王总:“你说你做了三年电商,具体负责哪些模块?技术栈怎么选型?”
谢飞机:“就商品、订单、库存那套。Spring Boot + MySQL + Redis,然后……写接口嘛,CRUD一把梭。”
王总眼角抽了一下:“你们订单表数据量大了怎么处理?”
谢飞机:“分库分表……吧?我们没用过,听说ShardingSphere挺火的。”
王总没追问,转向下一个问题:“JVM内存模型了解吗?垃圾回收算法有哪些?”
谢飞机:“JVM内存分堆和栈,堆里有新生代老年代,方法区……垃圾回收就是GC,回收垃圾对象,算法有复制、标记清除、标记整理……好像是这样的吧。。”
王总:“具体讲讲GC Roots?”
谢飞机:“GC Root……嗯……就像树的根,从根上遍历,够不到的就是垃圾。大概是这样,吧。”
王总沉默两秒,换了个问题:“那HashMap底层数据结构说一下。”
谢飞机眼睛一亮:“这个熟!JDK 8之后数组+链表+红黑树。默认容量16,负载因子0.75。当链表长度超过8并且数组长度超过64就转红黑树。put的时候先算hash找桶,冲突了链尾插入,查询复杂度O(1)最佳,最差O(logn)。”
王总微微点头:“不错,基础还可以。再聊聊Spring Boot自动配置原理。”
谢飞机:“自动配置……就是@EnableAutoConfiguration注解,从META-INF/spring.factories加载一堆AutoConfiguration类,根据条件注解如@ConditionalOnMissingBean生效。我平时用的时候确实没怎么配RedisTemplate。”
王总:“好,第一轮差不多。下面问点实际的业务场景。”
第二轮:电商业务与缓存、并发
王总:“你们做商品列表页,QPS上来了怎么防缓存穿透、击穿、雪崩?”
谢飞机:“这个我熟!缓存空值防穿透,热点key加互斥锁防击穿,过期时间加随机数防雪崩。我们当时就是Redis缓存,然后设置过期时间随机,查不到就查DB。”
王总:“那你怎么保证Redis和数据库的数据一致性?比如更新库存时。”
谢飞机:“呃……我们的做法是,先更新数据库,然后删除缓存,然后过一会再做延迟双删?好像是。但有时候还是会不一致,后来我们也没管了,反正数据最终一致。”
王总:“最终一致?那你倒是说说怎么个最终法?”
谢飞机:“就……过段时间它就一致了。用消息队列补偿?不过我们没做那么细。”
王总没有继续计较,抛出一个经典问题:“那在下单扣减库存时,如何防止超卖?”
谢飞机:“哦,我们用了synchronized!把方法锁了,就不会超卖了。”
王总:“如果多台机器呢?多个Tomcat实例同时执行呢?”
谢飞机:“啊?多台机器啊……那synchronized只能锁一个JVM啊……那……那用分布式锁?用Redis的setnx?嗯……好像有这么个东西。”
王总:“再往下问,你项目里遇到性能问题,怎么排查?”
谢飞机:“先看日志,搜Exception,再看Redis慢查询,MySQL慢SQL。然后……然后我就交给运维了。通常重启一下就好了。”
王总深吸一口气:“好。我们进入第三轮。”
第三轮:消息、安全与云原生
王总:“你们订单系统用Kafka,怎么保证消息不丢失?”
谢飞机:“Kafka有acks=all,生产者等所有副本都确认,然后消费者关闭自动提交,手动提交offset。但是……如果处理完业务提交offset之前挂了,会导致重复消费。”
王总:“重复消费怎么解决?”
谢飞机:“消费端幂等呗。用Redis setnx,或者数据库唯一约束。不过我们当时好像没做,所以偶尔订单会重复。”
王总微微皱眉:“好,那微服务之间调用,怎么做链路追踪?”
谢飞机:“链路追踪……听说过Sleuth和Zipkin。就是每个请求生成一个traceId,然后传到下游服务,在日志里串起来。但我们项目还没上,我是看博客说的大概。”
王总:“如果Redis集群突然全部宕机,你们的电商系统会怎么样?”
谢飞机:“全部宕机?那……那我们就直接挂了啊!用户打开页面白屏,下单接口全报错。”
王总:“有没有想过降级方案?本地缓存兜底?”
谢飞机:“本地缓存……好像可以用Caffeine。但没实际配过,应该能撑一会儿吧。”
王总:“说说JWT无状态登录的原理。”
谢飞机:“这个我知道!用户登录成功后,服务端生成一个JWT,里面有header、payload、signature三部分。之后前端每次请求把JWT放在Authorization头里,服务端只验签,不存session,天然适合分布式。”
王总:“JWT密钥怎么管理?遇到秘钥泄露怎么办?”
谢飞机:“啊?密钥……写在配置文件里。泄露了……就换一个?但那用户全部要重新登录。。”
王总无奈地摇摇头:“最后一个问题,你们的服务怎么部署的?Docker和Kubernetes用过吗?”
谢飞机:“我们用Docker,写个Dockerfile,jenkins构建镜像,然后docker run。Kubernetes……我只看了点视频,知道有Pod和Service,Deployment可以滚动更新。但真没在集群上操作过。”
王总合上笔记本,站起身,面无表情地说:“好,谢飞机,面试到这里。我们了解了你的基本情况,你先回去等通知吧。”
谢飞机挠挠头,走到门口又转身:“王总,三个工作日以内会给通知吗?”
王总嘴角勉强抽动了一下:“会的。等通知吧。”
答案解析:详细讲解每一个问题的业务场景与技术点
1. 电商项目模块与技术栈选型
业务场景:电商平台一般分为用户、商品、订单、购物车、库存、支付、营销等多个服务。Java后端常见技术选型为:
- 核心语言:Java 8/11/17,重点关注Stream、Optional、var等特性。
- Web框架:Spring Boot、Spring MVC,快速搭建REST API。
- 数据库:MySQL,主从复制、分库分表应对大数据量。
- ORM:MyBatis或Spring Data JPA。
- 缓存:Redis,用于热点数据、会话、分布式锁。
- 消息队列:Kafka或RabbitMQ,用于异步解耦订单、支付、库存。
- 构建工具:Maven或Gradle。
技术点:面试官问项目选型,实际是想了解候选人的技术广度与系统设计能力。回答时建议简述每个组件的职责,例如“MySQL存储业务数据,Redis做热点缓存,RabbitMQ异步发送订单通知”。
2. JVM内存模型与垃圾回收算法
业务场景:电商大促时,高并发会创建大量对象,导致频繁GC,影响响应时间。理解JVM有助于调优堆大小、选择合适的垃圾回收器。
JVM内存模型(运行时数据区):
- 堆:存放对象实例,分新生代、老年代。
- 虚拟栈:每个线程私有,存局部变量、方法调用。
- 本地方法栈:为native方法服务。
- 方法区/元空间:存类信息、常量、静态变量。
- 程序计数器:当前线程执行的字节码行号。
垃圾回收算法:
- 标记-清除:先标记垃圾,再清除。产生内存碎片。
- 复制:将内存分两块,活对象复制到另一块,清理原块。适合新生代。
- 标记-整理:让活对象向一端移动,再清理边界外内存。适合老年代。
GC Roots:包括栈中本地变量、静态变量、JNI引用等。从这些根向下搜索,未被引用的对象即为可回收对象。
3. HashMap底层原理
业务场景:电商中用HashMap缓存小规格配置项,同时面试高频。
- JDK 8+:数组 + 链表 + 红黑树。数组默认大小16,负载因子0.75。
- 当链表长度 > 8 且数组长度 >= 64 时,链表转为红黑树,降低查找复杂度到O(log n)。
- put流程:计算hash -> 定位桶 -> 若为空直接放;否则遍历链表/红黑树,key相同则覆盖,否则插入尾部。
- 扩容:超过阈值 capacity * loadFactor 时扩容为原来的2倍,重新分布元素。
4. Spring Boot自动配置原理
业务场景:开发时引入redis-start、web-start等依赖后,无需手动配置Bean,Spring Boot自动完成。
核心:
@SpringBootApplication包含@EnableAutoConfiguration。- 自动配置类在
META-INF/spring.factories或AutoConfiguration.imports中注册。 - 通过
@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,判断类路径是否存在对应依赖、用户是否已自定义Bean,从而自动生成默认配置。 - 例如
RedisAutoConfiguration提供了RedisTemplate、StringRedisTemplate的默认Bean。
注意:如需覆盖,自定义Bean即可,自动配置会失效。
5. 缓存穿透、击穿、雪崩
业务场景:商品列表页每天百万QPS,大量请求打向缓存。
| 问题 | 现象 | 解决方案 | |------|------|----------| |穿透| 查询一个不存在的数据,缓存没有,每次打到数据库 | 缓存空值(设置短过期时间);布隆过滤器 | |击穿| 热点key过期瞬间,大量请求同时打到数据库 | 互斥锁(setnx)重建缓存;热点key逻辑过期 | |雪崩| 大量key在同一时间过期,导致数据库压力骤增 | 过期时间加随机值;多级缓存;服务降级 |
6. 缓存与数据库一致性
业务场景:更新商品价格后,缓存中价格仍是旧值。
常用策略:
- Cache Aside(旁路缓存):读时先读缓存,未命中再读数据库并写回缓存;写时先更新数据库,再删除缓存。
- 删除缓存而不是更新缓存,避免计算复杂度。
- 延迟双删:先删缓存,再更新数据库,再延迟几百毫秒删一次,降低并发下的不一致窗口。
- 最终一致方案:通过订阅MySQL binlog同步缓存(如Canal),或使用消息队列异步删除缓存。
注意:强一致很难保证,通常接受最终一致。
7. 防止库存超卖
业务场景:秒杀场景下,100个库存,10000人同时下单。
- 方案一:数据库乐观锁。
UPDATE t_stock SET version=version+1 WHERE id=? AND version=?,或UPDATE t_stock SET stock=stock-1 WHERE id=? AND stock>0。 - 方案二:Redis预扣减。秒杀前将库存加载到Redis,用Lua脚本原子扣减,再异步同步到数据库。
- 方案三:分布式锁。使用Redis
SET lockKey value NX EX获取锁,释放锁时用Lua保证原子性。 - 注意:
synchronized只能锁单机进程,多实例无效。
8. 性能问题排查
业务场景:线上接口偶发超时,如何排查?
步骤:
- 看监控告警(Prometheus + Grafana):CPU、内存、GC频率、P99延迟。
- 查日志:错误日志、慢日志,尤其看有无OOM、Timeout。
- 排查慢SQL:开启MySQL慢查询日志,用explain分析索引。
- 排查Redis:慢日志、大key、热key。
- 排查线程栈:
jstack查看是否有死锁、线程阻塞。 - 在线定位:Arthas查看方法耗时、动态替换类。
9. Kafka消息不丢失与重复消费
业务场景:订单支付成功后,发送消息给积分服务,如果消息丢失,用户积分会少。
消息丢失防范:
- 生产者:设置
acks=all,同步等待发送结果。 - Broker:设置
replication.factor>=3,且min.insync.replicas>=2。 - 消费者:关闭自动提交
enable.auto.commit=false,业务处理成功后再手动提交offset。
重复消费:消费者处理完消息后、提交offset前宕机,重启后会从上次offset重新消费。解决方案:
- 消费端幂等:Redis
SETNX,数据库唯一键,状态机。 - 使用消息去重表:每条记录唯一消息ID,插入前检查是否已存在。
10. 微服务链路追踪
业务场景:一次查询商品请求会经过网关、商品服务、库存服务、优惠券服务。定位一次超时,需要串联整个调用链。
技术方案:
- Spring Cloud Sleuth(已退役,现在使用Micrometer Tracing)+Zipkin/Jaeger。
- 核心概念:
traceId(全局唯一)、spanId(一次调用单元)。 - 每个服务从HTTP Header中提取
traceId,写入日志,上报到Zipkin UI展示调用链和耗时。 - Micrometer支持多类监控后端,与Prometheus、Grafana整合。
11. Redis集群宕机如何应对
业务场景:大促时Redis挂了,所有读请求打到MySQL,系统雪崩。
- 多级缓存:本地缓存(Caffeine) + Redis + MySQL。Redis挂了,本地缓存可以支撑几分钟。
- 服务降级:关闭非核心功能,比如推荐流、个性化优惠券,仅保留下单能力。
- 限流熔断:Resilience4j或Sentinel对下游依赖做熔断,防止线程阻塞耗尽。
- 高可用:Redis Cluster / 哨兵模式,自动故障转移,但全局宕机概率仍存在,必须有兜底。
12. JWT无状态登录与安全
业务场景:电商各大服务之间需要认证用户身份,不用每个服务都读session。
- JWT结构:Header(算法)+ Payload(用户信息、过期时间)+ Signature(签名),Base64编码,用点分隔。
- 流程:用户登录 -> 服务端验证密码 -> 生成JWT返回 -> 前端存储(HttpOnly Cookie或LocalStorage)-> 后续请求携带Authorization: Bearer -> 服务端验签并解析用户信息。
- 密钥管理:JWT签名密钥必须保密,通常放在配置中心(Nacos / Apollo),且定期轮换。泄露后需要立即吊销所有token,让用户重新登录。
- 注意:JWT无状态,无法主动退出,可结合黑名单/短过期时间实现。
13. Docker和Kubernetes部署
业务场景:电商有几十个微服务,如何快速发布和扩容。
- Docker:将应用打成镜像(Dockerfile),包含JDK环境、依赖、应用代码,运行在容器中。
- Kubernetes:负责容器编排,管理Pod(一组容器)、Service(负载均衡)、Deployment(滚动更新)、ConfigMap(配置)。
- 常见流程:
- Jenkins/GitLab CI构建镜像,推送到镜像仓库。
- 更新Deployment镜像版本,K8s自动滚动升级。
- 使用Prometheus监控Pod指标,HPA根据CPU/内存自动扩缩容。
- 命名空间、Ingress:Ingress作为入口,按域名路由到不同Service,取代Nginx的配置。
14. 其余高频技术点补充
为了方便小白扩展,这里再补充几个本文中出现的名词:
- Flyway / Liquibase:数据库版本管理工具,将SQL脚本纳入版本库。
- Resilience4j:轻量级熔断、限流、重试库,用于提高分布式系统容错。
- OpenFeign:声明式HTTP客户端,微服务间调用更优雅。
- Apache Shiro / Spring Security:认证授权框架,Spring Security配合OAuth2/JWT使用更主流。
- Micrometer:监控门面,将JVM指标暴露给Prometheus。
- Elasticsearch:电商商品搜索,倒排索引,复杂查询。
- Thymeleaf / FreeMarker:服务端渲染模板,传统电商管理后台仍在用。
- MapStruct:Java Bean映射,性能高于BeanUtils,用于PO/DTO/VO转换。
- Lombok:@Data、@Builder等注解,减少样板代码。
面试结束,谢飞机离开房间。王总在面试评价上写下:“基础功不扎实,高并发经验不足,但HashMap回答尚可,人有点意思,可考虑外包岗。”
欢迎继续关注,下一期我们讲:谢飞机接到二面电话后,如何用一周时间突击背题,却在“Kafka重复消费”上再次翻车。