微众银行薪资出了,还算满意,签了!

微众银行薪资出了,还算满意,签了!

Spring通过这IOC、DI、AOP三大核心思想,实现了轻量级、高内聚低耦合的企业级应用开发框架,成为Java生态中不可或缺的基石。

IOC是什么?AOP是什么?有什么好处?

IOC是一种设计原则,将对象的创建和依赖关系的管理从应用代码中解耦出来,交由容器负责。在传统编程中,对象间依赖关系需程序员手动创建维护,比如类 A 依赖类 B,要在类 A 中用new关键字创建类 B 的实例。而在 IOC 模式下,对象由 IOC 容器创建和管理,开发者从容器获取所需对象。其好处如下:

松耦合:通过 IOC 容器管理对象创建和依赖注入,降低对象间耦合度,便于替换、升级和测试组件,减少维护成本。

配置灵活:可通过配置文件或注解定义 Bean 的创建和依赖关系,实现不同环境下的配置切换,使应用在不同部署环境中更灵活。

依赖注入:IOC 容器自动完成依赖注入,减少手动管理对象依赖关系的工作,提高代码可读性和可维护性。

松耦合:通过 IOC 容器管理对象创建和依赖注入,降低对象间耦合度,便于替换、升级和测试组件,减少维护成本。

配置灵活:可通过配置文件或注解定义 Bean 的创建和依赖关系,实现不同环境下的配置切换,使应用在不同部署环境中更灵活。

依赖注入:IOC 容器自动完成依赖注入,减少手动管理对象依赖关系的工作,提高代码可读性和可维护性。

AOP是一种编程范式,允许开发者将横切关注点(如日志、事务管理、安全等)从业务逻辑中分离出来,以模块化方式管理。其好处如下:

代码重用:将横切关注点从业务逻辑中抽离,相同切面逻辑可在多处重复使用,减少重复代码编写。

横切关注点集中管理:能将日志、事务、安全等与核心业务逻辑无关的功能集中管理,使代码更清晰、易维护。

运行时动态代理:AOP 使用动态代理技术,可在运行时将切面逻辑织入目标对象方法,无需修改原有代码。

代码重用:将横切关注点从业务逻辑中抽离,相同切面逻辑可在多处重复使用,减少重复代码编写。

横切关注点集中管理:能将日志、事务、安全等与核心业务逻辑无关的功能集中管理,使代码更清晰、易维护。

运行时动态代理:AOP 使用动态代理技术,可在运行时将切面逻辑织入目标对象方法,无需修改原有代码。

Java 虚拟机的内存空间分为 5 个部分:

程序计数器:它可以看作是当前线程所执行的字节码的行号指示器。在 JVM 的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一块较小的内存空间,也是唯一在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。每个线程都有一个独立的程序计数器,各线程间的计数器互不影响,独立存储。

Java 虚拟机栈:描述的是 Java 方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有,生命周期与线程相同,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,会抛出 OutOfMemoryError 异常。

本地方法栈:与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

堆:Java 堆是 Java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。是垃圾收集器管理的主要区域,因此也被称为 “GC 堆”。可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 8 中,方法区的实现由永久代(PermGen)改为元空间(Metaspace)。永久代使用的是 JVM 的堆内存,而元空间使用的是本地内存(Native Memory),这使得元空间的大小不再受 JVM 堆大小的限制。当元空间无法满足内存分配需求时,会抛出 OutOfMemoryError: Metaspace 异常。

程序计数器:它可以看作是当前线程所执行的字节码的行号指示器。在 JVM 的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一块较小的内存空间,也是唯一在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。每个线程都有一个独立的程序计数器,各线程间的计数器互不影响,独立存储。

Java 虚拟机栈:描述的是 Java 方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有,生命周期与线程相同,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,会抛出 OutOfMemoryError 异常。

本地方法栈:与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

