北京明光振铎数据科技 - java开发 二面 面经

1. 介绍一下你做过的最有挑战性的项目,遇到了什么技术难点,是如何解决的?

我之前参与过一个电商订单系统的开发。最大的挑战是在促销活动期间,订单量暴增导致系统响应变慢,甚至出现超卖问题。

技术难点主要有三个:

高并发下的库存扣减:最初使用的是先查询库存再更新的方式,存在并发问题。我们改用了乐观锁方案,在更新时加上版本号判断,同时在数据库层面使用UPDATE ... WHERE stock > 0的原子操作,确保不会超卖。

数据库压力过大:引入了Redis缓存热点商品库存,使用Lua脚本保证扣减的原子性。同时对订单表做了分库分表,按用户ID哈希分散到不同的数据库实例。

消息堆积问题:订单创建后需要发送通知、更新积分等操作,MQ出现堆积。我们优化了消费者逻辑,将非核心操作异步化,并增加了消费者实例数量。

通过这些优化,系统QPS从500提升到3000+,订单处理延迟从2秒降到200ms以内。这个项目让我深刻理解了高并发场景下的技术选型和优化思路。

2. 说说Spring Boot的自动装配原理,它是如何实现的?

Spring Boot的自动装配主要依赖@EnableAutoConfiguration注解和SPI机制。

核心流程是:

启动类的@SpringBootApplication注解包含了@EnableAutoConfiguration

@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)导入自动配置类。

AutoConfigurationImportSelector会读取META-INF/spring.factories文件,加载所有的自动配置类(如RedisAutoConfigurationDataSourceAutoConfiguration等)。

每个自动配置类通过@ConditionalOnClass@ConditionalOnMissingBean等条件注解判断是否生效。比如只有classpath下存在Redis相关类时,Redis的自动配置才会生效。

配置类会读取application.properties中的属性(通过@ConfigurationProperties),创建相应的Bean。

这种设计让开发者只需引入starter依赖,就能自动完成配置,大大简化了开发流程。如果需要自定义配置,可以通过配置文件覆盖默认值,或者自己定义Bean来替换自动配置的Bean。

3. MySQL的索引失效场景有哪些?如何避免?

常见的索引失效场景:

使用函数或表达式:WHERE YEAR(create_time) = 2024会导致索引失效,应该改为WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'

隐式类型转换:字段是varchar类型,但查询时用了数字WHERE phone = 12345678901,应该加引号WHERE phone = '12345678901'

like以通配符开头:WHERE name LIKE '%张%'无法使用索引,可以考虑全文索引或ES。

or连接的条件:如果or的某个条件没有索引,整个查询可能不走索引。可以改用union或确保所有条件都有索引。

联合索引不满足最左前缀:索引是(a, b, c),但查询条件是WHERE b = 1,无法使用索引。

not in、!=、<>:这些操作通常不走索引,可以考虑改写SQL或使用覆盖索引。

字段上使用is null:取决于数据分布,如果null值很多可能不走索引。

避免方法就是遵循索引使用规范,编写SQL时注意这些场景,同时使用EXPLAIN分析执行计划,确保索引生效。

4. 分布式事务了解吗?有哪些解决方案?

分布式事务是指事务操作跨越多个数据库或服务的场景。常见解决方案有:

两阶段提交(2PC):协调者先询问所有参与者是否可以提交(准备阶段),如果都同意则发送提交命令(提交阶段)。优点是强一致性,缺点是同步阻塞、单点故障、数据不一致风险。实际应用较少。

TCC(Try-Confirm-Cancel):业务层面的两阶段提交。Try阶段预留资源,Confirm阶段确认提交,Cancel阶段回滚。需要业务代码实现三个接口,开发成本高,但性能好。

本地消息表:在本地数据库记录消息,事务提交后发送MQ,消费者处理后更新状态。通过定时任务扫描未发送的消息重试。实现简单,但需要额外的表和定时任务。

消息事务(RocketMQ):发送半消息,执行本地事务,根据结果提交或回滚消息。消费者处理消息,失败则重试。适合异步场景。

Saga模式:将长事务拆分为多个本地短事务,每个事务有对应的补偿操作。如果某步失败,执行之前所有步骤的补偿。适合长流程业务。

实际项目中,我倾向于使用本地消息表或消息事务,因为它们实现相对简单,而且符合最终一致性的要求。强一致性场景尽量避免分布式事务,通过业务设计来规避。

5. JVM内存模型和垃圾回收机制能详细说说吗?

JVM内存模型主要分为:

堆(Heap):存放对象实例,是GC的主要区域。分为新生代(Eden、Survivor0、Survivor1)和老年代。

方法区(元空间):存放类信息、常量、静态变量等。JDK8后改为元空间,使用本地内存。

虚拟机栈:每个线程私有,存放局部变量、操作数栈、方法出口等。

本地方法栈:为native方法服务。

程序计数器:记录当前线程执行的字节码行号。

垃圾回收主要针对堆内存:

新生代使用复制算法:对象优先在Eden分配,Minor GC时将存活对象复制到Survivor区,经过多次GC仍存活的对象晋升到老年代。

老年代使用标记-清除或标记-整理算法:标记存活对象,清除未标记对象,或者整理内存避免碎片。

常见的垃圾回收器:

Serial/Serial Old:单线

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

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

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

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

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