面试
如何理解 Spring 当中的 Bean?
- Spring容器就是个HashMap
Spring容器本质上就是一个大的HashMap,用来存放和管理所有的Bean。这个Map的key是Bean的名字(默认是类名首字母小写,或者你指定的名字),value就是Bean的实例。你可以把这个Map理解为一个“对象池”,Spring负责把这些对象实例化、管理、并提供给你使用。
前置处理器就是准备好实例化类的条件
前置处理器(比如BeanFactoryPostProcessor
)的作用是在Bean实例化之前,准备好一些条件。比如,它可以修改Bean的定义(BeanDefinition),或者做一些配置的预处理工作。你可以把它理解为一个“装修工”,在房子建好之前,先把图纸改好。后置处理器就是把类按第2步的条件实例化以后放进HashMap
后置处理器(比如BeanPostProcessor
)的作用是在Bean实例化之后,做一些额外的处理。比如,你可以在这里对Bean进行一些定制化的操作,或者给Bean注入一些额外的属性。最后,这个Bean会被放进Spring的HashMap里,name或者你指定的名字是key,实例就是value。这个value也就是实例,就是你问的Bean,跟你手动new出来的没有本质区别,只不过Spring帮你管理了它的生命周期。依赖注入就是把HashMap里的类实例用name拿出来用
依赖注入(Dependency Injection, DI)就是把HashMap里的Bean实例通过name拿出来,然后注入到需要它的地方。比如,你有一个Service类,它依赖一个Dao类,Spring会自动从HashMap里找到这个Dao的实例,然后注入到Service里。你不用手动去new这个Dao,Spring帮你搞定。AOP就是允许在HashMap里面的实例使用的时候,在方法开始、进行、结束的时间点上搞一个回调函数
AOP(面向切面编程)就是在Bean的方法执行时,插入一些额外的逻辑。比如,你可以在方法执行前、执行后、或者抛出异常时,插入一些代码。这些插入的代码就是“切面”,Spring通过动态代理或者字节码增强来实现这个功能。你可以把它理解为一个“钩子”,在方法执行的特定时间点挂上一些额外的逻辑。自定义starter就是在SpringBootApplication实例化的时候把指定目录下配置的类也放到前置处理器等待实例化
自定义Starter就是在Spring Boot启动时,自动加载一些配置类和Bean。你可以把这些类放到指定的目录下,Spring Boot会自动扫描并加载它们。这些类会被放到前置处理器中,等待实例化。你可以把它理解为一个“自动装配”的过程,Spring Boot帮你把这些类自动加载到容器里。观察者模式也只不过是在第6步的实例里面给一个监听器,监听指定事件的发生罢了
观察者模式(Observer Pattern)在Spring中的应用就是事件监听机制。你可以在Bean里注册一个监听器(Listener),当某个事件发生时,监听器会自动触发。Spring内置了很多事件(比如ContextRefreshedEvent
、ContextClosedEvent
等),你也可以自定义事件。你完全可以自己实现一个Listener,监听指定的事件。所谓的几级缓存,也不过就是解决实例化的时候,AB类互相依赖的问题
Spring的几级缓存主要是为了解决循环依赖的问题。比如,A类依赖B类,B类又依赖A类。Spring通过三级缓存来解决这个问题:先创建一个A的引用(半成品)给B,然后实例化B,最后再实例化A。这样,A和B都能正常初始化。你可以把它理解为一个“先上车后补票”的过程,Spring通过这种方式解决了循环依赖的问题。Bean的生命周期
Bean的生命周期从Spring容器启动时开始,到容器关闭时结束。大致分为以下几个阶段:- 实例化:通过反射或者工厂方法创建Bean的实例。
- 属性赋值:将配置中的属性值注入到Bean中。
- 初始化:调用Bean的初始化方法(比如
@PostConstruct
注解的方法)。 - 使用:Bean可以被应用程序使用。
- 销毁:当容器关闭时,调用Bean的销毁方法(比如
@PreDestroy
注解的方法)。
Bean的作用域
Spring中的Bean有不同的作用域(Scope),常见的有:- Singleton:默认作用域,整个Spring容器中只有一个实例。
- Prototype:每次请求都会创建一个新的实例。
- Request:每个HTTP请求都会创建一个新的实例,仅在Web应用中有效。
- Session:每个HTTP Session都会创建一个新的实例,仅在Web应用中有效。
Bean的懒加载
默认情况下,Spring容器启动时会立即创建所有的Singleton Bean。如果你希望某个Bean在第一次使用时才创建,可以使用@Lazy
注解,将其标记为懒加载。这样可以加快应用的启动速度,尤其是当某些Bean的初始化比较耗时的时候。Bean的自动装配
Spring支持通过@Autowired
注解自动装配Bean。Spring会根据类型或者名称,自动从容器中找到合适的Bean并注入到目标对象中。如果容器中有多个相同类型的Bean,你可以通过@Qualifier
注解指定具体的Bean。
总结
Spring中的Bean就是一个由Spring容器管理的对象,它的生命周期、依赖注入、AOP、缓存等问题都由Spring帮你处理。你只需要关注业务逻辑,剩下的交给Spring就行了。
理解 MySQL 的存储与索引机制
Page:MySQL 存储的基本单元
- MySQL 存储数据的基本单元是 Page,默认大小为 16KB。
- Page 是磁盘和内存之间数据交换的最小单位,确保高效读写。
- 你可以把 Page 想象成一个增强的 HashMap,里面存了数据的 Key 和 Value:
- Key:建表时定义的主键(比如 ID)。
- Value:对应行的真实数据。
- 除了 Key 和 Value,Page 还包括:
- 前后指针:指向相邻的 Page,形成一个双向链表。
- 起始主键记录:标明当前 Page 从哪个主键开始。
数据多了怎么办?B+ 树来帮忙
- 随着数据量增加,Page 数量也会增加。如果每次查询都要遍历所有 Page,效率会很低。
- 为了解决这个问题,MySQL 引入了 B+ 树:
- 最底层的 Page 存储实际数据。
- 上层的 Page 存储指向下层 Page 的指针。
- 这样,MySQL 可以通过 B+ 树快速定位目标数据,即使数据量巨大。
- 举个例子:
- 第一层 B+ 树是一个管理 Page 的 Page。
- 当数据更多时,管理 Page 的 Page 也变多,就需要第二层、第三层 Page 来管理,最终形成一个层层嵌套的结构。
为什么 Page 是双链表?
- Page 之间通过双链表连接,主要是为了支持 范围查询。
- 比如,查询
ID > 10
的数据时:- MySQL 可以先找到 ID 为 10 的 Page。
- 然后沿着后指针遍历,直接获取后面的数据。
- 这样就避免了从 B+ 树根节点重新查找,效率更高。
为什么推荐自增主键?
- 使用 自增主键 的好处是:
- 新数据总是追加到 B+ 树的最后。
- 不需要频繁调整 Page 内的数据顺序,减少了 Page 分裂和数据的重新排列。
- 插入效率更高。
- 使用 自增主键 的好处是:
联合索引:多个字段怎么排序?
- 联合索引是根据多个字段(比如 A、B、C)建立的索引。
- 它会按照字段的顺序排序:先按 A 排序,再按 B,最后按 C。
- 查询时:
- 如果跳过了联合索引的第一个字段(比如 A),索引就会失效。
- 如果有 A 但没有 B,A 会生效,B 不会生效。
- 因为 B+ 树是按字段顺序构建的,缺少前面的字段就无法有效利用索引。
为什么
LIKE '%bc%'
会让索引失效?- 对
VARCHAR
字段使用LIKE '%bc%'
时,索引可能失效。 - 因为 B+ 树是按字典序构建的,如果查询模式不以索引的最左前缀开始(比如
%bc%
),MySQL 就无法有效利用索引。
- 对
回表:为什么要多查一次?
- 回表 是指在非主键索引中查找到主键后,还需要回到主键索引中查找完整的数据行。
- 举个例子:
- 你在某个非主键索引中找到了主键 ID。
- 然后需要根据 ID 去主键索引中查找完整的数据。
- 这个过程增加了额外的 I/O 操作,会影响查询性能。
覆盖索引:为什么更快?
- 覆盖索引 是指查询所需的所有字段都包含在索引中。
- 这样就不需要回表查询主键索引,减少了 I/O 操作,查询效率更高。
聚簇索引 vs 非聚簇索引
- 聚簇索引:
- 数据行直接存储在索引的叶子节点上。
- 主键索引通常是聚簇索引。
- 非聚簇索引:
- 叶子节点存储的是指向数据行的指针。
- 查询时需要回表获取完整数据。
- 聚簇索引:
为什么 MySQL 不适合全文索引?
- MySQL 使用 B+ 树索引,需要从左到右的顺序匹配。
- 对于全文搜索,这种结构效率较低。
- Elasticsearch (ES) 使用 倒排索引,通过分词技术快速定位包含特定词汇的文档。
- 因此,ES 更适合处理全文搜索。
索引的选择性与维护成本
- 索引的选择性:
- 高选择性索引(唯一或几乎唯一的索引)更能提高查询效率,因为它们能更有效地缩小搜索范围。
- 索引的维护成本:
- 索引会加速查询,但也会增加数据插入、更新和删除的成本,因为每次数据变动都可能需要更新索引。
- 索引的选择性:
查询优化器的作用
- MySQL 的查询优化器会根据查询条件和索引情况,选择最有效的查询计划。
- 理解这一点有助于更好地设计索引和编写查询语句。
总结
- MySQL 的存储和索引机制围绕 Page 和 B+ 树 展开。
- 通过合理设计索引(如自增主键、联合索引、覆盖索引),可以显著提高查询效率。
- 但同时也要注意索引的维护成本和适用场景,避免不必要的性能损耗。
如何理解SpirngCloud
微服务的核心思想
微服务是一种架构风格,它将应用程序拆分为一组小型、独立的服务,每个服务都围绕特定的业务功能构建。Spring Cloud是实现微服务架构的一组工具和框架。微服务的核心思想是让一个服务只做一件事,功能之间相互隔离,就像吃鸳鸯锅一样,每个格子只有一个味道。
服务通信方式
服务拆开后,微服务之间需要通信。Spring Cloud提供了多种通信方式:
异步通信:比如订单系统和发邮件系统,订单系统需要告诉发邮件系统给用户发送发票。这时候就用到MQ消息队列(如RabbitMQ、Kafka)。MQ就像发短信,发送方只管发,接收方只管收,互不干扰。
同步通信:有时候不能发短信,得打电话。比如用户下单时,订单系统需要立即知道风控系统的结果,不能等。这时候就需要RPC(如gRPC)或HTTP调用(如RESTful API)。RPC协议更精简,传输效率更高,适合微服务之间的实时通信。
服务发现与注册中心
随着服务越来越多,怎么知道该调用哪个服务呢?这就需要注册中心(如Eureka、Consul)。服务上线时把自己提供的或需要的能力告诉注册中心,由注册中心负责转接,就像电话交换机一样。
服务限流与熔断
运维同学可能会担心服务之间的调用过于频繁,导致CPU和内存扛不住。这时候可以加个限制,比如每秒只允许调用一次,这就是服务限流(如Sentinel)。如果某个服务扛不住了,还可以启用熔断策略(如Hystrix),直接返回默认结果,避免整个系统崩溃。或者把请求暂时转移到其他系统,这就是死信队列。
水平扩容与容器化
如果服务的并发量上去了,一个实例不够用,就需要水平扩容。人工扩容对运维压力很大,这时候可以用容器技术(如Docker)和编排工具(如Kubernetes),它会自动拉起服务实例,做好反向代理和负载均衡。
配置中心
有时候配置文件内容变了,不想改代码发版怎么办?配置中心(如Spring Cloud Config、Nacos)就派上用场了。配置中心和服务保持长连接,配置随改随更新,完全不用改代码发版。
统一网关与网络安全
网络安全也是个问题,后端服务的IP不能直接暴露,否则容易被DDoS攻击。这时候可以用统一网关(如Spring Cloud Gateway、Zuul),由网关层统一接收请求,再反向代理给真实服务。网关还可以提供身份验证、限流、日志记录等功能。
单点登录(SSO)
用户登录也是个麻烦事,每个功能都要登录一次。单点登录(SSO)可以解决这个问题,用户的token缓存在系统中,各个服务都能识别这个登录态。通过共享的认证机制(如OAuth2、JWT),各个服务可以识别用户的登录状态。
监控与预警
运维同学还希望有提前预警机制,避免每次出问题才救火。可以用Grafana和Prometheus,实时监控系统状态,设置阈值预警。
用户ID生成
最后,用户ID生成也是个问题,多个用户可能生成重复的ID。雪花算法(Snowflake)可以保证每个用户ID唯一,避免重复。雪花算法通过结合时间戳、机器ID和序列号,生成全局唯一的ID。