资讯动态

电商大厂Java面试:Spring Boot、JVM、缓存、消息队列、微服务、分库分表与数据一致性实战(谢飞机历险记)

发布时间:2026/8/25 18:42:02 来源:尧图企业网站定制
电商大厂Java面试Spring Boot、JVM、缓存、消息队列、微服务、分库分表与数据一致性实战谢飞机历险记下午两点某互联网大厂会议室。面试官老张一脸严肃面前摆着一份简历。对面坐着谢飞机穿着格子衫头发略油眼神里带着三分紧张、七分自信。老张敲了敲桌子“谢飞机是吧看你有两年Java经验那我们直接开始。今天模拟一个电商下单的场景结合你简历里写的技术栈咱们聊几轮。”谢飞机挺直腰板“好的面试官您尽管问。我背过……不是我学过”老张微微皱眉“第一轮先看看基本功。”第一轮从下单接口到JVM老张假设我们有一个电商系统用户点击“立即购买”后后端会创建一个订单。请你描述一下从HTTP请求进入Spring Boot到订单数据写入MySQL整个过程中Spring MVC的核心处理流程是怎样的谢飞机这个我知道请求先到DispatcherServlet然后它找到HandlerMapping再找到对应的Controller方法执行完返回一个JSON。特别简单老张点点头“说得还算流畅那补充一下Controller方法上标注的RequestMapping是怎么被解析的Spring容器初始化时Bean的声明周期你了解多少”谢飞机呃……RequestMapping就是写在方法上面的注解嘛Spring一看就知道访问路径是啥。Bean生命周期的话……先实例化再属性填充再初始化最后……就用了老张叹了口气“行下一个问题。订单创建时要扣库存两个操作需要在一个事务里。Spring的Transactional底层原理是什么它如何保证事务回滚”谢飞机就是AOP嘛用动态代理生成一个代理类方法执行前开启事务执行完提交或者回滚。如果方法内部同一个类调用另一个Transactional方法好像会失效这个我试过。老张眼睛一亮“哦那你说说为什么失效JDK动态代理和CGLIB有什么区别如果事务方法抛出了Exception但没被Spring捕获会发生什么”谢飞机因为……内部调用走的是this不是代理对象所以事务不生效。JDK代理要求接口CGLIB不用。抛异常……如果没捕获默认回滚但是checked exception默认不回滚好像要配rollbackFor。嗯……反正我一般直接抛RuntimeException。老张扶了扶眼镜“基础还行但深度差点。最后一问当订单量暴增一个JVM进程可能内存溢出。你说说JVM运行时数据区有哪些哪块最容易OOM如果是CPU飙高你会怎么排查”谢飞机JVM分为堆、栈、方法区、程序计数器、本地方法栈。堆最可能OOM。CPU飙高就top -Hp找线程然后jstack看堆栈。嗯这个我练过。老张挺好。第一轮就这样第二轮我们进入具体业务。第二轮缓存、消息队列与最终一致性老张现在要做电商秒杀。三百万用户同时抢一百件商品直接查数据库肯定挂。你说说怎么用Redis和消息队列来扛住流量谢飞机简单先用Redis做缓存把库存预加载到Redis里请求先打Redis用decr扣减库存。扣成功了再发消息给MQ让消费者异步去创建订单、更新数据库。这样数据库就压力很小了。老张追问“那Redis的decr是原子的吗一个请求扣减库存后如果消费者处理订单失败了库存怎么恢复Redis和数据库的缓存一致性怎么保证”谢飞机decr是原子的单线程嘛。处理失败就……发送消息失败重试或者用本地消息表还有消息队列的可靠投递生产者确认消费者手动ack。缓存一致性的话……先更新数据库再删除缓存或者先删缓存再更新数据库唉这个好像有个经典的坑就是并发下可能读到旧数据。老张“你提到了本地消息表说说它的核心思想。”谢飞机就是把消息和业务数据放在同一个本地事务里然后异步发送消息。成功就发不成功就定时任务扫表重发。这个能保证最终一致。老张“那再来一个场景Redis集群中如果一个key的hotkey访问量特别大你怎么处理Redis持久化用了哪种方式”谢飞机hotkey可以加本地缓存……用Caffeine或者把key复制多份分散到不同分片。持久化的话Redis有RDB和AOF我一般用AOF数据更安全。老张“好第三轮我们说说微服务和分布式事务。”第三轮微服务、熔断与分布式事务老张公司要把订单系统拆分成多个微服务比如订单服务、库存服务、用户服务、支付服务。服务间用OpenFeign调用。如果库存服务挂了订单服务怎么保护自己谢飞机用Resilience4j搞熔断和降级。设置调用超时、失败率超过阈值就熔断直接返回兜底数据防止雪崩。还可以用线程池隔离或者信号量隔离。老张“不错。那分布式事务呢下单要同时扣减库存、生成订单、给用户加积分这三个服务跨数据库了怎么保证一致性”谢飞机这个……用SeataAT模式两阶段提交还有TCCTry-Confirm-Cancel。但是TCC很复杂我写过一点。如果要求不高就用MQ做最终一致比如本地消息表加消息队列。老张“如果并发量极高TCC的Try阶段会有性能瓶颈你怎么优化”谢飞机嗯……可以先在Redis里做预扣减预热然后再异步TCC或者把一些非核心操作改成异步对账比如加积分失败了就记日志晚上跑批修正。老张“行最后一个问题。微服务请求链路很长从网关到订单服务再到库存服务如何追踪一次完整的调用用的什么技术栈”谢飞机这个我知道用SkyWalking或者Micrometer配合Zipkin在Feign调用时传递traceId把每个服务的时间跨度串起来。日志里也带上traceId排查问题很方便。老张笑着点点头“面试表现不错。你回去等通知吧。”谢飞机走出门擦了擦汗“完了我是不是说得太笼统了赶紧记下来回去补课”以下为完整答案详解第一轮详解Spring MVC流程、Bean生命周期、事务原理与JVM排查1. Spring MVC核心处理流程完整流程如下客户端发送HTTP请求DispatcherServlet前置控制器接收所有请求。DispatcherServlet调用HandlerMapping根据URL找到对应的HandlerExecutionChain包含Handler和拦截器。找到Handler后通过HandlerAdapter执行Controller方法。HandlerAdapter会处理参数绑定、校验、调用Controller方法。Controller执行完返回ModelAndView或ResponseBody的结果如JSON。如果是RestController结果由HttpMessageConverter如Jackson序列化成JSON返回给客户端。如果有视图渲染则交给ViewResolver解析视图如Thymeleaf模板。面试官背后的考点DispatcherServlet是Spring MVC的前端控制器整个流程是“前端控制器模式”。RequestMapping通过RequestMappingHandlerMapping注册内部维护一个MappingRegistry将请求路径映射到HandlerMethod。HandlerAdapter负责真正调用Controller方法支持RequestParam、RequestBody等参数解析器。2. Spring Bean生命周期Spring Bean的完整生命周期可以记忆为实例化通过构造器创建Bean实例无参构造或Autowired构造器。属性填充Spring通过AutowiredAnnotationBeanPostProcessor完成Autowired、Resource、Value等依赖注入。InitializingBean回调如果Bean实现了InitializingBean调用afterPropertiesSet()。自定义init方法如果配置了PostConstruct或init-method在此调用。BeanPostProcessorpostProcessBeforeInitialization和postProcessAfterInitializationSpring AOP的代理就是在这里通过AnnotationAwareAspectJAutoProxyCreator生成的。使用Bean业务方法被代理增强。销毁容器关闭时若实现了DisposableBean则调用destroy()否则调用PreDestroy或自定义destroy方法。3. Spring事务原理与失效场景Transactional基于AOP。当一个类被标注TransactionalSpring会为它创建一个代理类。使用JDK动态代理时目标类必须实现接口使用CGLIB时目标类可以没有接口通过生成子类实现。代理对象在方法调用前开启事务TransactionInterceptor调用后提交事务异常时回滚。事务失效的经典场景同一个类中方法A无事务调用方法B有TransactionalB的事务不生效因为调用的是this不是代理对象。方法不是publicTransactional不生效Spring AOP默认只代理public方法。数据库引擎不支持事务如MyISAM。异常被方法内部捕获Spring感知不到异常就不会回滚。抛出的是CheckedException如IOException默认不回滚。需要Transactional(rollbackFor Exception.class)。传播行为配置错误比如PROPAGATION_NOT_SUPPORTED。4. JVM运行时数据区与OOM、CPU排查运行时数据区程序计数器记录当前线程执行的字节码行号线程私有无OOM。虚拟机栈Java栈保存栈帧每个方法调用对应一个栈帧局部变量表、操作数栈、动态链接、返回地址。栈深不够会StackOverflowError。本地方法栈用于native方法。堆对象实例和数组分配内存的地方是GC主要区域。最可能OOM表现为java.lang.OutOfMemoryError: Java heap space。方法区元空间存储类信息、常量、静态变量。JDK8以后元空间使用本地内存一般不会OOM但加载大量类可能Metaspace溢出。OOM排查步骤加上JVM参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path。启动后OOM时自动生成dump文件。用MAT或jvisualvm分析查看大对象和引用链。CPU飙高排查步骤top找到CPU高的Java进程PID。top -Hp pid找到最耗CPU的线程TID。将TID转成16进制printf %x\n tid。jstack pid dump.txt在dump中搜索线程十六进制ID查看线程堆栈。分析是GC频繁结合jstat -gcutil还是死循环或锁竞争。第二轮详解Redis缓存、消息队列与最终一致性1. 秒杀系统缓存与异步削峰方案经典设计思路提前预热将库存数量加载到Redis例如扣减前执行SET stock 100。请求过滤网关层或Spring Boot层使用验证码、限流组件如Sentinel过滤掉非人类流量。在Redis中原子扣减使用DECR stock如果返回值大于等于0则扣减成功否则返回“已售罄”。把订单消息发送到MQ扣减成功后发送一条消息包含userId、skuId、秒杀场次消费者异步创建订单和写数据库。数据库最终扣减消费者拿着消息去数据库执行UPDATE stock SET stock stock - 1 WHERE id ? AND stock 0确保不会超卖。为什么DECR是原子的因为Redis是单线程执行命令DECR操作不会被打断。2. 消费者处理失败后库存如何恢复这是可靠消息最终一致性的关键。常见方案消息队列保证使用Kafka时消费者处理成功后再手动commit手动提交offset。如果处理失败不commit消息会重新投递。本地消息表在业务库中建一张message表把订单创建和消息写入放在同一个数据库事务中。事务成功则消息表有记录异步发送消息发送成功后标记消息状态为“已发送”。如果发送失败定时任务扫表重发。定时对账如果消费者多次重试仍然失败可以写入死信队列最终通过人工或对账系统调整库存。反向补偿如果订单最终没支付或取消需要回补库存可以通过另一个消息或定时任务恢复。3. Redis缓存与数据库一致性常见的Cache Aside模式读先读缓存缓存没有则读数据库再写入缓存。写更新数据库后删除缓存或者先删除缓存再更新数据库。坑点如果先删缓存、再更新数据库在删除缓存后、更新数据库前另一个线程读取数据发现缓存没有就去数据库读旧值并写回缓存导致缓存长期是旧数据。更稳妥的方案先更新数据库再删除缓存。虽然也存在极端情况更新数据库前读请求读到旧值但概率极低且可以给缓存设置过期时间兜底。延迟双删更新数据库后删除一次缓存过几百毫秒再删除一次把并发期间写入的旧缓存清掉。使用Canal监听MySQL binlog异步删除/更新缓存。4. Redis HotKey与持久化HotKey热点key优化本地缓存兜底如Caffeine在应用内缓存热点数据Redis压力下降。Key分散把同一个key复制为product_1product_1#1product_1#2等把请求分散到不同Redis分片。读写分离对于读多写少的场景使用Redis从节点分担读流量。限流和降级如果某个key数据不要求绝对实时可以降级为本地过期缓存。Redis持久化RDB定时生成全量快照文件适合备份、恢复快但可能丢失最后一次快照后的数据。AOF追加写命令日志每秒钟fsync可以最多丢失1秒数据数据更可靠。AOF文件较大需要定期用BGREWRITEAOF重写。生产环境通常RDB AOF混合使用Redis 4.0后支持混合持久化RDB作为全量基础之后的增量用AOF。第三轮详解微服务保护、分布式事务与链路追踪1. 服务容错与熔断当库存服务SLOW或挂掉时订单服务不可能无限等待。Resilience4j提供了超时控制TimeLimiter设置调用最大等待时间如300ms超过直接返回失败或兜底。隔离Bulkhead用固定线程池或信号量限制并发调用数防止某一个下游服务占满整个应用线程池。熔断CircuitBreaker统计最近一段时间内的失败率比如10秒内失败率超过50%则断开电路后续请求直接短路走降级方法。经过一段时间窗口后变为半开状态放少量请求探测恢复情况。限流RateLimiter限制QPS。在Spring Cloud中也可以使用Sentinel或Hystrix。但Sentinel和Resilience4j更新更活跃。2. 分布式事务解决方案跨服务跨数据库的事务不能使用本地事务常见方案1XA/两阶段提交2PC准备阶段所有参与者执行SQL但不提交资源锁定。提交阶段协调者通知所有参与者提交如果任一个失败则全部回滚。问题同步阻塞性能差不适合高并发。传统JTAAtomikos实现但微服务中少用。2TCCTry-Confirm-CancelTry预留资源。例如扣减库存时先锁定库存生成订单时先创建待确认订单。Confirm确认执行业务操作使用Try阶段预留的资源完成真正扣减。Cancel释放预留资源回补库存取消订单。优点性能比2PC好。缺点业务侵入大需要编写三套逻辑且要处理幂等、空回滚、悬挂等问题。3本地消息表 消息队列最终一致适合不强一致、最终一致的场景比如加积分。流程在订单服务本地事务中创建订单 插入消息表消息状态待发送。有一个定时任务扫描消息表把消息发送到MQ。MQ消费者积分服务消费消息累加积分消费成功后手动ack。如果消费失败MQ重试多次失败后进入死信队列人工处理。4Seata AT模式AT模式类比增强版2PC但无需侵入业务SQL第一阶段执行业务SQL同时保存数据快照和undo_log。第二阶段如果全局提交异步删除undo_log如果回滚则根据undo_log反向补偿数据。对业务基本无侵入但需要全局锁并发性能有损耗。3. 高并发下TCC的优化不要把所有操作都放进TCC。把核心的三类操作库存、订单、支付用TCC非核心的加积分、发短信、发优惠券等使用MQ异步最终一致。将“库存预扣”前置到Redis在Try阶段只做Redis的预扣减异步落库Confirm阶段将Redis扣减结果同步到数据库。使用异步批量提交消费者攒一批请求后批处理数据库更新。增加对账系统如果TCC流程中断由定时任务扫描订单状态调用Cancel或Confirm进行恢复。4. 链路追踪与日志关联成熟的微服务链路追踪方案OpenTelemetry业界标准Jaeger/Zipkin。Spring Cloud Sleuth已废弃被Micrometer Tracing替代会自动为每个请求生成traceId和spanId。在Feign/WebClient调用时Sleuth/Micrometer把traceId注入HTTP Header如X-B3-TraceId下游服务从Header中取出继续传递并把当前时间戳和调用信息上报给Zipkin。在日志配置中通过[%X{traceId}]将traceId打印在每行日志里排查问题时按traceId搜索所有日志。监控指标用Micrometer接入Prometheus配合Grafana展示QPS、RT、线程池活跃度等。最后谢飞机把这篇答案收藏了。下次面试他决定说得既有故事又有深度。而老张看着他的背影默默在简历上写了一句“潜力尚可但八股太水——建议回去多看书。”

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价