Java

ParameterizedType 意为参数化类型(泛型)

ParameterizedType是Type的子接口,可以通过ParameterizedType获取泛型参数Class类型

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* ParameterizedType 表示参数化类型,例如 Collection<String>。
* 参数化类型在反射方法第一次需要时创建,如本包中所指定。 创建参数化类型 p 时,解析 p 实例化的泛型类型声明,并递归创建 p 的所有类型参数。 有关类型变量创建过程的详细信息,请参阅TypeVariable 。 重复创建参数化类型无效。
* 实现此接口的类的实例必须实现一个 equals() 方法,该方法等同于共享相同泛型类型声明并具有相同类型参数的任何两个实例。
*
* @since:1.5
*/
public interface ParameterizedType extends Type {

/**
* 返回一个Type对象数组,表示该类型的实际类型参数。
* 请注意,在某些情况下,返回的数组为空。 如果此类型表示嵌套在参数化类型中的非参数化类型,则会发生这种情况。
*
* @return: 表示此类型的实际类型参数的Type对象数组
* @throws TypeNotPresentException 如果任何实际类型参数引用不存在的类型声明
* @throws MalformedParameterizedTypeException 如果任何实际类型参数引用了由于任何原因无法实例化的参数化类型
* @since 1.5
*/
Type[] getActualTypeArguments();

/**
* 返回表示声明此类型的类或接口的Type对象。
*
* @return 表示声明此类型的类或接口的Type对象
* @since 1.5
*/
Type getRawType();

/**
* 返回一个Type对象,表示该类型所属的类型。 例如,如果此类型为O<T>.I<S> ,则返回O<T> 。
* 如果此类型是顶级类型,则返回null 。
*
* @return 一个Type对象,表示该类型所属的类型。 如果此类型是顶级类型,则返回null。
* @throws 如果所有者类型引用不存在的类型声明
* @throws 如果所有者类型引用了由于任何原因无法实例化的参数化类型
* @since 1.5
*/
Type getOwnerType();
}
什么是BASE柔性事务?

BASE是基于可用、柔性状态和最终一致性这三个要素

  • 基本可用(Basically Available)保证分布式事务参与方不一定要同时在线;
  • 柔性状态(Soft state)则允许系统状态更新有一定的延迟,这个延时对客户来说不一定能够察觉到;
  • 最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性;
TCC 分布式事务

你原本的一个接口,要改造为 3 个逻辑,Try(尝试)-Confirm(确定)-Cancel(取消)

  • 先是服务调用链路依次执行 Try 逻辑。
  • 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
  • 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。

这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时:

  • 某个服务的数据库宕机了。
  • 某个服务自己挂了。
  • 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。
  • 某些资源不足了,比如说库存不够这些。

TCC 分布式事务框架(国内):ByteTCC,TCC-transaction,Himly。

最终一致性分布式事务如何保障实际生产中 99.99% 高可用?
SOA、分布式、微服务之间的关系和区别

1.分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
2.SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
3.微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

ReentrantLock中tryLock()和lock方法的区别

1.tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false

2.lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

类加载分几步?

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括7个阶段

加载(Loading)-> 链接(Linking) [ 验证(Verification)->准备(Preparation)->解析(Resolution) ] -> 初始化(Initialization)->使用(Using)->卸载(Unloading)

其中

1.第一过程的加载(Loading)也称为装载

2.验证,准备,解析3个部分统称为链接(Linking)

类的初始化

使用static + final 修饰的成员变量,称为:全局变量

什么时候在链接阶段的准备环节,给此全局常量赋的值是字面量或常量。不涉及到方法或构造器的调用,除此之外,都是在初始化环节赋值的

为什么要自定义类加载器

隔离加载类

修改类加载的方式

扩展加载源

防止源码泄漏

虚拟机栈大小

如何设置栈内存大小? -Xss size(-XX : ThreadStackSize)

栈的大小直接决定了函数调用的最大可达深度

一般默认为512k-1024k,取决于操作系统

jdk 5.0之前,默认栈大小:256k

jdk5.0之后,默认栈大小:1024k (Linux/mac/windows)

方法和栈帧之间存在怎么的关系

1.在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

2.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

栈帧内部结构

局部变量表

操作数栈

动态链接

方法返回地址

如何设置堆空间大小

-Xms 用于表示起始堆大小,等价于-XX:InitialHeapSize

-Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapSize

超出堆区的内存最大时,将会抛出OutOfMemoryError:heap异常

通常会将 -Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

Java中有哪些类加载器

JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootstrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA HOME%lib下的jar包和class文件
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA HOME%/lib/ext文件夹下的jar包和class类。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。

JVM有哪些垃圾回收算法

1.标记清除算法:
a.标记阶段:把垃圾内存标记出来
b.清除阶段:直接将垃圾内存回收
c.这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
2.复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
3.标记压缩算法:为了解:决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的

Java语言中,GC Roots 包括那些元素

1.虚拟机栈中引用的对象

2.类静态属性引用的对象

3.方法区中常量引用的对象

4.所有被同步锁synchronization持有的对象

5.Java虚拟机内部的引用

6.反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

小技巧

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root

Java中内存泄漏的8种情况

1.静态集合类

如HashMap,LinkedList等,如果这些容器为静态的,那么它们的生命周期与JVM程序一致。长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收

2.单例模式

原因和静态集合类似。如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏**

3.内部类持有外部类

4.各种连接,如数据库连接,网络连接和IO连接等

5.变量不合理的作用域

6.改变哈希值

7.缓存泄漏

8.监听器和回调

STW

STW(Stop-The-World),指在GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

GC评估指标

吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间

内存占用:Java堆区所占的内存的大小

垃圾收集开销:相对于应用程序的执行,收集操作发生的频率

收集频率:相对于应用程序的执行,收集操作发生的频率

