0%

MVC分层介绍一下

MVC用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

  • 视图(view): 为用户提供使用界面,与用户直接进行交互。
  • 模型(model): 代表一个存取数据的对象。它也可以带有逻辑,主要用于承载数据,并对用户提交请求进行计算的模块。模型分为两类,一类称为数据承载 Bean,一类称为业务处理Bean。所谓数据承载 Bean 是指实体类,专门为用户承载业务数据的;而业务处理 Bean 则是指Service 或 Dao 对象, 专门用于处理用户提交请求的。
  • 控制器(controller): 用于将用户请求转发给相应的 Model 进行处理,并根据 Model 的计算结果向用户提供相应响应。它使视图与模型分离。

了解SpringMVC的处理流程吗?

SpringMVC-2025-11-27-13-35-19

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用处理器映射器HandlerMapping。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
  4. DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作
  5. 执行处理器Handler(Controller,也叫页面控制器)。
  6. Handler执行完成返回ModelAndView
  7. HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体View
  10. DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
  11. DispatcherServlet响应用户

Handlermapping 和 handleradapter有了解吗?

HandlerMapping:

  • 作用:HandlerMapping负责将请求映射到处理器(Controller)。
  • 功能:根据请求的URL、请求参数等信息,找到处理请求的 Controller。
  • 类型:Spring提供了多种HandlerMapping实现,如BeanNameUrlHandlerMapping、
    RequestMappingHandlerMapping等。
  • 工作流程:根据请求信息确定要请求的处理器(Controller)。HandlerMapping可以根据URL、请求参数等规则确定对应的处理器。

HandlerAdapter:

  • 作用:HandlerAdapter负责调用处理器(Controller)来处理请求。
  • 功能:处理器(Controller)可能有不同的接口类型(Controller接口、HttpRequestHandler接口等),HandlerAdapter根据处理器的类型来选择合适的方法来调用处理器。
  • 类型:Spring提供了多个HandlerAdapter实现,用于适配不同类型的处理器。
  • 工作流程:根据处理器的接口类型,选择相应的HandlerAdapter来调用处理器。

spring的核心思想说说你的理解?

核心思想 解决的问题 实现手段 典型应用场景
IOC 对象创建与依赖管理的高耦合 容器管理Bean生命周期 动态替换数据库实现、服务组装
DI 依赖关系的硬编码问题 Setter/构造器/注解注入 注入数据源、服务层依赖DAO层
AOP 横切逻辑分散在业务代码中 动态代理与切面配置 日志、事务、权限校验统一处理

讲一下IoC

IoC即控制反转,是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由系统的外部实体,将其所依赖的对象的引用传递进来,而不是系统内部进行对象的创建

控制反转常用的实现方式是依赖注入

什么是依赖注入(DI)

依赖注入是一种编程技巧,我们不通过new的方式在类内部创建依赖类的对象,而是将依赖类的对象在外部创建好后,通过构造函数,函数参数等方式传递(或注入)给类使用

如何理解控制反转

所谓控制,就是对象的创建、初始化、销毁

  • 创建对象:原来是new一个,现在有Spring容器进行创建
  • 初始化对象:原来是对象自己通过构造器或setter方法给依赖的对象赋值,现在由Spring容器自动注入
  • 销毁对象:原来是直接给对象赋值为null或做一些销毁操作,现在是Spring容器管理生命周期负责销毁工作

反转:反转控制权,把控制权交给Spring,我们有对象的控制者变为被动控制者

如何设计SpringIoc,从哪些方面考虑

  • Bean的生命周期:设计Bean的创建、初始化、销毁等生命周期管理机制,考虑工厂模式和单例模式
  • 依赖注入:实现依赖注入的功能,如属性注入,构造函数注入,方法注入等,考虑使用反射和XML配置文件实现
  • AOP支持:考虑使用动态代理和切面编程实现
  • 异常处理:考虑Bean创建异常,依赖注入异常等
  • 配置文件加载:支持从不同配置文件中加载Bean的相关信息,考虑XML,注解或者Java配置类实现

IOC的实现机制是什么

  • 反射:SpringIOC容器利用Java的反射机制动态地加载类、创建对象实例以及调用方法对象,反射允许在运行时检查类、方法、属性等信息,从而实现灵活的对象实例化和管理
  • 依赖注入:Spring的核心概念
  • 工厂模式:SpringIOC采用工厂模式来管理对象的创建和生命周期。容器作为工厂负责实例化Bean并管理他们的生命周期,将Bean的实例化过程交给容器来管理
  • 容器实现:SpringIoC容器是实现IoC的核心,通常使用BeanFactory或ApplicationContext来管理Bean。BeanFactory是Ioc容器的基本形式,提供基本的IOC功能;ApplicationContext是BeanFactory的扩展,提供更多企业级功能

什么是AOP?

AOP:面向切面编程,能够将与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块的耦合度。

SpringAOP是基于动态代理的,如果要代理的对象,实现了某个接口,那么SpringAOP就会使用JDKproxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDKproxy去进行代理,这时候SpringAOP会使用Cglib生成一个被代理对象的子类作为代理

SpringAOP的实现机制

SpringAOP实现依赖于动态代理。

SpringAOP支持两种动态代理:

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

什么是动态代理

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

AOP内的概念

  • AspectJ:切面,只是一个概念,没有具体的接口或类与之对应,是Join Point,Advice和Pointcut的统称
  • Join point:连接点,指程序执行过程中的一个点,例如方法调用,异常处理等。在SpringAOP中仅支持方法级别的连接点
  • Advice:通知,即我们定义的切面中的横切逻辑,有”around”,”brfore”,”after”三种类型。在很多AOP实现框架中,Advice通常作为一个拦截器,也可以包含多个拦截器作为一条链路围绕着JoinPoint进行处理
  • Pointcut:切点,用于匹配连接点,一个Aspect包含哪些JoinPoint需要PointCut进行筛选
  • Introduction:引介,它能够让切面为被通知的对象引入额外的接口和实现,即便被通知对象原本并未实现这些接口
  • Weaving:织入,在切点的引导下,将通知逻辑插入到目标方法上,使得通知逻辑在方法调用时可以执行
  • AOP proxy:指在AOP实现框架中实现切面协议的对象。在SpringAOP中两种代理:JDK,CGLIB
  • Target Object:目标对象,即被代理的对象

什么是三级缓存

三级缓存指的是Spring在创建Bean的过程中,通过三级缓存来缓存正在创建的Bean,以及已经创建完成的Bean实例
通过三级缓存的机制,Spring能够在处理循环依赖时,确保及时暴露正在创建的Bean对象,并能够正确地注入已经初始化的Bean实例,从而解决循环依赖问题。

存储三级缓存的数据结构

都是 Map类型的缓存,比如Map {k:name; v:bean}。

  • 一级缓存(singletonObjects):这是一个Map类型的缓存,存储的是已经完全初始化好的bean,即完全准备好可以使用的bean实例。键是bean的名称,值是bean的实例。
  • 二级缓存(earlySingletonObjects):这同样是一个Map类型的缓存,存储的是早期的bean引用,即已经实例化但还未完全初始化的bean。这些bean已经被实例化,但是可能还没有进行属性注入等操作。
  • 三级缓存(singletonFactories):这也是一个Map类型的缓存,存储的是ObjectFactory对象,这些对象可以生成早期的bean引用。当一个bean正在创建过程中,如果它被其他bean依赖,那么这个正在创建的bean就会通过这个ObjectFactory来创建一个早期引用,从而解决循环依赖的问题。

循环依赖种类

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

Spring只解决了第三种循环依赖问题

三级缓存步骤

  • 首先从容器中获取A,获取不到然后创建实例,提前暴露添加到三级缓存中
  • 进行依赖注入,从容器中获取B,获取不到,创建B,发现循环依赖,将B放入三级缓存
  • 从三级缓存中发现A,将A从三级缓存中放入二级缓存然后注入B
  • B创建完成,将B从三级缓存转移至一级缓存
  • 继续进行A的依赖注入,注入B
  • A依赖注入成功,初始化完成,添加到一级缓存

Spring-2025-11-27-01-26-36

如果只有一级缓存,能不能解决循环依赖问题?

不能。一级缓存存放的是完整的对象,如果只有一级缓存,意味着半成品的对象和完整的对象放在一起,这样getBean()可能会拿到半成品对象,属性值都是null。

如果只有一三级缓存,能不能解决循环依赖问题

能,但要求Bean没有被AOP进行切面代理
如果 Bean 被 AOP 代理,在创建过程中需要生成代理对象。而只有一三级缓存时,三级缓存中存放的工厂方法在创建 Bean 时无法正确处理 AOP 代理逻辑,可能导致生成的对象不是最终需要的代理对象,从而在循环依赖场景中出现问题。所以在这种情况下,无法解决有 AOP 代理的 Bean 的循环依赖问题。

为什么需要二级缓存

三级缓存singletonFactory中存放的是ObjectFactory,当执行getObject时每次都会产生一个新的代理对象,但是Bean是单例的,因此引入二级缓存,将对象放入二级缓存中之后直接去二级缓存中去而不必每次调用getObject获取新的代理对象

spring三级缓存的数据结构是什么?

都是 Map类型的缓存,比如Map {k:name; v:bean}。

  1. 一级缓存(Singleton Objects):这是一个Map类型的缓存,存储的是已经完全初始化好的bean,即完全准备好可以使用的bean实例。键是bean的名称,值是bean的实例。这个缓存在DefaultSingletonBeanRegistry 类中的 singletonObjects 属性中。
  2. 二级缓存(Early Singleton Objects):这同样是一个Map类型的缓存,存储的是早期的bean引
    用,即已经实例化但还未完全初始化的bean。这些bean已经被实例化,但是可能还没有进行属性
    注入等操作。这个缓存在 DefaultSingletonBeanRegistry 类中的 earlySingletonObjects 属性
    中。
  3. 三级缓存(Singleton Factories):这也是一个Map类型的缓存,存储的是ObjectFactory对象,这些对象可以生成早期的bean引用。当一个bean正在创建过程中,如果它被其他bean依赖,那么这个正在创建的bean就会通过这个ObjectFactory来创建一个早期引用,从而解决循环依赖的问题。这个缓存在 DefaultSingletonBeanRegistry 类中的 singletonFactories 属性中。

