牛逼!小米汽车太炸裂了!

关系型数据库MySQL微服务

picture.image

我的面试和项目实战网站升级了:http://www.susan.net.cn

大家好,我是苏三。

雷总也没想到,卖个车发现跟卖手机一样简单。

上周小米汽车YU7发布,直接卖爆了,我朋友圈都看到好多朋友晒YU7订单,朋友圈看到晒订单的数量恍惚间位以为是买小米手机,打开一看这可是25w的车啊,我只能说朋友圈里的大佬们真多。

小米YU7开售 3 分钟大定就突破 20 万台,平均每秒售出约 1111 台,1 小时大定更是飙升至 28.9 万台 ,开售 18 小时,锁单量已突破 24 万辆 ,这是什么概念?

这一数据相当于国内市场 25-35 万元新能源 SUV 近半年销量总和,远超小米 SU7(24 小时 8.9 万台)和特斯拉 Model Y 焕新版(首日 5 万台)。

这个数据跟开挂没什么区别了,直接再入史册了,已经被业内称为 “汽车工业史奇迹”

按照小米工程目前的产能,目前小米YU7的订单量需要消耗掉,至少要超一年的了。

还是感慨雷总实在太强了,一天的发售,直接干满别的厂商一年的订单量。

小米汽车持续那么火爆,势必会带来新的岗位机会。

那么为了应对今年的小米汽车秋招,咱们来一起看看小米汽车的面试难度,提前感受一下。

这次来分享去年校招小米汽车Java开发的面经,主要是重点拷打了Spring的内容,还拷打了MySQL索引、Java并发、JVM的内容,问题都比较经典,值得学习一波。

picture.image

最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。

扫码加苏三的微信:li_su223,备注:所在城市,即可进群。

picture.image

小米汽车(一面)

1、B+树和B树的区别是什么?

  • 在B+树中,数据都存储在叶子节点上,而非叶子节点只存储索引信息;而B树的非叶子节点既存储索引信息也存储部分数据。
  • B+树的叶子节点使用链表相连,便于范围查询和顺序访问;B树的叶子节点没有链表连接。
  • B+树的查找性能更稳定,每次查找都需要查找到叶子节点;而B树的查找可能会在非叶子节点找到数据,性能相对不稳定。

2、MySQL中的Hash索引了解吗?

MySQL的Hash索引其实是个“特定场景下的加速器”,但得注意它和主流B+树索引完全不同。首先明确一点:InnoDB引擎本身是不支持手动创建Hash索引的 ,只有Memory引擎(数据放内存的那种临时表)才能直接用USING HASH来建索引。

Hash索引的原理是对键值算个哈希码,然后直接映射到存储位置。如果没冲突,等值查询比如找user\_id=100这种,速度确实比B+树快,毕竟一步到位。

但它有硬伤:第一,完全搞乱数据顺序 ,导致范围查询比如age BETWEEN 20 AND 30根本没法加速;第二,怕冲突 ,万一多个键值哈希到同一个槽位,得挨个遍历链表;第三,必须全键匹配 ,联合索引里只用前一列也不行。

不过InnoDB有个折中方案叫自适应哈希索引(AHI) 。它不是咱手动建的,而是InnoDB自己盯着那些频繁访问的索引页(比如主键热点数据),偷偷在内存里建个哈希索引来加速路径查找。相当于给B+树加了层缓存,对高并发的点查询挺有用,但范围查询还是得靠B+树。

所以实际用的时候:如果数据能全放内存且只需等值查询,比如配置表、会话表,用Memory引擎配Hash索引确实快;但但凡涉及范围查询、排序或者数据量大,必须上InnoDB的B+树索引。开发中与其纠结Hash索引,不如关注索引覆盖和查询优化更实在。

3、索引失效的情况有哪些?

  • 当对索引列使用函数时,数据库无法直接使用索引进行查找,因为函数改变了索引列的原始值,使得索引失效。比如下面这个例子, YEAR(user\_age) 对索引列 user\_age 运用了函数,数据库无法直接依据索引定位到满足条件的记录,只能进行全表扫描。
  