快速:一个对象从诞生到被回收所经历的时间

现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间

OOM示例

堆溢出

元空间溢出

GC overhead limit exceeded(死循环)

线程溢出(在windows测试会死机,建议在虚拟机尝试)

栈上分配

发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配(JDK1.6后默认使用逃逸分析,提高性能)

JAVA中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换

如何优雅关闭Spring Boot?

四种优雅关闭Spring Boot应用程序的方案。

第一种是通过调用activator的shutdown接口,但需要配置认证和权限控制。

需添加依赖

shutdown接口默认关闭,需开启

第二种是调用应用程序上下文的close方法,也需要处理认证和权限问题。

第三种是调用Spring Application的方法来触发关闭钩子函数。

第四种是直接杀进程,可以在启动时将进程ID写入固定文本文件,然后执行脚本自动关闭应用程序。

Mybatis的优缺点

优点:
1.基于SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML 标
签,支持编写动态 SQL语句,并可重用。
2.与JDBC 相比,减少了 50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接:
3.很好的与各种数据库兼容(因为 MyBatis 使用JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)。
4.能够与 Spring 很好的集成。
5.提供映射标签,支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, 支持对象关系组件维护。

缺点:
1.SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2.SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

Redis和MySQL如何保证数据一致

1.先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致
2.先删除Redis缓存数据,再更新Msl,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysgl,此时另外一个查询再查询,那么就会把Mvsal中老数据又查到Redis中
3.延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysl,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致

#{}和${}的区别是什么?

#{}是预编译处理、是占位符,${}是字符串替换、是拼接符
Mybatis在处理**#{}时,会将sql中的#{}替换为?号,调用 PreparedStatement 来赋值
Mybatis在处理
${}时,会将sql中的${}**替换成变量的值,调用 Statement 来赋值
使用#{}可以有效的防止 SQL注入,提高系统安全性

Spring容器启动流程是怎样的

1.在创建Spring容器,也就是启动Spring时
2.首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
3.然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
4.利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并Beanlefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
5.单例Bean创建完了之后,Spring会发布一个容器启动事件
6.Spring启动结束
7.在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
8.在Spring启动过程中还会去处理@Import等注解

Spring AOP常见的失效场景

1.当前类没有被 Spring 容器所管理。Spring 的 AOP 是在 Bean 创建的初始化后阶段进行的,如果当前类没有被 Spring 容器所管
理,那么它的 Spring AOP 功能肯定会失效。

2.同一个类中方法的调用

3.内部类方法的调用。(该方式会直接调用内部类实例对象的方法,同样没有使用代理对象,所以 AOP 会失效)

4.私有方法。(私有方法,代理对象是无法调用的,所以 AOP 会失效)

5.static 修饰的方法。(因为 static 修饰的方法属于类对象,而不属于对象实例,所以无法被代理对象调用。)

6.final 修饰的方法。(因为被 final 修饰的方法是无法被重写的,所以代理对象也是无法调用的。)

Java面向对象的三大特征以及理解

1.封装

Java中的封装是指一个类把自己内部的实现细节进行隐藏,只暴露对外的接口(setter和getter方法)。封装又分为属性的封装和方法的封装。把属性定义为私有的,它们通过setter和getter方法来对属性的值进行设定和获取。

2.继承

Java中的继承是指在一个现有类(父类)的基础上在构建一个新类(子类),子类可以拥有父类的成员变量以及成员方法(但是不一定能访问或调用,例如父类中private私有的成员变量以及方法不能访问和调用)。继承的作用就是能提高代码的复用性。子类拥有父类中的一切(拥有不一定能使用),它可以访问和使用父类中的非私有成员变量,以及重写父类中的非私有成员方法。

3.多态

多态就是指多种状态,就是说当一个操作在不同的对象时,会产生不同的结果。

在Java中,实现多态的方式有两种,一种是编译时的多态,另外一种是运行时多态,编译时的多态是通过方法的重载实现的,而运行时多态是通过方法的重写实现的。

方法的重载是指在同一个类中,有多个方法名相同的方法,但是这些方法有着不同的参数列表,在编译期我们就可以确定到底调用哪个方法。

方法的重写,子类重写父类中的方法(包括接口的实现),父类的引用不仅可以指向父类的对象,而且还可以指向子类的对象。当父类的引用指向子类的引用时,只有在运行时才能确定调用哪个方法。

其实在运行时的多态的实现,需要满足三个条件:1.继承(包括接口的实现)2.方法的重写 3.父类的引用指向子类对象

String 为什么不可变?不可变有什么好处?

为什么不可变?

1)value使用final修饰

2)没有暴露成员变量

3)内部方法不会改动 value

一旦初始化之后,String 类中的方法就不会去改动 value 中的元素,需要的话都是直接新建一个 String 对象。

4)类使用final修饰,不可继承

这个设计主要是避免有人定义一个子类继承 String,然后重写 String 的方法,将这个子类设计成可变对象。我们知道在 java 中,有父类引用指向子类对象这种用法,这种情况下,我们需要一个String 对象,可能返回的是String 子类的对象,这会导致 String 看起来是可变的。所以 java 直接将 String定义成不可继承,避免出现这种情况。

不只是 String 类,其实所有的不可变类大致的设计思想都是按这四步来。后续如果我们自己想要设计一个不可变类,也可以按这四点来设计。

不可变的好处?为什么这么设计?

1)安全性

String 是 Java 中最基础也是最长使用的类,经常用于存储一些敏感信息,例如用户名、密码、网络连接等。因此,String 类的安全性对于整个应用程序至关重要。

2)节省空间——字符串常量池

通过使用常量池,内容相同的字符串可以使用同一个对象,从而节省内存空间。如果 String 是可变的,试想一下,当字符串常量池中的某个字符串对象被很多地方引用时,此时修改了这个对象,则所有引用的地方都会改变,这可能会导致预期之外的情况。