堆:Java 堆是 Java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。是垃圾收集器管理的主要区域,因此也被称为 “GC 堆”。可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 8 中,方法区的实现由永久代(PermGen)改为元空间(Metaspace)。永久代使用的是 JVM 的堆内存,而元空间使用的是本地内存(Native Memory),这使得元空间的大小不再受 JVM 堆大小的限制。当元空间无法满足内存分配需求时,会抛出 OutOfMemoryError: Metaspace 异常。

JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

如何判断垃圾可以被回收?

主要有以下两种判断垃圾是否可回收的方法。

第一种方法「 引用计数法」:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器值加 1;当引用失效时,计数器减 1。任何时刻计数器为 0 的对象,就代表不可能再被使用,可被判定为垃圾从而进行回收。

优点:实现简单,判断效率高。

缺点:无法解决对象之间相互循环引用的问题。例如对象 A 和对象 B 相互引用,即使实际上它们已经不可能被外部访问,但因互相引用导致引用计数器都不为 0,就无法被回收。

优点:实现简单,判断效率高。

缺点:无法解决对象之间相互循环引用的问题。例如对象 A 和对象 B 相互引用,即使实际上它们已经不可能被外部访问,但因互相引用导致引用计数器都不为 0,就无法被回收。

第二种方法「 可达性分析法」:在垃圾回收之前对堆内所有对象进行扫描,通过一系列被称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到该对象不可达)时,则证明此对象是不可用的,可以被判定为垃圾。

可作为 GC Roots 的对象包括:

系统类(System Class):由启动类加载器加载的类。

操作系统方法使用时引用的 Java 对象(Native Stack)。

活动线程中引用的对象(Thread):活动线程执行过程中,局部变量所引用的对象作为根对象。

被加锁的对象(Busy monitor):例如被 Synchronize 修饰加锁的对象 。

系统类(System Class):由启动类加载器加载的类。

操作系统方法使用时引用的 Java 对象(Native Stack)。

活动线程中引用的对象(Thread):活动线程执行过程中,局部变量所引用的对象作为根对象。

被加锁的对象(Busy monitor):例如被 Synchronize 修饰加锁的对象 。

标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。

复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。

标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。

分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。

复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。

标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。

分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

StackOverFlowError 是一个 Error 类型的异常,当线程的栈深度超过虚拟机所允许的最大深度时抛出。栈是线程私有的,主要用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法在执行时都会在栈中创建一个栈帧,方法的调用过程就是栈帧入栈和出栈的过程。

比如,在方法递归调用无终止条件时,会导致方法不断地调用自身,栈帧不断入栈,最终导致栈空间耗尽。或者是,在方法调用层次过深时,也可能会导致栈空间不足。例如在一些复杂的算法或者嵌套调用中,栈帧的数量会随着调用层次的增加而增加,当超过栈的最大深度时,就会抛出 StackOverFlowError。

OutOfMemoryError 也是一个 Error 类型的异常,它表示 JVM 在尝试分配内存时无法满足请求,并且垃圾回收器也无法提供更多的内存。OutOfMemoryError 有多种不同的具体情况,常见的产生场景如下:

堆内存溢出:堆是 JVM 中用于存储对象实例的区域,如果创建的对象过多,或者对象占用的内存过大,导致堆内存无法满足对象分配的需求,就会抛出 OutOfMemoryError: Java heap space。

永久代 / 元空间溢出:在 Java 8 之前,永久代用于存储类的元数据信息,如类的结构、方法、字段等。如果加载的类过多,或者类的字节码文件过大,可能会导致永久代内存溢出,抛出 OutOfMemoryError: PermGen space。Java 8 及以后,永久代被元空间取代,元空间使用本地内存,如果元空间配置过小,同样可能会出现 OutOfMemoryError: Metaspace。

直接内存溢出:直接内存并不是 JVM 运行时数据区的一部分,但可以通过 Unsafe 类或者 ByteBuffer.allocateDirect 方法直接分配堆外内存。如果直接内存分配过多,并且没有及时释放,就可能导致直接内存溢出,抛出 OutOfMemoryError。