-- 假设 user\_age 列有索引  
SELECT * FROM users WHERE YEAR(user\_age) = 1990;  

  • 如果索引列参与了计算,数据库同样无法使用索引进行查找。比如下面的例子,这里 price + 10 让索引列 price 参与了计算,数据库不能直接通过索引来确定符合条件的记录,只能全表扫描。
  
-- 假设 price 列有索引  
SELECT * FROM products WHERE price + 10 < 100;  

  • 在使用 LIKE 进行模糊查询时,若通配符 % 位于开头,数据库无法使用索引进行查找。比如下面的例子,由于 LIKE '%test' 以通配符 % 开头,数据库无法通过索引快速定位到满足条件的记录,只能全表扫描。不过,如果通配符不在开头,如 LIKE 'test%' ,数据库仍可使用索引。
  
-- 假设 username 列有索引  
SELECT * FROM users WHERE username LIKE '%test';  

  • 当查询条件中的数据类型与索引列的数据类型不一致时,数据库可能无法使用索引。比如下面的例子,在这个例子中, user\_id 是整数类型,而查询条件中使用了字符串 '123' ,数据库可能需要进行类型转换,从而导致索引失效。
  
-- 假设 user\_id 列是整数类型且有索引  
SELECT * FROM users WHERE user\_id = '123';  

  • 如果使用 OR 连接多个条件,且其中部分条件未使用索引,数据库可能不会使用索引。比如下面的例子,由于 user\_name 列没有索引,数据库可能不会使用 user\_id 列的索引,而是进行全表扫描。
  
-- 假设 user\_id 列有索引,user\_name 列无索引  
SELECT * FROM users WHERE user\_id = 1 OR user\_name = 'test';  

4、ReentrantLock原理是什么?

ReentrantLock 的核心是依赖 AQS 这个同步器框架。简单说,AQS 内部维护了一个 state 变量和一个线程等待队列。

picture.image

当线程尝试加锁时,会先用 CAS 操作去抢 state,如果 state 是 0(表示锁空闲),就直接抢到锁并把 state 加 1,同时记录自己是锁的持有者。

如果抢锁失败(比如锁已被占用),线程就会被封装成一个节点,通过 CAS 安全地插入到等待队列尾部。然后进入自旋检查:看看自己是不是队列里的第二个节点(第一个是正在持有锁的节点),如果是就再次尝试抢锁;否则就挂起自己,等待被唤醒。

这里的关键优化是 非公平锁 的设计:新来的线程不用排队,可以直接插队抢锁,虽然可能让队列里的线程等更久,但整体吞吐量更高。而 公平锁 会严格按队列顺序唤醒,避免饥饿问题。

解锁的时候更巧妙:持有锁的线程将 state 减 1(可重入锁要减到 0 才算完全释放),然后精准唤醒队列中的下一个等待线程。整个过程中,AQS 用自旋减少线程挂起次数,用 CAS 管理队列避免阻塞,所以性能比传统的 synchronized 更可控,还能支持超时、中断等灵活特性。

5、JVM中的双亲委派作用是什么?如何破坏双亲委派?

双亲委派模型的核心作用有三点:

  • 第一是避免重复加载 ,比如 java.lang.Object 这种核心类,启动类加载器加载后,应用类加载器就不会再加载了;
  • 第二是保护Java核心库安全 ,防止有人写个恶意 java.lang.Hack 类来破坏JVM;
  • 第三是保证类的一致性 ,同一个类在不同加载器环境下会被认为是不同的类,委派机制能确保基础类全局唯一。

不过实际开发中有几种场景需要打破这个机制。最常见的是 SPI机制 ,像JDBC的接口是由启动类加载器加载的,但数据库厂商的实现包在应用classpath里,必须让应用类加载器来加载。解决办法是用 线程上下文类加载器 ,把应用类加载器“传递”进核心库的代码里。

另外像 Tomcat这种Web容器 ,每个Web应用要隔离自己的类库,比如两个应用都用Spring,但版本不同。Tomcat的做法是为每个应用创建独立的 WebappClassLoader,它加载类时先自己扫描WEB-INF目录 ,找不到再委派给父加载器,相当于把双亲委派顺序倒过来了。

比如下图中,蓝色部分依旧和原来一样,使用双亲委派模型;绿色部分是 Tomcat 第一部分自定义的类加载器,这一部分加载 Tomcat 包中的类,依旧采用双亲委派模型;紫色部分是 Tomcat 第二部分自定义的类加载器,正是这一部分,打破了双亲委派模型