典型的使用字符串常量池的场景:json 工具类,fastjson、jackson 等。

3)线程安全
String 对象是不可修改的,如果线程尝试修改 String 对象,会创建新的 String,所以不存在并发修改同一个对象的问题。

4)性能
String 被广泛应用于 HashMap、HashSet 等哈希类中,当对这些哈希类进行操作时,例如 HashMap 的 get/put,hashCode 会被频繁调用。

由于不可变性,String 的 hashCode 只需要计算1次后就可以缓存起来,因此在哈希类中使用 String 对象可以提升性能。

IO流

1.什么是IO流

Java对数据的操作是通过流的方式,IO是java中实现输入输出的基础,它可以很方便的完成数据的输入输出操作,Java把不同的输入输出抽象为流,通过流的方式允许Java程序使用相同的方式来访问不同的输入、输出。

IO又分为流IO(java.io)和块IO(java.nio),Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂。

2. IO流原理

IO流是基于流的概念,它将数据的输入和输出看作是一个连续的流。数据从一个地方流向另一个地方,流的方向可以是输入(读取数据)或输出(写入数据)。Java中的IO流分为字节流和字符流两种类型,分别用于处理字节数据和字符数据。

IO流的原理是通过流的管道将数据从源头传输到目标地。源头可以是文件、网络连接、内存等,而目标地可以是文件、数据库、网络等。IO流提供了一组丰富的类和方法来实现不同类型的输入和输出操作。

3.IO流分类
Java中的IO流可以按照数据的类型和流的方向进行分类。

1.按数据类型分类
字节流(Byte Stream):以字节为单位读写数据,适用于处理二进制数据,如图像、音频、视频等。常见的字节流类有InputStream和OutputStream。

字符流(Character Stream):以字符为单位读写数据,适用于处理文本数据。字符流会自动进行字符编码和解码,可以处理多国语言字符。常见的字符流类有Reader和Writer。

2 按流的方向分类
输入流(Input Stream):用于读取数据。输入流从数据源读取数据,如文件、网络连接等。常见的输入流类有FileInputStream、ByteArrayInputStream、SocketInputStream等。

输出流(Output Stream):用于写入数据。输出流将数据写入到目标地,如文件、数据库、网络等。常见的输出流类有FileOutputStream、ByteArrayOutputStream、SocketOutputStream等。

4.IO流的使用场景
IO流主要用于处理输入和输出操作,适用于以下场景:

读写文件:IO流可以方便地读取和写入文件中的数据,从而实现文件的读写操作,例如读取配置文件、处理日志文件、读取用户上传的文件等。

网络通信:IO流可以用于处理网络通信中的数据输入和输出,例如通过Socket进行网络通信时,可以使用IO流来传输数据。

数据库操作:IO流可以将数据从程序中传输到数据库中,或者从数据库中读取数据到程序中,从而实现数据库的读写操作。

内存操作:IO流也可以用于处理内存中的数据输入和输出,例如通过ByteArrayInputStream和ByteArrayOutputStream可以在内存中读写数据。

用户交互:IO流可以用于处理用户输入和输出,例如从控制台读取用户输入的数据,或者向控制台输出提示信息和结果。

ArrayList线程不安全的几种表现,怎么解决?

一、线程不安全的三种表现

1.空指针异常

2.数组越界异常

3.并发修改异常

二、解决方法

1.将ArrayList替换成Vector

1
Vector<Integer> arrayList = new Vector<>();

2.Collections.synchronizedList()

1
List<Integer> arrayList = Collections.synchronizedList(new ArrayList<>());

3.使用CopyOnWriteArrayList

1
List<Integer> arrayList1 = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理是读写分离,其对写操作使用ReentrantLock来上锁,对读操作则不加锁;CopyOnWriteArrayList在写操作的时候,会将list中的数组拷贝一份副本,然后对其副本进行操作(如果此时其他线程需要读的事,那么其他线程读取的是原先的没有修改的数组,如果其他写操作的线程要进行写操作,需要等待正在写的线程操作完成,释放ReentrantLock后,去获取锁才能进行写操作),写操作完成后,会讲list中数组的地址引用指向修改后的新数组地址。

总结
1、本文介绍了ArrayList在多线程的情况下可能会出现的三种异常,并分析了原因,结尾给出了三种解决ArrayList线程不安全的方案,一和二两种方法都是将所有的方法都加锁,那会导致效率低下,只能一个线程操作完,下一个线程获取到锁才能操作。

2、CopyOnWriteArrayList由于写时进行复制,内存里面同时存在两个对象占用内存,如果对象过大容易发送YongGc和FullGc,如果使用场景的写操作十分频繁的话,建议还是不要实现CopyOnWriteArrayList。

SpringMVC是如何处理一个请求
Spring中的Bean的生命周期有哪些步骤
Spring的Aop的完整实现流程

以 JavaConfig为主

当@EnableAspectJAutoProxy 会通过@Import注册一个BeanPostProcessor处理AOP

1.解祈切面:在Bean创建之前的第一个Bean后置处理器会去解析切面(解析切面中通知、切点,一个通知就会解析成一个advisor(通知、切点))

2.创建动态代理:正常的Bean初始化后调用BeanPostProcessor 拿到之前缓存的advisor,再通过advisor中poitcut判断当前Bean是合微功点表达式业配,如果匹配,就会为Bean创建动态代理(创建方式1.jdk动态代理2.cglib)。

3.调用:拿到动态代理对象,调用方法就会判断当前方法是否增强的方法,就会通过调用链的方式依次去执行通知.

SpringIOC容器的加载过程

从概念态到定义态的过程(1-6)

1、实例化一个ApplicationContext的对象

2、调用bean工厂后置处理器完成扫描

3、循环解析扫描出来的类信息;(就是有写@component 注解)