线程创建过多:每个线程都需要占用一定的栈空间,如果创建的线程数量过多,会导致栈空间总和超过系统的可用内存,从而抛出 OutOfMemoryError。

堆内存溢出:堆是 JVM 中用于存储对象实例的区域,如果创建的对象过多,或者对象占用的内存过大,导致堆内存无法满足对象分配的需求,就会抛出 OutOfMemoryError: Java heap space。

永久代 / 元空间溢出:在 Java 8 之前,永久代用于存储类的元数据信息,如类的结构、方法、字段等。如果加载的类过多,或者类的字节码文件过大,可能会导致永久代内存溢出,抛出 OutOfMemoryError: PermGen space。Java 8 及以后,永久代被元空间取代,元空间使用本地内存,如果元空间配置过小,同样可能会出现 OutOfMemoryError: Metaspace。

直接内存溢出:直接内存并不是 JVM 运行时数据区的一部分,但可以通过 Unsafe 类或者 ByteBuffer.allocateDirect 方法直接分配堆外内存。如果直接内存分配过多,并且没有及时释放,就可能导致直接内存溢出,抛出 OutOfMemoryError。

线程创建过多:每个线程都需要占用一定的栈空间,如果创建的线程数量过多,会导致栈空间总和超过系统的可用内存,从而抛出 OutOfMemoryError。

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

视图(view):为用户提供使用界面,与用户直接进行交互。

模型(model):代表一个存取数据的对象或 JAVA POJO(Plain Old Java Object,简单java对象)。它也可以带有逻辑,主要用于承载数据,并对用户提交请求进行计算的模块。模型分为两类,一类称为数据承载 Bean,一类称为业务处理Bean。所谓数据承载 Bean 是指实体类(如:User类),专门为用户承载业务数据的;而业务处理 Bean 则是指Service 或 Dao 对象, 专门用于处理用户提交请求的。

控制器(controller):用于将用户请求转发给相应的 Model 进行处理,并根据 Model 的计算结果向用户提供相应响应。它使视图与模型分离。

视图(view):为用户提供使用界面,与用户直接进行交互。

模型(model):代表一个存取数据的对象或 JAVA POJO(Plain Old Java Object,简单java对象)。它也可以带有逻辑,主要用于承载数据,并对用户提交请求进行计算的模块。模型分为两类,一类称为数据承载 Bean,一类称为业务处理Bean。所谓数据承载 Bean 是指实体类(如:User类),专门为用户承载业务数据的;而业务处理 Bean 则是指Service 或 Dao 对象, 专门用于处理用户提交请求的。

控制器(controller):用于将用户请求转发给相应的 Model 进行处理,并根据 Model 的计算结果向用户提供相应响应。它使视图与模型分离。

流程步骤:

用户通过View 页面向服务端提出请求,可以是表单请求、超链接请求、AJAX 请求等;

服务端 Controller 控制器接收到请求后对请求进行解析,找到相应的Model,对用户请求进行处理Model 处理;

将处理结果再交给 Controller(控制器其实只是起到了承上启下的作用);

根据处理结果找到要作为向客户端发回的响应View 页面,页面经渲染后发送给客户端。

用户通过View 页面向服务端提出请求,可以是表单请求、超链接请求、AJAX 请求等;

服务端 Controller 控制器接收到请求后对请求进行解析,找到相应的Model,对用户请求进行处理Model 处理;

将处理结果再交给 Controller(控制器其实只是起到了承上启下的作用);

根据处理结果找到要作为向客户端发回的响应View 页面,页面经渲染后发送给客户端。

提高代码的可维护性:MVC 模式将应用程序划分为模型、视图和控制器三个独立的部分,每个部分都有明确的职责。模型专注于数据处理和业务逻辑,视图负责数据的呈现,控制器则处理用户的请求和协调模型与视图之间的交互。这种清晰的职责划分使得代码结构更加清晰,当需要修改某个功能时,开发人员可以快速定位到相关的代码部分,而不必担心影响其他部分的功能,而且这种低耦合度的设计使得代码的修改和维护更加容易,一个部分的变化不会对其他部分产生重大影响。