spring框架中都用到了哪些设计模式

工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

Spring的事务,使用this调用是否生效?

不能生效。
因为Spring事务是通过代理对象来控制的,只有通过代理对象的方法调用才会应用事务管理的相关规则。当使用 this 直接调用时,是绕过了Spring的代理机制,因此不会应用事务设置

spring 常用注解有什么?

  • @Autowired 注解:主要用于自动装配bean。当Spring容器中存在与要注入的属性类型匹配的bean时,它会自动将bean注入到属性中。就跟我们new 对象一样。
  • @Component:这个注解用于标记一个类作为Spring的bean,并将其添加到Spring容器中。
  • @Configuration,注解用于标记一个类作为Spring的配置类。配置类可以包含@Bean注解的方法,用于定义和配置bean,作为全局配置。
  • @Bean注解用于标记一个方法作为Spring的bean工厂方法。当一个方法被@Bean注解标记时,Spring会将该方法的返回值作为一个bean,并将其添加到Spring容器中,如果自定义配置,经常用到这个注解。
  • @Service,这个注解用于标记一个类作为服务层的组件。它@Component注解的特例,用于标记服务层的bean,一般标记在业务service的实现类。
  • @Repository注解用于标记一个类作为数据访问层的组件。
  • @Controller注解用于标记一个类作为控制层的组件。它也是@Component注解的特例,用于标记控制层的bean。这是MVC结构的另一个部分,加在控制层

Spring的事务什么情况下会失效?

Spring Boot通过Spring框架的事务管理模块来支持事务操作。事务管理在Spring Boot中通常是通过@Transactional 注解来实现的。事务可能会失效的一些常见情况包括:

  1. 未捕获异常: 如果一个事务方法中发生了未捕获的异常,并且异常未被处理或传播到事务边界之外,
    那么事务会失效,所有的数据库操作会回滚。
  2. 非受检异常: 默认情况下,Spring对非受检异常(RuntimeException或其子类)进行回滚处理,这意味着当事务方法中抛出这些异常时,事务会回滚。
  3. 事务传播属性设置不当: 如果在多个事务之间存在事务嵌套,且事务传播属性配置不正确,可能导致事务失效。特别是在方法内部调用有 @Transactional 注解的方法时要特别注意。
  4. 多数据源的事务管理: 如果在使用多数据源时,事务管理没有正确配置或者存在多个@Transactional 注解时,可能会导致事务失效。
  5. 跨方法调用事务问题: 如果一个事务方法内部调用另一个方法,而这个被调用的方法没有@Transactional 注解,这种情况下外层事务可能会失效。
  6. 事务在非公开方法中失效: 如果 @Transactional 注解标注在私有方法上或者非 public 方法上,事务也会失效。

Bean的生命周期说一下?

  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的
    postProcessBeforeInitialization()方法。
  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的
    postProcessAfterInitialization()方法。
  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。

Bean是否单例?

Spring 中的 Bean 默认都是单例的。
就是说,每个Bean的实例只会被创建一次,并且会被存储在Spring容器的缓存中,以便在后续的请求中重复使用。这种单例模式可以提高应用程序的性能和内存效率。
但是,Spring也支持将Bean设置为多例模式,即每次请求都会创建一个新的Bean实例。要将Bean设置为多例模式,可以在Bean定义中通过设置scope属性为”prototype”来实现。

Bean的单例和非单例,生命周期是否一样

不一样的,Spring Bean 的生命周期完全由 IoC 容器控制。Spring 只帮我们管理单例模式 Bean 的完整
生命周期,对于 prototype 的 Bean,Spring 在创建好交给使用者之后,则不会再管理后续的生命周期

Spring bean的作用域有哪些?

Spring框架中的Bean作用域(Scope)定义了Bean的生命周期和可见性。不同的作用域影响着Spring容器如何管理这些Bean的实例,包括它们如何被创建、如何被销毁以及它们是否可以被多个用户共享。

  • Singleton(单例):在整个应用程序中只存在一个 Bean 实例。默认作用域,Spring 容器中只会
    创建一个 Bean 实例,并在容器的整个生命周期中共享该实例。
  • Prototype(原型):每次请求时都会创建一个新的 Bean 实例。次从容器中获取该 Bean 时都会
    创建一个新实例,适用于状态非常瞬时的 Bean。
  • Request(请求):每个 HTTP 请求都会创建一个新的 Bean 实例。仅在 Spring Web 应用程序中
    有效,每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用中需求局部性的 Bean。
  • Session(会话):Session 范围内只会创建一个 Bean 实例。该 Bean 实例在用户会话范围内共
    享,仅在 Spring Web 应用程序中有效,适用于与用户会话相关的 Bean。
  • Application:当前 ServletContext 中只存在一个 Bean 实例。仅在 Spring Web 应用程序中有效,该 Bean 实例在整个 ServletContext 范围内共享,适用于应用程序范围内共享的 Bean。
  • WebSocket(Web套接字):在 WebSocket 范围内只存在一个 Bean 实例。仅在支持WebSocket 的应用程序中有效,该 Bean 实例在 WebSocket 会话范围内共享,适用于 WebSocket会话范围内共享的 Bean。
  • Custom scopes(自定义作用域):Spring 允许开发者定义自定义的作用域,通过实现 Scope 接口来创建新的 Bean 作用域。

Spring容器里存的是什么?

在Spring容器中,存储的主要是Bean对象。
Bean是Spring框架中的基本组件,用于表示应用程序中的各种对象。当应用程序启动时,Spring容器会根据配置文件或注解的方式创建和管理这些Bean对象。Spring容器会负责创建、初始化、注入依赖以及销毁Bean对象。

在Spring中,在bean加载/销毁前后,如果想实现某些逻辑,可以

  • 使用init-method和destroy-method
  • 使用@PostConstruct和@PreDestroy注解
  • 实现InitializingBean和DisposableBean接口
  • 使用@Bean注解的initMethod和destroyMethod属性

Bean注入和xml注入最终得到了相同的效果,它们在底层是怎样做的

在Spring框架中,基于注解的Bean注入和基于XML的依赖注入虽然
在配置方式上不同,但在底层最终都通过Spring容器的统一机制实现依赖注入。

XML 注入

使用 XML 文件进行 Bean 注入时,Spring 在启动时会读取 XML 配置文件,以下是其底层步骤:

  • Bean 定义解析:Spring 容器通过 XmlBeanDefinitionReader 类解析 XML 配置文件,读取其中的 <bean> 标签以获取 Bean 的定义信息。注册 Bean 定义:解析后的 Bean 信息被注册到 BeanDefinitionRegistry (如DefaultListableBeanFactory )中,包括 Bean 的类、作用域、依赖关系、初始化和销毁方法等。
  • 实例化和依赖注入:当应用程序请求某个 Bean 时,Spring 容器会根据已经注册的 Bean 定义:首先,使用反射机制创建该 Bean 的实例。
    然后,根据 Bean 定义中的配置,通过 setter 方法、构造函数或方法注入所需的依赖 Bean。

注解注入

使用注解进行 Bean 注入时,Spring 的处理过程如下:

  • 类路径扫描:当 Spring 容器启动时,它首先会进行类路径扫描,查找带有特定注解的类。
  • 注册 Bean 定义:找到的类会被注册到 BeanDefinitionRegistry 中,Spring 容器将为其生成Bean 定义信息。这通常通过 AnnotatedBeanDefinitionReader 类来实现。
  • 依赖注入:与 XML 注入类似,Spring 在实例化 Bean 时,也会检查字段上是否有 @Autowired 、@Inject 或 @Resource 注解。如果有,Spring 会根据注解的信息进行依赖注入。

尽管使用的方式不同,但 XML 注入和注解注入在底层的实现机制是相似的,主要体现在以下几个方面:

  1. BeanDefinition:无论是 XML 还是注解,最终都会生成 BeanDefinition 对象,并存储在同一个 BeanDefinitionRegistry 中。
  2. 后处理器:Spring 提供了多个 Bean 后处理器(如 AutowiredAnnotationBeanPostProcessor ),用于处理注解(如 @Autowired )的依赖注入。对于 XML,Spring 也有相应的后处理器来处理 XML 配置的依赖注入。
  3. 依赖查找:在依赖注入时,Spring 容器会通过 ApplicationContext 中的 BeanFactory 方法来查找和注入依赖,无论是通过 XML 还是注解,都会调用类似的查找方法。

Spring给我们提供了很多扩展点,这些有了解吗?

Spring框架提供了许多扩展点,使得开发者可以根据需求定制和扩展Spring的功能。以下是一些常用的扩展点:

  1. BeanFactoryPostProcessor:允许在Spring容器实例化bean之前修改bean的定义。常用于修改bean属性或改变bean的作用域。
  2. BeanPostProcessor:可以在bean实例化、配置以及初始化之后对其进行额外处理。常用于代理bean、修改bean属性等。
  3. PropertySource:用于定义不同的属性源,如文件、数据库等,以便在Spring应用中使用。
  4. ImportSelector和ImportBeanDefinitionRegistrar:用于根据条件动态注册bean定义,实现配置类的模块化。
  5. Spring MVC中的HandlerInterceptor:用于拦截处理请求,可以在请求处理前、处理中和处理后执行特定逻辑。
  6. Spring MVC中的ControllerAdvice:用于全局处理控制器的异常、数据绑定和数据校验。
  7. Spring Boot的自动配置:通过创建自定义的自动配置类,可以实现对框架和第三方库的自动配置。
  8. 自定义注解:创建自定义注解,用于实现特定功能或约定,如权限控制、日志记录等。

mysql的explain有什么作用?

explain 是查看 sql 的执行计划,主要用来分析 sql 语句的执行过程,比如有没有走索引,有没有外部排序,有没有索引覆盖等等。