4、实例化一个BeanDefinition对象来存储解析出来的信息 (存入一个Map)

5、把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来,以便后面实例化bean;

6、再次调用其他bean工厂后置处理器;

从定义态到纯净态(7-9)

7、当然spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步是spring调用finishBeanFactoryInitialization方法来实例化单例的bean,实例化之前spring要做验证,需要遍历所有扫描出来的类,依次判断这个bean是否Lazy,是否prototype,是否abstract等等;

8、如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;

9、推断完构造方法之后spring调用构造方法反射实例化一个对象;注意我这里说的是对象、对象、对象;这个时候对象已经实例化出来了,但是并不是一个完整的bean,最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean

从纯净态到成熟态

10、spring处理合并后的beanDefinition

11、判断是否需要完成属性注入

12、如果需要完成属性注入,则开始注入属性

初始化

[

​ 13、判断bean的类型回调Aware接口

​ 14、调用生命周期回调方法 (13,14如果需要AOP就创建AOP动态代理)

​ 15、如果需要代理则完成代理

]

创建完成

16、put到单例池——bean完成——存在spring容器当中

Spring IOC的扩展点

1.执行BeanFactoryPostProcessor的postProcessBeanFactory方法(作用:在注册BeanDefinition的可以对beanFactory进行扩展,调用时机:IOC加载时注册BeanDefinition的时候会调用) 后

2.执行BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法(作用:动态注册BeanDefinition,调用时机:IOC加载时注册BeanDefinition的时候会调用) 先

3.加载BeanPostProcessor实现类:在Bean的生命周期会调用9次Bean的后置处理器

Spring IOC的实现机制

简单来说就是:简单工厂+反射

什么是工厂模式:很简单,就是调用一个方法(工厂方法)根据传入的参数返回一个对象。

IOC的优点
1、集中管理对象,方便维护

2、减低耦合度

3、IOC容器支持懒汉式和饿汉式的方式加载, 默认单例

springboot项目哪里用到了 AOP?怎么用的?

AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,提高系统可拓展性和可维护性。

一般项目主要有下面这些地方用到了 AOP

1.基于 AOP 实现统一的日志管理。
2.基于 Redisson + AOP 实现了接口防刷,一个注解即可限制接口指定时间内单个用户可以请求的次数。
3.基于 Spring Security 提供的 @PreAuthorize 实现权限控制,其底层也是基于 AOP。

日志记录
利用 AOP 方式记录日志,只需要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操作记录到数据库。

1
2
3
4
5
6
@Log(description = "新增用户")
@PostMapping(value = "/users")
public ResponseEntity create(@Validated @RequestBody User resources){
checkLevel(resources);
return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);
}

AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogAspect {

private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

// 定义切点,拦截带有 @Log 注解的方法
@Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改
public void logPointcut() {
}

// 环绕通知,用于记录日志
@Around("logPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//...
}
}

限流
利用 AOP 方式对接口进行限流,只需要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可。

1
2
3
4
5
6
7
/**
* 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test,
*/
@RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")
public int test() {
return ATOMIC_INTEGER.incrementAndGet();
}

AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处理:

1
2
3
4
5
6
7
8
9
@Slf4j
@Aspect
public class RateLimitAspect {
// 拦截所有带有 @RateLimit 注解的方法
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
//...
}
}

关于限流实现,并没有自己写 Redis Lua 限流脚本,而是利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。

权限控制
Spring Security 使用 AOP 进行方法拦截。在实际调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满足对应的条件才能执行。

1
2
3
4
5
6
7
@Log(description = "修改菜单")
@PutMapping(value = "/menus")
// 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
//...
}
SpringBoot是如何启动Tomcat的

1.首先,SpringBoot在启动时会先创建一个Spring容器
2.在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
3.Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

SpringBoot中常用注解及其底层实现

1.@SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
a.@SpringBootConfiquration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
b.@EnableAutoConfiguration:向Spring容器中导入了一个selector,用来加载Classpath下springFactories中所定义的自动配置类,将这些自动加载为配置Bean
c.@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
2.@Bean注解:用来定义Bean,类似于XML中的< bean >标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
3.@Controller、@Service、@ResponseBody、@Autowired等常见注解

java反射的原理

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

1
2
Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面这样子进行类对象的初始化,我们可以理解为「正」。

而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

这时候,我们使用 JDK 提供的反射 API 进行反射调用:

1
2
3
4
5
Class clz = Class.forName("com.chenshuyi.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Apple),而第二段代码则是在运行时通过字符串值才得知要运行的类(com.chenshuyi.reflect.Apple)。

所以说什么是反射?

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

@Component和@Bean的区别在哪里?

1.用途不用

@Component用于标识普通类

@Bean是在配置类中声明和配置Bean对象

2.使用方法不同

@Component是一个类级别的注解,Spring通过@ComponentScan注解扫描并注册为Bean

@Bean通过方法级别的注解使用,在配置类中手动声明和配置Bean

3.控制权不同

@Component注解修饰的类是由Spring框架来创建和初始化的

@Bean注解允许开发人员手动控制Bean的创建和配置过程

SpringBoot字段注入和构造函数注入的区别

在使用Spring开发项目时,我们经常需要使用依赖注入来管理对象之间的依赖关系。Spring提供了多种依赖注入方式,如构造函数注入、Setter方法注入和字段注入等。这些方式各有优缺点,需要根据具体情况选择合适的注入方式。

在本文中,我将分享我在开发过程中遇到的一些问题,以及我对这些问题的思考和解决方法。主要涉及以下几个方面:

  1. 字段注入和构造函数注入的区别和联系
  2. 为什么字段注入和Setter方法注入不会导致循环依赖的问题,而构造函数注入会导致循环依赖的问题
  3. 为什么Spring不推荐使用字段注入,而推荐使用构造函数注入

什么是字段注入和构造函数注入?
在SpringBoot中,我们可以使用@Autowired注解来实现依赖注入,即让Spring容器自动为我们的类提供所需的对象。有三种常见的注入方式:字段注入,Setter方法注入和构造函数注入。

  1. 字段注入:直接在类的属性上使用@Autowired注解,无需编写额外的代码。
  2. Setter方法注入:在类的Setter方法上使用@Autowired注解,需要编写相应的Setter方法。
  3. 构造函数注入:在类的构造函数上使用@Autowired注解,需要编写相应的构造函数。

下面是一个简单的例子,假设我们有一个UserService接口和一个UserServiceImpl实现类,以及一个UserController类,我们想要在UserController中使用UserService对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// UserService接口
public interface UserService {
void saveUser(User user);
}

// UserServiceImpl实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
// 保存用户到数据库
}
}