增强软件的可扩展性:在 MVC 架构中,可以独立地扩展模型、视图或控制器。例如,当需要添加新的业务逻辑时,只需要在模型中添加相应的方法;当需要改变用户界面时,只需要修改视图部分的代码;当需要处理新的用户请求时,只需要在控制器中添加新的处理逻辑。这种模块化的设计使得软件能够方便地适应不断变化的需求。

提高代码的可维护性:MVC 模式将应用程序划分为模型、视图和控制器三个独立的部分,每个部分都有明确的职责。模型专注于数据处理和业务逻辑,视图负责数据的呈现,控制器则处理用户的请求和协调模型与视图之间的交互。这种清晰的职责划分使得代码结构更加清晰,当需要修改某个功能时,开发人员可以快速定位到相关的代码部分,而不必担心影响其他部分的功能,而且这种低耦合度的设计使得代码的修改和维护更加容易,一个部分的变化不会对其他部分产生重大影响。

增强软件的可扩展性:在 MVC 架构中,可以独立地扩展模型、视图或控制器。例如,当需要添加新的业务逻辑时,只需要在模型中添加相应的方法;当需要改变用户界面时,只需要修改视图部分的代码;当需要处理新的用户请求时,只需要在控制器中添加新的处理逻辑。这种模块化的设计使得软件能够方便地适应不断变化的需求。

消息幂等指的是对同一个消息进行多次处理和只进行一次处理,所产生的业务结果是相同的,不会因为重复处理而导致额外的影响或错误。例如,在电商系统中,处理用户的下单消息,无论该消息被消费一次还是多次,最终订单的状态和数量等信息应该保持一致。

要保证消息幂等性可以有下面这些方式:

唯一标识法:在消息生产时,为每条消息生成一个全局唯一的标识(如 UUID、雪花算法),并将该标识随消息一起发送。消息消费者在处理消息前,先检查该标识是否已经被处理过。可以通过数据库、缓存等存储介质来记录已经处理过的消息标识。

状态机法:对于一些有状态的业务操作,可以使用状态机来控制消息的处理。每个业务对象都有一个状态,根据不同的消息和当前状态,定义相应的状态转移规则。当收到重复消息时,根据当前状态判断是否需要处理。比如,在订单系统中,订单有 “未支付”“已支付”“已发货” 等状态。当收到 “支付成功” 的消息时,如果订单状态已经是 “已支付”,则直接忽略该消息。

业务幂等设计:在业务逻辑层面设计幂等操作。例如,在进行数据更新时,使用数据库的唯一索引或乐观锁机制,确保重复操作不会产生额外的影响。

唯一标识法:在消息生产时,为每条消息生成一个全局唯一的标识(如 UUID、雪花算法),并将该标识随消息一起发送。消息消费者在处理消息前,先检查该标识是否已经被处理过。可以通过数据库、缓存等存储介质来记录已经处理过的消息标识。

状态机法:对于一些有状态的业务操作,可以使用状态机来控制消息的处理。每个业务对象都有一个状态,根据不同的消息和当前状态,定义相应的状态转移规则。当收到重复消息时,根据当前状态判断是否需要处理。比如,在订单系统中,订单有 “未支付”“已支付”“已发货” 等状态。当收到 “支付成功” 的消息时,如果订单状态已经是 “已支付”,则直接忽略该消息。

业务幂等设计:在业务逻辑层面设计幂等操作。例如,在进行数据更新时,使用数据库的唯一索引或乐观锁机制,确保重复操作不会产生额外的影响。

查看 mysql 的慢查询日志,找到耗时超过设定值的 sql 语,然后将 sql 语句用 explian 执行计划分析 sql,了解查询是如何执行的,包括使用的索引、扫描的行数等信息。

EXPLAIN SELECT * FROM users WHERE age > 18;

解决慢查询 sql 的方式:

