字节跳动 Java后端开发 二面 面经

1. 介绍一下你项目中遇到的最复杂的技术问题,如何解决的?

参考答案:

我在做电商系统时,遇到过一个分布式环境下的数据一致性问题。场景是用户下单时,需要同时操作订单服务、库存服务、积分服务三个服务,如果其中一个失败,其他服务需要回滚。

最初我使用了Seata的AT模式实现分布式事务,但在压测时发现性能很差,TPS只有200左右。通过分析发现,Seata的全局锁机制导致了严重的性能瓶颈,特别是在高并发场景下,大量请求在等待全局锁。

我重新设计了方案,采用基于消息队列的最终一致性。下单时先创建订单,发送消息到Kafka,库存服务和积分服务监听消息异步处理。为了保证可靠性,实现了以下机制:

消息可靠性方面,生产者使用同步发送+重试机制,Kafka开启副本和持久化,消费者使用手动确认模式。幂等性方面,消息中携带唯一ID(订单号),消费者使用Redis记录已处理的消息ID,重复消息直接忽略。补偿机制方面,如果处理失败超过重试次数,将消息放入死信队列,通过定时任务扫描并人工介入处理。

还实现了本地消息表方案作为兜底。订单创建时,在同一个事务中写入订单表和消息表,定时任务扫描未发送成功的消息重新发送。这样即使Kafka暂时不可用,也不会丢失消息。

优化后,系统TPS提升到2000+,P99延迟控制在100ms以内。这个问题让我深刻理解了分布式系统中一致性和性能的权衡,以及如何设计可靠的异步系统。

2. 如果让你设计一个高可用的分布式系统,你会从哪些方面考虑?

答案:

高可用系统的核心是消除单点故障,保证系统在部分组件失败时仍能正常运行。

服务层面,使用集群部署,每个服务至少3个实例,部署在不同的机器或机房。使用服务注册与发现(Nacos、Eureka),实例动态上下线,自动感知。实现健康检查,定期探测实例状态,异常实例自动摘除。使用熔断器(Sentinel、Hystrix),服务异常时快速失败,避免雪崩。实现服务降级,核心功能优先保证,非核心功能可以暂时关闭。使用限流保护系统,防止突发流量压垮系统。

数据层面,使用主从复制,主库负责写,从库负责读,提高并发能力。使用双主或多主架构,任意节点都可以写入,提高可用性。定期备份数据,支持快速恢复。使用分布式数据库(TiDB、CockroachDB),数据自动分片和复制,节点故障自动切换。使用缓存(Redis Cluster),减少数据库压力,缓存节点故障不影响核心功能。

网络层面,使用多机房部署,机房之间互为备份。使用CDN加速,静态资源就近访问。使用DNS负载均衡,将域名解析到不同IP。使用专线或VPN保证机房间通信质量。使用API网关统一入口,实现路由、限流、熔断等功能。

监控和告警方面,实时监控系统的QPS、响应时间、错误率、资源使用率等指标。设置告警规则,异常时及时通知。使用链路追踪(Skywalking、Zipkin),快速定位问题。使用日志收集系统(ELK),集中管理和分析日志。定期进行故障演练,验证系统的容错能力。

容灾方面,制定灾难恢复计划,明确RTO和RPO。定期演练灾难恢复流程。准备应急预案,包括降级方案、数据恢复方案、通信方案等。

3. 讲讲Spring Boot的启动流程和自动配置原理

答案:

Spring Boot的启动流程从main方法的SpringApplication.run()开始。

首先创建SpringApplication对象,推断应用类型(Servlet、Reactive、None),加载ApplicationContextInitializer和ApplicationListener,推断主类。

然后执行run方法,创建StopWatch记录启动时间,获取SpringApplicationRunListeners并发布starting事件。准备Environment,包括配置文件、环境变量、命令行参数等。打印Banner。创建ApplicationContext,根据应用类型创建对应的上下文。准备上下文,设置Environment,执行ApplicationContextInitializer,发布contextPrepared事件,加载Bean定义。

接着刷新上下文,这是Spring容器的核心流程。执行BeanFactoryPostProcessor,注册BeanPostProcessor,初始化MessageSource和ApplicationEventMulticaster,注册Listener,实例化所有非懒加载的单例Bean,发布contextRefreshed事件。