picture.image

6、spring如何解决循环依赖?

循环依赖指的是两个类中的属性相互依赖对方:例如 A 类中有 B 属性,B 类中有 A属性,从而形成了一个依赖闭环,如下图。

picture.image

循环依赖问题在Spring中主要有三种情况:

  • 第一种:通过构造方法进行依赖注入时产生的循环依赖问题。
  • 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。

Spring 解决单例模式下的setter循环依赖问题的主要方式是通过三级缓存解决循环依赖。三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。具体步骤如下:

  • 实例化 Bean :Spring 在实例化 Bean 时,会先创建一个空的 Bean 对象,并将其放入一级缓存中。
  • 属性赋值 :Spring 开始对 Bean 进行属性赋值,如果发现循环依赖,会将当前 Bean 对象提前暴露给后续需要依赖的 Bean(通过提前暴露的方式解决循环依赖)。
  • 初始化 Bean :完成属性赋值后,Spring 将 Bean 进行初始化,并将其放入二级缓存中。
  • 注入依赖 :Spring 继续对 Bean 进行依赖注入,如果发现循环依赖,会从二级缓存中获取已经完成初始化的 Bean 实例。

通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。

7、Spring为什么用3级缓存解决循环依赖问题?用2级缓存不行吗?

Spring 必须用三级缓存解决循环依赖,核心是为了正确处理需要 AOP 代理的 Bean 。如果只用二级缓存,会导致注入的对象形态错误,甚至破坏单例原则。

举个例子:假设 Bean A 依赖 B,B 又依赖 A,且 A 需要被动态代理(比如加了 @Transactional)。如果只有二级缓存,当 B 创建时去注入 A,拿到的是 A 的原始对象。但 A 在后续初始化完成后才会生成代理对象,结果就是:B 拿着原始对象 A,而 Spring 容器里存的是代理对象 A —— 同一个 Bean 出现了两个不同实例,这直接违反了单例的核心约束。

三级缓存中的 ObjectFactory 就是解决这个问题的关键。它不是直接缓存对象,而是存了一个能生产对象的工厂。当发生循环依赖时,调用这个工厂的 getObject() 方法,这时 Spring 会智能判断:如果这个 Bean 最终需要代理,就提前生成代理对象并放入二级缓存;如果不需要代理,就返回原始对象。这样一来,B 注入的 A 就是最终形态(可能是代理对象),后续 A 初始化完成后也不会再创建新代理,保证了对象全局唯一。

简单说,三级缓存的本质是 “按需延迟生成正确引用” 。它既维持了 Bean 生命周期的完整性(正常流程在初始化后生成代理),又在循环依赖时特殊处理,避免逻辑矛盾。而二级缓存缺乏这种动态决策能力,因此无法替代三级缓存。

8、AOP的原理是什么?

Spring AOP的实现依赖于动态代理技术 。动态代理是在运行时动态生成代理对象,而不是在编译时。它允许开发者在运行时指定要代理的接口和行为,从而实现在不修改源码的情况下增强方法的功能。

Spring AOP支持两种动态代理:

  • 基于JDK的动态代理 :使用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现。这种方式需要代理的类实现一个或多个接口。
  • 基于CGLIB的动态代理 :当被代理的类没有实现接口时,Spring会使用CGLIB库生成一个被代理类的子类作为代理。CGLIB(Code Generation Library)是一个第三方代码生成库,通过继承方式实现代理。

9、Spring动态代理的两种方式说一下

Java的动态代理是一种在运行时动态创建代理对象的机制,主要用于在不修改原始类的情况下对方法调用进行拦截和增强。

Java动态代理主要分为两种类型:

  • 基于接口的代理 (JDK动态代理): 这种类型的代理要求目标对象必须实现至少一个接口。Java动态代理会创建一个实现了相同接口的代理类,然后在运行时动态生成该类的实例。这种代理的实现核心是 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口。每一个动态代理类都必须实现 InvocationHandler 接口,并且每个代理类的实例都关联到一个 handler 。当通过代理对象调用一个方法时,这个方法的调用会被转发为由 InvocationHandler 接口的 invoke() 方法来进行调用。
  • 基于类的代理 (CGLIB动态代理): CGLIB(Code Generation Library)是一个强大的高性能的代码生成库,它可以在运行时动态生成一个目标类的子类。CGLIB代理不需要目标类实现接口,而是通过继承的方式创建代理类。因此,如果目标对象没有实现任何接口,可以使用CGLIB来创建动态代理。