避免全表扫描:尽量使用索引来加速查询。例如,在 WHERE 子句中使用索引列进行过滤,避免使用函数或表达式对索引列进行操作。

减少**SELECT \*** 的使用:只查询需要的列,避免不必要的数据传输和处理。

创建合适的索引:根据查询条件和排序规则,创建合适的索引。例如,对于经常用于过滤和排序的列,可以创建单列索引或复合索引。

分库分表:对于数据量较大的数据库,可以采用分库分表的方式将数据分散到多个数据库或表中,以减少单个数据库或表的负载。

读写分离:对于读写压力不均衡的系统,可以采用读写分离的架构,将读操作和写操作分别分配到不同的数据库实例上,提高系统的并发处理能力。

避免全表扫描:尽量使用索引来加速查询。例如,在 WHERE 子句中使用索引列进行过滤,避免使用函数或表达式对索引列进行操作。

减少**SELECT \*** 的使用:只查询需要的列,避免不必要的数据传输和处理。

创建合适的索引:根据查询条件和排序规则,创建合适的索引。例如,对于经常用于过滤和排序的列,可以创建单列索引或复合索引。

分库分表:对于数据量较大的数据库,可以采用分库分表的方式将数据分散到多个数据库或表中,以减少单个数据库或表的负载。

读写分离:对于读写压力不均衡的系统,可以采用读写分离的架构,将读操作和写操作分别分配到不同的数据库实例上,提高系统的并发处理能力。

当对索引列使用函数时,数据库无法直接使用索引进行快速查找,只能进行全表扫描,比如在 MySQL 中,若 orders 表的 order_date 列有索引,以下查询会使索引失效,这是因为 YEAR 函数作用于 order_date 列,数据库无法直接利用索引定位数据。

当对索引列使用函数时,数据库无法直接使用索引进行快速查找,只能进行全表扫描,比如在 MySQL 中,若 orders 表的 order_date 列有索引,以下查询会使索引失效,这是因为 YEAR 函数作用于 order_date 列,数据库无法直接利用索引定位数据。

当索引列的数据类型和查询条件的数据类型不一致,发生隐式类型转换时,索引会失效。比如 users 表的 user_id 列是 VARCHAR 类型,以下查询会导致索引失效,这里会将 user_id 隐式转换为数字类型,从而无法使用索引。

当索引列的数据类型和查询条件的数据类型不一致,发生隐式类型转换时,索引会失效。比如 users 表的 user_id 列是 VARCHAR 类型,以下查询会导致索引失效,这里会将 user_id 隐式转换为数字类型,从而无法使用索引。

当 OR 连接的条件中有未使用索引的列时,整个查询可能无法使用索引,比如 customers 表中 name 列有索引, phone 列无索引,以下查询会使索引失效。

当 OR 连接的条件中有未使用索引的列时,整个查询可能无法使用索引,比如 customers 表中 name 列有索引, phone 列无索引,以下查询会使索引失效。

当使用 LIKE 进行模糊查询且以通配符 % 开头时,索引会失效。比如,在 articles 表中, title 列有索引,以下查询无法使用索引。

当使用 LIKE 进行模糊查询且以通配符 % 开头时,索引会失效。比如,在 articles 表中, title 列有索引,以下查询无法使用索引。

除了这个还有别的可能产生慢查询的原因吗?

表数据膨胀:随着业务的发展,表中的数据量不断增加,全表扫描或复杂查询的执行时间会显著变长。例如,一个包含数百万条记录的表进行全表扫描,会消耗大量的时间和资源。

索引维护成本高:数据量过大时,索引的维护成本也会增加,每次数据的插入、更新或删除操作都可能需要更新索引,影响查询性能。

行锁和表锁:在事务处理过程中,如果存在大量的行锁或表锁竞争,会导致其他事务等待锁释放,从而使查询变慢。例如,一个事务长时间持有表锁,会阻塞其他事务对该表的查询操作。

