集合篇
为了满足hashmap集合的不重复存储,为什么要重写hashcode和equals方法?
首先理解一下hashmap的插入元素的前提:
hashmap会根据元素的hashcode取模进行比较,当hashcode相等时,会再次去比较元素之间的内容值,当内容值也相等时就代表元素重复。
所以当元素与元素之间的hashcode值
与内容值
相同时,hashmap就会认为元素重复
-
重写hashCode是为了让两个具有相同值的对象的Hash值相等。
-
重写equals方法是为了比较两个不同对象的值是否相等。
-
同时重写hashCode()与equals()是为了满足HashSet、HashMap等此类集合的相同对象的不重复存储。
当前有两个不同对象,他们的内容是相同的,但是在hashmap看来他们就是重复的,所以我们重写hashcode方法,保证相同值的两个对象的hashcode相同,重写equals方式是比较两个不同对象的值是否相同。
基础篇
== 与 equals的区别
默认
情况下 equals方法也是比较的两个对象之间的内存地址是否相同
,但是我们可以重写equals方法达到不同的效果
,如String类就重写equals方法,String类的equals方法会先去比较两个对象的内存是否相同,相同就返回true,如果不相同也不会立马放回false,而是会再次比较两个String对象的数值是否相同。
多线程
- interrupt()方法
interrupt方法用于中断线程,需要注意的是,只是将线程的状态设置为“中断”状态
,并没有真正的停止这个线程
;需要线程自己去监视(interrupted、isinterrupted)线程的状态为并做处理
通常与interrupted()、isinterrupted()配合使用,从而达到停止一个线程。
interrupted()、isinterrupted()都是监视当前线程的中断状态,当这两个方法返回的中断状态为true,可以使用return或者抛出异常来结束线程方法。
代码示例如下:
public class IsinterruptedTest {
public static void main(String[] args) {
Runnable runnable = () ->{
Thread thisThread = Thread.currentThread();
int num =0;
while (true){
// 检查当前中断标志是否为true
if (thisThread.isInterrupted()){
System.out.println("当前线程任务已被中断....");
return;
}
System.out.println(num++);
}
};
Thread thread =new Thread(runnable);
// 启动线程
thread.start();
// 让主线程休眠2ms,之后再去中断子线程
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断子线程
thread.interrupt();
}
}
- interrupted、isinterrupted的区别?
- 为何stop()和suspend()方法不推荐使用
stop方法: stop()是立即终止
一个线程,并立刻释放被它锁住的所有对象的锁
。当线程执行到一般时突然被终止后,可能会导致资源没有被正确释放,也会导致数据损坏、数据不一致的问题
。如一个线程任务中做了两件事(增加订单、减少库存),而这时线程执行一半就被强制终止了,就导致订单增加了,库存却没有减少,出现数据不一致问题
。
需要注意的是,通过stop()终止线程,finally代码块中的代码也不会被执行
;finally代码块通常用于资源的释放或者清理操作,所以会导致资源没有被正确释放
。
suspend():作用是挂起/暂停某个线程直到调用resume方法来恢复该线程,但是调用了suspend方法后并不会释放线程获取到的锁,容易造成死锁
。
-
sleep方法:放弃cpu使用权,使当前线程暂停一段时间,当前暂停时间结束后,会重新进入就绪状态并与其它线程等待cpu调度。sleep()会释放cpu资源,但是
不会释放同步锁(类锁、对象锁)
-
yield方法:与sleep方法相似,暂停线程,放弃cpu使用权,并
马上进入就绪状态
,等待cpu调度。不会释放同步锁(类锁、同步锁)
;需要注意的是:yield方法可能会不起作用,因为cpu调度是不可控制,我们无法控制cpu去调用指定的线程,所以可能会导致出现,当前线程调用了yield()放弃cpu使用权进入就绪状态后,cpu下次调用的线程还是当前线程。 -
锁池与等待池的区别:
每个对象都有一个同步锁/内置锁(互斥锁),同时也会锁池和等待池
锁池是用来存放那些想要获取对象锁,但是还没有拿到锁的线程
。当拿到锁资源后,线程会进入就绪状态。
等待池存放的是那些主动释放(wait)锁
去成全其它线程的线程。当等待池中的某一个线程被notfy、notfyall方法唤醒后,会进入锁池重新争夺锁,之后再从中断处继续执行任务。
线程池
线程池的好处
- 降低资源的消耗:利用线程池中已存在的线程重复执行任务,这样就可以不用每次都创建、销毁线程,有效的降低了资源的损耗
- 提高响应速度:当线程池已存在空闲线程,可以直接执行任务,不用等待线程的创建
- 有效管理线程:线程是稀缺资源,放入线程池中可有效管理线程,不用每次创建后就销毁,降低资源损耗。
线程池的七大参数
- maximumPoolSize:核心线程数,默认情况下,这些线程数被创建后不会被销毁的,除非设置了allowCoreThreadTimeOut。
- maximumPoolSize:线程池中最大线程数量,核心线程数也包含在里面
- keepAliveTime :空闲(非核心)线程存活时间,空闲线程会在指定时间内销毁
- unit :空闲时间单位
- workQueue :阻塞队列,异步任务基本都会放入阻塞队列中等待线程调用执行,注意是基本不是全部。
- threadFactory :线程工厂,线程池创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
- handler :拒绝策略,当阻塞队列&线程数都达到最大限制,会采用传入的拒绝策略。jdk也为我们提供了四种拒绝策略
常用阻塞队列
- ArrayBlockingQueue
基于数组
的有界阻塞队列
。队列先进先出
。 - LinkedBlockingQueue
基于链表
的阻塞队列,可以说是无界的队列
,当未指定容量时,则等于Integer.MAX.VALUE(2^31-1)
。 - DelayQueue
是一个无界的阻塞队列,可以实现延迟获取元素,所以添加进入队列的元素必须实现Delayed接口(指定延迟时间),在延迟期满后元素才被提取。调用put之类的方法加入元素
时,会触发Delayed接口中的compareTo方法进行排序
,也就是说队列中元素的顺序是按到期时间排序的
,而非它们进入队列的顺序
常用拒绝策略
- AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
- DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。
- DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。
- CallerRunsPolicy:调用者运行策略。该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由调用者线程执行该任务。
线程池的执行流程
- 先查看当前线程池的线程数量是否小于
核心线程数大小
,小于则新建线程并直接执行任务 - 大于则放入阻塞队列中,加入阻塞后,任务在未来某个时间点被一个空闲的线程取出执行,但是在加入到阻塞队列后,还会检查当前是否有线程存在(因为当核心线程数量设置为0的时候,程序也会执行到一步,但是会出现一个情况:阻塞队列里的任务无法被提取执行,因为当前线程池并没有线程,所有这里会创建一个线程)
- 当阻塞队列也满了,则会新建线程并立即执行任务
- 若线程数量也达到最大限制就会执行对应的拒绝策略
线程池的最大线程数量如何设计
- CPU密集型任务:
CPU数+1
,这是为了防止线程在执行任务时发生突发情况导致线程暂停,从而使CPU空闲,这时候多加一个线程就可以利用CPU的空闲时间去完成任务。 - IO密集型任务:
CPU数*2
,程序在执行IO操作时,是不会用到CPU的,此时的CPU是空闲,所以我们可以多设置一些线程去利用CPU的空闲时间。 - 混合型任务:
(线程等待时间+CPU使用时间)/CPU使用时间*CPU数
,如当前的任务都是1.5s的IO时间,0.5s的CPU使用时间,CPU核数是4,那最大的线程量应该为:(1.5+0.5)/0.5*4=4;
对于核心线程数:不管是什么样的任务,一般设计为CPU核数即可,这也是保证在没有任务的情况下,防止过多的线程一直未被销毁,占用系统资源。
在高并发的情况下,刚开始时,处理能力会稍微慢,但因为是高并发,所以我们的阻塞队列是会很快被填满的,当队列满了之后,就会创建我们相对应的最大线程数,此时处理能力就会上来。
线程池中的空闲线程能成为核心线程吗?
有一定概率;我们常说的空闲、核心线程只是一个概念,在线程池实际概念中并没有标识哪个是核心、空闲线程,线程池只会保留核心线程数大小的线程,其它线程就会被销毁,保留下来的就是核心线程。且销毁是随机的,那可能这次保留下来的核心线程,在下一次销毁时,核心线程是有可能被销毁,而它的位置就空闲线程替代。
线程只能在任务到达时才启动吗?
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程
使用队列有什么需要注意的吗?
使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出
线程池如何关闭
- shutdown():将线程池状态变成SHUTDOWN状态,此时不能再往线程池中添加任务,否则抛出异常。此时线程池不会立即关闭,而是等待线程池中所有任务完成后才关闭。
- shutdownNow():将线程池状态立即变成STOP方法,并尝试停止正在执行的任务,注意这里也并不是立即关闭任务,然后将未执行的任务都返回。
线程池的五种状态
- RUNNING:运行
状态说明:线程池处于RUNNING状态时,能够接收新任务以及对已添加的任务进行处理。
状态切换:线程池的初始状态为RUNNING。换句话说线程池一旦被创建,就处于RUNNING状态,且线程池中的任务数为0 - SHUTDOWN:关闭
- 状态说明:线程池处于SHUTDOWN状态时,不接收新任务,但能处理已添加的任务
- 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING->SHUTDOWN
- STOP:停止
- 状态说明:线程池处于STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
- 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING)或者(SHUTDOWN)->STOP
- TIDYING:调整
- 状态说明:当所有的任务已终止,队列任务数为0,线程池的状态会变为TIDYING状态;当线程池的状态变为TIDYING状态时,会调用钩子函数terminated(),该方法在ThreadPoolExecutor中是空的,若用户想在线程池变为TIDYING时进行相应的处理,就需要重载terminated()函数实现
- 状态切换:当线程池状态为SHUTDOWN时,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING
当线程池为STOP时,线程池中执行的任务为空时,就会又STOP->TIDYING
- TERMINATED:终止
- 状态说明:线程池彻底终止,就会变成TERMINATED状态
- 状态切换:线程池处于TIDYING状态时,调用terminated()就会由TIDYING->TERMINATED
Spring
Spring是什么
- 是一个轻量级的开源的JavaEE(企业开发)框架,使开发者只需要关心业务需求,不用操心配置文件的搭建
- 主要是为代码解耦,降低代码间的耦合度,让对象与对象(模块和模块)之间的关系不是由代码进行说明,而是用配置来说明
SpringIOC(控制反装)
解释:就是将对象创建和对象之间的调用过程,交给Spring管理,由Spring来管理对象的创建和销毁及对象之间的依赖关系,降低对象之间的耦合度
解耦如下:
没有引入IOC容器之前
对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候, 自己必须主动去创建对象B或者使用已经创建的对象B,无论是创建还是使用对象B,控制权都在自己手上
引入IOC容器之后
对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方
通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是"控制反转"这个名称的由来
”由主动行为变为了被动行为“:对象A之前需要自己创建B对象,此时对象A就依赖与对象B,对象A、B之间就耦合了,而此时引入IOC之后,对象A就不需要主动创建对象B,对象A等待Spring容器创建好之后去调用即可,对象A也不用关心对象B是怎么创建的。这里就弱化了对象A与对象B的直接联系,这里其实就起到了松耦合
的作用,转而加强了对IOC的联系。
虽然是加强了对IOC的联系,但是Spring的主旨就是这么干的: 主要是为代码解耦,降低代码间的耦合度,让对象与对象(模块和模块)之间的关系不是由代码进行说明,而是用配置来说明
如果你还是不太理解IOC,那么举个例子就比如说人饿了想要吃饭。如果不使用IOC的话,你就得自己去菜市场买菜、做饭才能吃上饭。用了IOC以后,你可以到一家饭店,想吃什么菜你点好就可以了,具体怎么做你不用关心,饭店做好了,服务员端上来你负责吃就可以了,其它的交给饭店来做
控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法
DI(依赖注入)的实现方式
在程序运行时,动态的向某个对象提供它所需要的其他对象
,主要有构造方法、setting方法、注解注入
- 构造方法注入,使用配置文件
以下是一个使用XML配置实现Spring依赖注入的示例。假设你有一个名为UserService的服务类,它依赖于UserRepository:
public class UserRepository {
// UserRepository的实现
}
public class UserService {
private UserRepository userRepository;
// 构造函数注入
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void getUserInfo() {
// 使用userRepository获取用户信息
}
}
这时我们需要创建一个配置文件-applicationConytext.xml,这个配置文件包含了对Bean的定义与依赖注入的篇日志,我们将UserService与UserRepository对象在配置文件定义好,并声明UserRepository对象可以构造方法的形式注入到UserService对象中
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置UserRepository Bean -->
<bean id="userRepository" class="com.example.UserRepository" />
<!-- 配置UserService Bean,并注入UserRepository -->
<bean id="userService" class="com.example.UserService">
// 这行代码是关键
<constructor-arg ref="userRepository" />
</bean>
</beans>
主要注意这一行: <constructor-arg ref="userRepository" />
,这是构造方法注入的关键,通过constructor-arg标签
将对象userRepository通过构造方法的形式注入到userService中
- setter方法注入,使用配置文件
业务代码
public class UserRepository {
// UserRepository的实现
}
public class UserService {
private UserRepository userRepository;
// 使用setter方法注入依赖
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void getUserInfo() {
// 使用userRepository获取用户信息
}
}
配置文件:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置UserRepository Bean -->
<bean id="userRepository" class="com.example.UserRepository" />
<!-- 配置UserService Bean,并使用setter方法注入UserRepository -->
<bean id="userService" class="com.example.UserService">
// 这行代码是关键
<property name="userRepository" ref="userRepository" />
</bean>
</beans>
主要注意这一行:<property name="userRepository" ref="userRepository" />
,这是构造方法注入的关键,通过property标签
将对象userRepository通过setter方法的形式注入到userService中
- 注解注入
假设你有一个名为UserService的服务类,它依赖于UserRepository
public class UserRepository {
// UserRepository的实现
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void getUserInfo() {
// 使用userRepository获取用户信息
}
}
接下来,你需要配置Spring框架以扫描注解并创建Bean。通常,这可以通过配置Spring的扫描包来实现。以下是一个示例Spring配置文件(applicationContext.xml):
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.example" />
</beans>
在上述配置中,<context:component-scan> 标签
用于指示Spring扫描指定包下的类,并自动创建Bean。这样,Spring会扫描 com.example 包下的所有类,包括UserService和UserRepository,并根据@Autowired和@Service注解来处理依赖注入
当然你也可以使用@Component
注解将对象注册为Spring容器的bean。这样就可以不使用配置文件的<context:component-scan> 标签
。区别就是context:component-scan 标签可以将指定包下的所有类注册为bean,放入到Spring容器中,而@Component注解只能将该注解修饰下的一个类注册为bean并放入到容器中。
Spring注解
在使用注解注入的时候,先理解几个注解的意思。
-
@Component:将一个类标识为Spring容器管理的Bean。
-
@Autowired:自动装配,通过 @Autowired 注解,Spring容器(IOC)会自动将匹配的Bean注入到标记了 @Autowired 的字段、方法参数或构造函数参数中。
-
@Service:标识一个类为服务层的Bean。与@Component相似,但提供了更明确的语义
-
@Controller:标识一个类为Spring MVC控制器
-
@Repository:标识一个类为数据访问层的Bean
-
@ComponentScan:会自动扫描包路径下面的所有被@Controller、@Service、@Repository、@Component 注解标识的类,然后装配到Spring容器中
-
@RequestMapping:用于映射web请求,包括访问路径和参数
-
@ResponseBody:将返回值转换为json格式并放入到response中。
-
@RequestBody:接收请求体中的json数据。
-
@PathVariable:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)声明的路径,将注解放在参数前,即可获取该值,通常作为Restful的接口实现方法。
-
@RestController:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合
-
@Configuration:标识一个类为配置类,可替换xml配置文件。通常与@bean注解配合使用
-
@Bean:定义一个bean,并放入到Spring容器中。
通常是与configuration一起使用,若是在非Spring管理的类中定义bean,则需要将该类添加到Spring容器中。如下:
public class Test{
@Bean
public MyBean myBean(){
return new MyBean();
}
}
这时你需要将Test类注册进入Spring容器中,MyBean这个bean才能被注册进入Spring容器中。
可以使用@Component注解,如下
@Component
public class Test{}
bean的作用域
总结:作用域session、request,在Spring中每次用户获取到的bean都是不同的,也就多例的
Spring的bean是线程安全的吗?不安全的话如何处理?
线程安全取决于bean的特性
,如bean的单例作用域,如果它是无状态的(线程中的操作不会对Bean的成员执行查询以外的操作)
,那么该bean就是线程安全的
。如果多个线程要对bean中变量/共享资源进行修改
,那它就是不安全的
;要想保证线程安全,可以使用同步锁(使用 synchronized 关键字),确保多个线程不会同时访问或者修改共享资源
BeanDeFinition是什么
在Spring框架中,“BeanDefinition”是一个很重要的概念,它描述了一个Bean实例的基本信息,每个被Spring容器管理的Bean都有一个对应的“BeanDefinition”,其中包含了它的类名、作用域、构造函数参数、属性值、Bean之间的依赖关系、初始方法和销毁方法。
bean的加载流程
- bean的启动阶段:Spring通过
读取器BeanDefinationReader
通过读取元信息(xml、properties、注解配置)将对象注册成为BeanDeFinition,并将BeanDeFinition以键值对的形式存储在BeanDefinationRegistry容器中,在实例化之前,BeanFactoryPostProcessor还可以对BeanDefinition进行一定程度上的修改与替换,如读取外部配置文件或数据库,替换$占位符为配置文件中的真实的数据、对 Bean 的属性值进行加工或者修改
- 实例化Bean,在实例化Bean之前,会先从缓存中查看是否有bean,没有则实例化,
参考文章:
Spring的Bean加载流程
Spring缓存
作用: 在Spring 容器启动时提高 Bean 的创建效率,特别是在解决循环依赖问题时发挥重要作用。
- 一级缓存:
存储的是完全初始化完成的Bean实例
。当Bean初始化完成后,会从二级缓存移动到一级缓存中
。 - 二级缓存:用于
存储尚未初始化完成的Bean实例(早期对象),通常是已经通过构造函数创建但尚未进行属性注入和初始化的Bean实例
,由三级缓存注入
。 - 三级缓存:
存储Bean工厂
,可以动态获取指定类型的对象实例
,它的作用是懒延迟加载对象实例
,及在被需要的时候去创建和获取对象。是解决循环依赖的重大功臣。
注:缓存中存放的都是单例模式下的bean
,原型模式下的bean直接创建就好,其中三个缓存都是互斥的,只会保持bean在一个缓存中
,而且,最终都会在一级缓存中
。获取bean顺序
:一级->二级->三级;存储bean:三级->二级->一级
这三个缓存都是在Spring容器启动时就创建并管理的
bean的生命周期
- 实例化bean:当客户向容器请求一个尚未初始化的bean时,容器就会调用doCreateBean()方法进行实例化,实际上就是通过反射的方式创建出一个bean对象
- Bean属性填充:通过获取BeanDefinition对象中的信息,注入这个Bean依赖的其它Bean对象
- 初始化Bean:第一步首先执行前置方法,我们也可以实现前置接口,对Bean进行一些自定义的前置处理,第二步查看是否实现
InitializingBean
接口,将会执行接口中的初始化方法,第三步如果在配置文件中有定义init-method属性,执行执行用户自定义的初始化方法,其中第二、三步只会执行一种,首先查询InitializingBean
接口;第四步执行后置处理方法。 - 查询是否生成代理对象
- 销毁Bean
参考链接:
Spring中bean的生命周期
Spring循环依赖
循环是指对象与对象互相调用或者间接调用
,最后形成一个闭环的依赖;如对象A依赖对象B,对象B也依赖对象A;或者A依赖B,B依赖C,C依赖A。
Spring是通过缓存来解决部分循环依赖的,若是在原型模式下的循环依赖是无法解决的,如A依赖B,B依赖A,A在实例化好发现属性注入时需要B就去实例化吧B,实例化B时发现依赖A,这时发现依赖A,这时它会实例化一个新A,因为A、B是原型模式的,所以这样会导致无限实例化,导致内存溢出。
解决单例模式的循环依赖:
Spring解决循环依赖的核心思想在于提前曝光(已实例化bean,但未属性注入及初始化),
先从一级缓存中找BeanA,未找到就去二级缓存,还未找到就去三级缓存中找到bean工厂并立马将该beanA从三级缓存中移动到二级缓存中,之后对A进行属性注入是依赖beanB,就再次走一遍流程,将B放入到二级缓存中后进行属性注入时,发现依赖A,但此时可以在二级缓存获取A,所以B就可以完成属性注入和初始化,初始化完成之后就将B放入到一级缓存中,此时A就可以从一级缓存中获取B从而完成属性注入及初始化,自此解决了循环依赖。
Spring缓存解决循环依赖的流程
- Spring实例化bean后,会判断当前容器是否支持循环依赖(默认为true)&&当前bean是否正在创建中&&当前bean是否单例,为true的情况下,会将当前bean封装成beanFactory放入三级缓存中。并且将当前beanName放入到一个singletonsCurrentlyInCreation集合容器(正在创建的bean)
- 属性注入时,会先从单例池中依赖对象,没有就去正在创建的bean容器找,没有就从二级缓存中找,再没有就从三级缓存中,找到后就从三级缓存中取出,在取出的同时并判断当前依赖对象是否循环依赖了,是则返回代理对象(生成代理的同时,会将代理对象放入到代理容器earlyProxyReferences中存储),否则返回普通对象,并将它放入到二级缓存中,同时在三级缓存中删除当前beanFactory。
- 初始化
- 其它步骤:生成代理对象->判断是否需要生成代理,需要则根据beanName从代理容器取出bean并与当前bean比较,如果已经完成AOP,就不再处理。
- 最终放入当一级缓存中
参考文献:
Spring源码学习(九)–循环依赖
Spring为什么要用三级缓存解决循环依赖
一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的【享学Spring】
一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的【享学Spring】
Spring无法解决的循环依赖
- 构造方法导致的循环依赖:构造方法注入的依赖对象必须是实例化好的,而构造方法是发生在对象实例化的过程中的,也就是说两个对象通过构造方法循环依赖会导致一直等待。也就是说,对象A在实例化过程中通过构造方法需要传入已实例化好的对象B,此时A没有实例化完成,这里去创建对象B,在实例化B过程中,需要传入已实例化好的A,但A没有完成实例化,这里A、B就会一直等待对方实例化完成。示例代码如下:
class A{
private B b;
public A(B b){
this.b=b;
}
}
class B{
private A a;
public B(A a){
this.a=a;
}
}
- 原型模式中bean的循环依赖:这是因为原型模式中,每次请求时都会创建一个新的实例,对象A依赖对象B,此时对象A实例化好了,set属性注入时需要对象B,创建对象B,发现需要对象A,此时会再次创建新的对象A,不会引用旧A。最后导致类增多,内存溢出。
Spring AOP(面向切面)
AOP(面向切面):就是在不修改源代码(业务代码)的情况下对程序(方法)添加额外的功能
,主要场景有日志记录、权限管理、异常处理等方面。
AOP术语
joinpoint 连接点
:就是指那些潜在的可以被拦截(增强)的方法,方法一旦被拦截就转为切点,切点的候选。这里可以理解成 所有方法都可以时连接点pointcut 切入点
:在那些方法上切入。即被拦截的连接点。advice 通知
:指拦截到连接点之后要做的事,即对切入点增强的内容,通知有很多种,如下
befor(前置通知):通知方法在目标方法(切入点)调用之前执行
after-returing(返回后通知):通知方法在目标方法返回后执行
after-throwing(抛出异常通知):通知方法在目标方法抛出异常后执行
after(后置通知):通知方法在目标方法返回或异常后执行
around(环绕通知):环绕相比其它通知更加灵活,可以在方法中自定义业务逻辑,可以同时实现其它通知,甚至决定目标方法是否继续执行!
切面
:用一种声明式(在类中修饰注解@Aspect
)的方式来描述横切关注点的实现,切面是一个模块化的类,是切入点和通知的组合
;Target(目标对象)
:切入点的对象(被拦截的方法的对象)。Weaving(织入)
:将通知和目标对象的功能织入到代理对象中,以实现切面的横切关注点。Proxy(代理对象)
:代理对象包装了目标对象,并包含了切面的通知代码。客户端代码通常与代理对象交互,因此客户端对目标对象的调用实际上是通过代理对象进行的
通知的执行顺序
静态代理和动态代理
静态代理:
优点:在编译时创建代理类,代理类编译后存在,与目标对象的关系在编译时已经确定。代码易于理解和实现
缺点:不灵活:静态代理需要为每个被代理的类创建一个代理类,导致类增加。
动态代理:
优点:在运行时创建代理类,代理类在程序运行期间动态生成,减少了代码的重复和冗余
缺点:相对于静态代理,动态代理的实现更加复杂,需要深入理解反射机制和代理类的生成。
静态、动态代理示例代码可以参考下面这篇文章:
静态代理和动态代理
JDK动态代理与CGLIB动态代理
JDK动态代理:
优点:Java本身支持,随着版本稳定升级
缺点:目标类必须实现某个接口,没有实现接口的类型不能生成代理对象;代理的方法必须申明在接口中,否则无法代理;执行速度性能相对于cglib较低。
CGLIB动态代理:
优点:目标类无需实现接口;但是会针对目标类生成一个子类,覆盖其中的所有方法;执行速度性能会比JDK代理高
缺点:若是目标类和目标类中的方法被final修饰,则无法代理;动态创建代理对象的代价比较高
可以参考下面这篇文章:
谁与争锋,JDK动态代理大战CGLib动态代理
文章中JDK、CGLIB代理都没有明确使用通知(Advice)方法,而是仅演示了如何使用代理创建对象和调用方法。这里只是演示如何创建的,在实际开发中JDK、CGLIB动态代理是不用我们自己实现的,Spring会自己创建。在Spring中默认动态代理策略是智能的,若是目标类没有实现任何接口,Spring会尝试使用CGLIB动态代理来创建代理对象,反之则使用JDK动态代理。
实际开发中使用AOP可以参考下面链接
基于springboot实现一个简单的aop
Spring缓存
缓存是一种将数据临时存储在内存中,以便后续访问时可以快速获取数据。
好处及作用:
提高性能、降低延迟、提高并发能力、减少资源消耗、实现数据共享:将数据放入到缓存中,实现资源共享、提高并发能力,同时减少底层资源(数据库、文件)的访问次数,降低资源消耗。
SpringBoot
SpringBoot是Spring开源组织下的子项目,主要简化了Spring的难度,减去了繁重的配置,是开发者能快速上手。
约定大约配置是什么意思
约定大于配置是一种开发原则,就是为了减少人为的配置数量,能使用默认的配置就使用默认的;默认配置就是所谓的“约定”;当存在特殊需求的时候,也可以自定义配置覆盖默认配置。,如我们需要在SpringBoot使用redis,我们需要去连接redis,需要用到URL、端口、密码等,SpringBoot其实就有默认的约定,默认连接本地端口6379的redis,没有密码
SpringBoot的核心注解
- @SpringBootApplication:标识该类为SpringBoot应用程序的主配置类;该类的main方法用于启动SpringBoot程序,在main方法中写入代码SpringApplication.run(xxx.class,args)可启动Spring容器。它是一个组合注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration、和@ComponentScan。
- @SpringBootConfiguration:标识这是一个配置类;被@configuration修饰。二者功能一致。
- @ComponentScan:开启自动配置。
SpringBoot支持什么前端模板
thymeleaf、freemarker、jsp
SpringBoot实现热部署有哪几种方式?
热部署:就是程序检测到代码改动后会自动重新启动SpringBoot项目,程序员就不用手动的重启项目了。
主要有两种方式
- Spring Loaded
- Spring-boot-devtools
SpringBoot事务的使用
首先使用注解@EnableTransactionManagement开启事务管理,然后在对应的方法添加注解@Transactional即可
SpringBoot有哪几种读取配置的方式
Spring Boot 可以通过 @PropertySource,@Value, @ConfigurationPropertie注
解来读取配置并赋值。
bootstrap.properties 和application.properties 有何区别 ?
- 记载顺序:bootstrap(.yml、properties)会比application配置文件先加载。
- 属性覆盖:如果同一个属性在bootstrap和application中都有定义,bootstrap 中的属性将会覆盖 application 中的属性。这意味着 bootstrap 具有更高的优先级
Spring Profiles
Spring Profiles (Spring配置文件)是Spring框架中的一种机制,可以让程序在不同环境下使用不用的配置。对于开发、测试、生产环境之间的配置切换非常有用。
当前有三个配置文件,application.yml、application-dev.yml、application-test.yml,你可以在application.yml指定使用哪套配置文件,被指定的配置文件与application.yml会组合成一个配置文件。
applicatiom.yml:
spring:
profiles:
active: dev // 指定配置文件
application-dev.yml
my:
applicationName: dev
application-test.yml
my:
applicationName: test
SpringBoot如何实现跨域
跨域是指定客户端在使用浏览器/app发生ajax请求时,会触发同源策略(协议、ip
、端口必须相同),当发现请求的服务器地址不同源时就会出现跨域
解决跨域常用有两种方式
- jsonp:前端通过jsonp来解决,但是jsonp只能发生get请求。弊端很明显
- CROS
常见的解决方案有 实现WebMvcConfigurer接口并重写addCorsMappings方法解决跨域问题
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}
如何使用 Spring Boot 实现全局异常处理
可以结合注解 @RestControllerAdvice、@ExceptionHandler 一起使用实现全局异常处理
- @RestControllerAdvice:可以用于定于是一个组合注解,內部包括注解@ResponseBody、@ControllerAdvice
- @ControllerAdvice:可以对整个应用的控制器(所有的controller中的所有方法)进行一致性的处理。常用于定义全局异常处理、全局数据绑定、全局数据预处理等功能。
- @ExceptionHandler:用于在控制器中处理异常。当控制器中的方法抛出指定类型异常时,该注解修饰的方法就会被调用。
Spring Boot 中如何实现定时任务 ?
@Scheduled注解:用于创建定时任务;指定方法在特定的时间间隔或时间点执行。
MYSQL
MYSQL的锁
全局锁
使用全局锁之后,数据库的所有数据就处于只读状态,后续对于数据的CRUD操作都会被阻塞。
一般是用来进行数据备份的。对于MYISAM搜索引擎,它不支持事务,因此在备份数据的时候就需要使用全局锁来处理了。
表锁
表锁分为以下三种
- 表锁
表锁:作用是对于某个特定的表加锁,支持读锁与写锁。
- 表共享读锁:允许其它线程/事务读表,但不能写
- 表独占写锁:
写锁只存在一个
。其它线程/事务就不能再对这个表加任何读锁或写锁。
读锁与写锁之间:读读共享,读写互斥,写写互斥
- 元数据锁(MDL)
MDL加锁过程是系统控制,也就是说对于用户而言是不透明的。MDL锁主要是为了解决DDL操作与DML操作之间的数据一致性问题。內部也是有写锁与读锁
- 读锁:当我们对一个表的数据进行CRUD(DML)操作时,会自动加MDL读锁。
- 写锁:当我们对一个表的结构(删除字段、更改字段的数据类型)进行更改时(DDL操作),会自动加MDL写锁,拿到写锁也是可以对表的数据进行DML操作的,但是需要注意的是
MDL写锁也只会存在一个
。
其中 读读共享,读写互斥,写写互斥
参考:MySQL(十二)MDL锁介绍
- Autu-INC锁
Auto-INC锁是InnoDB存储引擎用来管理自增长列(主键自增)的一个锁。该锁确保每个事务获取的自增值是唯一的且是按照递增的顺序进行分配的。 其中auto-inc锁分为以下三种模式
- 传统模式
innodb_autoinc_lock_mode=0;在传统模式中,事务结束后才会释放锁
。事务中如果有对表进行添加操作添加1/n条数据,该事务就会获取该表的auto-inc锁,其它事务若想得到该表的auto-inc锁则会阻塞,直到该事务提交。这种模式保证了在事务中进行了多次添加操作,都可以保证该事务中添加的数据的自增值都是连续的。但有一个很大的弊端:在高并发的情况下,所有事务都需要排队获取auto-inc锁,性能与并发度不是很高。
- 连续模式
innodb_autoinc_lock_mode=0;截止到现在,mysql默认的auto-inc锁是连续模式;,连续模式就是在性能和自增值连续性之间进行一个折中选择;某种情况下放弃连续性保证性能。 连续模式针对单条/批量插入语句会出现以下两种情况。
单条插入:对于A事务中单行的插入语句,mysql在生成自增值后就会立即释放auto-inc锁。这时其它事务不用等待事务/插入语句结束才能拿到auto-inc锁。完全放弃了连续性保并发性能。实例如下:
当前有事务A、B,事务中有两条插入语句,事务B插入一条数据
流程:事务A插入第一条数据并释放锁 此时事务A需要做其它表的DML操作导致事务B拿到auto-inc锁,事务B获取到锁插入一条数据,事务A再次获取锁插入一条数据。锁的资源占用顺序为:A->B->A 来看看事务A中的添加的两条数据自增值是否是连续的
事务A:
begin
insert into auto_inc_test(name)
value ('A1');
接着事务A需要执行其它表的DML操作。
事务A开启事务并添加第一条数据。insert执行完后释放了auto-inc锁,接着事务A需要执行其它表的DML操作。导致事务B拿到了该表的auto-inc锁。
事务B:
begin
insert into auto_inc_test(name)
value ('B');
commit
事务B开启事务,获取到auto-inc锁添加了一条数据并释放锁,同时提交事务
事务A:
insert into auto_inc_test(name)
value ('A2');
commit
事务A再次获取auto-inc锁,并添加数据及提交事务
此时我们查看auto_inc_test表中的数据:
可以发现在事务A中插入的两条数据的自增值并不是连续的,所以在连续模式中针对单条的insert语句,是完全放弃了连续性保证性能。
批量插入:对于批量插入操作,事务会一直持有auto-inc锁直到语句结束。在语句结束后批量插入的数据的自增值是连续的,一定程度上放弃了连续性并保证性能。
当前有事务A、B,事务中有两条批量插入语句,事务B一条批量插入语句。
流程:事务A执行第一条批量插入并释放锁,此时事务A需要做其它表的DML操作导致事务B拿到auto-inc锁,事务B获取到锁执行批量插入,事务A再次获取锁执行批量数据。
事务A:
begin
insert into auto_inc_test(name) values
('A1'),
('A2')
接着事务A需要执行其它表的DML操作。
事务A获取auto-inc锁,执行第一条批量插入语句,语句执行完成后释放锁。
事务B:
begin
insert into auto_inc_test(name) values
('B1'),
('B2')
COMMIT
事务B获取auto-inc锁,执行批量插入语句后释放锁并提交事务
insert into auto_inc_test(name) values
('A3'),
('A4')
COMMIT
事务A再次获取auto-inc锁,执行完第二条插入语句后释放锁并提交事务
我们此时来看看事务A中批量插入的数据的自增值是否是连续,两条批量插入的数据的自增值是否是连续的
可以看到单条批量插入的数据是连续的,但是在同一个事务中多条批量插入语句之间数据的自增值并不连续。也就是该情况下 放弃一定程度的连续性保证性能。
- 交错模式
innodb_autoinc_lock_mode=2;它完全放弃了自增值的连续,从而提高并发插入的性能。无论是单条/批量插入时,锁都是在生成自增值后立即释放。与连续模式中的批量插入不同的是,连续模式是批量插入语句执行完成之后才释放,也就是说在交错模式下可能会出现以下情况。
当前事务A、B都同时执行了批量插入语句。那他们的自增值可能会出现下面的情况。
事务A:
begin
inert into auto_inc_test(name) values
('A1'),
('A2')
commit
事务B:
begin
insert into auto_inc_test(name) values
('B1'),
('B2')
commit
仔细看自增值的连续性是完成没办法保证的,而连续模式的批量插入一定程度上是可以保证单次批量插入语句的数据的自增值是连续的。但交错模式的并发性能肯定是会比连续模式好的。
行锁
作用:是InnoDB引擎锁定数据表中的特定行,是最细粒度的锁,可以最大程度的支持并发处理,但是也很容易造成死锁。 支持共享锁及排它锁。
- 共享锁:加锁语句:LOCK IN SHARE MODE ,对于读操作(如SELECT table where … LOCK IN SHARE MODE查询),行锁通常在读取操作完成后隐式释放;需要注意的是:普通的SELECT语句是不会加共享锁的,必须显示拼接加锁语句(LOCK IN SHARE MODE)
- 排它锁:加锁语句:FOR UPDATE;对于写操作(如INSERT、UPDATE、DELETE),行锁通常在对数据行的修改操作完成后隐式释放,INSERT、UPDATE、DELETE语句会默认加上排它锁,不用显示拼接加锁语句。
注意:
- 行锁与表锁之间也会互斥的,其中
行读锁与表读锁兼容,行读锁与表读锁互斥,行写锁与表锁互斥
- 锁与锁之间的关系为:
读读共享、读写阻塞
。锁的获取与释放都是隐式的,通常在对应的操作完成后锁会隐式释放,但是对于事务,行锁就必须要等待事务提交后才会释放,这是因为事务的一致性要求,需要确保整个事务内的操作是原子的,要么全部执行成功,要么全部回滚(auto-inc锁除外)。这就是为什么说行锁很容易造成死锁。
在这种情况下,就很容易造成死锁。
当事务B发现获取锁的资源被事务A占用,而事务A刚好也在等待事务B的锁资源,MYSQL就会立马判定这是死锁,会回滚某一事务从而解决死锁。
如果事务获取锁的等待时间超过时间限制就会被认为是死锁的一部分。
当MYSQL检测到死锁的存在,会自动中止/回滚某一个事务来解决死锁。但是对于回滚事务也是会占用系统资源(CPU)的,所以我们应尽量避免死锁。
间隙锁(Gap lock)
- 锁定范围:间隙锁用于
锁定一个范围内的索引键之间的间隙(Gap),包括范围的起始点和结束点
。只锁定索引键值之间的间隙!!!!
- 目的:
防止其他事务在指定范围内插入新的数据
,确保查询结果的一致性和事务隔离性,尤其用于防止幻读现象。 - 例子:如果执行一个范围查询 SELECT * FROM table WHERE id > 100 AND id < 200 FOR UPDATE;,间隙锁会锁定 id 值在 100 到 200 之间的所有索引键值的间隙,阻止其他事务在这个范围内插入新的数据。
临键锁(Next-Key Lock)
- 锁定范围:临键锁是
间隙锁和行锁的组合
,它不仅锁定了一个索引键值范围,还锁定了范围内的所有数据行,确保范围内的数据行不被其他事务修改或插入
。除了锁定索引范围外,还锁定了范围内的所有数据行。
- 目的:解决幻读问题,除了
防止插入外,还防止范围内数据的修改。
- 例子:类似间隙锁,临键锁也可以用于范围查询,但它不仅锁定了范围内的索引键值,还锁定了范围内的所有数据行,阻止其他事务在范围内插入新的数据或修改已有数据。
如何避免死锁?
- 让两个线程/事务的获取锁的顺序一致,这样就可以避免死锁。
- 可以限制锁的持有时间,或者是完成锁的操作后立即释放锁,减少死锁的风险。
如何解决死锁?
- 在mysql中会自动处理死锁,当死锁产生时,会终止/回滚一个或者多个事务来解决死锁。
- 在Java中是需要手动处理死锁的,也就是说我们需要自己找到死锁的线程,从而终止线程来解决线程,但是要找到死锁的线程是非常难的,所以在java中没有很好的办法去解决已经产生的死锁,所以编写代码时就需要去思考避免死锁。
意向锁
什么是意向锁?
加意向锁的目的是为了表明某个事务正在锁定一行或者将要锁定一行。表名加锁的“意图”。
意
向锁的粒度也可以看成是表级别的锁;支持共享锁与排它锁。
- 意向共享锁:表明事务打算在表中的行上设置共享锁。
- 意向排它锁:表明事务打算在表中的行上设置共享锁。
它的作用:
- 加快上层资源对下层资源是否被占用的检查时间。
场景:在innoDB引擎中一个事务A正在对某个表T的第r行加了写锁,另一个事务B尝试去对整个表做操作,B尝试去对整个表加一个写锁。
那它此时就要检查这张表是否有行锁,不管是有行写锁还是行读锁,此时如果要加入表的写锁都会被阻塞。
那mysql如何判断表中是否有行锁?
未引入意向锁前:一行一行查询是否有行锁,如果都没加,就代表可以锁表。但这样的效率太低,数据表的数据可能是百万级别的。检索起来非常麻烦。
引入意向锁后:想要加入表锁,直接去获取意向锁即可,如果获取不到,就代表有行锁,就不能锁表。
引入意向锁后:事务A想对某一个加行锁,会先对表加上意向锁,之后再对具体行加上行锁。
事务A加行读锁:事务A—> 加意向共享锁---->加行共享锁。
事务A加行写锁:事务A----> 加意向排它锁---->加行排它锁。
这样就可以快速检测到这个表是否有行锁锁定。
注意:意向锁之间是相互兼容的
为什么是都是兼容的?
事务A加了表的IX锁,或者IS锁,只代表事务A已锁定一行或者将要锁定一行。事务B当然也可以锁定其他的行,所以事务B肯定也是可以获得表的IS锁或者IX锁的
关于意向锁的理解:
意向锁的真正作用是加快加入表锁时查询表中是否有互斥的行锁的检索效率
。所以加入表锁时,意向锁会先比较它与表锁的兼容关系;而加入行锁时,意向锁都是兼容的,这是因为锁定的数据行不同,即使锁定的数据行一致,也会是兼容的,这是因为它的作用是针对加入表锁时对行锁的检索效率,如果此时两个事务同时对一个行加入行写锁,也是能共享意向排它锁的,但是其中一个事务还是会阻塞,这是因为行锁的互斥条件。
这样也说明了 innoDB引擎支持表锁与行锁共存。
意向锁与表锁之间的兼容关系如下:
这里的S锁和X锁是表级别的,意向锁不会与行级别的S锁和X锁冲突
参考:意向锁的理解
索引
索引是帮助MYSQL高效获取数据的数据结构。简单来说,数据库索引就像是书前面的目录,能加快数据库的查询速度。
索引会占据磁盘空间,索引虽然会提高查询速度,但是会降低更新表的效率。比如每次对表进行增删改操作,MYSQL不仅要保存数据,还要保存或更新对应的索引文件。
常见的索引
- 唯一索引:索引列中的所有值必须是唯一的,允许有空值。
- 主键索引:是一种特殊的唯一索引,每个表只能有一个主键索引,不允许有控制。
- 普通索引:是最基本的索引类型,没有唯一性或空值的限制。
- 组合索引:将多个列组合在一起形成的索引,可以加速多列的查询。但要遵循最左前缀原则。
- 全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用,通过关键字找到记录行。
B+tree指的是三层结构的树状结构类型,相对于二叉树而言,会降低IO成本。
在B+tree中叶子节点指的是最底层的节点,其它层都叫非叶子节点,根节点也可以被称为非叶子节点,B+tree中的根节点可以是由多个节点组成的。
在B+tree中,叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。这样做的好处是使用范围查询时,不用每次都需要回到根节点重新遍历查找,而是从叶子节点中往后遍历即可。
B树:其它都与B+tree相同,但是叶子节点之间是没有双向指针相连的。当MYSQL使用范围查询时,B树的查询效率相对于B+tree而言就慢许多。
聚簇索引:也叫主键索引,在InnoDB中,如果表中没有设置主键索引,MYSQL会创建一个6字节的长整型的列作为主键索引。
辅助索引:除聚簇(主键)索引之外的所有索引都称为辅助索引,InnoDB的辅助索引的叶子节点只会存储主键值而非行记录。
mysql中不同的存储引擎下的主键索引文件和非主键索引文件的叶子节点和非叶子存储的信息都有哪些
InnoDB存储引擎:
主键索引文件:
- 非叶子节点:存储
主键值
和``子节点指针 - 叶子节点:存储
实际行数据
辅助索引文件
- 非叶子节点:存储
索引列值
和子节点指针 - 叶子节点:存储
索引列值
和主键值
MyIsam存储引擎
主键和辅助索引文件
- 非叶子节点:存储
索引列值
和子节点指针
- 叶子节点:存储
索引列值
和数据行地址的指针
回表
定义:通过索引定位到数据行后,不满足返回条件,还需要再次访问表来获取完整的行数据的过程
在innoDB存储引擎中,如果是主键索引文件,叶子节点中存储的是整行的记录
,在这种情况下是大部分是不会出现回表现象;但是如果行数据过大或者定义了可变长度的列(如VARCHAR、BLOB)可能只存储了部分数据,需要回表操作获取完整数据
在MYISAM存储引擎,主键索引文件的叶子节点存储的是行记录的内存记录
,而不是完整的数据行,所以在MYISAM存储引擎中,主键索引文件是有回表操作的。
不管是innoDB还是MYISAM存储引擎,如果是非主键索引文件,叶子节点存储的是索引键值和指向主键的指针。
索引覆盖
通常是指一个查询可以通过索引直接满足所有需要返回的列,及返回的所有列都包含在一个索引中
,这种情况就叫覆盖索引。
覆盖索引可以避免出现回表操作。
最左前缀法则
我们在使用组合索引时,最好遵循最左前缀法则,否则在查询时MYSQL会放弃索引查询,而去选择全表查询,这时效率就会低效。
最左前缀法则:如果你创建一个多列索引,查询中的条件必须从索引的最左侧的列开始,并且不能跳过前面的列,按照一定顺序命中索引。这样的查询才能充分利用索引。
这里可以参考:原创:史上最全最通俗易懂的,索引最左前缀匹配原则(认真脸)
其中关于使用EXPLAIN语句中出现type列的解释如下
- type=index:表示索引的全值匹配或前缀匹配;mysql使用索引查询,从索引文件中的第一个节点数据查找到最后一个节点数据,直到找到符合判断条件的某个索引
- ref:表示索引的部分匹配;只是用索引文件的部分节点
- range:表示 MySQL 在索引的一个范围内进行扫描
- all:表示表执行全表扫描,不使用索引。查找表中每一行及每一列数据,效率最慢。
索引失效的几种情况:
- 语句前后没有同时使用索引。当 or 左右查询字段只有一个是索引,该索引失效,只有左右查询字段均为索引时,才会生效;
- 数据类型出现隐式转化。如 varchar 不加单引号的话可能会自动转换为 int 型,使索引无效,产生全表扫描;
- 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的
- 当 MySQL 觉得全表扫描更快时(数据少);
具体使用全表扫描还是索引查询,取决于查询优化器的决策。
索引篇参考文章如下:
存储引擎
InnoDB存储引擎
- 支持事务:提供ACID(原子性、一致性、隔离性、持久性)事务特性,使InnoDB可以更好的处理复杂的业务逻辑及保证数据的一致性。
- 支持行锁:锁定粒度更小,有助于提高并发性能。
- 支持行锁与表锁并存:在特定的场景下,支持意向锁和行锁共存。
- 自动增长列:聚簇(主键)索引,方便插入新数据生成唯一的值。
- 支持外键:可以定义和管理表之间的关系,确保数据的完整性。
- 崩溃恢复:通过事务日志(redo log)实现崩溃恢复功能,可以在数据库崩溃后还原未完成的事务。
InnoDB适用于需要事务支持和高并发的场景。可以更好的确保数据的一致性。
MyISAM存储引擎
- 支持表锁:用表级锁定,这意味着在对数据进行并发访问和修改时,会锁定整个表,可能导致并发性能下降。
- 全文索引:支持全文索引,适用于对文本(CHAR,VARCHAR,TEXT)类型进行搜索的场景。如倒排索引
MyISAM在读取密集型的场景中可能更有优势,MyISAM不支持事务,可能在某些情况下无法保证数据一致性。
为什么说读取会比InnoDB引擎快,这是因为在使用辅助索引进行查询时,不会再出现回表的现象。因为辅助索引的叶子节点存储的也是这行数据的内存地址。
事务
一个事务由一组DML语句组成的,事务内的DML语句要么同时成功、要么同时失败。
事务特性
- 原子性(Atomicity): 原子性关注事务的执行单元(DML语句),确保它们同时成功/失败;即使事务中的一步操作失败,整个事务也会被回滚;确保数据的一致性,起到承上启下的作用。
- 持久性:一旦事务被提交,它对数据库的改变就是永久的(写入到磁盘中),即使系统崩溃,也能回复到事务提交后的状态。
- 隔离性:事务之间相互隔离。这意味着一个事务在执行过程中对数据的修改对其他事务是不可见的,直到事务被提交。例如读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)
- 一致性::事务在执行前后,数据库的数据应该保持一致。如数据在事务执行前后都是一致的。比如当前事务操作修改两个记录,记录修改前数据为1和A,修改后为2和B,那事务执行完成后,数据必须为2和B,回滚后,数据必须为1和A。保证数据完整性和一致性。
注:一致性建立在原子性、持久性、隔离性的基础上。
MYSQL是如何保证事务原子性的?
- 当事务开始时:MYSQL会为事务生成一个唯一的事务ID;
- 在事务过程中:DML操作对数据的修改都会生成相应的Undo Log 记录,存储在Undo Log表中;
- 事务提交:在事务执行提交后,MYSQL会将数据持久化到磁盘中,Undo Log的记录也会被刷新到磁盘中。
- 事务回滚:如果事务执行过程中发生错误,MYSQL根据Undo Log表回滚数据。
MYSQL如何保证事务持久性
在事务中的DML除了会写入Undo Log日志中,也会被写入Redo Log日志中,但它们的用途不同,Undo Log 主要用于回滚,而Redo Log日志用来保证持久性。
当系统崩溃后异常关闭,MYSQL在重新启动时会检查系统崩溃的情况,会根据Redo Log日志来恢复数据。
- 回滚未提交的事务:在系统崩溃前未提交的事务,MYSQL会根据Redo Log日志记录来回滚这些事务,确保数据库回到一致的状态。
- 重做已提交的事务:对于系统崩溃前已提交的事务,MYSQL会重新执行这些事务,确保数据库持久化到磁盘中(防止事务在提交后持久化到磁盘中时,持久化一半就突然断电)。
事务的隔离级别
-
读未提交(Read Uncommitted):
- 特点: 事务可以读取其他未提交的事务所做的修改。
- 可能带来的问题: 脏读(读到其他事务未提交的数据)、不可重复读、幻读。
-
读已提交(Read Committed):
- 特点: 一个事务只能读取已经提交的其他事务的数据修改。
- 可能带来的问题: 不可重复读、幻读。
-
可重复读(Repeatable Read):
- 特点: 事务在执行期间看到的数据是一致的,即使其他事务提交了修改。
- 可能带来的问题: 幻读。
-
串行化(Serializable):
- 特点: 所有事务按照严格的顺序依次执行,完全隔离事务之间的影响。
- 可能带来的问题: 性能开销高,可能导致事务的等待时间增加。
脏读:一个事务读取了另一个事务未提交的数据
不可重复读:在一个事务内,相同的查询返回不同的结果。主要是同一行数据的修改。
幻读:在一个事务内,相同的查询返回不同的行数。查询的数据行数与前一次查询发生改变。
写偏斜:两个事务同时修改同一行数据,并最终只有一个事务的修改生效。发生在读已提交和可重复读隔离级别下。
MVCC
mvcc(多版本并发控制)是帮助mysql解决幻读问题的一个机制。同时也解决不可重复读。这是因为在可重复读隔离级别下,第一次快照读和后续相同的快照读操作都是复用相同的试图(第一次生成的试图)。
RR(可重复读):mysql在事务开启时生成视图,后续相同的查询都会复用这个试图,避免幻读问题。这个试图遵循一个可见性算法,根据可见性算法可以得到当前事务能看到的数据版本。
RC(不可重复读):事务中每执行一次快照读都会生成一个新的视图,即使是相同的快照读操作。
- 当前读
像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁 - 快照读
像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
undo log日志
记录类型主要分为两种
- insert undo log:代表事务再
insert
新纪录时产生的undo log日志,只在事务回滚时需要,当事务提交后立即删除丢弃 - update undo log:事务再进行
update
或delete
时产生的undo log日志;不仅在事务回滚时需要,在快照时也需要;所以不能随便删除。
undo log产生的日志记录是以单链表形式存储。其中头链表是最新的修改记录。如下:
每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段
- DB_TRX_ID
6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID - DB_ROLL_PTR
7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里) - DB_ROW_ID
6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
Read View(读试图)
什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最新的事务,ID 值越大)
Read View遵循一个可见性算法
,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )
取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR
回滚指针去取出Undo Log
中的 DB_TRX_ID
再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件
的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本
Read View的四个全局属性
- create_trx_id:当前事务ID
- trx_list(随意取的):读试图生成时刻,系统中正在活跃的事务ID列表
- up_limit_id:是trx_list列表中最小的事务ID
- low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务 ID ,也就是 目前已出现过的事务 ID 的最大值 + 1
可见性算法如下:
- 比较当前记录
DB_TRX_ID(最近修改的事务ID)=create_trx_id(当前事务ID)
,是则说明当前记录是可见的,不是则进行下一步 - 比较
DB_TRX_ID(最近修改的事务DI) < up_limit_id(当前活跃事务最小ID )
小于则说明当前的记录的事务在快照生成时刻就已经提交了,说明该记录可见同时退出判断
,如果大于则进行下一步 DB_TRX_ID(最近修改的事务ID) >= low_limit_id(即将分配的事务ID)
,如果大于等于
则代表当前记录在 Read View 生成后才出现的,那对当前事务肯定不可见,此时根据undo log 记录表回滚
上一条数据,并重新判断,如果小于则进入下一个判断- 判断
DB_TRX_ID 是否在活跃事务列表之中
,trx_list.contains (DB_TRX_ID),如果不在
,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,记录可见
,如果在就回滚并重新判断
。
在rr级别下,事务开启时就生成视图,后续的查询的数据在第一次快照读的时候就有了,这时mysql会如何得到数据?
在记录生成时刻就记录这个四个核心属性
,事务内的快照读的记录都是基于这些属性进行计算的
,所以即使后续快照读的操作包含了第一次快照读要查询的数据,那后面的快照读得到的数据肯定也是和第一次快照读的数据是一致的。
在RR级别下,视图只会生成一次,后续的快照读操作得到的记录都会更新到视图上。
mvcc什么时候会失效?
在同一个事务,当两次快照读之间存在当前读,ReadView会重新生成,导致幻读。
参考视频:IT老齐030】这可能是最直白的MySQL MVCC机制讲解啦
redis篇
它是一种非关系型数据库,数据存储在内存中,因此读取速度非常快,被广泛应用于缓存。
关系型数据库与非关系型数据库的区别
关系型数据库
采用表格形式存储数据,数据以行和列的形式存储在表中
,每个表都有一个唯一的名称,并且表之间通过外键进行关联
。
好处:
严格的数据一致性
:遵循ACID事务特性,确保数据的完整性和一致性,事务支持使得数据操作是原子性的(同时成功或同时失败)强大的查询能力
:支持复杂查询操作,如联表、过滤、聚合等,使数据检索和分析变得简单高效。
坏处:
- 关系型数据库需要提前定义表的结构和模式,这种固定的模式不适合存储非结构化或频繁变化的数据。
非关系型数据库
采用更灵活的数据模型
,可以是文档,键值对,图形等形式
。这些数据库不需要遵循固定的表结构,可以动态的存储数据
。
好处:
灵活的数据模型
:可以根据应用的需要选择合适的数据模型,从而灵活存储数据。高性能
:可以设置集群,方便处理大量数据和高并发访问。
坏处:
缺少事务支持
:大多数NOSQL数据库放宽了对事务一致性的要求,牺牲了一致性来换取更好的性能和可扩展性
。放弃了强一致性,采用最终一致性
;这意味着在某些情况下,数据操作可能不是原子性的,可能会导致数据的不一致性。
八大数据类型
String(字符串)
数据类型:key value
- 存储:set key value
- 取出:get key
- 删除一个或多个键:del key[key key…]
哈希(hash)
数据类型:key field value1,其中field1,value可以看出是key的value之一,也就是说hash类型是集合,在这个key中也会有field2 value2等。
- 存储:hset key field value1
- 取出:hget key field
- 删除一个或多个指定字段的值:hdel key field[field field…]
- 获取哈希keu中所有字段:hkeys key
列表(list)
数据类型:key values
该数据类型是有序,根据你插入的顺序进行排序
- 存储:lpush key value[value value …];往队头插入,最先插入的元素会往队尾靠近。
- 存储:rpush key value;往队尾插入,最先插入的元素会往对头靠近。
- 取出:lindex key index(下标):每次插入的元素都有对应下标。
- 范围取出:lrange key index index;lrange key 0 -1 取出全部
- 删除:lrem key count value:count为要删除的元素个数,当count>0时:从列表头部开始向列表尾部移除指定元素,移除数量为 count;count<0:从列表尾部开始向列表头部移除指定元素,移除数量为 count 的绝对值;count=0:移除所有与指定元素相等的元素。
- 从对头删除第一个元素:Lpop key
- 从队尾删除第一个元素:Rpop key
集合(set)
数据类型:key values ,其中內部元素是无序并且不允许重复元素
- 存储:Sadd key value [value value …]
- 全部取出:Smembers key
- 随机取出:srandmember key num;num为要随机取出多少个元素。
- 删除:Srem key value[value value…]:删除一个或多个成员元素,value为具体元素内容,无法一次删除集合中所有元素,只有元素都被删除了,这个集合才会被自动删除。
有序集合(Zset)
数据类型:key values;其中內部元素有序且不允许重复元素,其中value被拆分为 score(分数) member(成员名称,也可以将这个看成value)
- 存储:Zadd key score member[score member score member …];score为自定义的分数,分数的数据类型必须为int类型,Zset会根据分数对这个集合的元素进行排序。
- 范围取出:Zrange key score score:元素按分数从小到大排序返回
- 范围取出:Zrevrange key start end:元素按分数从大到小排序返回
- 删除:Zrem key member[member member…]:删除一个或多个元素
地理空间数据类型(GEO)
数据类型:key values,其中value被拆分为 longitude(经度) latitude(纬度) member(位置名称),常用于存储地理位置信息。
-
存储一个或多个地理空间:GEOADD key longitude latitude member [longitude latitude member …]
-
取出key中指定成员的地理位置:GEOpos key member[member …]
-
范围取出:Zrange key start end
-
获取两个成员之间的距离:GEODIST key member1 member2 [unit];unit可以不填,默认单位为m
-
在指定的键中,查找给定成员周围一定范围内地临近成员:GEORADIUSBYMEMBER key member radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
-
删除指定成员:ZREM key member[member …];与Zset数据类型的删除指令一样
基数统计(Hyperloglog)
基数:不重复的元素
Hyperloglog常用来统计一个网站的浏览量,统计出来的数据是去重的,但是估算值不是特别准确。但在大多数实际应用场景中,其对基数估算的性能和内存效率都是相当可观的
数据类型:key values
- 存储一个或多个元素:PFADD key element[element …]
- 返回估算值:PFCOUNT key[key …],这里需要注意的是,估算的数值是去重的
- 合并多个HyperLogLog:PFMERGE destkey sourcekey[sourcekey]:destkey为合并后的key,数据也都会出现在这里,但这些数据是去重过的。可以看成是无重复的并集。
位存储(Bitmap)
应用场景,统计网站中登录和未登录的用户人数,这时候我们知道用户都是一个个user对象,统计起来也是特别麻烦,这时候可以使用Bitmaps,它是位图
它是一种数据结构,都是操作二进制来进行记录,就只有0和1两个状态
模拟一个用户该周的七天登录情况
语法:setbit key offset(偏移量) value(0/1)
zhangsan:泛指单个用户
0:本周的第一天
1:登录 0 :未登录
这样该周的打卡情况就出来了
查看某一天有没有登录
getbit zhangsan 0:可以看成是该用户周一有没有登录,
统计打卡的天数
语法:bitcount key
统计key为zhangsan的value为1的总和,看成是总打卡量
发布订阅
缺点:消息无法持久化,PubSub的消息是不会持久化的,redis宕机就相当于一个消费者都没有,所有消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
参考链接:Redis发布订阅以及应用场景介绍
redis事务
redis事务的本质是一组命令的集合。
- 它没有隔离性的概念,一组命令在发送EXEC(提交)命令前会放入队列缓存中,并不会立马执行,而是在提交后才执行,也就不存在在事务查看具体的数据。在事务执行期间,其它客户端仍然可以读写事务锁定的键。
- 不保证原子性:在redis中单条命令是原子性的,但事务不保证原子性,且没有回滚,事务中任意命令执行失败(出现语法错误),其余的命令仍会执行。但是事务内的命令出现命令级(错误指令-setter key)错误时,事务内所有命令都不会执行。
- 在redis中,同一个客户端的事务是不能同时提交的,也就是说同一客户端的事务是串行执行的,来自不同客户端的事务可以在同一时间内并发执行。
参考链接:Redis之Redis事务
watch监视器
watch监视器通常与事务一起使用,用来实现乐观锁。
watch用来监视一个或多个键,如果被监视的任何一个键被其他客户端修改,当前客户端的事务就会被打断,EXEC 命令将返回 nil,表示事务执行失败。
WATCH 命令必须在事务开始前使用,通常是在 MULTI 命令之后,EXEC 命令之前。
主要使用场景为:
- 防止并发修改:当多个事务需要使用同一个键时,可以使用watch命令确保在同一时间只有一个事务执行成功。
微服务
Eureka(服务注册)
什么是服务注册?它的好处是什么?
注册中心是一种在分布式系统中用于管理和协调服务的组件,可以跟踪服务实例的位置和状态,让各个服务之间能够方便地发现并通信。
好处:
- 高可用性:注册中心支持
动态的服务实例集群环境
,一个新服务上线时,也能够快速的分担服务调用压力。 - 负载均衡:根据
负载均衡策略
,确保请求被分配到健康且负载均衡的服务实例上
。 - 提高系统可靠性:通过定期的
健康检查
,确保只有健康的服务实例被发现和调用,提高系统的整体可靠性。
注册表
注册表用于存储和管理所有已注册的服务实例信息,有eureka服务维护,并为消费者提供服务发现功能。
注册表中会存储服务实例的元数据:服务名、IP地址、端口、实例状态(在线/宕机)、自定义的元数据
服务续约
服务注册成功之后,会按照预设的间隔时间(默认30s
)向注册中心发送续约请求告诉注册中心该服务实例仍然在线且是健康的,如果注册中心在一个特定的时间段内(默认90s)
没有收到某个实例的续约请求,就会在服务注册表中移除
,防止无效的服务实例被其它服务调用。
好处:通过定期的心跳检查,确保eureka注册表中的服务实例信息是最新的,并且能够快速检查并移除姑臧的服务实例,提高系统的可靠性
。准确的服务实例信息有助于负载均衡策略的执行,确保请求被路由到健康且可用的服务实例,提升系统的高可用性
。
自我保护机制
自我保护机制的设计是用于提高系统稳定性和容错性;当由于网络分区或者其它临时性问题导致的大量服务实例的下线,注册中心会进入自我保护模式并在控制台上提示。
注册中心会持续监听所有已注册服务实例的续约请求,如果在一个配置时间窗口(默认15分钟)内,续约比例低于预设阈值(默认85%)就会开启自我保护模式,在自我保护模式下,注册中心将停止移除未能及时续约的服务实例。
其中这些失效的实例仍然会提供给服务消费者,若某一个实例节点是可以提供服务的,只是因为网络分区问题无法与注册中心通信,此时消费者拿到实例节点后,发现可以与该实例节点通信,此时接口是调用成功的。若真的是实例节点宕机了,那消费者的请求会失败。
好处:自我保护模式提高了系统在面对网络分区或暂时性网络问题时的容错能力。
坏处:保留失效的坏实例可能会影响用户体验感。
当一个配置时间段(默认15分钟)内收到的续约请求比例达到或者超过预设阈值(85%),注册中心就会退出自我保护机制,并开始移除无效服务实例。
注册表中服务实例的状态有哪些?
- STARTING:
服务实例正在启动过程中
,还未准备好接收请求,该状态下的实例不会
提供给服务消费者,启动完成后会切换到”UP“状态 - UP:服务实例
处于运行状态,能够接收请求
,该状态下的服务实例会
提供给消费者 - OUT_OF_SERVICE:
服务实例暂时不可用
,一般优雅停服时服务实例会设为此状态
,该状态下的服务实例不会
提供给消费者 - DOWN:服务实例处
于非运行/停止状态
,无法接收请求,不会
提供给消费者 - UNKNWN:服务实例的
状态未知
,通常在实例初始注册时或健康检查未能确定状态时使用
,该状态下的服务实例不会
提供给消费者
优雅停服
在服务实例停止或下线时,通过一些机制确保服务不会立即从 Eureka 注册表中删除,而是经过一段时间的缓冲期,在缓冲期期间新的请求不会被路由到即将下线的实例,并且等待当前的请求处理完毕,保证业务的完整性。也可以有效避免注册中心触发自我保护机制。
注:在缓冲区时间段内仍然会发送续约请求,避免在优雅下线期间出现服务实例被错误移除的情况。
工作流程:
开启优雅停服后,当服务实例准备下线时,会告知eureka服务器,将自身的状态从”UP“转为”OUT_OF_SERVICE“
,在此期间,负载均衡器(ribbon)不再将新请求路由到该实例
,服务实例会有一段缓冲期(可自行配置)
,在缓冲期内实例会继续处理已经接收到的请求
,确保正在进行的任务可以正常完成。当缓冲期时间到达后,服务实例会再次向eureka服务发送注销请求
。将状态转为”DOWN“
负载均衡器
负载均衡器将请求有效分配到多个后端服务器(或实例)上,以实现负载均衡和提高系统的性能、可用性和可扩展性的重要组件。通常分为以下两种:
- 服务器负载均衡器:在消费者和生产者之间使用
独立的负载均衡设施
,可以是硬件(F5)也可以是软件(nginx) - 客户端负载均衡器:将负载均衡器继承到服务消费者中,消费者从注册中心拉取可用地址。然后根据负载均衡策略选择一个合适的地址进行业务交互,Ribbon就是客户端负载均衡器。
Ribbon的策略:
在eureka中是继承Ribbon的,Ribbon是在服务消费端进行工作的。
工作流程:
- 当客户端需要调用某个服务时,例如使用 RestTemplate 或 FeignClient,Ribbon 会拦截该请求。
- Ribbon 从缓存的注册表中获取目标服务的所有实例列表
- 根据配置的负载均衡策略(如轮询、随机、加权等),Ribbon 选择一个服务实例进行调用。
当客户端启动时,Ribbo会向eureka服务请求所有已注册的服务实例并缓存在本地,之后定期(默认30s)请求最新实例列表,更新本地缓存,避免频繁地直接请求eureka服务
ribbon为什么要拦截RestTemplate?
RestTemplate 是 Spring 提供的一个用于简化 HTTP 请求的模板类,它封装了 HTTP 请求的各种操作,包括发送 GET、POST、PUT、DELETE 等请求,以及处理响应结果等功能
Ribbon 在拦截 RestTemplate 请求的时机是在 RestTemplate 发起 HTTP 请求之前,会使用负载均衡策略从服务实例列表中选择一个可用的服务实例。根据选定的服务实例,Ribbon 将原始请求的目标地址替换为选定的服务实例的地址,以确保请求能够发送到正确的目标服务实例。
nacos注册中心
nacos的数据模型
- 配置集(Data ID):在系统中,一个配置文件通常就是一个配置集。一个配置集包含了系统的各种配置信息,如数据源、日志级别等配置项。
- 配置项:配置集中一个个配置内容就是配置项,通常以key-value形式存在。
- 配置分组(group):配置分组就是对配置集进行分组,不同的配置分组下可以有相同的配置集。配置分组通常表示为某项目。若在nacos中创建一个配置集时,未填写配置分组的名称时默认分组是DEFAULT_GROUP
- 命名空间(NameSpace):可用于进行不同环境的配置隔离,如隔离开发、生产、测试环境。不同的命名空间下,可以存在相同名称的配置分组和配置集
- 服务:在注册中心,一个服务下是由1/多个服务实例组成的
- 集群:一个服务有多个实例,而多个实例之间也可以通过集群来进行隔离,在远程调用时,集群也可以筛选条件。可以帮助实现
就近路由
。
路由规则:
- 命名空间:首先,nacos会根据命名空间进行区分
- 分组:在区分完命名空间后,再根据分组进行进一步的区分
- 服务:根据路由的服务名称,找到指定的服务
- 集群:服务是由多个实例组成的,在服务之下又由集群进行逻辑分组,所以又会根据集群来筛选出一部分的服务实例。
- 负载均衡:最后根据负载均衡策略,选出合适的服务实例进行调用
就近路由
就近路由是一种优化网络请求
的方法,其目的时将请求路由到同一IP区域内或者与请求更接近的IP区域内,这是因为一个IP区域的请求访问同一个区域会比访问其它IP区域快
。如北京的请求访问北京的服务实例肯定是比访问上海的服务实例快。
在nacos,我们可以根据服务实例的IP地址,将它们通过集群进行分组,如北京的服务实例统一放在北京集群中,上海的服务实例放在上海集群中。
在nacos的路由规则中,若消费者没有指定集群查询服务实例,会根据消费者所在的集群查询目标服务实例,从而实现就近路由。
如果目标服务实例在另一个集群中,就需要我们显示指定目标服务实例的集群信息,或者自己实现跨集群容错(发现指定集群不可用时,就切换到其它集群中进行筛选)
其实我们也可以通过配置分组来进行就近路由,如北京的服务实例就放入到北京的配置分组中,但不知道为什么nacos要引导我们使用集群进行就近路由,我的猜测是可能是nacos规范了每个数据模型的行为,如命名空间区分开发/测试环境,配置分组区分某项目,集群区分IP区域。所以nacos想让我们尽量使用集群进行就近路由。
心跳包
在nacos中已注册的服务实例需要定期向nacos发送心跳包,默认是5s,若nacos服务器检查到某一个服务实例在一定时间(默认15s)内没有发送心跳包,nacos就会将该实例标记为不健康/失效,并从服务列表中移除。
通常心跳包机制,可以提高系统的可靠性,确保每次被调用的服务实例都是健康的
自我保护机制
自我保护机制是一种提高系统稳定性和容错性的策略。
注册中心会持续监听所有已注册服务实例的续约请求,如果在一个配置时间窗口(默认15分钟)内,续约比例低于预设阈值(默认85%)就会开启自我保护模式,在自我保护模式下,注册中心将停止移除未能及时续约的服务实例。
nacos的路由策略
-
nacos自身支持一些简单的路由策略,我们可以在控制台上为每个服务实例配置一个权重值,权重越高,被选中的概率就越高。
-
nacos依赖并没有ribbon,我们需要在项目手动集成ribbon
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
yml文件示例:
spring:
application:
name: example-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
example-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 使用Ribbon的随机负载均衡策略
这样就可以结合ribbon来实现负载均衡
优雅下线
在服务实例停止或下线时,通过一些机制确保服务不会立即从 Eureka 注册表中删除,而是经过一段时间的缓冲期,在缓冲期期间新的请求不会被路由到即将下线的实例,并且等待当前的请求处理完毕,保证业务的完整性。
注:在缓冲区时间段内仍然会发送续约请求,避免在优雅下线期间出现服务实例被错误移除的情况。,即便是实现了优雅下线,nacos或者eureka下线的比例还是达到了保护阈值的最大限制,仍然会触发自我保护机制
好处:优雅下线能够保证正在处理的请求都完成,保证业务的完整性。
与eureka注册中心一致
nacos配置中心
配置中心是指将分布式环境的服务中的配置文件进行统一管理,同时支持动态更新配置。
在分布式环境,由于会将单体应用的业务拆分为一个个的子服务,而子服务通常会复制多个服务,在注册中心注册多个成为一个集群来保证整个分布式的可用性。因此,系统中就会出现大量的服务,由此也会产生大量的配置文件,所以我们需要配置文件进行集中式,动态化管理。
好处:
当我们将一个服务复制多份启动并注册到注册中心后,这时我们发现配置文件需要修改,那此时我们需要停止服务,修改配置文件,而这时我们是复制了多份的,也就是说复制了多少份就需要修改了多少份,最后再启动服务,非常的繁琐。
而这时引用配置中心,这些服务都指向一个配置中心的同一个配置,这时我们只需要修改配置中心的配置,不用修改所有服务的配置,简化配置的繁琐,而且配置中心是支持动态更新的,也就是是说这些是不要我们手动的停止/重启服务。在服务运行时就可以修改配置文件,并且读取更新后的配置信息。
nacos是如何实现动态更新的?
nacos订阅者在启动时,会与nacos建立长轮询连接,持续监听配置的变化,当nacos服务器检测到某个配置项发生变化后,会立即将新的配置数据通过长轮询推送给订阅者,并触发 EnvironmentChangeEvent 事件。监听到 EnvironmentChangeEvent 事件后,会检查应用中使用了 @RefreshScope 注解的 Bean,对于这些 Bean,Spring 会重新创建新的实例,让代理对象指向这个新的实例,从而实现配置的动态更新。
@RefreshScope注解的作用
@RefreshScope是SpringCloud提供的一个注解,它可以实现配置的动态更新,支持细粒度的刷新(局部刷新,刷新监听的配置),不需要刷新整个Spring上下文
- 在Spring启动时,会扫描并初始化被“@RefreshScope”注解的bean,并为这些Bean创建
代理对象
,以便在配置刷新时替换原始的目标实例
。 RefreshScope注解会监听配置中心的配置(所有配置
),当检查到配置发生变化时,会触发“EnvironmentChangeEvent”事件- 事件触发后,会通知”@RefreshScope“注解的Bean进行刷新
- 当”@RefreshScope“发现自己监听的配置发生了改变就
会销毁旧的Bean实例,并根据新的配置属性重新创建Bean实例
,最后代理对象会指向这个新的Bean实例
,使配置变化立即生效。
nacos通常与@RefreshScope注解配合使用来达到动态更新的效果,我们需要在nacos配置中开启支持动态,并在类中声明@RefreshScope。
yml
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #配置中心地址
prefix: nacos-simple-demo
file-extension: yaml # {prefix}+{file-extension}=nacos-simple-demo.yaml 对应着nacos中的 dataId
namespace: e5fb86f3-1116-48c5-b16d-5b20d20ad0c0 #命名空间ID
group: DEFAULT_GROUP #分组
extension-configs:
- data-id: test-demo.yaml # 扩展 第二个配置集
group: TEST_GROUP # 默认为 DEFAULT_GROUP
refresh: true # 动态更新
- data-id: test-demo-o1.yaml # 第三个配置集
refresh: true # 动态更新
java类
@RestController
@RefreshScope
public class TestController {
@Value("${common.config1}")
private String config1;
@Value("${common.name}")
private String name;
}
openfeign(服务调用)
openfeign是一种声明式、模板化的HTTP客户端,使开发者只需要定义接口和注解,就可实现HTTP请求的调用。同时还可以结合ribbon和sentinel还可以负载均衡、服务熔断,确保服务调用的高可用性和稳定性
好处:
- 简化HTTP客户端:开发者只需定义接口和注解,即可实现HTTP请求的调用,使代码更加简洁易读
- 集成ribbon和sentiel:结合Ribbon和sentiel提供负载均衡和熔断机制,确保服务调用的高可用性和稳定性
openfeign是内置集成了Ribbon和Hystrix/Resilience4j的,在老版本中是集成Hystrix,但由于Hystrix不再维护了,在新版本是集成了Resilience4j。但是在实际应用中更多是openfeign与sentinel组件结合使用,实现服务降级。
注:拉取服务实例列表和选择具体的服务实例都是通过Ribbon来实现的
Feign与openFeign的关系
Feign时openFeign的前身,Feign是由Netfix开发和维护的,而openFeign是SpringCloud维护的,它基于Feign构建,并提高了更多的功能和特性
openfeign是如何实现远程调用?
openFeign的@FeignClient注解可以解析SpringMVC的@RequestMapping注解等等,拿到相关的请求信息后发送Http请求
Spring在启动会自动扫描被@FeignClient注解修饰的接口,并为它们生成一个代理类。
超时控制
在openfeign中,默认的连接超时、读取时间是基于Ribbon的,Openfeign使用ribbon进行负载均衡,并继承了ribbon的配置,包括超时时间,因此,openfeign的默认超时时间与ribbon是一致的,ribbon的默认超时、读取时间都是1s
但是我们可以通过配置项来修改openfeign的超时时间,如下:
# application.yml
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时时间为 5 秒
readTimeout: 10000 # 读取超时时间为 10 秒
- 连接超时:当客户端尝试连接到服务器时,如果在连接超时时间内无法建立连接,就会抛出连接超时异常
- 读取超时:当与服务器建立连接后,客户端等待服务器发送响应数据的最长时间。
openfeign向提供者发送请求,发现读取时间超时了,那提供者的请求还会继续执行吗?
服务提供者的处理不会因为消费者的超时而中断,除非服务提供者也设置了相应的超时机制来处理请求,通常情况下,服务提供者的请求会继续执行,直到完成或者达到了提供者自身的超时设置
提供者根据tomcat设置自身的连接、读取超时时间:
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TomcatConfig {
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers((TomcatConnectorCustomizer) connector -> {
if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol<?>) {
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
protocol.setConnectionTimeout(5000); // 设置连接超时时间为 5 秒
protocol.setSocketTimeout(10000); // 设置读取超时时间为 10 秒
}
});
return factory;
}
}
如何优化通信?
在项目中,我们通常使用GZIP来达到优化通信的效果。
GZIP:将文件或数据压缩成较小的尺寸,从而减少网络传输的数据量,提高系统的性能
GZIP压缩传输原理如下图:
- 客户端向服务器发送请求时,请求头会携带
Accept-Encoding:gzip,deflate
属性,这表示客户端支持 GZIP 和 Deflate 压缩算法 - 服务器接收到请求后,发现请求头中
Accept-Encoding
属性,就会将响应数据压缩返回给客户端,同时响应请求中携带Content-Encoding:gzip
属性,表示该响应数据时该格式压缩过的 - 客户端接收到响应后,会检测是否有content-Encoding属性,如果有,就按该格式解压响应数据
Sentinel(服务限流)
Sentinel是alibaba开源的一款轻量级的库,可以帮助我们解决分布式系统的流量控制、熔断降级和系统负载保护等问题,帮助开发者构建稳定、高可用用的微服务架构。同时支持实时监控、规则动态调整功能
- 流量控制(服务限流):QPS-每s查询率、并发线程数、热点参数
- 熔断降级:错误比例、慢调用比例、异常比例
- 系统负载保护:根据系统的CPU、内存等资源使用情况进行限流、防止系统过载,提高系统的稳定性和可用性
- 实时监控:通过senitel控制台,我们能能查看到各个请求的调用情况,帮助开发者及时发现和定位系统问题,进行调优和优化。
- 动态调整:Sentiel的限流、熔断规则,是可以在程序运行时动态调整的
为什么大家都使用Sentinel,而不是hystrix组件呢
这是因为Hystrix只支持基本熔断和降级,而Sentinel支持多种熔断策略,限流策略,同时支持动态规则调整,热点参数限流,同时Hystix目前停止更新了,也就无法给我们带来新的功能。
服务降级与服务熔断的区别
服务降级和服务熔断都是常见的容错机制,用于保护服务免受外部/内部故障的影响,提高系统的稳定性和可用性
服务降级:针对的是内部资源;当系统负载过高、资源不足时,为了保证核心功能的稳定运行,主动的临时关闭或减少一些非核心功能,从而减少系统负载,提高响应速度。如对于自身的某个接口进行限流(接口的请求到达一定阈值时,就不再接收新的请求)也是一种服务降级的实现方法
服务熔断:针对的外部资源;当外部服务或者响应时间过长时,为了防止雪崩效应,服务熔断会中止对该服务调用,并且在一段时间内拒绝对该服务的调用,直到服务恢复正常
懒加载
懒加载是一种机制,在应用程序启动时延迟加载相关组件和配置,只加载基本的组件/配置,从而减少启动时间和内存占用
sentinel的懒加载也是如此,主要针对以下几个方面
- 规则加载:服务启动时,只加载基本的规则(系统规则,控制台规则),而动态规则()
流控规则、熔断降级规则)会在第一次需要时进行加载。 - 实时监控数据拉取:Sentinel控制台会监控服务的请求数据,这些数据在控制台首次被访问才会被加载