对于执行计划,参数有:

  • possible_keys 字段表示可能用到的索引;
  • key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
  • key_len 表示索引的长度;
  • rows 表示扫描的数据行数。
  • type 表示数据扫描类型,我们需要重点看这个。

type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:

  • All(全表扫描):在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。
  • index(全索引扫描):index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。
  • range(索引范围扫描):range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、
  • in、between 等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
  • ref(非唯一索引扫描):ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并
    不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
  • eq_ref(唯一索引扫描):eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。
  • const(结果只有一条的主键或唯一索引扫描):const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。需要说明的是 const 类型和 eq_ref 都使
    用了主键或唯一索引,不过这两个类型有所区别,const 是与常量进行比较,查询效率会更快,而eq_ref 通常用于多表联查中。

extra 显示的结果,这里说几个重要的参考指标:

  • Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
  • Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
  • Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。

给你张表,发现查询速度很慢,你有哪些解决方案

分析查询语句:使用EXPLAIN命令分析SQL执行计划,找出慢查询的原因,比如是否使用了全表扫描,是否存在索引未被利用的情况等,并根据相应情况对索引进行适当修改。

  • 创建或优化索引:根据查询条件创建合适的索引,特别是经常用于WHERE子句的字段、Orderby 排序的字段、Join 连表查询的字典、 group by的字段,并且如果查询中经常涉及多个字段,考虑创建联合索引,使用联合索引要符合最左匹配原则,不然会索引失效
  • 避免索引失效:比如不要用左模糊匹配、函数计算、表达式计算等等。
    查询优化:避免使用SELECT *,只查询真正需要的列;使用覆盖索引,即索引包含所有查询的字段;联表查询最好要以小表驱动大表,并且被驱动表的字段要有索引,当然最好通过冗余字段的设计,避免联表查询。
    *分页优化:针对 limit n,y 深分页的查询优化,可以把Limit查询转换成某个位置的查询:select *from tb_sku where id>20000 limit 10,该方案适用于主键自增的表,
  • 优化数据库表:如果单表的数据超过了千万级别,考虑是否需要将大表拆分为小表,减轻单个表的查询压力。也可以将字段多的表分解成多个表,有些字段使用频率高,有些低,数据量大时,会由于使用频率低的存在而变慢,可以考虑分开。
  • 使用缓存技术:引入缓存层,如Redis,存储热点数据和频繁查询的结果,但是要考虑缓存一致性的问题,对于读请求会选择旁路缓存策略,对于写请求会选择先更新 db,再删除缓存的策略。

如果Explain用到的索引不正确的话,有什么办法干预吗?

可以使用 force index,强制走索引

MySQL主从复制了解吗

MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。

这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。

MySQL 集群的主从复制过程梳理成 3 个阶段:

  • 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
  • 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
  • 回放 Binlog:回放 binlog,并更新存储引擎中的数据。

具体详细过程如下:

  • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
  • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
  • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
  • 在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。

主从延迟都有什么处理方法?

强制走主库方案:对于大事务或资源密集型操作,直接在主库上执行,避免从库的额外延迟。

分表和分库是什么?有什么区别?

  • 分库是一种水平扩展数据库的技术,将数据根据一定规则划分到多个独立的数据库中。每个数据库只负责存储部分数据,实现了数据的拆分和分布式存储。分库主要是为了解决并发连接过多,单机mysql扛不住的问题。
  • 分表指的是将单个数据库中的表拆分成多个表,每个表只负责存储一部分数据。这种数据的垂直划分能够提高查询效率,减轻单个表的压力。分表主要是为了解决单表数据量太大,导致查询性能下降的问题。

分库与分表可以从:垂直(纵向)和 水平(横向)两种纬度进行拆分。下边我们以经典的订单业务举例,看看如何拆分。

  • 垂直分库:一般来说按照业务和功能的维度进行拆分,将不同业务数据分别放到不同的数据库中,核心理念 专库专用。按业务类型对数据分离,剥离为多个数据库,像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。
  • 垂直分表:针对业务上字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。数据库它是以行为单位将数据加载到内存中,这样拆分以后核心表大多是访问频率较高的字段,而且字段长度也都较短,因而可以加载更多数据到内存中,减少磁盘IO,增加索引查询的命中率,进一步提升数据库性能。
  • 水平分库:是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,以此实现水平扩展,是一种常见的提升数据库性能的方式。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此系统的复杂度也被提升了。
  • 水平分表:是在同一个数据库内,把一张大数据量的表按一定规则,切分成多个结构完全相同表,而每个表只存原表的一部分数据。水平分表尽管拆分了表,但子表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分散到不同的机器上,还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。

怎么把一个对象从一个jvm转移到另一个jvm?

  • 使用序列化和反序列化:将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的 ObjectOutputStream 和 ObjectInputStream 来实现。
  • 使用消息传递机制:利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。
  • 使用远程方法调用(RPC):可以使用远程方法调用框架,如 gRPC,来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JVM 上的对象的方法。
  • 使用共享数据库或缓存:将对象存储在共享数据库(如 MySQL、PostgreSQL)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景。

序列化和反序列化让你自己实现你会怎么做?

Java默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。

  • 无法跨语言:Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
  • 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
  • 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

我会考虑用主流序列化框架,比如FastJson、Protobuf来替代Java 序列化。

如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好,在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。

BIO、NIO、AIO区别是什么?

  • BIO:就是传统的java.io包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是IO的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO:Java 1.4引入的java.nio包,提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO:是Java 1.7之后引入的包,是NIO的升级版本,提供了异步非堵塞的IO操作方式,所以人们叫它AIO,异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

你知道有哪个框架用到NIO了吗?

Netty的I/O模型是基于非阻塞I/O实现的,底层依赖的是NIO框架的多路复用器Selector。采用epoll模式后,只需要一个线程负责Selector的轮询。当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。

事件分发器有两种设计模式:

  • Reactor,采用同步I/O,适合处理耗时短的场景,对于耗时长的I/O操作容易造成阻塞。
  • Proactor,采用异步I/O,Proactor性能更高,但是实现逻辑非常复杂,适合图片或视频流分析服务器,目前主流的事件驱动模型还是依赖select或epoll来实现。

volatile和synchronized如何实现单例模式

正确的双重检查锁定模式需要需要使用volatile。volatile主要包含两个功能。

  • 保证可见性。使用volatile定义的变量,将会保证对所有线程的可见性。
  • 禁止指令重排序优化。

由于volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

代理模式和适配器模式有什么区别?

目的不同:代理模式主要关注控制对对象的访问,而适配器模式则用于接口转换,使不兼容的类能够一起工作。
结构不同:代理模式一般包含抽象主题、真实主题和代理三个角色,适配器模式包含目标接口、适配器和被适配者三个角色。
应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。

事务的特性是什么?如何实现的?

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性是通过 redo log (重做日志)来保证的;
  • 原子性是通过 undo log(回滚日志) 来保证的;
  • 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  • 一致性则是通过持久性+原子性+隔离性来保证;

mysql可能出现什么和并发相关问题?

MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。那么在同时处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。

  • 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
  • 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
  • 幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

哪些场景不适合脏读,举个例子?

脏读是指一个事务在读取到另一个事务未提交的数据时发生。脏读可能会导致不一致的数据被读取,并可能引起问题。

  • 银行系统:在银行系统中,如果一个账户的余额正在被调整但尚未提交,另一个事务读取了这个临时的余额,可能会导致客户看到不正确的余额。
  • 库存管理系统:在一个库存管理系统中,如果一个商品的数量正在被更新但尚未提交,另一个事务读取了这个临时的数量,可能会导致库存管理错误。
  • 在线订单系统:在一个在线订单系统中,如果一个订单正在被修改但尚未提交,另一个事务读取了这个临时的订单状态,可能导致订单状态显示错误,客户收到不准确的信息。

mysql的是怎么解决并发问题的?

  • 锁机制:Mysql提供了多种锁机制来保证数据的一致性,包括行级锁、表级锁、页级锁等。通过锁机制,可以在读写操作时对数据进行加锁,确保同时只有一个操作能够访问或修改数据。
  • 事务隔离级别:Mysql提供了多种事务隔离级别,包括读未提交、读已提交、可重复读和串行化。通过设置合适的事务隔离级别,可以在多个事务并发执行时,控制事务之间的隔离程度,以避免数据不一致的问题。
  • MVCC(多版本并发控制):Mysql使用MVCC来管理并发访问,它通过在数据库中保存不同版本的数据来实现不同事务之间的隔离。在读取数据时,Mysql会根据事务的隔离级别来选择合适的数据版本,从而保证数据的一致性。

事务的隔离级别有哪些?

  • 读未提交,指一个事务还没提交时,它做的变更就能被其他事务看到;
  • 读提交,指一个事务提交之后,它做的变更才能被其他事务看到;
  • 可重复读,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
  • 串行化;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

针对不同的隔离级别,并发事务时可能发生的现象也会不同。

  • 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
  • 在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
  • 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;
  • 在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生

mysql默认级别是什么?

可重复读隔离级别

可重复读隔离级别下,A事务提交的数据,在B事务能看见吗?

可重复读隔离级是由 MVCC(多版本并发控制)实现的

开始事务后(执行 begin 语句后),在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的。

举个例子说可重复读下的幻读问题

可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读。

在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时
刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。

因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。

Mysql设置了可重读隔离级后,怎么保证不发生幻读?

尽量在开启事务之后,马上执行 select … for update 这类锁定读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录,就避免了幻读的问题。

串行化隔离级别是通过什么实现的?

是通过行级锁来实现的,序列化隔离级别下,普通的 select 查询是会对记录加 S 型的 next-key 锁,其他事务就没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读现象。

介绍MVCC实现原理

MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。

  • 「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;
  • 「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View。

Read View 有四个重要的字段:

  • m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列
    表,“活跃事务”指的就是,启动了但还没提交的事务。
  • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
  • creator_trx_id :指的是创建该 Read View 的事务的事务 id。

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:

  • trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列
    里;
  • roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然
    后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
  • 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在m_ids 列表中:
    • 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
    • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

一条update是不是原子性的?为什么?