内存分配不足:如 MySQL 的 innodb_buffer_pool_size 过小,无法将常用的数据和索引加载到内存中,会频繁进行磁盘 I/O,导致查询变慢。

多层嵌套子查询:过多的嵌套子查询会使查询的执行计划变得复杂,数据库需要多次解析和执行子查询,增加查询的时间开销。

表数据膨胀:随着业务的发展,表中的数据量不断增加,全表扫描或复杂查询的执行时间会显著变长。例如,一个包含数百万条记录的表进行全表扫描,会消耗大量的时间和资源。

索引维护成本高:数据量过大时,索引的维护成本也会增加,每次数据的插入、更新或删除操作都可能需要更新索引,影响查询性能。

行锁和表锁:在事务处理过程中,如果存在大量的行锁或表锁竞争,会导致其他事务等待锁释放,从而使查询变慢。例如,一个事务长时间持有表锁,会阻塞其他事务对该表的查询操作。

内存分配不足:如 MySQL 的 innodb_buffer_pool_size 过小,无法将常用的数据和索引加载到内存中,会频繁进行磁盘 I/O,导致查询变慢。

多层嵌套子查询:过多的嵌套子查询会使查询的执行计划变得复杂,数据库需要多次解析和执行子查询,增加查询的时间开销。

分库是一种水平扩展数据库的技术,将数据根据一定规则划分到多个独立的数据库中。每个数据库只负责存储部分数据,实现了数据的拆分和分布式存储。分库主要是为了解决并发连接过多,单机 mysql扛不住的问题。

分表指的是将单个数据库中的表拆分成多个表,每个表只负责存储一部分数据。这种数据的垂直划分能够提高查询效率,减轻单个表的压力。分表主要是为了解决单表数据量太大,导致查询性能下降的问题。

分库是一种水平扩展数据库的技术,将数据根据一定规则划分到多个独立的数据库中。每个数据库只负责存储部分数据,实现了数据的拆分和分布式存储。分库主要是为了解决并发连接过多,单机 mysql扛不住的问题。

分表指的是将单个数据库中的表拆分成多个表,每个表只负责存储一部分数据。这种数据的垂直划分能够提高查询效率,减轻单个表的压力。分表主要是为了解决单表数据量太大,导致查询性能下降的问题。

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

垂直分库:一般来说按照业务和功能的维度进行拆分,将不同业务数据分别放到不同的数据库中,核心理念 专库专用。按业务类型对数据分离,剥离为多个数据库,像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。

垂直分表:针对业务上字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。数据库它是以行为单位将数据加载到内存中,这样拆分以后核心表大多是访问频率较高的字段,而且字段长度也都较短,因而可以加载更多数据到内存中,减少磁盘IO,增加索引查询的命中率,进一步提升数据库性能。

水平分库:是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,以此实现水平扩展,是一种常见的提升数据库性能的方式。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此系统的复杂度也被提升了。

水平分表:是在 同一个数据库内,把一张大数据量的表按一定规则,切分成多个结构完全相同表,而每个表只存原表的一部分数据。水平分表尽管拆分了表,但子表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分散到不同的机器上,还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。

垂直分库:一般来说按照业务和功能的维度进行拆分,将不同业务数据分别放到不同的数据库中,核心理念 专库专用。按业务类型对数据分离,剥离为多个数据库,像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。

垂直分表:针对业务上字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。数据库它是以行为单位将数据加载到内存中,这样拆分以后核心表大多是访问频率较高的字段,而且字段长度也都较短,因而可以加载更多数据到内存中,减少磁盘IO,增加索引查询的命中率,进一步提升数据库性能。

水平分库:是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,以此实现水平扩展,是一种常见的提升数据库性能的方式。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此系统的复杂度也被提升了。

水平分表:是在 同一个数据库内,把一张大数据量的表按一定规则,切分成多个结构完全相同表,而每个表只存原表的一部分数据。水平分表尽管拆分了表,但子表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分散到不同的机器上,还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。

在 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 的组合,锁定一个范围,并且锁定记录本身

全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做 全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