// UserController类
@Controller
public class UserController {
// 字段注入
@Autowired
private UserService userService;

// Setter方法注入
// private UserService userService;
// @Autowired
// public void setUserService(UserService userService) {
// this.userService = userService;
// }

// 构造函数注入
// private final UserService userService;
// @Autowired
// public UserController(UserService userService) {
// this.userService = userService;
// }

public void createUser(User user) {
userService.saveUser(user);
// 其他逻辑
}
}

这两种方式有什么区别?
这两种方式在功能上没有区别,都可以实现依赖注入。但是在一些细节上有一些差异,主要有以下几点 :

  1. 可读性:字段注入的代码更简洁,依赖项被隔离在一个地方,更容易阅读。构造函数注入的代码更冗长,当有多个依赖项时,构造函数可能会变得臃肿。
  2. 不变性:构造函数注入支持不变性,即可以将依赖项声明为final类型,保证对象创建后不会被修改。这有利于线程安全性,状态安全性和可读性。字段注入不支持不变性,无法将依赖项声明为final类型。
  3. 状态安全性:构造函数注入保证了对象被实例化为完整状态或完全不被实例化。如果使用者使用new关键字创建对象,则必须提供所有依赖项作为参数。字段注入无法保证状态安全性,如果使用者使用new关键字创建对象,则无法设置对象的状态。唯一的选择是使用反射设置私有字段。
  4. 循环依赖:循环依赖是指两个或多个类相互依赖对方,导致无法正常创建对象。例如,如果A类依赖B类,B类依赖A类,则会产生循环依赖。循环依赖是一种不良的设计模式,应该避免。

字段注入和Setter方法注入的联系
字段注入和Setter方法注入都是通过反射来实现的,它们都可以在类的属性上使用@Autowired注解来标注依赖关系。它们的区别在于,字段注入是直接在属性上使用@Autowired注解,而Setter方法注入是在属性对应的Setter方法上使用@Autowired注解。

字段注入和Setter方法注入的联系有以下几点:

  1. 它们都是基于名称或者类型来匹配依赖关系的。如果属性名字或者Setter方法名字与Bean定义中的id或者name相同,则按照名称匹配;否则按照属性类型或者Setter方法参数类型匹配。
  2. 它们都不支持不变性,即无法将依赖项声明为final类型。这可能会导致线程安全性,状态安全性和可读性的问题。
  3. 它们都可以避免循环依赖的问题,因为它们是在对象创建后才进行依赖注入的,而不是在对象创建时。这样可以避免构造函数注入时可能出现的循环依赖异常。

为什么字段注入和Setter方法注入不会导致循环依赖的问题?
循环依赖是指两个或多个类相互依赖对方,导致无法正常创建对象。例如,如果A类依赖B类,B类依赖A类,则会产生循环依赖。循环依赖是一种不良的设计模式,应该避免。

在Spring中,循环依赖主要发生在构造函数注入的情况下,因为构造函数注入是在对象创建时就进行依赖注入的,而不是在对象创建后。这样就会导致一个死锁的情况,即A类要等待B类创建完成才能创建,而B类又要等待A类创建完成才能创建。

字段注入和Setter方法注入不会导致循环依赖的问题,因为它们是在对象创建后才进行依赖注入的,而不是在对象创建时。这样就可以避免死锁的情况,即A类和B类都可以先创建出来,然后再互相注入对方。

Spring解决循环依赖的方法是通过提前暴露半成品对象(Early-Stage Object)来解决。半成品对象是指已经实例化但还没有完成初始化的对象。Spring会将半成品对象放入一个缓存中,当其他对象需要依赖它时,就可以从缓存中获取它,并进行后续的属性赋值和初始化操作。

两种方式的流程

字段注入和构造函数注入的流程如下:

字段注入:当IOC容器创建Bean时,它会先通过反射调用无参构造函数来实例化对象,然后再通过反射获取属性上的@Autowired注解,并根据名称或者类型来匹配依赖关系,最后通过反射将依赖关系注入到属性中。

构造函数注入:当IOC容器创建Bean时,它会先通过反射获取构造函数上的@Autowired注解,并根据名称或者类型来匹配依赖关系,然后再通过反射调用带参构造函数来实例化对象,并将依赖关系作为参数传递进去。

为什么Spring不推荐使用字段注入?

Spring不推荐使用字段注入的原因有以下几点:

字段注入违反了单一职责原则,因为它使得添加新的依赖项非常容易,而不会引起警告。这可能导致类有太多的责任和关注点,需要进一步的检查和重构。

字段注入隐藏了依赖关系,因为它没有使用公共接口(方法或构造函数)来清楚地与依赖项通信。这样就不利于类的可测试性和可重用性,也不利于依赖项的可选性和强制性的区分。

字段注入导致了依赖注入容器的耦合,因为它使得类无法脱离容器独立运行。这意味着类不能通过new关键字来创建,也不能切换到其他的依赖注入框架。