是原子性,主要通过锁+undolog 日志保证原子性的执行 update 的时候,会加行级别锁,保证了一个事务更新一条记录的时候,不会被其他事务干
扰。

事务执行过程中,会生成 undolog,如果事务执行失败,就可以通过 undolog 日志进行回滚。

滥用事务,或者一个事务里有特别多sql的弊端?

事务的资源在事务提交之后才会释放的,比如存储资源、锁。

  • 如果一个事务特别多 sql,锁定的数据太多,容易造成大量的死锁和锁超时。
  • 回滚记录会占用大量存储空间,事务回滚时间长。在MySQL中,实际上每条
    记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值,sql 越多,所需要保存的回滚数据就越多。
  • 执行时间长,容易造成主从延迟,主库上必须等事务执行完成才会写入binlog,再传给备库。所以,如果一个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟

讲一下mysql里有哪些锁?

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

  • 全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
  • 表级锁:MySQL 里面表级别的锁有这几种:
    • 表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
    • 元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
    • 意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
  • 行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
    • 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
    • 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
    • Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本

数据库的表锁和行锁有什么作用?

表锁的作用:

  • 整体控制:表锁可以用来控制整个表的并发访问,当一个事务获取了表锁时,其他事务无法对该表进行任何读写操作,从而确保数据的完整性和一致性。
  • 粒度大:表锁的粒度比较大,在锁定表的情况下,可能会影响到整个表的其他操作,可能会引起锁竞争和性能问题。

适用于大批量操作:表锁适合于需要大批量操作表中数据的场景,例如表的重建、大量数据的加载等。

行锁的作用:

  • 细粒度控制:行锁可以精确控制对表中某行数据的访问,使得其他事务可以同时访问表中的其他行数据,在并发量大的系统中能够提高并发性能。
  • 减少锁冲突:行锁不会像表锁那样造成整个表的锁冲突,减少了锁竞争的可能性,提高了并发访问的效率。
  • 适用于频繁单行操作:行锁适合于需要频繁对表中单独行进行操作的场景,例如订单系统中的订单修改、删除等操作。

MySQL两个线程的update语句同时处理一条数据,会不会有阻塞?

如果是两个事务同时更新了 id = 1,比如 update … where id = 1,那么是会阻塞的。因为 InnoDB 存储引擎实现了行级锁。
当A事务对 id =1 这行记录进行更新时,会对主键 id 为 1 的记录加X类型的记录锁,这样第二事务对 id =1 进行更新时,发现已经有记录锁了,就会陷入阻塞状态。

两条update语句处理一张表的不同的主键范围的记录,一个<10,一个>15,会不会遇到阻塞?底层是为什么的?

不会,因为锁住的范围不一样,不会形成冲突。
第一条 update sql 的话( id<10),锁住的范围是(-♾️,10)
第二条 update sql 的话(id >15),锁住的范围是(15,+♾️)

如果2个范围不是主键或索引?还会阻塞吗?

如果2个范围查询的字段不是索引的话,那就代表 update 没有用到索引,这时候触发了全表扫描,全部索引都会加行级锁,这时候第二条 update 执行的时候,就会阻塞了。

因为如果 update 没有用到索引,在扫描过程中会对索引加锁,所以全表扫描的场景下,所有记录都会被加锁,也就是这条 update 语句产生了 4 个记录锁和 5 个间隙锁,相当于锁住了全表。

日志文件是分成了哪几种?

  • redo log 重做日志,是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
  • undo log 回滚日志,是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
  • bin log 二进制日志,是 Server 层生成的日志,主要用于数据备份和主从复制;
  • relay log 中继日志,用于主从复制场景下,slave通过io线程拷贝master的bin log后本地生成的日志
  • 慢查询日志,用于记录执行时间过长的sql,需要设置阈值后手动开启

UndoLog日志的作用是什么?

undo log 是一种用于撤销回退的日志,它保证了事务的 ACID 中的原子性。
在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:

  • 插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
  • 删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
  • 更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。

在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时,undolog 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行insert 操作。

redo log怎么保证持久性的?

Redo log是MySQL中用于保证持久性的重要机制之一。它通过以下方式来保证持久性:

  1. Write-ahead logging(WAL):在事务提交之前,将事务所做的修改操作记录到redo log中,然后
    再将数据写入磁盘。这样即使在数据写入磁盘之前发生了宕机,系统可以通过redo log中的记录来恢复数据。
  2. Redo log的顺序写入:redo log采用追加写入的方式,将redo日志记录追加到文件末尾,而不是随机写入。这样可以减少磁盘的随机I/O操作,提高写入性能。
  3. Checkpoint机制:MySQL会定期将内存中的数据刷新到磁盘,同时将最新的LSN(Log Sequence Number)记录到磁盘中,这个LSN可以确保redo log中的操作是按顺序执行的。在恢复数据时,系统会根据LSN来确定从哪个位置开始应用redo log

能不能只用binlog不用relo log?

不行,binlog是 server 层的日志,没办法记录哪些脏页还没有刷盘,redolog 是存储引擎层的日志,可以记录哪些脏页还没有刷盘,这样崩溃恢复的时候,就能恢复那些还没有被刷盘的脏页数据

binlog 两阶段提交过程是怎么样的?

事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。

在 MySQL 的 InnoDB 存储引擎中,开启 binlog 的情况下,MySQL 会同时维护 binlog 日志与 InnoDB的 redo log,为了保证这两个日志的一致性,MySQL 使用了内部 XA 事务,内部 XA 事务由 binlog 作为协调者,存储引擎是参与者。

当客户端执行 commit 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成XA 事务的提交:

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作
    用);
  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),

接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redolog 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

我们来看看在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象?

不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态。

在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log中的 XID 去 binlog 查看是否存在此 XID:

  • 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
  • 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。对应时刻 B 崩溃恢复的情况。

可以看到,对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就可以保证redo log 和 binlog 这两份日志的一致性了。

所以说,两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在binlog 中查找到与 redo log 相同的 XID。

update语句的具体执行过程是怎样的?

具体更新一条记录 UPDATE t_user SET name = ‘xiaolin’ WHERE id = 1; 的流程如下:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
    • 如果一样的话就不进行后续更新流程;
    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
  3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
  4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先
    写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
  5. 至此,一条记录更新完了。
  6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
  7. 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):
    prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
  8. 至此,一条更新语句执行完成。

MySQL是如何保障数据不丢失的?

主要是通过 redolog 来实现事务持久性的,事务执行过程,会把对 innodb 存储引擎中数据页修改操作记录到 redolog 里,事务提交的时候,就直接把 redolog 刷入磁盘,即使脏页中途没有刷盘成功,mysql 宕机了,也能通过 redolog 重放,恢复到之前事务修改数据页后的状态,从而保障了数据不丢失。

RedoLog是在内存里吗?

事务执行过程中,生成的 redolog 会在 redolog buffer 中,也就是在内存中,等事务提交的时候,会把redolog 写入磁盘。

为什么要写RedoLog,而不是直接写到B+树里面?

因为 redolog 写入磁盘是顺序写,而 b+树里数据页写入磁盘是随机写,顺序写的性能会比随机写好,这样可以提升事务提交的效率。
最重要的是redolog具备故障恢复的能力,Redo Log 记录的是物理级别的修改,包括页的修改,如插入、更新、删除操作在磁盘上的物理位置和修改内容。例如,当执行一个更新操作时,Redo Log 会记录修改的数据页的地址和更新后的数据,而不是 SQL 语句本身。
在数据页实际更新之前,先将修改操作写入 Redo Log。当数据库重启时,会进行恢复操作:

  • 首先,根据Redo Log 检查哪些事务已经提交但数据页尚未完全写入磁盘。
  • 然后,使用 Redo Log 中的记录对这些事务进行重做(Redo)操作,将未完成的数据页修改完成,确保事务的修改生效。

mysql 两次写(double write buffer)了解吗?

我们常见的服务器一般都是Linux操作系统,Linux文件系统页的大小默认是4KB。而MySQL的页大小默认是16KB。

MySQL程序是跑在Linux操作系统上的,需要跟操作系统交互,所以MySQL中一页数据刷到磁盘,要写4个文件系统里的页。

需要注意的是,这个操作并非原子操作,比如我操作系统写到第二个页的时候,Linux机器断电了,这时候就会出现问题了。造成”页数据损坏“。并且这种”页数据损坏“靠 redo日志是无法修复的。

Doublewrite Buffer的出现就是为了解决上面的这种情况,虽然名字带了Buffer,但实际上Doublewrite Buffer是内存+磁盘的结构。

Doublewrite Buffer 作用是,在把页写到数据文件之前,InnoDB先把它们写到一个叫doublewrite buffer(双写缓冲区)的共享表空间内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复,所以本质上是一个最近写回的页面的备份拷贝。

当有页数据要刷盘时:

  • 页数据先通过memcpy函数拷贝至内存中的Doublewrite Buffer(大小为约 2MB)中,Doublewrite Buffer 分为两个区域,每次写入一个区域(最多 1MB 的数据)。
  • Doublewrite Buffer的内存里的数据页,会fsync刷到Doublewrite Buffer的磁盘上,写两次到到共享表空间中(连续存储,顺序写,性能很高),每次写1MB;
  • 写入完成后,再将脏页刷到数据磁盘存储.ibd文件上(随机写);

当MySQL出现异常崩溃时,有如下几种情况发生:

  • 情况一:步骤1前宕机,刷盘未开始,数据在redo log,后期可以恢复
  • 情况二:步骤1后,步骤2前宕机,因为是在内存中,宕机清空内存,和情况1一样
  • 情况三:步骤2后,步骤3前宕机,因为DWB的磁盘有完整的数据,可以修复损坏的页数据

由此我们可以得出结论,double write buffer是针对实际的buffer数据页的原子性保证,就是避免MySQL异常崩溃时,写的那几个data page不会出错,要么都写了,要么什么都没有做。

为什么redolog无法代替double write buffer?