10、能使用静态代理的方式实现AOP吗?

当然可以 用静态代理实现 AOP 的基本功能,比如你在代码里手动写个代理类,在目标方法前后加日志或者事务控制,技术上完全可行。但问题是,静态代理在实际生产中基本没人用,因为它有三大硬伤:

  • 第一是代码爆炸 。比如你有 100 个 Service 类需要加事务,就得写 100 个对应的静态代理类,里面全是重复的 try-catch 提交回滚代码,维护起来简直是灾难。
  • 第二是僵化 。一旦业务接口改了个方法名,所有相关的代理类都得跟着改,而动态代理通过反射调用目标方法,根本不怕这种变动。
  • 第三是无法动态筛选 。比如你想只给带 @Transactional 注解的方法加事务,静态代理只能写死逻辑,而 Spring AOP 可以在运行时通过切点表达式精准匹配需要增强的方法。

所以 Spring 才选了 JDK 动态代理和 CGLIB:它们能在运行时动态生成代理类 ,一个切面配置就能覆盖成百上千个方法。像事务管理这种全局需求,用静态代理手动绑定根本不可行/

11、Spring Bean是如何销毁的?

Spring Bean 的销毁过程由容器在关闭时统一触发,具体流程如下:

首先,容器会遍历所有已注册的 Bean,判断是否满足销毁条件(如容器关闭或显式调用销毁方法)。对于每个 Bean,按以下顺序执行销毁逻辑:

  1. 接口回调 :若 Bean 实现了 DisposableBean 接口,容器会调用其 destroy() 方法,这是 Spring 定义的标准销毁接口;
  2. 注解处理 :若 Bean 使用 @PreDestroy 注解标注了方法,容器会调用该方法,优先级高于 destroy-method 配置;
  3. 自定义方法 :若在 XML 配置中通过 destroy-method 指定了销毁方法,或在 @Bean 注解中通过 destroyMethod 属性配置,容器会调用该自定义方法;
  4. 生命周期处理器 :若容器配置了 DestructionAwareBeanPostProcessor 后置处理器,其 postProcessBeforeDestruction() 方法会被调用,用于在销毁前做额外处理。

销毁过程中,容器会处理依赖关系,先销毁依赖其他 Bean 的实例,再销毁被依赖的 Bean,避免资源释放时的引用异常。此外,若 Bean 配置了@Lazy懒加载,其销毁会在容器关闭时统一处理,而非初始化时注册销毁回调。整个流程确保 Bean 持有的资源(如数据库连接、线程池)被正确释放,避免内存泄漏。

12、SpringCloud组件有哪些?