字段注入不支持不变性,因为它无法将依赖项声明为final类型,也无法注入静态变量。这可能会导致线程安全性,状态安全性和可读性的问题。

总结

字段注入和构造函数注入都是Spring中常用的依赖注入方式,它们各有优缺点,开发人员应根据具体情况选择合适的注入方式。一般来说,以下几点可以作为参考:

  1. 如果依赖关系是必须的,且不需要重新配置或者重新注入,则推荐使用构造函数注入,因为它可以支持不变性和状态安全性。
  2. 如果依赖关系是可选的,或者需要重新配置或者重新注入,则推荐使用字段注入或者Setter方法注入,因为它们可以提高代码的简洁性和灵活性。
  3. 如果有循环依赖的问题,则不能使用构造函数注入,只能使用字段注入或者Setter方法注入,因为它们可以避免死锁的情况。
Spring是如何整合MyBatis将Mapper接口注册为Bean的原理?

1.首先MyBatis的Mapper接口核心是JDK动态代理

2.Spring会排除接口,无法注册到IOC容器中

3.MyBatis实现了BeanDefinitionRegistryPostProcessor可以动态注册BeanDefinition

4.需要自定义扫描器(继承Spring内部扫描器ClassPathBeanDefinitionScanner)重写排除接口的方法(isCandidateComponent)

5.但是接口虽然注册成了BeanDefinition但是无法实例化Bean,因为接口无法实例化

6.需要将BeanDefinition的BeanClass,替换成JDK动态代理的实例(偷天换日

7.MyBatis通过FactoryBean的工厂方法设计模式可以自由控制Bean的实例化过程,可以在getObject方法中创建JDK动态代理

SpringCloud有哪些常用组件,作用是什么

1.Eureka: 注册中心
2.Nacos: 注册中心、配置中心
3.Consul: 注册中心、配置中心
4.Spring Cloud Config: 配置中心
5.Feign/OpenFeign: RPC调用
6.Kong: 服务网关
7.Zuul: 服务网关
8.Spring Cloud Gateway: 服务网关
9.Ribbon: 负载均衡
10.Spring CLoud sleuth: 链路追踪
11.Zipkin: 链路追踪
12.Seata: 分布式事务
13.Dubbo: RPC调用
14.Sentinel: 服务熔断
15.Hystrix: 服务熔断

SpringCloud和Dubbo有哪些区别

SpringCloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用框架,核心是解决服务调用间的问题,SpingCloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有SpingChoud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过SpringCioud和Dubbo并不是对立的,是可以结合起来一起使用的。

Dubbo是如何完成服务导出的

1.首先Dubbo会将程序员所使用的@Dubboserrce注解或@Senice注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
2.然后调用ServiceBean的export方法进行服务导出
3.然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
4.将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
5.还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等

Dubbo是如何完成服务引入的

1.当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么
2.然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
3.并绑定一些监听器用来监听动态配置中心的变更
4,然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean

Dubbo支持哪些负载均衡策略

1.随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
2.轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
3.最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
4.一致性哈希:相同参数的请求总是发到同一个服务提供者

epoll和poll的区别

1.select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件
2.poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件
3.epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询

HashMap的扩容机制原理

1.7版本
1.先生成新数组
遍历老数组中的每个位置上的链表上的每个元素
2.)
3.取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
将元素添加到新数组中去
4.
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本
1.先生成新数组
2.遍历老数组中的每个位置上的链表或红黑树
3.如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4.如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
a.统计每个下标位置的元素个数
b.如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
c.如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

接口优化

后端优化

1.缓存机制

2.并发调用

3.同步接口异步化

4.避免大事务

5.优化日志记录

数据库

1.数据库查询优化

2.表设计冗余数据

3.使用连接池管理数据库连接

4.使用数据压缩技术

5.加机器,或者换成合适的数据库

RocketMQ的事务消息是如何实现的

1.生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
2.再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
3.并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
4.一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
5.如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理

Redis

Redis是单线程吗?

Redis单线程指的是**{接受客户端请求->解析请求->进行数据读写扽操作->发送数据给客户端}这个过程是由一个线程(主线程)**来完成的,这是我们常说Redis是单线程的原因

但是,Redis程序并不是单线程的,Redis在启动的时候,是会启动后台线程 (BIO)

2.6版本,会启动2个后台线程,分别处理关闭文件,AOF刷盘这两个任务

4.0版本之后,新增了一个新的后台线程,用来异步释放Redis内存,也就是lazyfree线程

6.0版本之后,采用了多个I/O线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上。但是对于命令的执行,Redis仍然使用单线程来处理。

Redis持久化

方式:

AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里

RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘

混合持久化方式:Redis 4.0 新增的方式,集成了AOF和RBD的优点

Redis集群

如何是实现高可用

主从复制

主从复制时Redis高可用服务的最基础的保证,实现方案就是将从前的一台Redis服务器,同步数据到多台从Redis服务器上,即一主多从的模式,且从服务器之间采用的时(读写分离)的方式

注意,主从服务器之间的命令复制是异步进行的

哨兵模式

在使用Redis主从服务的时候,会有一个问题,就是当Redis的主从服务器出现故障宕机时,需要手动进行恢复

为了解决 这个问题,Redis增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能

切片集群模式

当Redis缓存数据量大到一台服务器无法缓存时,就需要使用Redis切片集群(Redis Cluster)方案,它将数据分布在不同的服务器上吗,以此来降低系统对单主节点的依赖,从而提高Redis服务的读写性能

Redis过期删除与内存淘汰

Redis使用的过期策略策略是(惰性删除+定时删除)这两种策略配合使用

1.惰性删除

不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key

优点

只会使用很少的系统资源,对CUP时间友好

缺点

造成一定的内存空间浪费,对内存不友好

2.定期删除

每一段时间(随机)从数据库中取出一定数量的key进行检查,并删除其中的过期key