redolog的设计之初,是“账本的作用”,是一种操作日志,用于MySQL异常崩溃恢复使用,是InnoDB引擎特有的日志,本质上是物理日志,记录的是 “ 在某个数据页上做了什么修改 ” ,但如果数据页本身已经发生了损坏,redolog来恢复已经损坏的数据块是无效的,数据块的本身已经损坏,再次重做依然是一个坏块。 所以此时需要一个数据块的副本来还原该损坏的数据块,再利用重做日志进行其他数据块的重做操作,这就是double write buffer的原因作用。

怎么理解面向对象?简单说说封装继承多态

面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。

Java面向对象的三大特性包括:封装、继承、多态:

  • 封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。
  • 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
  • 多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。

多态体现在哪几个方面?

多态在面向对象编程中可以体现在以下几个方面:

  • 方法重载
    • 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。
  • 方法重写
    • 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。
  • 接口与实现
    • 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。
  • 向上转型和向下转型
    • 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。
    • 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException。

多态解决了什么问题?

多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。
多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

面向对象的设计原则你知道有哪些吗

面向对象编程中的六大原则:

  • 单一职责原则:一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。
  • 开放封闭原则:软件实体应该对扩展开放,对修改封闭。
  • 里氏替换原则:子类对象应该能够替换掉所有父类对象。
  • 接口隔离原则:客户端不应该依赖那些它不需要的接口,即接口应该小而专。
  • 依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
  • 最少知识原则:一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。

重载与重写有什么区别?

  • 重载指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表,编译器根据调用时的参数类型来决定调用哪个方法。
  • 重写指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@Override注解来明确表示这是对父类方法的重写。

重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。

抽象类和普通类区别?

  • 实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。
  • 方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现。
  • 继承:一个类可以继承一个普通类,而且可以继承多个接口;而一个类只能继承一个抽象类,但可以同时实现多个接口。
  • 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。

Java抽象类和接口的区别是什么?

两者的特点:

  • 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
  • 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8以后可以有默认方法和静态方法)。适用于定义类的能力或功能。

两者的区别:

  • 实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
  • 方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
  • 访问修饰符:接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
  • 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。

抽象类可以被实例化吗?

在Java中,抽象类本身不能被实例化。

这意味着不能使用new关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法,这些方法需要在子类中被实现。

抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。

抽象类能加final修饰吗?

不能,Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此,抽象类和final修饰符是互斥的,不能同时使用。

接口里面可以定义哪些方法?

  • 抽象方法:抽象方法是接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是public和abstract,这些修饰符可以省略。
  • 默认方法:默认方法是在Java 8中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。
  • 静态方法:静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。
  • 私有方法:私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。

接口可以包含构造函数吗?

在接口中,不可以有构造方法,在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors,因为接口不会有自己的实例的,所以不需要有构造函数。

解释Java中的静态变量和静态方法

在Java中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。

静态变量是在类中使用static关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点:

  • 共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
  • 初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
  • 访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。

静态方法是在类中使用 static 关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点:

  • 无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
  • 访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
  • 多态性:静态方法不支持重写,但可以被隐藏。

使用场景:

  • 静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
  • 静态方法:常用于助手方法、获取类级别的信息或者是没有依赖于实例的数据处理。

非静态内部类和静态内部类的区别?

区别包括:

  • 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
  • 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
  • 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
  • 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
  • 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。

非静态内部类可以直接访问外部方法,编译器是怎么做到的?

非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。
这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。

== 与 equals 有什么区别?

  • ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
  • equals():比较的是两个字符串的内容,属于内容比较。

hashCode和equals方法有什么关系?

在Java中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定:

  • 一致性:如果两个对象使用 equals 方法比较结果为 true,那么它们的 hashCode 值必须相同。也就是说,如果 obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 必须等于 obj2.hashCode()。
  • 非一致性:如果两个对象的 hashCode 值相同,它们使用 equals 方法比较的结果不一定为 true。即 obj1.hashCode() == obj2.hashCode() 时,obj1.equals(obj2) 可能为 false,这种情况称为哈希冲突。

hashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。

String、StringBuffer、StringBuilder的区别和联系

  1. 可变性:String 是不可变的,一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder 和 StringBuffer 是可变的,可以直接对字符串内容进行修改而不会创建新对象。
  2. 线程安全性:String 因为不可变,天然线程安全。StringBuilder 不是线程安全的,适用于单线程环境。StringBuffer 是线程安全的,其方法通过 synchronized 关键字实现同步,适用于多线程环境。
  3. 性能:String 性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder 性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer 性能略低于 StringBuilder,因为它的线程安全机制引入了同步开销。
  4. 使用场景:如果字符串内容固定或不常变化,优先使用 String。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer。

Java 8新特性

特性名称 描述 示例或说明
Lambda 表达式 简化匿名内部类,支持函数式编程 (a, b) -> a + b 代替匿名类实现接口
函数式接口 仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记 Runnable、Comparator,或自定义接口
@FunctionalInterface interface MyFunc { void run(); }
Stream API 提供链式操作处理集合数据,支持并行处理 list.stream().filter(x -> x > 0).collect(Collectors.toList())
Optional 类 封装可能为 null 的对象,减少空指针异常 Optional.ofNullable(value).orElse("default")
方法引用 简化 Lambda 表达式,直接引用现有方法 System.out::println 等价于 x -> System.out.println(x)
接口的默认方法与静态方法 接口可定义默认实现和静态方法,增强扩展性 interface A { default void print() { System.out.println("默认方法"); } }
并行数组排序 使用多线程加速数组排序 Arrays.parallelSort(array)
重复注解 允许同一位置多次使用相同注解 @Repeatable 注解配合容器注解使用
类型注解 注解可应用于更多位置(如泛型、异常等) List<@NonNull String> list
CompletableFuture 增强异步编程能力,支持链式调用和组合操作 CompletableFuture.supplyAsync(() -> "result").thenAccept(System.out::println)

Lambda表达式了解吗?

Lambda表达式它是一种简洁的语法,用于创建匿名函数,主要用于简化函数式接口(只有一个抽象方法的接口)的使用。其基本语法有以下两种形式:

  • (parameters) -> expression:当Lambda体只有一个表达式时使用,表达式的结果会作为返回值。
  • (parameters) -> { statements; }:当Lambda体包含多条语句时,需要使用大括号将语句括起来,若有返回值则需要使用 return 语句。
  1. Lambda表达式可以用更简洁的语法实现相同的功能。
  2. Lambda 表达式使得 Java 支持函数式编程范式,允许将函数作为参数传递,从而可以编写更灵活、可复用的代码。比如定义一个通用的计算函数。

虽然Lambda表达式优点蛮多的,不过也有一些缺点,比如会增加调试困难,因为Lambda表达式是匿名的,在调试时很难定位具体是哪个Lambda表达式出现了问题。尤其是当Lambda表达式嵌套使用或者比较复杂时,调试难度会进一步增加。

Java中stream的API介绍一下

Java 8引入了Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。Stream API不仅可以提高代码的可读性和简洁性,还能利用多核处理器的优势进行并行处理。

Java 21 新特性知道哪些?

  • Switch 语句的模式匹配:该功能在 Java 21 中也得到了增强。它允许在 switch 的 case 标签中使用模式匹配,使操作更加灵活和类型安全,减少了样板代码和潜在错误。例如,对于不同类型的账户类,可以在 switch 语句中直接根据账户类型的模式来获取相应的余额,如 case SavingsAccount sa -> result = sa.getSavings();
  • 数组模式:将模式匹配扩展到数组中,使开发者能够在条件语句中更高效地解构和检查数组内容。例如,if (arr instanceof int[] {1, 2, 3}),可以直接判断数组 arr 是否匹配指定的模式。
  • 字符串模板(预览版):提供了一种更可读、更易维护的方式来构建复杂字符串,支持在字符串字面量中直接嵌入表达式。例如,以前可能需要使用 "hello " + name + ", welcome to the geeksforgeeks!" 这样的方式来拼接字符串,在 Java 21 中可以使用 hello {name}, welcome to the geeksforgeeks! 这种更简洁的写法

新并发特性方面:

  • 虚拟线程:这是 Java 21 引入的一种轻量级并发的新选择。它通过共享堆栈的方式,大大降低了内存消耗,同时提高了应用程序的吞吐量和响应速度。可以使用静态构建方法、构建器或 ExecutorService 来创建和使用虚拟线程。
  • Scoped Values(范围值):提供了一种在线程间共享不可变数据的新方式,避免使用传统的线程局部存储,促进了更好的封装性和线程安全,可用于在不通过方法参数传递的情况下,传递上下文信息,如用户会话或配置设置。

Stream流的并行API是什么?

是 ParallelStream。

并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果

Stream串行流与并行流的主要区别:

  • 串行流:数据源通过stream()等方法生成单个流,按顺序执行操作后输出结果;
  • 并行流:数据源通过parallelStream()/parallel()生成多个子流,并行执行操作后汇总结果)

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。

completableFuture怎么用的?

CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步。

  • Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致回调地狱
  • CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。

Java 中 final 作用是什么?

final 关键字主要有以下三个方面的作用:用于修饰类、方法和变量。

  • 修饰类:当 final 修饰一个类时,表示这个类不能被继承,是类继承体系中的最终形态。例如,Java中的 String 类就是用 final 修饰的,这保证了 String 类的不可变性和安全性,防止其他类通过继承来改变 String 类的行为和特性。
  • 修饰方法:用 final 修饰的方法不能在子类中被重写。比如,java.lang.Object 类中的 getClass方法就是 final 的,因为这个方法的行为是由 Java 虚拟机底层实现来保证的,不应该被子类修改。
  • 修饰变量:当 final 修饰基本数据类型的变量时,该变量一旦被赋值就不能再改变。例如,finalint num = 10; ,这里的 num 就是一个常量,不能再对其进行重新赋值操作,否则会导致编译错误。对于引用数据类型,final 修饰意味着这个引用变量不能再指向其他对象,但对象本身的内容是可以改变的。

Java中static的作用是什么?