表级锁:MySQL 里面表级别的锁有这几种:

表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。 意向锁的目的是为了快速判断表里是否有记录被加锁。

表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。 意向锁的目的是为了快速判断表里是否有记录被加锁。

行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥

间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身

记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥

间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身

悲观锁的核心思想是 “悲观” 地认为在并发环境下,数据随时可能被其他事务修改。因此,在对数据进行操作之前,它会先获取锁,将数据锁定,阻止其他事务对该数据进行读写操作,直到当前事务完成并释放锁。这种锁机制可以保证数据的一致性和完整性,但会降低系统的并发性能。

应用场景:适用于对数据一致性要求较高、并发冲突可能性较大的场景,如金融系统中的资金账户操作、库存管理系统中的库存扣减等。

在数据库中,可以使用 SELECT ... FOR UPDATE 语句来实现悲观锁。例如,在 MySQL 中:

START TRANSACTION;

SELECT * FROM accounts WHERE account_id = 1FOR UPDATE;

-- 进行数据修改操作

UPDATE accounts SET balance = balance - 100WHERE account_id = 1;

COMMIT;

在这个例子中,SELECT ... FOR UPDATE 语句会将 accounts 表中 account_id 为 1 的记录锁定,其他事务在当前事务提交或回滚之前无法对该记录进行读写操作。

在 Java 中,可以使用 synchronized 关键字或 ReentrantLock 类来实现悲观锁。例如:

importjava.util.concurrent.locks.ReentrantLock;

publicclassPessimisticLockExample{

privatefinalReentrantLock lock = newReentrantLock;

privateintdata;

publicvoidupdateData{

lock.lock;

try{

// 进行数据修改操作

data++;

} finally{

lock.unlock;

}

}

}

乐观锁的核心思想是 “乐观” 地认为在并发环境下,数据发生冲突的可能性较小。因此,在对数据进行操作时,它不会先获取锁,而是直接进行数据的读取和修改。在提交修改时,会检查数据在读取之后是否被其他事务修改过,如果没有被修改,则提交修改;如果被修改了,则回滚操作或采取其他处理方式。

应用场景:适用于对数据一致性要求相对较低、并发冲突可能性较小的场景,如网站的点赞数统计、文章的浏览量统计等。

通常使用版本号机制或时间戳机制来实现乐观锁。例如,在数据库表中添加一个 version 字段,每次更新数据时,会比较当前记录的版本号与读取时的版本号是否一致,如果一致则更新数据并将版本号加 1;如果不一致,则表示数据已被其他事务修改,更新失败。示例 SQL 如下:

-- 读取数据

SELECT id, balance, version FROM accounts WHERE account_id = 1;

-- 更新数据

UPDATE accounts

SET balance = balance - 100, version = version + 1

WHERE account_id = 1AND version = [读取时的版本号];

在 Java 中,可以使用 AtomicInteger 等原子类来实现乐观锁。例如:

importjava.util.concurrent.atomic.AtomicInteger;

publicclassOptimisticLockExample{

privateAtomicInteger data = newAtomicInteger( 0);

publicvoidupdateData{

intoldValue;

intnewValue;

do{

oldValue = data.get;

newValue = oldValue + 1;

} while(!data.compareAndSet(oldValue, newValue));

}

}

在这个例子中,compareAndSet 方法会比较当前值与期望值是否相等,如果相等则更新值,否则返回 false,继续重试。

其他JSP做过吗?

没用过。。(为啥问这个,难道公司还用这个技术吗...,如果是那就是有点老呢 T.T)

项目的数据量大概有多少?

不敢吹太大,保守一点,就说几百个人在用。。

看过哪些底层源码?

hashmap 文章看的最多,就顺说看过 hashmap 的源码。。

你在项目中碰到过什么问题吗?

真是每次必问的项目问题,还好已经最好小抄了,背完就完事。返回搜狐,查看更多

相关探索

荸荠炒虾仁
亚洲365bet官网

荸荠炒虾仁