优点

通过限制删除操作执行的时长和频率,来减少删除操作对CPU的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用

缺点

难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对CPU不友好,如果执行的太少,那又和惰性删除一样了,过期key占用的内存不会及时得到释放

Redis缓存设计

如何避免缓存雪崩?

大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户的用户请求,都无法在Redis中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩

解决方案

将缓存失效时间随机打散:我们可以在原有的失效时间基础上增加一个随机值(比如1到10分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率

设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题

如何避免缓存击穿?

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题

可以认为缓存击穿时缓存雪崩的一个子集,应对缓存击穿可以采取前面两种方案

1.互斥锁方案(Redis中使用setNX方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值

2.不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期时,提前通知后台线程更新缓存以及重新设置过期时间

如何避免缓存穿透?

当用户访问的数据,既不在缓存中,又不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求,那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题

缓存穿透的发生 一般有这两种情况:

1.业务误操作:缓存中的数据和数据库中的数据都被误删除了,所有导致缓存和数据库中都没有数据

2.黑客恶意攻击:故意大量访问某些读取不存在数据的业务

解决方案

非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

缓存更新策略

常见的缓存更新策略共有3种:

1.Cache Aside(旁路缓存)策略

2.Read/Write Through(读写/写穿)策略

3.Write Back(写回)策略

实际开发中,Redis和MySQL的更新策略用的是Cache Aside,另外两种策略应用不了

Cache Aside(旁路缓存)策略

可以细分为读策略写策略

写策略步骤:

先更新数据库中的数据,再删除缓存中的数据

读策略步骤

如何读取的数据命中了缓存,则直接返回数据

如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户

Redis实战
Redis 如何实现延迟队列?

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

1.在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消

2.打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单

3.点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单

在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

Redis管道有什么用?

管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个Redis命令,从而提高整个交互性能

使用管道技术可以解决多个命令执行时的网络等待,它是吧多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。

但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞

要注意的是,管道技术本质上是客户端提供的功能,而非Redis服务器的功能

如何用Redis实现分布式锁的?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源同一时刻只能被一个应用所使用

Redis的SET命令有个NX参数可以实现(key不存在才插入),所以可以用它来实现分布式锁:

1.如歌key不存在,则显示插入成功,可以用来表示加锁成功

2.如果key存在,则会显示插入失败,可以用来表示加锁失败

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件

1.加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁

2.锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间

3.锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端

满足这三个条件的分布式命令如下:

1
set lock_key unique_value NX PX 10000

lock_key 就是 key 键
unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

基于 Redis 实现分布式锁的优点:
1.性能高效(这是选择缓存实现分布式锁最核心的出发点)。
2.实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
3.避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。

基于 Redis 实现分布式锁的缺点:
1.超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。

  • 那么如何合理设置超时时间呢?我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。

2.Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)

它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

Redlock 算法加锁三个过程:

第一步是,客户端获取当前时间(t1)。

第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:

  1. 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。

  2. 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时 时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。

加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):

条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;

条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。

Redis数据结构的应用场景

Redis的数据结构:
1.**字符串:**可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个ison格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
2.**哈希表:**可以用来存储一些key-value对,更适合用来存储对象
3.**列表:**Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
4.**集合:**和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
5.**有序集合(ZSet):**集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

MySQL

MySQL执行流程

MySQL的架构共分为两层:Server层和存储引擎层

Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等)都在 Server 层实现。

存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。

第一步:连接器

1.连接的过程需要先经过TCP三次握手,因为MySQL是基于TCP协议进行传输的。

2.校验客户端的用户和密码,如果用户名或密码不对,则会报错

3.如果用户名和密码都对,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取的权限

第二步:查询缓存

连接器得工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。

如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。

如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。

这么看,查询缓存还挺有用,但是其实查询缓存挺鸡肋的。

对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,相当于缓存了个寂寞。

所以,MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。

对于 MySQL 8.0 之前的版本,如果想关闭查询缓存,我们可以通过将参数 query_cache_type 设置成 DEMAND

第三步:解析SQL

在正式执行SQL查询语句之前,MySQL会先对SQL语句做解析,这个工作交由(解析器)来完成

解析器

第一件事情,词法分析。MySQL 会根据你输入的字符串识别出关键字出来,例如,SQL语句 select username from userinfo,在分析之后,会得到4个Token,其中有2个Keyword,分别为select和from。

关键字 非关键字 关键字 非关键字
select username from userinfo

第二件事情,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。

如果我们输入的SQL语句语法不对,就会在解析器这个阶段报错。

第四步:执行SQL

经过解析器后,接着就要进入执行 SQL 查询语句的流程了,每条SELECT 查询语句流程主要可以分为下面这三个阶段:

1.prepare 阶段,也就是预处理阶段;
2.optimize 阶段,也就是优化阶段;
3.execute 阶段,也就是执行阶段;

预处理器

1.检查 SQL 查询语句中的表或者字段是否存在

2.将 select * 中的 * 符号,扩展为表上的所有列

优化器

经过预处理阶段后,还需要为 SQL 查询语句先制定一个执行计划,这个工作交由「优化器」来完成的。

优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。

例如查询语句(select * from product where id = 1),就是选择使用主键索引。

要想知道优化器选择了哪个索引,我们可以在查询语句最前面加个 explain 命令,这样就会输出这条 SQL 语句的执行计划,然后执行计划中的 key 就表示执行过程中使用了哪个索引。

explain的type字段(访问类型)的10个状态(从左到右,越靠左的越优秀)

NULL system const eq_ref ref ref_or_null index_merge range index ALL

执行器

经历完优化器后,就确定了执行方案,接下来 MySQL 就真正开始执行语句了,这个工作是由「执行器」完成的。在执行的过程中,执行器就会和存储引擎交互了,交互是以记录为单位的。