static关键字主要用于修饰类的成员和内部类,其核心作用是将成员与类本身关联,而非与类的实例关联。具体作用如下:

  • 修饰变量:被static修饰的变量属于类本身,而非类的某个实例。所有对象共享同一份静态变量,内存中只存在一份副本。可以通过“类名.变量名”直接访问,无需创建对象(也可通过对象访问,但不推荐)。通常用于存储所有对象共享的数据,如常量、计数器等。
  • 修饰方法:静态方法属于类,不属于任何实例,因此不能直接访问类中的非静态成员,但可以访问静态成员。通过“类名.方法名”直接调用,无需创建对象。通常用于工具类方法(如 Math.random())、工厂方法等,不需要依赖对象状态即可完成操作
  • 修饰代码块:静态代码块在类加载时执行,且只执行一次(优于对象构造方法),用于初始化静态变量或执行类级别的预处理操作。多个静态代码块按定义顺序执行,且先于非静态代码块和构造方法。
  • 修饰内部类静态内部类不依赖于外部类的实例,可以独立存在,不能直接访问外部类的非静态成员(需通过外部类实例访问)。当内部类与外部类的实例无关时使用,避免内部类持有外部类的引用导致的内存泄漏。

深拷贝和浅拷贝的区别?

  • 浅拷贝:浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段
  • 深拷贝:深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。

实现深拷贝的三种方法是什么?

  • 实现 Cloneable 接口并重写 clone() 方法:这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone () 方法。在 clone () 方法中,通过递归克隆引用类型字段来实现深拷贝。
  • 使用序列化和反序列化:通过将对象序列化为字节流,再从字节流反序列化为对象来实现深拷贝。要求对象及其所有引用类型字段都实现 Serializable 接口。
  • 手动递归复制:针对特定对象结构,手动递复制对象及其引用类型字段。适用于对象结构复杂度不高的情况。

什么是泛型?

泛型是Java编程语言中的一个重要特性,它允许类、接口和方法在定义时使用一个或多个类型参数,这些类型参数在使用时可以被指定为具体的类型。

泛型的主要目的是 在编译时提供更强的类型检查,从而减少运行时类型转换异常的风险

为什么需要泛型?

  • 适用于多种数据类型执行相同的代码:如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
  • 泛型中的类型在使用时指定,不需要强制类型转换:引入泛型,它将提供类型的约束,提供编译前的检查:

Java创建对象有哪些方式

方式 核心原理 调用构造器 特点与应用场景
new 关键字 JVM 指令 最标准、最常用,紧密耦合
反射 运行时类信息 灵活,解耦,用于框架
clone() 复制现有对象 基于原型创建副本,需实现 Cloneable
反序列化 从字节流恢复 用于持久化和网络通信,需实现 Serializable
工厂模式 方法封装 new 解耦,隐藏创建逻辑,控制实例

New出的对象什么时候回收?

通过关键字new创建的对象,由Java的垃圾回收器负责回收。垃圾回收器的工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收释放内存。

具体来说,Java对象的回收时机是由垃圾回收器根据一些算法来决定的,主要有以下几种情况:

  1. 引用计数法:某个对象的引用计数为0时,表示该对象不再被引用,可以被回收。
  2. 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。
  3. 终结器(Finalizer):如果对象重写了finalize()方法,垃圾回收器会在回收该对象之前调用finalize()方法,对象可以在finalize()方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。

如何获取私有对象?

在Java中,私有对象通常指的是类中被声明为private的成员变量或方法。由于private访问修饰符的限制,这些成员只能在其所在的类内部被访问。

不过,可以通过下面两种方式来间接获取私有对象。

  • 使用公共访问器方法(getter方法)
  • 反射机制。反射机制允许在运行时检查和修改类、方法、字段等信息,通过反射可以绕过private访问修饰符的限制来获取私有对象。

什么是反射?

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java 语言的反射机制。

反射具有以下特性:

  1. 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
  2. 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
  3. 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现,允许你传入对象实例和参数值来执行方法。
  4. 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的get()和set()方法完成的。

反射在代码或框架中的应用场景

  1. 加载数据库驱动:项目底层数据库可能切换(如mysql、oracle),需动态加载对应驱动类。通过反射的Class.forName()方法,可根据实际情况传入不同数据库的驱动类全限定名,实现驱动的动态加载。
  2. 配置文件加载:以Spring框架的IOC为例,通过配置文件(XML/properties)动态管理Bean

讲一讲 Java 注解的原理

注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。我们通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象。
通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。

对注解解析的底层实现了解吗?

注解本质上是一种特殊的接口,它继承自 java.lang.annotation.Annotation 接口,所以注解也叫声明式接口,编译后,Java 编译器会将其转换为一个继承自 Annotation 的接口,并生成相应的字节码文件。

根据注解的作用范围,Java 注解可以分为以下几种类型:

  • 源码级别注解:仅存在于源码中,编译后不会保留(@Retention(RetentionPolicy.SOURCE))。
  • 类文件级别注解:保留在 .class 文件中,但运行时不可见(@Retention(RetentionPolicy.CLASS))。
  • 运行时注解:保留在 .class 文件中,并且可以通过反射在运行时访问(@Retention(RetentionPolicy.RUNTIME))。

只有运行时注解可以通过反射机制进行解析。

当注解被标记为 RUNTIME 时,Java 编译器会在生成的 .class 文件中保存注解信息。这些信息存储在字节码的属性表(Attribute Table)中,具体包括以下内容:

  • RuntimeVisibleAnnotations:存储运行时可见的注解信息。
  • RuntimeInvisibleAnnotations:存储运行时不可见的注解信息。
  • RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations:存储方法参数上的注解信息。

注解的解析主要依赖于 Java 的反射机制。以下是解析注解的基本流程:

1、获取注册信息:通过反射 API 可以获取类、方法、字段等元素上的注解。例如:

1
2
3
4
5
Class<?> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
if (annotation != null) {
System.out.println(annotation.value());
}

2、底层原理:反射机制的核心类是 java.lang.reflect.AnnotatedElement,它是所有可以被注解修饰的元素(如 ClassMethodField 等)的父接口。该接口提供了以下方法:

  • getAnnotation(Class<T> annotationClass):获取指定类型的注解。
  • getAnnotations():获取所有注解。
  • isAnnotationPresent(Class<? extends Annotation> annotationClass):判断是否包含指定注解。

这些方法的底层实现依赖于 JVM 提供的本地方法(Native Method),例如:

  • native Annotation[] getDeclaredAnnotations0(boolean publicOnly);
  • native <A extends Annotation> A getAnnotation(Class<A> annotationClass);

JVM 在加载类时会解析 .class 文件中的注解信息,并将其存储在内存中,供反射机制使用。

因此,注解解析的底层实现主要依赖于 Java 的反射机制和字节码文件的存储。通过 @Retention 元注解可以控制注解的保留策略,当使用 RetentionPolicy.RUNTIME 时,可以在运行时通过反射 API 来解析注解信息。在 JVM 层面,会从字节码文件中读取注解信息,并创建注解的代理对象来获取注解的属性值。

Java注解的作用域

注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java注解的作用域可以分为三种:

  1. 类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别、继承关系、注释等。
  2. 方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
  3. 字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。

除了这三种作用域,Java还提供了其他一些注解作用域,例如构造函数作用域和局部变量作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。

介绍一下Java异常

Java的异常体系主要基于两大类:Throwable类及其子类。Throwable有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。

  1. Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError等。

  2. Exception(异常):表示程序本身可以处理的异常条件。异常分为两大类:

    • 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
    • 运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。

Java异常处理有哪些?

异常处理是通过使用try-catch语句块来捕获和处理异常。以下是Java中常用的异常处理方式:

  • try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常。(可选的finally块,用于定义无论是否发生异常都会执行的代码)
  • throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
  • throws关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用throws关键字将异常传递给调用者来处理。

抛出异常为什么不用throws?

如果异常是未检查异常或者在方法内部被捕获和处理了,那么就不需要使用throws。

  • Unchecked Exceptions:未检查异常(unchecked exceptions)是继承自RuntimeException类或Error类的异常,编译器不强制要求进行异常处理。因此,对于这些异常,不需要在方法签名中使用throws来声明。示例包括NullPointerException、ArrayIndexOutOfBoundsException等。
  • 捕获和处理异常:另一种常见情况是,在方法内部捕获了可能抛出的异常,并在方法内部处理它们,而不是通过throws子句将它们传递到调用者。这种情况下,方法可以处理异常而无需在方法签名中使用throws。

try catch中的语句运行情况

try块中的代码将按顺序执行,如果抛出异常,将在catch块中进行匹配和处理,然后程序将继续执行catch块之后的代码。如果没有匹配的catch块,异常将被传递给上一层调用的方法。

try{return “a”} finally{return “b”}这条语句返回啥

finally块中的return语句会覆盖try块中的return返回,因此,该语句将返回”b”。

说一下Java的特点

  • 平台无关性:Java“编写一次,运行无处不在”。Java编译器将源代码编译成字节码,该字节码可以在任何安装了Java虚拟机的系统上运行。
  • 面向对象:Java是一门严格的面向对象编程语言,几乎一切都是对象。面向对象编程特性使得代码更易于维护和重用,包括类、对象、继承、多态、抽象和封装。
  • 内存管理:Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。这样,开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关的问题。

Java 的优势和劣势是什么?

优势:

  • 跨平台
  • 面向对象
  • 强大的生态系统
  • 自动垃圾回收机制
  • 多线程支持
  • 稳定性,企业级应用长期使用

劣势:

  • 性能不如C++/Rust。
  • 语法繁琐,比如样板代码多
  • 内存消耗,JVM本身占内存,对于资源有限的环境可能不太友好
  • 相比动态语言如Python,Java需要更多代码,编译过程也可能拖慢开发节奏

Java为什么是跨平台的?

Java能支持跨平台,主要依赖于JVM关系比较大。

JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种.class文件,称为字节码文件。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。

而这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一“中间层”,就能在不同平台上运行,真正实现了“一次编译,到处运行”的目的。

编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。

跨平台的是Java程序,不是JVM。JVM是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。

jvm,jdk,jre区别