微服务常用的组件:

  • 注册中心 :注册中心是微服务架构最核心的组件。它起到的作用是对新节点的注册与状态维护, 解决了「如何发现新节点以及检查各节点的运行状态的问题」 。微服务节点在启动时会将自己的服务名称、IP、端口等信息在注册中心登记,注册中心会定时检查该节点的运行状态。注册中心通常会采用心跳机制最大程度保证已登记过的服务节点都是可用的。
  • 负载均衡 :负载均衡解决了「如何发现服务及负载均衡如何实现的问题」,通常微服务在互相调用时,并不是直接通过IP、端口进行访问调用。而是先通过服务名在注册中心查询该服务拥有哪些节点,注册中心将该服务可用节点列表返回给服务调用者,这个过程叫服务发现,因服务高可用的要求,服务调用者会接收到多个节点,必须要从中进行选择。因此服务调用者一端必须内置负载均衡器,通过负载均衡策略选择合适的节点发起实质性的通信请求。
  • 服务通信 :服务通信组件解决了「 服务间如何进行消息通信的问题 」,服务间通信采用轻量级协议,通常是HTTP RESTful风格。但因为RESTful风格过于灵活,必须加以约束,通常应用时对其封装。例如在SpringCloud中就提供了Feign和RestTemplate两种技术屏蔽底层的实现细节,所有开发者都是基于封装后统一的SDK进行开发,有利于团队间的相互合作。
  • 配置中心 :配置中心主要解决了「 如何集中管理各节点配置文件的问题 」,在微服务架构下,所有的微服务节点都包含自己的各种配置文件,如jdbc配置、自定义配置、环境配置、运行参数配置等。要知道有的微服务可能可能有几十个节点,如果将这些配置文件分散存储在节点上,发生配置更改就需要逐个节点调整,将给运维人员带来巨大的压力。配置中心便由此而生,通过部署配置中心服务器,将各节点配置文件从服务中剥离,集中转存到配置中心。一般配置中心都有UI界面,方便实现大规模集群配置调整。
  • 集中式日志管理 :集中式日志主要是解决了「 如何收集各节点日志并统一管理的问题 」。微服务架构默认将应用日志分别保存在部署节点上,当需要对日志数据和操作数据进行数据分析和数据统计时,必须收集所有节点的日志数据。那么怎么高效收集所有节点的日志数据呢?业内常见的方案有ELK、EFK。通过搭建独立的日志收集系统,定时抓取各节点增量日志形成有效的统计报表,为统计和分析提供数据支撑。
  • 分布式链路追踪:分布式链路追踪解决了「 如何直观的了解各节点间的调用链路的问题 」。系统中一个复杂的业务流程,可能会出现连续调用多个微服务,我们需要了解完整的业务逻辑涉及的每个微服务的运行状态,通过可视化链路图展现,可以帮助开发人员快速分析系统瓶颈及出错的服务。
  • 服务保护 :服务保护主要是解决了「 如何对系统进行链路保护,避免服务雪崩的问题 」。在业务运行时,微服务间互相调用支撑,如果某个微服务出现高延迟导致线程池满载,或是业务处理失败。这里就需要引入服务保护组件来实现高延迟服务的快速降级,避免系统崩溃。

SpringCloud Alibaba实现的微服务架构:

picture.image

  • SpringCloud Alibaba中使用 Alibaba Nacos 组件实现 注册中心 ,Nacos提供了一组简单易用的特性集,可快速实现动态服务发现、服务配置、服务元数据及流量管理。

  • SpringCloud Alibaba 使用 Nacos服务端均衡 实现负载均衡,与Ribbon在调用端负载不同,Nacos是在服务发现的同时利用负载均衡返回服务节点数据。

  • SpringCloud Alibaba 使用 Netflix FeignAlibaba Dubbo 组件来实现服务通行,前者与SpringCloud采用了相同的方案,后者则是对自家的 RPC 框架Dubbo 也给予支持,为服务间通信提供另一种选择。

  • SpringCloud Alibaba 在 API服务网关 组件中,使用与SpringCloud相同的组件,即: SpringCloud Gateway

  • SpringCloud Alibaba在配置中心组件中使用 Nacos内置配置中心 ,Nacos内置的配置中心,可将配置信息 存储保存在指定数据库

  • SpringCloud Alibaba在原有的 ELK方案 外,还可以使用阿里云日志服务(LOG)实现日志集中式管理。

  • SpringCloud Alibaba在 分布式链路组件 中采用与SpringCloud相同的方案,即: Sleuth/Zipkin Server

  • SpringCloud Alibaba使用 Alibaba Sentinel 实现系统保护,Sentinel不仅功能更强大,实现系统保护比Hystrix更优雅,而且还拥有更好的UI界面。

最后欢迎加入苏三的星球,你将获得:商城微服务实战、AI开发项目课程、苏三AI项目、秒杀系统实战、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

扫描下方二维码,加入星球可以优惠40元:

picture.image 目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1700+小伙伴加入学习。

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
如何利用云原生构建 AIGC 业务基石
AIGC即AI Generated Content,是指利用人工智能技术来生成内容,AIGC也被认为是继UGC、PGC之后的新型内容生产方式,AI绘画、AI写作等都属于AIGC的分支。而 AIGC 业务的部署也面临着异构资源管理、机器学习流程管理等问题,本次分享将和大家分享如何使用云原生技术构建 AIGC 业务。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论