谢飞机勇闯大厂面试:从JVM到微服务,一场关于电商秒杀与云原生的技术修罗场
谢飞机勇闯大厂面试:从JVM到微服务,一场关于电商秒杀与云原生的技术修罗场
面试开场
“谢飞机是吧?坐。我是今天的面试官,你可以叫我老K。我们部门做的是电商核心交易链路,高并发、秒杀、分布式事务、云原生都碰。今天咱们不聊虚的,就聊你简历上写的那些。准备好没有?”老K面无表情地翻了翻屏幕,语气像在念死刑判决书。
“准备好了!K老师,我虽然叫飞机,但我绝不飘,稳如老狗!”谢飞机一紧张,差点把椅子坐翻。
老K没理他,直接开了第一轮。
第一轮:基础热场与JVM与构建
老K:“先别急着表忠心。你简历上写Java SE 8/11/17都熟。那我问你——JDK 8、11、17之间,你日常开发最关注哪个版本的什么特性?说三个就行。”
谢飞机:“啊,这个我熟。8的Lambda和Stream,11的var,17的密封类……哦不对,密封类是17的,但线程本地握手也是17的。反正就是越来越强!”
老K:“……行,算你蒙对一半。那第二个问题,线上有个Java服务CPU飙到100%,但内存没涨,你怎么排查?”
谢飞机:“先用top看PID,然后top -Hp看线程,再jstack转换线程号看堆栈。要是看到业务线程死循环或者GC线程疯狂跑,就定位了。如果栈看不出,就用arthas的thread -n 3看看。反正就是一条龙!”
老K:“哦?还知道arthas。那第三个问题——你们项目构建是用Maven还是Gradle?Maven的依赖冲突怎么解决?如果A依赖B的1.0,C依赖B的2.0,最终生效哪个?”
谢飞机:“用Maven。冲突当然是看依赖树mvn dependency:tree,然后排除或者统一版本。最终生效看谁在dependencyManagement里声明,或者看深度,越浅越优先——不对,是先声明者优先?哎呀,反正就是最近原则……我昨天还排查过呢!”
老K:“最近原则?你确定?那我提醒一下:Maven是‘谁先声明谁赢’,Gradle才是‘谁依赖近谁赢’。不过你提到了tree命令,不错。最后一个问题:你打包的时候,Ant、Maven、Gradle有什么区别?”
谢飞机:“Ant是老太太,手动拧螺丝;Maven是流水线工人,约定大于配置;Gradle是机器人,又灵活又快。我们公司已经全面拥抱Gradle了,但我其实还是写Maven多。”
老K嘴角微微抽动:“行,第一轮勉强算你过。这轮问题背后是让你知道,Java版本、JVM排查、依赖管理是你在电商系统里写安稳代码的底线。下一轮上点硬菜。”
第二轮:Spring与数据库与缓存与消息
老K:“你们电商项目,用的是Spring Boot吧。现在有个秒杀活动,前一万个用户能抢到一件商品。你怎么设计接口才能避免超卖?从Spring事务、数据库锁、Redis、MQ这几个层面给我说说。”
谢飞机:“秒杀?我熟!首先在Redis里用原子递减预减库存,减到0就拒绝。然后请求丢到MQ,异步下单。数据库层面用乐观锁更新库存,UPDATE t_goods SET stock=stock-1 WHERE id=? AND stock>0,这样不会超卖。事务嘛,@Transactional把扣库存和生成订单包一起。Redis崩了就用分布式锁兜底。”
老K:“嗯?居然说得有条理。那我再追问——如果Redis是单节点,秒杀流量极高,Redis的QPS顶不住了怎么办?还有,消息队列你自己怎么选?Kafka和RabbitMQ在订单场景的区别是什么?”
谢飞机:“顶不住……那就加Redis集群?或者本地缓存先挡一层?但一致性不好搞。消息队列……我们项目用的Kafka,吞吐高,RabbitMQ功能全,延迟低。订单场景我听说RabbitMQ更稳,但秒杀大流量用Kafka更爽。反正都行吧?”
老K:“你那个'都行'让我血压上来了。第三个问题:你们用MyBatis还是JPA?如果我现在要写一个动态排序的分页查询,条件有商品ID、价格区间、状态,还要关联分类表,你怎么实现?”
谢飞机:“我们用的MyBatis-Plus,直接LambdaQueryWrapper,分页插件PageHelper,条件用if判断。关联表就写XML SQL,动态SQL用和。JPA也行,Specification太想念咒语。”
老K:“行吧,动态SQL是个基本功。刚才你提到缓存和MQ,我再问一个——如果秒杀成功后,用户订单已经创建,但Redis里的库存因为某些原因没回滚,导致数据不一致,你怎么办?或者说,Redis、数据库、MQ三者之间怎么做最终一致性?”
谢飞机:“最终一致性……那就发消息出去,有人监听,发现不一致就补偿。或者本地消息表,先写数据库和消息表,再发MQ。还有,用事务消息?RocketMQ支持。但我们用的Kafka不支持,所以……嗯……我们有个定时任务扫对账表,把Redis里的库存刷新成数据库的库存。”
老K:“定时任务对账,这个方案土但能用。第二轮算你及格。记住:秒杀不是堆中间件,而是先想清楚数据一致性和最终吞吐的权衡。下一轮你可能会哭。”
第三轮:微服务与云原生与监控与安全
老K:“看来你确实背过八股。现在换个场景——我们这有个直播带货的订单服务,用户同时在看弹幕、下订单、领优惠券。整个系统拆成了服务A(用户)、服务B(订单)、服务C(营销)。服务之间用OpenFeign调用,注册中心用了Nacos。请问:如果订单服务突然挂了,调用方应该怎么办?”
谢飞机:“用Feign的话,那就配置超时和重试。挂了就报错呗,再不行就熔断,用Resilience4j。把降级逻辑写在@FeignClient的fallback里。还有,不能重试写操作,防止重复下单。”
老K:“哦?那你再说说,订单服务恢复了,怎么保证它不会把刚启动但还没缓过来的流量瞬间打死?也就是你有没有听过'优雅上线'和'预热'?”
谢飞机:“优雅上线?就是启动后不要让流量立刻进来,Spring Boot内置了优雅停机,但上线预热……Kubernetes有readiness探针,探针通过后才能接流量。还有,Ribbon的weighted响应时间权重?不过那是老黄历了。我们现在用的K8s,所以……就靠探针呗。”
老K:“嗯。接下来,你们服务都容器化了,K8s部署。我想看一个服务的实时日志、CPU、内存、链路追踪,你分别用什么?Jaeger和Zipkin有什么区别?Prometheus和Micrometer如何配合?”
谢飞机:“日志用ELK,指标用Prometheus+Grafana+Micrometer,链路用Jaeger。Jaeger和Zipkin都是分布式追踪,不过Jaeger是CNCF项目,Zipkin是Twitter捐的。Micrometer是一个门面,像SLF4J一样,把指标暴露给Prometheus。嗯……就这些。”
老K:“那安全呢?你们的API用的是JWT还是OAuth2?如果第三方合作方要调用你们的接口,你会用哪种方案?密钥怎么管理?”
谢飞机:“内部用JWT,第三方肯定要OAuth2的授权码模式。JWT的签名K、K8s Secret、Keyloak统一认证,就这么干。”
老K:“好——最后一个问题。假设你现在发现系统里有个接口被刷,攻击者通过脚本快速注册大量账号并领取新用户优惠券,导致营销成本飙升。你怎么止损?从风控、幂等、限流、网关层面说。”
谢飞机:“这个……IP限流?接入层用令牌桶。设备指纹?手机号验证码。幂等表?同一个设备只能领一次。管理员封号?总之就是多管齐下,黑名单+红线。”
老K:“可以。三轮问完了。谢飞机,你还挺有意思——理论上能聊,细节一问就飘,典型的面试放大器。不过比那些完全不会的强。”
面试结束
老K合上电脑,靠在椅背上:“咱们今天聊了JVM、构建、Spring、数据库、缓存、消息、微服务、K8s、监控、安全。你回去等通知吧。”
谢飞机心里一凉:“K老师……‘等通知’是不是就是凉了?”
老K难得笑了一下:“不一定。好好复盘今天的问题。回去把我问的每个点都能自己讲明白,下一次面试你就有谱了。走吧。”
谢飞机鞠躬,退出会议室时还在嘀咕:“等通知……到底有没有通知啊?”
附录:面试问题深度解析(小白学习版)
第一轮:Java版本、JVM排查、构建工具
1. JDK 8/11/17 各自的核心特性
- JDK 8(2014年):实现了真正的函数式编程——Lambda表达式、Stream API、接口默认方法、Optional类、新的日期时间API(
LocalDate、LocalDateTime)以及方法引用。这些特性让Java从面向对象语言变成兼顾函数式风格的语言。 - JDK 11(2018年):LTS版本,带来了局部变量类型推断(
var),只能用于局部变量,不能用于成员变量;还引入了ZGC(实验性)、HTTP Client(标准化的java.net.http);同时移除了一些废弃的API。 - JDK 17(2021年):又一个LTS版本,引入了密封类(
sealed,限制类的继承范围)、强封装JDK内部元素(提升了安全性)、预览版虚拟线程(正式版在21)以及Switch表达式增强(17中已成为正式特性)。
面试重点:能说出8/11的经典特性,说明你有工程经验;能说17的密封类和后续虚拟线程,说明你有技术好奇心。
2. 线上CPU 100%排查方法
这是一道经典的故障排查题,标准步骤:
top找到CPU占用最高的Java进程PID。top -Hp PID找到该进程内CPU占用最高的线程TID。printf "%x\n" TID将十进制TID转换为十六进制。jstack PID | grep -A 20 "0x<十六进制线程号>"找到线程堆栈,定位到具体的业务代码行。- 如果看不到业务调用,可能是GC线程,用
jstat -gcutil PID看GC频率;如果GC异常,用jmap -dump:format=b,file=heap.hprof PID导出堆,再用MAT分析。如果CPU高但内存不涨,有可能是死循环、正则灾难、或者JIT编译(较少见)。 - 生产环境推荐用 Arthas 的
thread -n 3,能直接输出最忙线程和堆栈。
3. Maven依赖版本冲突与规则
- 三个构建工具:Ant(没有约定,需要手动定义目标)、Maven(约定目录结构,使用XML配置依赖)、Gradle(基于Groovy/Kotlin DSL,支持增量构建和并发缓存)。
- Maven的依赖传递机制会导致同一个库出现多个版本。版本仲裁原则:
- 最短路径优先:如果A→B→C→D 1.0,且A→E→D 2.0,那么D 2.0胜出(路径更短)。
- 如果路径长度相同:则先声明者优先(在POM中谁先定义依赖,谁传递的版本生效)。
- 强制覆盖:使用
<dependencyManagement>可以强制指定版本,或者使用<exclusions>排除传递依赖。
- 排查命令:
mvn dependency:tree,可以加-Dverbose查看详细冲突。
第二轮:秒杀、事务、缓存、MQ、ORM
1. 秒杀不超卖的完整设计方案
秒杀系统需要“前端拦截 + 缓存扣减 + 削峰填谷 + 数据库最终一致”。
- 前端/网关层:验证码、答题、限流(令牌桶/滑动窗口),拦截大部分流量。
- Redis层:用
Lua脚本+DECR/DECRBY原子扣减库存。注意,先要判断库存是否大于0。Redis的原子操作能承受每秒10万级别的扣减。 - MQ层:把成功扣减库存的用户ID发送到MQ,返回“排队中”。订单服务消费MQ异步创建订单,避免直接写数据库的高并发压力。
- 数据库层:使用乐观锁
UPDATE t_sku SET stock = stock - 1 WHERE id = ? AND stock > 0;订单表插入时使用唯一键(如user_id + sku_id + activity_id)防止重复下单。 - 事务边界:
@Transactional方法内只做数据库操作,不要在事务内调用RPC或等待MQ响应,否则会拉长事务时间。 - 分布式锁兜底:如果Redis不可用,可以使用Redisson的
RLock(基于Redis)或ZooKeeper锁,但注意性能。
2. Redis顶不住流量怎么办?
- Redis集群:使用Redis Cluster分片,把秒杀key的哈希槽分散到多个节点。但单key仍可能热点,要设计
key后缀(如sku_id_1,sku_id_2),把读流量分散。 - 本地缓存前置:秒杀商品数量很少,可以在网关或应用层使用Caffeine本地缓存提前返回“已售罄”。但要注意一致性,因为本地缓存是进程级的,所以通常只用于“售罄”这种标志位,而非真实库存。
- 分层降级:如果Redis整体不可用,直接走数据库,但要设置最大并发量(如Semaphore限流),并使用强事务锁(
SELECT ... FOR UPDATE),不过性能会下降很多。
3. Kafka vs RabbitMQ
| 维度 | Kafka | RabbitMQ |
|---|---|---|
| 吞吐量 | 极高(百万级/秒) | 中高(万级/秒) |
| 延迟 | 毫秒级但偏高(批量发送) | 微秒级,低延迟 |
| 消息模型 | 分区+消费者组 | Exchange+Queue+Binding |
| 消费模式 | 拉模式(消费者主动拉取) | 推模式(多协议) |
| 消息顺序 | 分区内有序 | 单队列内有序 |
| 可靠性 | 配置acks=all可高可靠 | 支持Publisher Confirm和事务 |
| 典型场景 | 日志、流量削峰、大数据管道 | 业务消息,需要灵活路由和事务性 |
秒杀场景用Kafka更合适,因为吞吐优先;订单支付结果通知用RabbitMQ更灵活。
4. MyBatis vs JPA 与动态SQL
- MyBatis:半自动ORM,SQL写在XML或注解中,灵活可控,适合复杂查询和优化。动态SQL使用
<if>、<where>、<set>、<foreach>。 - JPA(Hibernate实现):全自动ORM,根据实体映射自动生成SQL,适合CRUD简单的系统。复杂查询可以用
Specification、QueryDSL或@Query。 - 分页:MyBatis-Plus用
Page,JPA用Pageable。 - 注意:数据库性能瓶颈往往在SQL和索引,ORM只是工具。在面试中,你能写出复杂的动态SQL比背诵ORM原理更重要。
5. 缓存、数据库、MQ最终一致性
- 常见方案:本地消息表(事务消息):
- 在订单服务本地数据库开启事务,创建订单 + 写一条“库存扣减消息”到本地消息表。
- 事务提交后,后台线程把消息发送给MQ。
- 库存服务消费消息,执行库存扣减。
- 如果消费失败,MQ重试;如果多次失败,发告警人工处理。
- 另一种:事务消息(RocketMQ),在发送半消息后执行本地事务,事务成功则commit消息,事务失败则rollback。Kafka原生不支持事务消息,需要用其他流程。
- 补偿:定期用对账任务扫数据库订单和Redis库存,以数据库为准覆盖Redis。这属于“最终一致 + 主动修复”。
第三轮:微服务、云原生、监控、安全
1. OpenFeign调用失败的处理:超时、重试、熔断、降级
- 超时:
connectTimeout(建立连接超时)和readTimeout(等待响应超时)要分别设置,通常readTimeout比connectTimeout更长。 - 重试:Feign默认不重试。可以配置
RequestInterceptor或FeignBuilder的retryer。注意:写操作(POST/PUT/DELETE)不能盲目重试,否则可能产生重复数据。读操作可以重试,但要配合幂等。 - 熔断:引入
resilience4j-spring-boot2,在@FeignClient的fallback属性指定降级类。熔断器有三种状态:关闭(正常)、打开(直接失败)、半开(放少量请求试探)。 - 限流:Resilience4j还支持RateLimiter和Bulkhead(信号量隔离/线程池隔离)。
2. 优雅上下线和预热
- 优雅下线:设置
server.shutdown=graceful,在Spring Boot 2.3+支持。K8s中配合preStop钩子先停止接收流量,再等待在途请求完成(spring.lifecycle.timeout-per-shutdown-phase)。 - 优雅上线:K8s的
readinessProbe和livenessProbe。就绪探针返回200才将Pod加入Service Endpoint。可以自定义一个HealthIndicator,检查依赖(数据库连接池、Redis、MQ)是否准备好。 - 预热:对于缓存服务,服务启动后可主动加载热点数据到本地缓存或Redis,避免刚启动时大量请求直接打穿数据库。高层网关的灰度发布功能也可以实现分批放流。
3. 监控与链路追踪
- 日志:ELK(Elasticsearch + Logstash + Kibana),或EFK(Fluentd替代Logstash)。统一日志格式,包含traceId。
- 指标:Micrometer是一个指标门面,类似SLF4J。在Spring Boot中使用
micrometer-registry-prometheus暴露/actuator/prometheus,Prometheus定期拉取,Grafana展示。 - 链路追踪:Jaeger和Zipkin都支持
OpenTracing/OpenTelemetry标准。区别:- Zipkin:由Twitter开源,较早,支持Cassandra/MySQL存储。
- Jaeger:由CNCF托管,支持Jaeger查询语言和更丰富的过滤器,原生支持gRPC。
- 在实际工程中,通常是通过Spring Cloud Sleuth(或Micrometer Tracing)生成traceId,然后自动上报到Zipkin或Jaeger。
- Sleuth + Zipkin:在日志里自动带上
[app, traceId, spanId],通过MQ或HTTP发送到Zipkin Server。
4. JWT vs OAuth2 vs Keycloak
- JWT是token格式,不是协议。它包含header、payload、signature,用HMAC或RSA/ECDSA签名。适合无状态认证,但无法主动吊销。
- OAuth2是授权框架,有四种模式:授权码(第三方网页登录)、密码模式(轻量级,不推荐)、客户端凭据(服务间调用)、隐式模式(已废弃)。
- 对于“第三方合作方调用你们API”,正确做法是使用OAuth2的客户端凭据模式:合作方拿到
client_id和client_secret换取访问token,携带token调用API。为了区分用户授权,则使用授权码模式。 - Keycloak是开源IAM,提供统一登录、用户管理、SSO。企业可以使用Keycloak作为OAuth2授权服务器,内部服务使用JWT校验权限,认证和业务逻辑解耦。
- 密钥管理:JWT签名密钥可以用
vault或K8sSecret存放,不要写进代码仓库。
5. 接口防刷的风控设计方案
- 接入层限流:Nginx
limit_req或网关层(Spring Cloud Gateway)使用令牌桶算法,按IP、设备ID、用户ID限流。 - 设备指纹:通过前端生成设备ID,结合浏览器指纹,存储到Redis,判断短时间内设备数量是否异常。
- 行为特征:分析请求频率、单位时间内注册账号数量、IP变化、点击路径等,用规则引擎(如Drools)或风控算法。
- 幂等校验:前端每次访问领取优惠券接口时带上一个
requestId(UUID)。服务端用RedisSETNX或者数据库唯一索引保证同一设备同一活动只能领一次。 - 验证码:注册时用滑块验证码或手机验证码,增加自动化成本。
- 黑名单/红线:发现异常流量后,将IP或设备加入黑名单,并限制该账号的提券资格。
- 异步风控:可以把风控判断放在MQ中异步处理,不阻塞正常请求。
谢飞机回家后,把上面的笔记抄了三遍。后来他终于收到通知,虽然没过,但他说:“至少把面试官问的题都学会了,下一次我能飞得更高。”
更多推荐




所有评论(0)