JVM ⊆ JRE ⊆ JDK

  • JVM是Java虚拟机,是Java程序运行的环境。它负责将Java字节码解释或编译成机器码,并执行程序。JVM提供了内存管理、垃圾回收、安全性等功能,使得Java程序具备跨平台性。
  • JDK是Java开发工具包,是开发Java程序所需的工具集合。它包含了JVM、编译器(javac)、调试器(jdb)等开发工具,以及一系列的类库。JDK提供了开发、编译、调试和运行Java程序所需的全部工具和环境。
  • JRE 是 Java 运行时环境,是 Java 程序运行所需的最小环境。它包含了 JVM 和一组 Java 类库,用于支持 Java 程序的执行。JRE 不包含开发工具,只提供 Java 程序运行所需的运行环境。

为什么 Java 解释和编译都有?

首先在 Java 经过编译之后生成字节码文件,接下来进入 JVM 中,就有两个步骤编译解释

  • 编译性:Java 源代码首先被编译成字节码,JIT 会把编译过的机器码保存起来,以备下次使用。
  • 解释性:JVM 中一个方法调用计数器,当累计计数大于一定值的时候,就使用 JIT 进行编译生成机器码文件。否则就是用解释器进行解释执行,然后字节码也是经过解释器进行解释运行的。

所以 Java 既是编译型也是解释性语言,默认采用的是解释器和编译器混合的模式。

jvm是什么

JVM是java虚拟机,主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。

JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的字节码,就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。

编译型语言和解释型语言的区别?

编译型语言和解释型语言的区别在于:

  • 编译型语言:在程序执行之前,整个源代码会被编译成机器码或者字节码,生成可执行文件。执行时直接运行编译后的代码,速度快,但跨平台性较差。
  • 解释型语言:在程序执行时,逐行解释执行源代码,不生成独立的可执行文件。通常由解释器动态解释并执行代码,跨平台性好,但执行速度相对较慢。
  • 典型编译型语言如C、C++,解释型语言如Python、JavaScript。

Python和Java区别是什么?

  • Java是一种已编译的编程语言,Java编译器将源代码编译为字节码,而字节码则由Java虚拟机执行
  • python是一种解释语言,翻译时会在执行程序的同时进行翻译。

八种基本数据类型

数据类型 占用大小(字节) 位数 取值范围
byte 1 8 -27 到 27 - 1
short 2 16 -215 到 215 - 1
int 4 32 -231 到 231 - 1
long 8 64 -263 到 263 - 1
float 4 32 1.4E-45 到 3.4E38
double 8 64 4.9E-324 到 1.8E308
char 2 16 ‘\u0000’ 到 ‘\uffff’
boolean 无明确字节大小(理论上1位) 无明确位数 true 或 false

int和long是多少位,多少字节的?

  • int 类型是32位,占4个字节,int是有符号整数类型,其取值范围是从-2^31到2^31-1。
  • long 类型是64位,占8个字节, long 类型也是有符号整数类型,它的取值范围是从-2^63到2^63-1,在处理较大的整数数值时,果 int 类型的取值范围不够,就需要使用 long 类型。

long和int可以互转吗?

可以的。由于 long 类型的范围比 int 类型大,因此将 int 转换为 long 是安全的,而将 long 转换为 int 可能会导致数据丢失或溢出。

  • 将 int 转换为 long 可以通过直接赋值或强制类型转换来实现。
  • 将 long 转换为 int 需要使用强制类型转换,但需要注意潜在的数据丢失或溢出问题。

在将 long 转换为 int 时,如果 longValue 的值超出了 int 类型的范围,转换结果将是截断后的低位部分。因此,在进行转换之前,建议先检查 longValue 的值是否在 int 类型的范围内,以避免数据丢失或溢出的问题。

数据类型转换方式你知道哪些?

  • 自动类型转换(隐式转换):当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,不需要显式的类型转换。
  • 强制类型转换(显式转换):当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。
  • 字符串转换:Java提供了将字符串表示的数据转换为其他类型数据的方法。例如,将字符串转换为整型 int ,可以使用 Integer.parseInt() 方法;将字符串转换为浮点型 double ,可以使用 Double.parseDouble() 方法等。
  • 数值之间的转换:Java 提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现

类型互转会出现什么问题吗?

  • 基本数据类型转换的问题
    • 当把小范围数据类型赋值给大范围数据类型时,Java 会自动进行类型转换,这种转换一般是安全的。
    • 大范围数据类型赋值给小范围数据类型时,会发生数据溢出或者精度损失的问题。
  • 对象引用转换的问题
    • 向上转型是自动进行的,而且是安全的。
    • 向下转型需要手动进行,并且存在风险。如果父类对象实际上并不是目标子类的实例,在转型时就会抛出异常。

Java的对象在运行时会记录其真实类型,当进行向下转型时,Java会检查对象的实际类型是否与目标类型兼容。如果不兼容,就会抛出ClassCastException。解决方式是需要使用instanceof检查:只有确认对象是目标子类的实例时才进行转型。

为什么用BigDecimal不用double?

double会出现精度丢失的问题,因为double执行的是二进制浮点运算,二进制有时不能准确表示一个小数——二进制只能表示成1/2n的和的组合,但0.1无法表示为这种形式,因此会出现精度误差。

比如Java中浮点数运算的输出会偏离预期(如0.05+0.01结果是0.060000000000000005),这在商品价格计算等场景中会造成严重问题(比如0.05+0.01的总和无法匹配0.06元的支付),尤其高并发场景下会导致下单、对账异常。

而BigDecimal是精确计算,所以涉及金钱的计算一般用BigDecimal。

这样使用BigDecimal可以确保十进制数值的精确计算,避免double的舍入误差。需要注意的是:创建BigDecimal对象时应使用字符串作为参数,而非直接用浮点数值,以防浮点精度丢失

装箱和拆箱是什么?

装箱和拆箱是将基本数据类型和对应的包装类之间进行转换的过程。
自动装箱主要发生在两种情况:

  • 赋值时:这是最常见的一种情况,在Java 1.5以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。
  • 方法调用时:当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。

自动装箱的弊端:
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作,会创建多余的对象,会降低程序的性能并且加重了垃圾回收的工作量。

Integer相比int有什么优点?

int是Java中的原始数据类型,而Integer是int的包装类。

Integer和int的区别:

  • 基本类型和引用类型:首先,int是一种基本数据类型,而Integer是一种引用类型。基本数据类型是Java中最基本的数据类型,它们是预定义的,不需要实例化就可以使用。而引用类型则需要通过实例化对象来使用。这意味着,使用int来存储一个整数时,不需要任何额外的内存分配,而使用Integer时,必须为对象分配内存。在性能方面,基本数据类型的操作通常比相应的引用类型快。
  • 自动装箱和拆箱:其次,Integer作为int的包装类,它可以实现自动装箱和拆箱。自动装箱是指将基本类型转化为相应的包装类类型,而自动拆箱则是将包装类类型转化为相应的基本类型。
  • 空指针异常:另外,int变量可以直接赋值为0,而Integer变量必须通过实例化对象来赋值。如果对一个未经初始化的Integer变量进行操作,就会出现空指针异常。这是因为它被赋予了null值,而null值是无法进行自动拆箱的。

那为什么还要保留int类型?

包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身。

因此,基本类型数据在读写效率方面,要比包装类高效。除此之外,在64位JVM上,在开启引用压缩的情况下,一个Integer对象占用16个字节的内存空间,而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。

也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效。

说一下integer的缓存

Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。

默认情况下,这个范围是-128至127。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。

索引是什么?有什么好处?

索引类似于书籍的目录,可以减少扫描的数据量,提高查询效率。

  • 如果查询的时候,没有用到索引就会全表扫描,这时候查询的时间复杂度是O(n)
  • 如果用到了索引,那么查询的时候,可以基于二分查找算法,通过索引快速定位到目标数据,mysql 索引的数据结构一般是b+树,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

讲讲索引的分类是什么?

MySQL可以按照四个角度来分类索引。

  • 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引
  • 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)
  • 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引
  • 按「字段个数」分类:单列索引、联合索引

按数据结构分类

从数据结构的角度来看,MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
每一种存储引擎支持的索引类型不一定相同
以下是该表格的Markdown格式:

索引类型 InnoDB MyISAM Memory
B+Tree索引 Yes Yes Yes
HASH索引 No No Yes
Full-Text索引 Yes Yes No

InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采最多的索引类型。

在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:

  • 如果有主键,默认会使用主键作为聚簇索引的索引键(key);
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;

其它索引都属于辅助索引,也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引。

按物理存储分类

从物理存储的角度来看,索引分为聚簇索引、二级索引。
这两个区别在前面也提到了:

  • 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+Tree 的叶子节点里;
  • 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据

所以,在查询时使用了二级索引:

  • 如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引
  • 如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表

按字段特性分类

从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。

  • 主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
  • 唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
  • 普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
  • 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率

按字段个数分类

从字段个数的角度来看,索引分为单列索引、联合索引。

  • 建立在单列上的索引称为单列索引,比如主键索引
  • 建立在多列上的索引称为联合索引

使用联合索引时,存在最左匹配原则:按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。

联合索引有一些特殊情况,可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。
这种特殊情况就发生在范围查询。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配

MySQL聚簇索引和非聚簇索引的区别是什么?

  • 数据存储:在聚簇索引中,数据行按照索引键值的顺序存储,也就是说,索引的叶子节点包含了实际的数据行。这意味着索引结构本身就是数据的物理存储结构。非聚簇索引的叶子节点不包含完整的数据行,而是包含指向数据行的指针或主键值
  • 索引与数据关系:由于数据与索引紧密相连,当通过聚簇索引查找数据时,可以直接从索引中获得数据行。当通过非聚簇索引查找数据时,可能需要回表查找。
  • 唯一性:聚簇索引通常是基于主键构建的,因此每个表只能有一个聚簇索引,因为数据只能有一种物理排序方式。一个表可以有多个非聚簇索引,因为它们不直接影响数据的物理存储位置。
  • 效率:对于范围查询和排序查询,聚簇索引通常更有效率,因为它避免了额外的寻址开销。非聚簇索引在使用覆盖索引进行查询时效率更高,因为它不需要读取完整的数据行。但是需要进行回表的操作,使用非聚簇索引效率比较低,因为需要进行额外的回表操作。