最后执行Runners(CommandLineRunner、ApplicationRunner),发布started和running事件,启动完成。

自动配置的核心是@EnableAutoConfiguration注解,它通过@Import导入AutoConfigurationImportSelector类。这个类会读取classpath下所有jar包中的META-INF/spring.factories文件,找到所有的自动配置类。

自动配置类使用@Configuration注解标注,内部使用@Bean定义需要自动配置的组件。通过@Conditional系列注解控制配置的生效条件,比如@ConditionalOnClass(某个类存在时生效)、@ConditionalOnMissingBean(某个Bean不存在时生效)、@ConditionalOnProperty(某个配置属性存在时生效)等。

用户可以通过application.properties或application.yml配置参数,Spring Boot会自动读取并应用。用户自定义的Bean优先级最高,自动配置的Bean优先级较低。可以通过exclude属性排除某些自动配置类。

4. 深入讲讲JVM的类加载机制和双亲委派模型

答案:

类加载机制是JVM将类的字节码加载到内存,并转换为Class对象的过程。分为加载、验证、准备、解析、初始化五个阶段。

加载阶段,通过类的全限定名获取二进制字节流,将字节流转换为方法区的运行时数据结构,在堆中生成Class对象作为方法区数据的访问入口。

验证阶段,确保字节码符合JVM规范,包括文件格式验证、元数据验证、字节码验证、符号引用验证。

准备阶段,为类变量分配内存并设置初始值,这里的初始值是数据类型的零值,不是代码中赋的值。

解析阶段,将常量池中的符号引用替换为直接引用,包括类或接口的解析、字段解析、方法解析、接口方法解析。

初始化阶段,执行类构造器<clinit>方法,这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。

双亲委派模型是类加载器的层次关系。从上到下分为启动类加载器(Bootstrap ClassLoader,加载核心类库)、扩展类加载器(Extension ClassLoader,加载扩展类库)、应用程序类加载器(Application ClassLoader,加载应用类路径的类)、自定义类加载器。

双亲委派的工作流程是:类加载器收到加载请求时,先委派给父加载器加载,父加载器无法加载时,才由子加载器加载。这样保证了Java核心类库的安全性,避免核心类被篡改。比如自定义的java.lang.String类不会被加载,因为启动类加载器已经加载了核心的String类。

打破双亲委派的场景包括:JDBC驱动加载,使用线程上下文类加载器。Tomcat的类加载器,为了实现应用隔离,每个应用使用独立的类加载器。OSGi的类加载器,实现模块化和热部署。

5. 如何排查和解决线上的内存泄漏问题?

答案:

内存泄漏是指对象不再使用但无法被垃圾回收,导致内存占用持续增长。

排查步骤首先是确认问题。通过监控系统观察内存使用趋势,如果内存持续增长且Full GC后无法回收,可能存在内存泄漏。使用jstat命令查看堆内存使用情况和GC情况,关注老年代的使用率和Full GC频率。

然后dump堆内存快照。在不同时间点使用jmap命令dump多次,对比分析哪些对象在增长。命令是jmap -dump:format=b,file=heap.bin <pid>。注意dump会暂停应用,在业务低峰期操作。

接着分析dump文件。使用MAT(Memory Analyzer Tool)或jvisualvm打开dump文件。查看Histogram,按对象数量或占用内存排序,找出占用内存最多的对象。查看Dominator Tree,找出占用内存最多的对象及其引用链。使用OQL查询特定对象,分析对象的属性和引用关系。

定位问题后,分析引用链,找出为什么对象无法释放。常见原因包括:静态集合类持有对象引用,对象无法释放。监听器和回调没有注销,持有外部类引用。ThreadLocal使用后没有remove,线程池中的线程会一直持有。数据库连接、IO流等资源使用后没有关闭。缓存没有设置过期时间,对象一直存在。

修复问题后,重新测试验证。使用压力测试工具模拟生产环境,长时间运行观察内存使用情况。确认内存使用稳定,Full GC后能够回收大部分内存。

预防措施包括:及时释放不再使用的对象

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论

相关推荐

评论
点赞
5
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务