接下来,用三种方式执行过程,说一下执行器和存储引擎的交互过程

1.主键索引查询

以下面这个查询语句为例,看看执行器是怎么工作的。

1
select * from product where id = 1;

这条查询语句的查询条件用到了主键索引,而且是等值查询,同时主键 id 是唯一,不会有 id 相同的记录,所以优化器决定选用访问类型为 const 进行查询,也就是使用主键索引查询一条记录,那么执行器与存储引擎的执行流程是这样的:

  1. 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件 id = 1 交给存储引擎,让存储引擎定位符合条件的第一条记录。
  2. 存储引擎通过主键索引的 B+ 树结构定位到 id = 1的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
  3. 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
  4. 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 - 1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。

2.全表扫描

举个全表扫描的例子:

1
select * from product where name = 'iphone';

这条查询语句的查询条件没有用到索引,所以优化器决定选用访问类型为 ALL 进行查询,也就是全表扫描的方式查询,那么这时执行器与存储引擎的执行流程是这样的:

  1. 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 all,这个函数指针被指向为 InnoDB 引擎全扫描的接口,让存储引擎读取表中的第一条记录;
  2. 执行器会判断读到的这条记录的 name 是不是 iphone,如果不是则跳过;如果是则将记录发给客户的(是的没错,Server 层每从存储引擎读到一条记录就会发送给客户端,之所以客户端显示的时候是直接显示所有记录的,是因为客户端是等查询语句查询完成后,才会显示出所有的记录)。
  3. 执行器查询的过程是一个 while 循环,所以还会再查一次,会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 all,read_record 函数指针指向的还是 InnoDB 引擎全扫描的接口,所以接着向存储引擎层要求继续读刚才那条记录的下一条记录,存储引擎把下一条记录取出后就将其返回给执行器(Server层),执行器继续判断条件,不符合查询条件即跳过该记录,否则发送到客户端;
  4. 一直重复上述过程,直到存储引擎把表中的所有记录读完,然后向执行器(Server层) 返回了读取完毕的信息;
  5. 执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。

3.索引下推

索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将Server层部分负责的事情,交给存储引擎层去处理了

举一个具体的例子,age 和 height字段建立了联合索引(age,height):

1
select * from Students where age > 20 and height = 180;

联合索引当遇到范围查询 (>、<) 就会停止匹配,也就是 age 字段能用到联合索引,但是 height字段则无法利用到索引。

不使用索引下推(MySQL 5.6 之前的版本)时,执行器与存储引擎的执行流程是这样的:

  1. Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录
  2. 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给 Server 层
  3. Server 层在判断该记录的 height 是否等于 180,如果成立则将其发送给客户端;否则跳过该记录
  4. 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层
  5. 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,没有索引下推的时候,每查询到一条二级索引记录,都要进行回表操作,然后将记录返回给 Server,接着 Server 再判断该记录的 height是否等于 180。

而使用索引下推后,判断记录的 height是否等于 180 的工作交给了存储引擎层,过程如下 :

  1. Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录
  2. 存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引中包含的列(height列)的条件(height是否等于 180)是否成立。如果条件不成立,则直接跳过该二级索引。如果成立,则执行回表操作,将完成记录返回给 Server 层。
  3. Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
  4. 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,使用了索引下推后,虽然 height 列无法使用到联合索引,但是因为它包含在联合索引(age,height)里,所以直接在存储引擎过滤出满足 height= 180的记录后,才去执行回表操作获取整个记录。相比于没有使用索引下推,节省了很多回表操作。

当你发现执行计划里的 Extr 部分显示了 “Using index condition”,说明使用了索引下推。

总结
  • 连接器:建立连接,管理连接、校验用户身份

  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块

  • 解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型

  • 执行 SQL:执行 SQL 共有三个阶段:

    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。

    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划

    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

常见面试题
索引的分类

按照四个角度来分类索引

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

B树和B+树的区别,为什么MySQL使用B+树

B树的特点:

1.节点排序

2.一个节点可以存多个元素,多个元素也排序了

B+树的特点:

1.拥有B树的特点

2.叶子节点之间有指针

3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所以的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql!中一个nnodb页就是一个B+树节点,一个mnodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

Innodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,
以一个update语句为例:
1.Innodb在收到一个update语句后,会先根据条件找到数据所在的页,
并将该页缓存在Buffer Pool中
2.执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
3.针对update语句生成-个RedoLog对象,并存入LogBuffer中
4.针对update语句生成undolog日志,用于事务回滚
5.如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Bufer Pool中所修改的数据页持久化到磁盘中
6.如果事务回滚,则利用undolog日志进行回滚

MySQL的rewriteBatchedStatements的使用场景
批量保存方式 数据量(条) 耗时(ms)
单条循环插入 1000 121011
mybatis-plus saveBatch 1000 59927
mybatis-plus saveBatch(添加rewtire参数) 1000 2589
手动拼接sql 1000 2275
jdbc executeBatch 1000 55663
jdbc executeBatch(添加rewtire参数) 1000 324

所以如果有使用 JDBC的 Batch 性能方面的需求,要将 rewriteBatchedStatements 设置为 true,这样能提高很多性能。

然后如果喜欢手动拼接 sql 要注意一次拼接的数量,分批处理。

MySQL慢查询该如何优化?

1.检查是否走了索引,如果没有则优化SQL利用索引
2.检查所利用的索引,是否是最优索引
3.检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
4.检查表中数据是否过多,是否应该进行分库分表了
5.检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

MySQL锁有哪些,如何理解

按锁粒度分类:
1.行锁:锁某行数据,锁粒度最小,并发度高
2.表锁:锁整张表,锁粒度最大,并发度低
3.间隙锁:锁的是一个区间

还可以分为:
1.共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
2.排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读(不能加读锁),也不能写

还可以分为:
1.乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2.悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2025 Immanuel
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

微信