如果聚簇索引的数据更新,它的存储要不要变化?

  • 如果更新的数据是非索引数据,也就是普通的用户记录,那么存储结构是不会发生变化
  • 如果更新的数据是索引数据,那么存储结构是有变化的,因为要维护 b+树的有序性

MySQL主键是聚簇索引吗?

在MySQL的InnoDB存储引擎中,主键确实是以聚簇索引的形式存储的。

InnoDB将数据存储在B+树的结构中,其中主键索引的B+树就是所谓的聚簇索引。这意味着表中的数据行在物理上是按照主键的顺序排列的,聚簇索引的叶节点包含了实际的数据行。

InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:

  • 如果有主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;

一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引,它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。

什么字段适合当做主键?

  • 字段具有唯一性,且不能为空的特性
  • 字段最好的是有递增的趋势的,如果字段的值是随机无序的,可能会引发页分裂的问题,造型性能影响。
  • 不建议用业务数据作为主键,比如会员卡号、订单号、学生号之类的,因为我们无法预测未来会不会因为业务需要,而出现业务字段重复或者重用的情况。

通常情况下会用自增字段来做主键,对于单机系统来说是没问题的。但是,如果有多台服务器,各自都可以录入数据,这时候就需要考虑分布式 id的方案了。

性别字段能加索引么?为什么?

不建议针对性别字段加索引。
实际上与索引创建规则之一区分度有关,性别字段假设有100w数据,50w男、50w女,区别度几乎等于0 。

区分度的计算方式 :select count(DISTINCT sex)/count(*) from sys_user

表中十个字段,你主键用自增ID还是UUID,为什么?

用的是自增 id。
因为 uuid 相对顺序的自增 id 来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb 无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。

这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

  • 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机 IO。
  • 因为写入是乱序的,innodb 不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,影响性能。
    由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片。

结论:使用 InnoDB 应该尽可能的按主键的自增顺序插入,并且尽可能使用单调的增加的聚簇键的值来插入新行

为什么自增ID更快一些,UUID不快吗,它在B+树里面存储是有序的吗?

自增的主键的值是顺序的,所以 Innodb 把每一条记录都存储在一条记录的后面,所以自增 id 更快的原因:

  • 下一条记录写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
  • 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗减少了页分裂和碎片的产生

但是 UUID 不是递增的,MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。

而且,UUID 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。

Mysql中的索引是怎么实现的?

MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表

数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么查询过程经历 3 个节点,也就是进行了 3 次 I/O 操作。

B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。

查询数据时,到了B+树的叶子节点,之后的查找数据是如何做

数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个页目录,起到记录的索引作用。

那 InnoDB 是如何给记录创建页目录的呢?
页目录与记录的关系如下图:
select * from product where id= 5;

页目录创建的过程如下:

  1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
  2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。

页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽,定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。

B+树的特性是什么?

  • 所有叶子节点都在同一层:这是B+树的一个重要特性,确保了所有数据项的检索都具有相同的I/O延迟,提高了搜索效率。每个叶子节点都包含指向相邻叶子节点的指针,形成一个链表,由于叶子节点之间的链接,B+树非常适合进行范围查询和排序扫描。可以沿着叶子节点的链表顺序访问数据,而无需进行多次随机访问。
  • 非叶子节点存储键值:非叶子节点仅存储键值和指向子节点的指针,不包含数据记录。这些键值用于指导搜索路径,帮助快速定位到正确的叶子节点。并且,由于非叶子节点只存放键值,当数据量比较大时,相对于B树,B+树的层高更少,查找效率也就更高。
  • 叶子节点存储数据记录:与B树不同,B+树的叶子节点存储实际的数据记录或指向数据记录的指针。这意味着每次搜索都会到达叶子节点,才能找到所需数据。
  • 自平衡:B+树在插入、删除和更新操作后会自动重新平衡,确保树的高度保持相对稳定,从而保持良好的搜索性能。每个节点最多可以有M个子节点,最少可以有ceil(M/2)个子节点(除了根节点),这里的M是树的阶数。

说说B+树和B树的区别

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

B+树的好处是什么?

B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储既存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
  • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
  • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

B+树的叶子节点链表是单向还是双向?

双向的,为了实现倒序遍历或者反向排序

Innodb 使用的 B+ 树有一些特别的点,比如:

  • B+ 树的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。
  • B+ 树点节点内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16KB。

MySQL为什么用B+树结构?和其他结构比的优点?

  • B+Tree vs B Tree:B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做
    到这一点。
  • B+Tree vs 二叉树:对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 34 层左右,也就是说一次数据查询操作只需要做34 次的磁盘 I/O 操作就能查询到目标数据。而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多
  • B+Tree vs Hash:Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。但是 Hash 表不适合做范围查询,它更适合做等值的查询

为什么 MysSQL 不用 跳表?

B+树的高度在3层时存储的数据可能已达千万级别,但对于跳表而言同样去维护千万的数据量那么所造成的跳表层数过高而导致的磁盘io次数增多,也就是使用B+树在存储同样的数据下磁盘io次数更少。

联合索引的实现原理?

将将多个字段组合成一个索引,该索引就被称为联合索引。

使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。

创建联合索引时需要注意什么?

建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的SQL 使用到。

区分度就是某个字段 column 不同值的个数「除以」表的总行数

联合索引ABC,现在有个执行语句是A = XXX and C < XXX,索引怎么走

根据最左匹配原则,A可以走联合索引,C不会走联合索引,但是C可以走索引下推

联合索引(a,b,c) ,查询条件 where b > xxx and a = x 会生效吗

索引会生效,a 和 b 字段都能利用联合索引,符合联合索引最左匹配原则。

索引失效有哪些?

6 种会发生索引失效的情况:

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
  • 当我们在查询条件中对索引列使用函数,就会导致索引失效。
  • 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
  • MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效

什么情况下会回表查询

在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引
如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表

什么是覆盖索引?

覆盖索引是指一个索引包含了查询所需的所有列,因此不需要访问表中的数据行就能完成查询。
换句话说,查询所需的所有数据都能从索引中直接获取,而不需要进行回表查询。覆盖索引能够显著提高查询性能,因为减少了访问数据页的次数,从而减少了I/O操作。

如果一个列即使单列索引,又是联合索引,单独查它的话先走哪个?

mysql 优化器会分析每个索引的查询成本,然后选择成本最低的方案来执行 sql。

索引已经建好了,那我再插入一条数据,索引会有哪些变化?

插入新数据可能导致B+树结构的调整和索引信息的更新,以保持B+树的平衡性和正确性,这些变化通常由数据库系统自动处理,确保数据的一致性和索引的有效性。
如果插入的数据导致叶子节点已满,可能会触发叶子节点的分裂操作,以保持B+树的平衡性。

索引字段是不是建的越多越好?

不是,建的的越多会占用越多的空间,而且在写入频繁的场景下,对于B+树的维护所付出的性能消耗也会越大

如果有一个字段是status值为0或者1,适合建索引吗

不适合,区分度低的字段不适合建立索引。

索引的优缺点?

索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:

  • 需要占用物理空间,数量越大,占用空间越大;
  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
  • 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。

怎么决定建立哪些索引?

什么时候适用索引?

  • 字段有唯一性限制的,比如商品编码
  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引
  • 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了

什么时候不需要创建索引?

  • WHERE 条件, GROUP BY , ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。
  • 字段中存在大量重复数据,不需要创建索引, MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
  • 表数据太少的时候,不需要创建索引;
  • 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改

索引优化详细讲讲

常见优化索引的方法:

  • 前缀索引优化:使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
  • 覆盖索引优化:覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
  • 主键索引最好是自增的
    • 如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
    • 如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。

防止索引失效:

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;
  • 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

了解过前缀索引吗?

使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

执行一条SQL请求的过程是什么?

  • 连接器:建立连接,管理连接、校验用户身份;
  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
  • 解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
  • 执行 SQL:执行 SQL 共有三个阶段:
    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号* 扩展为表上的所有列。
    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

讲一讲mysql的引擎吧,你有什么了解?

  • InnoDB:InnoDB是MySQL的默认存储引擎,具有ACID事务支持、行级锁、外键约束等特性。它适
    用于高并发的读写操作,支持较好的数据完整性和并发控制。
  • MyISAM:MyISAM是MySQL的另一种常见的存储引擎,具有较低的存储空间和内存消耗,适用于大量读操作的场景。然而,MyISAM不支持事务、行级锁和外键约束,因此在并发写入和数据完整
    性方面有一定的限制。
  • Memory:Memory引擎将数据存储在内存中,适用于对性能要求较高的读操作,但是在服务器重启或崩溃时数据会丢失。它不支持事务、行级锁和外键约束。

MySQL为什么InnoDB是默认引擎?

InnoDB引擎在事务支持、并发性能、崩溃恢复等方面具有优势,因此被MySQL选择为默认的存储引擎。

  • 事务支持:InnoDB引擎提供了对事务的支持,可以进行ACID(原子性、一致性、隔离性、持久性)属性的操作。Myisam存储引擎是不支持事务的。
  • 并发性能:InnoDB引擎采用了行级锁定的机制,可以提供更好的并发性能,Myisam存储引擎只支持表锁,锁的粒度比较大。
  • 崩溃恢复:InnoDB引引擎通过 redolog 日志实现了崩溃恢复,可以在数据库发生异常情况(如断电)时,通过日志文件进行恢复,保证数据的持久性和一致性。Myisam是不支持崩溃恢复的。

数据管理里,数据文件大体分成哪几种数据文件?

我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。

共有三个文件,这三个文件分别代表着:

  • db.opt,用来存储当前数据库的默认字符集和字符校验规则。
  • t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
  • t_order.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以(默认)存放在独占表空间文件(文件名:表名字.ibd)。

mysql8.0以后db.opt和.frm都被移除。表的元数据信息被统一存储在 数据字典(mysql.idb)中