文章目录
这是本人整理的数万字的面试笔记,基本上涵盖了 Java 领域的所有技术栈,本人也是凭借这份面试笔记斩获了近 10 个 offer,面试成功率高达80%。当然这份笔记是我根据自身的经验和技术栈整理的,自己觉得很重要的或者记不清的就会记录记录下来,面试被问到的时候也有回答的思路。现在共享给大家,希望对准备面试的小伙伴有帮助。
1. 微服务
1.1 主流注册中心对比
zookeeper:zookeeper 作为注册中心主要是因为它具有节点变更通知功能。只要客户端监听相关服务节点,服务节点有所变更就能及时的通知到监听客户端,非常方便。zookpeeper 是cp模式的。
eureka:是 netflix 开源的 基于RestFulAPI 风格开发的服务注册和发现组件。现在不会再更新。
consul:是使用go 语言开发的支持多数据中心分布式高可用的服务发布和注册的服务软件。
nacos:是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
1.2 eureka 心跳检测和自我保护机制
心跳检测:eureka 每隔30 秒就会向注册中心续约心跳一次,也就是报活。如果没有续约,租约在90s 后到期,然后服务就会失效,每隔30s的续约操作,就是心跳检测。
自我保护:eureka 服务端如果在15 分钟内,超过 85% 的客户端节点都没有正常心跳,那么Eureka 就认为客户端和注册中心出现了网络故障。而微服务本身是可正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。在自我保护状态下,不会移除任何服务,能接受新服务的注册和查询请求,但是不会和集群中其他节点同步。
1.3 Ribbon 负载均衡策略
1、轮寻策略
2、随机策略
3、重试策略:一定时间内循环重试。继承随机策略。
4、最小连接数策略:遍历serverList,选出可用且连接数最小的server.
5、可用过滤策略:扩展了轮寻策略,会先通过默认的轮询选取一个 server,再判断 server 是否超时可用。
6、区域权衡策略:扩展了轮询策略。除了过滤连接超时和连接数过多的server,还会过滤掉不符合要求的 zone 区域里面所有的节点。然后在剩下的节点中轮询获取。
1.4 Hystrix 舱壁模式
使用舱壁避免了单个工作负载(或服务)消耗掉所有资源,从而导致其他服务出现故障的场景。这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。通过应用舱壁模式,可以保护有限的资源不被耗尽。为了避免问题服务请求过多导致正常服务⽆法访问,Hystrix 不是采⽤增加线程数,⽽是单独的为每⼀个控制⽅法创建⼀个线程池的⽅式,这种模式叫做“舱壁模式"。
1.5 Hystrix 工作模式
1、当调⽤出现问题时,开启⼀个时间窗(10s)
2、在这个时间窗内,统计调⽤次数是否达到最⼩请求数?
- 如果没有达到,则重置统计信息,回到第1步;
- 如果达到了,则统计失败的请求数占所有请求数的百分⽐,是否达到阈值?
- 如果达到,则跳闸(不再请求对应服务)
- 如果没有达到,则重置统计信息,回到第1步
3、如果跳闸,则会开启⼀个活动窗⼝(默认5s),每隔5s,Hystrix 会让⼀个请求通过,到达那个问题服务,看是否调⽤成功,如果成功,重置断路器回到第1步,如果失败,回到第3步。
1.6 Fein
fegin 是NetFlix 开发的一个轻量级 RestFulf 的 HTTP 服务客户端。用来进行远程调用。
1.7 gateway
gateway 核心逻辑就是路由转发加执行过滤器链。
客户端向gateway 发送请求。然后在gateway handler Mapping 中找到与请求相匹配的路由,然后handler 通过制定了过滤器链来将请求发送到我们实际服务执行业务逻辑,然后返回。在过滤器中可以进行参数加解密,参数校验、权限校验、日志输出、协议转换等等。
2. 并发编程
2.1 多线程
2.1.1 创建一个阻塞队列
核心思想就是当队列为空时,调用出队列的方法,会进行wait() 等待。直到有数据入队列时,调用notify() 方法。同样的在队列满是,入队列会阻塞,直到有出队列才唤醒。
public class MyBlockingQueue {
private int[] data=new int[10];
private int putIndex;
private int getIndex;
private int size;
public synchronized void put(int val){
if(size==data.length){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
put(val);
}else {
data[putIndex]=val;
putIndex++;
if(putIndex==data.length){
putIndex=0;
}
size++;
}
}
public synchronized int get(){
if(size==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return get();
}else {
int val = data[getIndex];
getIndex++;
if(getIndex==data.length){
getIndex=0;
}
size--;
return val;
}
}
}
2.1.2 为什么wait() 方法要释放锁。
因为当一个线程进入一个 synchronized 修饰的同步方法后,会锁住当前对象,调用 wait() 方法进入阻塞状态。如果不释放当前对象的锁的话,其他线程永远获取不到当前对象的锁,也就没有办法唤醒当前线程了,这样就形成了死锁。所以在调用wait() 方法会释放锁,等待其他线程获取当前对象锁,调用notify() 唤醒此线程。然后此线程重新获得锁,执行剩下的操作,执行结束后,会再次的释放锁。
2.1.3 volatile 关键字的作用
1、保证64位写入的原子性。
2、内存可见性
3、禁止指令重新排序。
2.1.4 synchronized
synchronized 实现原理是获取不到锁先自旋,自旋依然获取不到锁,再阻塞。
2.2 JUC
2.2.1 阻塞队列
阻塞队列有基于数组实现的ArrayBlockQueue 和基于 链表实现的 LinkBlockQueue。以及还有优先级的 PriorityBlockQuere,按照元素的优先级出队列,实现了Comparable 接口。以及延时队列。根据延时时间大小出队列的,实际上是未来时间减去当前时间放入,DelayQueue 中的元素。如果getDelay() 小于等于0,说明该元素到期,可以出队列了。同步队列没有容量,先调用put(),线程就会阻塞,只要等另一个线程调用了task(),两个线程才会唤醒。
2.2.2 CopyOnWrite
是指在写的时候,不会直接操作源数据,而是先copy 一份数据进行修改,然后通过悲观锁或者乐观锁的方式写回。这样的好处是,读不用加锁。
CopyOnwriteArrayList 读操作的时候不会加锁,只有写的时候才会加同步锁,所以是线程安全的 ArrayList。
CopyOnwriteArraySet 就是用array 实现的一个线程安全的 Set .保证所有元素不重复,封装的是CopyOnwriteArrayList 。利用CopyOnwriteArrayList 的addAllAbsent() 方法。
2.2.3 锁
为了实现一把具有阻塞或唤醒功能的锁,需要几个核心要素:
-
需要一个state变量,标记该锁的状态。state变量至少有两个值:0、1。对state变量的操作,使用CAS保证线程安全。
-
需要记录当前是哪个线程持有锁。
-
需要底层支持对一个线程进行阻塞或唤醒操作。
-
需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,也需要使用CAS。
2.2.4 AQS
AbstractQueuedSynchronizer 的核心就是一个双向链表形成的阻塞队列以及CAS.
2.2.5 CAS
CAS 名为:compare and swap。是比较内存中的值是否和预期值一致,如果一致就进行更新。这个过程是原子的。cas 有三个操作数,内存值,预期值和新值。只有当内存值和预期值相等时,才会将内存值更新为新值。否则什么都不做。底层是通过unsafe 类保证原子性的,unsafe类中都是native 方法,可以直接操作内存。
缺点:存在ABA 问题。解决方案就是加版本号,或者加时间戳。
2.2.5 ReentrantLock 互斥锁
Condition 本身也是一个接口,其功能和wait/notify类似,Condition 也必须和Lock一起使用。因此,在Lock的接口中,有一个与Condition相关的接口。
await() 是获取锁线程阻塞方法。signal() 是唤醒线程的方法。
2.3 线程池
线程池是一个典型的生产者消费者模型。线程池的核心是使用阻塞队列。我们常用的就是 ThreadPoolExecutor。
2.3.1 ThreadPoolExecutor
ThreadPoolExecutor 主要是 包含一个阻塞队列和一组线程集合 Workers。每个线程是一个worker 对象,worker 继承了AQS 。
ThreadPoolExecutor 包含7 个参数。
corePoolSize:在线程池中始终维护的线程个数。核心线程数
maxPoolSize:在corePooSize已满、队列也满的情况下,扩充线程至此值。最大线程数
keepAliveTime/TimeUnit:maxPoolSize 中的空闲线程,销毁所需要的时间,总线程数收缩回corePoolSize。存活时间和单位
blockingQueue:线程池所用的队列类型。阻塞队列
threadFactory:线程创建工厂
RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize 已满,最后的拒绝策略。
提交过程的核心流程:
1、先判读当前线程数是否小于 corePoolSize 。如果小于就新建线程执行
2、如果大于,就判断阻塞队列是否已经满了,如果没有满,就加入阻塞队列中。
3、如果满了,就判断当前线程数是否小于 maxPoolSize 。如果小于,就直接创建线程执行。如果大于就根据拒绝策略,拒绝任务。
线程池的关闭:
在调用shutdown() 或者shutdownNow() 之后,线程池并不会立即关闭,会等待所有任务执行完成之后,才会关闭线程池。
shutdown() 不会清空任务队列,并且只会中断空闲线程。
shutdownNow() 会清空任务队列,并且中断所有的线程。
2.3.2 线程池拒绝策略
线程池有四种拒绝策略
1、丢弃任务,并抛出异常
2、丢弃任务,单不抛出异常
3、丢弃队列最前面的任务,然后重新提交当前任务。
4、线程池什么都不做,由当前线程自己处理。
2.3.3 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor实现了按时间调度来执行任务。有延迟任务和周期性任务。底层实现一个延迟队列来实现的。
3. java 基础
3.1 spring 启动的流程(12步)
1、prepareRefresh()。刷新前预处理。主要是设置启动时间,以及初始化配置文件中的占位符,以及校验配置信息是否正确。
2、obtainFreshBeanFactory()。获取 Beanfactory。
3、prepareBeanFactory(beanFactory)。Beanfactory 的准备工作,对BeanFactory 的一些属性进行配置。比如 context 的类加载器
4、postProcessBeanFactory()。BeanFactory 准备工作完成后,进行的后置处理工作。是一个钩子函数。
5、invokeBeanFactoryPostProcessors(beanFactory)。实例化并调⽤实现了BeanFactoryPostProcessor接⼝的Bean
6、registerBeanPostProcessors(beanFactory)。注册BeanPostProcessor(Bean的后置处理器),在创建Bean的前后执行
7、initMessageSource()。初始化 MessageSource 组件,并将信息加入allpication.singletonObjects 中。
8、initApplicationEventMulticaster()。初始化事件派发器,并加入到singletonObjects 中
9、onRefresh()。子类重写这个方法,在容器刷新时可以自定义逻辑。
10、registerListeners()。注册应用监听器。 就是 注册 ApplicationListener 接口监听器 bean
11、finishBeanFactoryInitialization()。初始化所有没有设置延时加载的Bean
12、finishRefresh()。完成context 的刷新,发布事件。
3.2 BeanFactory 创建流程
1、判断是否已经存在BeanFactory,如果存在,就销毁Bean 和BeanFactory。
2、创建 BeanFactory
3、为当前BeanFactory 设置序列化id
4、将bean 对象加载到 BeanFactory 对象的 beanDefinitionMap 中。
5、返回 BeanFactory
3.3 Bean 创建流程
1、初始化所有剩下的⾮懒加载的单例bean
2、初始化创建⾮懒加载⽅式的单例Bean实例(未设置属性)
3、填充属性
4、初始化⽅法调⽤(⽐如调⽤afterPropertiesSet⽅法、init-method⽅法)
5、调⽤BeanPostProcessor(后置处理器)对实例bean进⾏后置处
3.4 Bean 生命周期(11步)
1、根据配置情况调用 Bean的构造方法或者工厂方法实例化Bean
2、利用依赖注入完成Bean的所以属性值的配置注入
3、如果Bean 实现了BeanNameAware 接口,则spring 调用Bean的setBeanName() 传入当前Bean的id
4、如果Bean实现了BeanFactoryAware 接口,调用setBeanFactory() 方法传入当前工厂实例的引用
5、如果Bean 实现了ApplicationContextAware 接口,通过调用setApplicationContext 传入当前applicationContext 实例的引用。
6、如果BeanPostProcessor 和Bean 关联,则Spring 将调用改接口的预初始化方法。postProcessBeforeInitialization() 是前置处理的方法,Spring的AOP就是利用它实现的。
7、如果Bean 实现了InitializingBean 接口,需要实现afterPropertiesSet 方法
8、如果在配置文件中通过init-method 属性指定了初始化方法。则的调用该方法。
9、如果BeanPostProcessor 和Bean 关联,则Spring 将调用该接口的初始化方法postProcessAfterInitialization().此时Bean可以被应用系统使用。
10、如果在Bean标签中指定了Bean的作用范围的scope=“singleton” 则将改Bean 方法singletonObjects的缓存池中
11、如果Bean 实现了DisposableBean 接口,则spring会调用destory() 方法 将Spring中的Bean销毁。
3.5 Autowired 和Resource 注解的区别
1、autowired 是spring 提供的注解,@Resource是javaee 提供的注解。
2、Autowired 采用的是按类型注入。当一个类有多个Bean的时候需要配合@Qualifier 来指定唯一的Bean。而@Resource 默认安装byName 自动注入。
3、@Resource 可以执行 name 和type .可以通过type 注入,也可以通过name 来注入。
3.6 循环依赖
利用三级缓存来解决循环依赖的。
1、当创建对象A的Bean 的时候,会先将A对象Bean 放入三级缓存,然后填充属性;
2、发现依赖对象B ,但是对象B 还没有创建,所以就创建对象B
3、对象B在创建过程中,发现依赖对象A,就先从一级缓存中获取,没有获取到就从二级缓存中找,没有找到就从三级缓存中找,找到了还没有创建完成的对象A .然后将对象A 进行一些处理移入二级缓存中。
4、这样对象B就可以继续完成Bean 创建的其他步骤知道完全创建好。
5、B 对象的Bean 创建好之后,放入了一级缓存SingLetonObject中,对象A可以继续完成创建。最终创建好的A对象也会进入一级缓存中。
3.7 事务的传播行为(7中类型)
事务往往在service 层控制,如果在 service 层方法A调用另个 service 的方法 B 。A和B 本身都本身都添加了事务控制,那么在A调用B 的时候就需要进行事务的一些协商,这就是事务的传播行为。
有如下7中类型:
A调用B,站在B的角度来定义传播行为。
1、如果当前没有事务,就新建事务;如果存在一个事物,就加入到当前事务中。这是最常见的一种类型。
2、支持当前事务,如果没有事务,就以非事务执行。
3、支持当前事务,如果没有事务,就抛出异常。
4、新建事务,如果当前存在事务,就将当前事务挂起。
5、以非事务运行,如果存在当前事务,就将当前事务挂起。
6、以非事务运行,如果存在当前事务,就抛出异常。
7、如果当前存在事务,就在嵌套事务内执行。如果当前没有事务,就创建一个事物执行。
3.8 spring mvc 请求处理流程(10步)
1、DispatcherServlet 接收到客户端发送的请求。
2、DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
3、HandleMapping 根据请求URL 找到对应的handler 以及处理器 拦截器,返回给DispatcherServlet
4、DispatcherServlet 根据handler 调用HanderAdapter 处理器适配器。
5、HandlerAdapter 根据handler 执行处理器,也就是我们controller层写的业务逻辑,并返回一个ModeAndView
6、HandlerAdapter 返回ModeAndView 给DispatcherServlet
7、DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
8、ViewResolve 解析ModeAndView 并返回真正的view 给DispatcherServlet
9、DispatcherServlet 将得到的视图进行渲染,填充到request域中
10、返回给客户端响应结果。
3.9 过滤器拦截器
-
过滤器(Filter):对Request请求起到过滤的作⽤,作⽤在Servlet之前,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进⾏过滤处理
-
拦截器(Interceptor):是SpringMVC、Struts 等表现层框架⾃⼰的,不会拦截 jsp/html/css/image 的访问等,只会拦截访问的控制器⽅法(Handler)。拦截器会作用三个地方:在 handler 执行之前执行一次,用来校验参数合理性,handler 之后、视图解析器之前执行一次,视图解析之后执行一次。
3.10 springboot 启动流程(9步)
1、加载并启动监听器
2、创建项目运行环境,并加载配置
3、创建 spring 容器。
4、运行spring 容器的前置处理。主要是容器刷新之前的准备工作,设置容器环境,并且将启动类注入容器,为后面开启自动化配置服务。
5、刷新是spring 容器。通过refresh() 方法对整个IOC 容器初始化。包含bean 资源点定位解析注册等。
6、运行spring 容器后置处理器。扩展接口,如果有自定义需求,可以重写该方法。
7、发布结束执行的事件。
8、执行自定义执行器。
9、返回容器。
3.11. 说说 hashMap 的原理
在jdk 1.7 版本,hashmap 的数据结构是 数组加链表
在jdk 1.8 版本,hashmap 的数据结构是数组加链表叫红黑树。当数组长度大于64 且链表长度大于8时,链表就会转换成红黑树。
3.12. synchronized 和 ReentrantLock 的区别
1、synchronized 是 JVM 隐式实现的,而 ReentrantLock 是 Java 语言提供的 API;
2、ReentrantLock 可设置为公平锁,而 synchronized 却不行;
3、ReentrantLock 只能修饰代码块,而 synchronized 可以用于修饰方法、修饰代码块等;
4、ReentrantLock 需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而 synchronized 无需手动释放锁;
5、ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行。
4. jvm
4.1 jvm 内存模型
jvm 内存分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
程序计数器:线程私有。是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
虚拟机栈:线程私有。用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame)**,**用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:线程私有。用于 虚拟机 Native 方法
堆:线程共享、保存对象实例,所有的对象实例都在堆上存储。
方法区:线程共享。存储被虚拟机加载的类信息、常量、静态变量等。
4.2 jvm 堆
4.2.1 堆内存分布
jvm 将堆内存分为 年轻代和年老代。年轻代和年老代的比例为1:2 。年老代用来存储存活时间比较长或者大对象。年轻代用来存储错过时间比较短的对象。年轻代中又分为Eden 区和两个Survivor 区,比例为8:1:1。
4.2.2 对象分配过程
1、首先会将对象放入年轻代的 Eden 区,如果Eden 区能发下就放入。
2、如果 Eden 区放不下了,就会触发 YangGC。会对Eden 区和使用的 Survivor 区进行垃圾回收,存活的对象保存到另一个空闲的Survivor 区。然后将新对象放入 Eden 区。
3、当Survivor 区有对象经历 15 次 yangGC 后还存活,就迁移到年老代。15次是默认的,可以调整。
4、如果新增对象大小超过Eden 区 一半时,会直接加入年老区。如果年老区 能放下就放入。
5、如果年老区放不下,就会触发OldGC。
6、如果OldGC 后,还是放不下对象,就会触发FullGC
7、如果FullGC 还放不下,就会报OOM 异常。
4.3 方法区
方法区保存的内容:类型信息、域信息、方法信息、运行时常量池。元空间用来存储类的元信息,存储位置为本地内存。静态变量和常量池存储在堆中。
4.4 类加载的执行过程
类的生命周期:包含7个阶段,有加载、验证、准备、解析、初始化、使用和卸载。
加载:分为预加载和运行时加载、预加载是在JVM启动的时候就加载的class 文件。加载的都是lib/rt.jar 下的 .class 文件。运行时加载是指我们一般的class 文件,只有在用到的时候会先去内存中查一下有没有,没有才会进行加载到内存中。主要获取 .class 文件的二进制流。类信息、静态变量、字节码、常量这些信息会存入 方法区中。
连接分为验证、准备和解析
验证:保证 .class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
准备:为类的变量分配内存并设置其初始值。这里的初始值是 默认初始值。,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
解析:是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
初始化:类的初始化阶段是类加载的最后一个过程。初始化阶段就是执行类构造器方法的过程。为类变量进行初始化赋值。这里就是我们代码中定义的值了。
4.5 双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给自己的父类加载器完成,每个类加载器都是如此,只有当父类加载器在自己范围内找不到指定类时,又会委派自己的子类进行加载。这就是双亲委派模型。这样做的好处是:让类加载尽量都交给父类加载,这样就可以避免不同的子类都需要进行类加载,提升了效率,也更加安全。
4.6 判断对象已死算法
1、引用计数算法。当该对象被应用了,计数器就+1,引用失效了,就-1。当计数器值为0 时表示没有被使用,可以回收了。不能解决循环引用的问题。
2、可达性分析算法。从一系类的 GC root 的对象作为起点,向下搜索到对象的路劲。如果对象没有和 GC root 对象不存在路劲。就会被标记需要清除。
GC root 对象有哪些?
1、栈帧中局部变量表的reference 引用所引用的对象。
2、方法区中 static 静态引用的对象。
3、方法区中final 常量引用的对象。
4、所有被同步锁持有的对象。
5、java 虚拟机内部的引用,如基本数据类型的对象、异常对象、系统类加载器等
finalize() 方法
如果对象进行可达性分析,发现对象没有和 GCroot 相连接的引用链,会被第一次标记,随后进行一次筛选,判断是否有必要执行 finalize() 方法。
如果没有重载finalize() 方法,或者虚拟机已经执行过了该对象的 finalize() 方法,则判定为没有必要,直接等待回收。如果判定有必要执行 finalize() 方法。就会放入F-Queue 队列中,随后由低优先级的线程执行 finalize() 方法。在方法中,可以将当前对象和GCroot 对象建立引用链,摆脱被回收的命运。否则执行完 finalize() 依旧会被回收。
4.7 引用类型
java虚拟机有4中引用类型:
强引用:进行垃圾回收时,垃圾回收器不会对强引用的对象进行回收。
软引用:只要内存足够,垃圾回收器就就不会回收,只要当内存不足时才会对软引用的对象进行回收。
弱引用:在垃圾回收器执行垃圾回收时,不管内存够不够,都会对弱引用的对象进行回收。
虚引用: 它是最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
4.8 垃圾回收算法
4.8.1 分代收集理论
基于两个假说:
1、大部分对象都是朝生熄灭的
2、存活时间越久的对象越难以消亡。
4.8.2 标记清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回 收的对象, 在标记完成后,统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回收所有未被标记的对象
缺点:内存空间碎片化严重。
4.8.3 标记复制算法
当这一块的内存用完了, 就将还存活着 的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。
缺点:每次有一半的内存空闲,利用率不高。如果99%的对象都是存活的(老年代),那么老年代是无法使用这种算法的。
4.8.4 标记整理算法
的“标记-整 理”(Mark-Compact) 算法, 其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。
4.9 垃圾收集器
4.9.1 CMS 收集器
CMS 收集器分为4个步骤:
1、初始标记: 这个阶段会stop-the-world 。主要是来标记 GC roots 能够直接关联到的对象。标记之后恢复暂停的所有应用。操作速度是比较快的。
2、并发标记:这个阶段是GC roots 直接关联到的对象开始遍历标记整个对象图的过程,耗时较长,但不用暂停用户线程。可以并发执行。
3、重新标记:为了修正并发标记阶段用户线程运行导致标记发生变动,从而进行重新标记,也会stop-the-world 。耗时比初始标记会长一点。
4、并发清理:清理哪些被标记为死亡的对象,并释放内存空间。采用标记清除算法,不用移动存活对象,可以并发执行。
4.9.2 G1 收集器
G1 收集器特点:
1、G1 把堆内存分为多个独立的 region 区域。
2、G1 沿用了分代思想。保留了年轻代、年老代。但他们不再物理隔离。都通过Region 存储。
3、G1 采用标记整理算法,局部采用标记复制算法。不会产生内存碎片化。
4、G1 能充分利用多CPU 、多核硬件环境,尽量缩短STW
过程:
初始标记:和CMS 初始标记阶段一样,标记 GC roots 能够直接关联到的对象。会STW
并发标记:和CMS 并发标记阶段一样。GC roots 直接关联的对象整个对象树进行标记。
最终标记:修正并发标记阶段,因程序运行产生变化的那部分对象。
筛选回收:根据时间来进行价值最大化收集。
4.9.3 ZGC 收集器
ZGC 是java 11 版本中提供的高效垃圾回收算法,有以下特点:
1、使用了着色指针技术,利用指针额外信息位,在指针上对对象进行着色标记。
2、使用了读屏障,使得进行垃圾回收大部分时间都不需要STW,因此大部分时间都是并发处理的。
3、基于Region ,没有进行分代,也灭有固定Region 的大小,Region 是可以动态创建和销毁的。对大对象可以更好的管理。
4、压缩整理。在回收后会对 Region 对象进行移动合并,解决碎片化问题。
过程:
1、初始标记:在开始的时候会有短暂的 STW ,用户标记 GC Roots。
2、并发标记:这个阶段通过对象指针着色来标记,结合读屏障解决单个对象的并发问题。在最后会有一个短暂的STW 来解决边缘情况。
3、清理阶段:这个阶段会对不再使用的对象进行回收。
4、并发重定位:就是对存活的对象进行移动,来解决碎片化的问题。也是利用读屏障和用户线程并发处理的。
5. 设计模式
5.1 单例模式
保证一个类仅有一个实例,并提供一个访问它的方法。包含一个静态变量,一个私有的构造方法,一个 public 的静态方法。
饿汉式、懒汉式、双重校验锁。
public class SingLeton{
private volalite static SingLeton sing;
private SingLeton(){
}
public static SingLeton getSingLeton(){
if(sing==null){
synchronized(SingLeton.class){
if(sing==null){
sing=new SingLeton();
}
}
}
return sing;
}
}
静态内部类。
public class SingLeton{
private static class SingLetonInner{
public static final SingLeton sing=new SingLeton();
}
private SingLeton(){}
public static SingLeton getInstance(){
return SingLetonInner.sing;
}
}
5.2 工厂模式
工厂模式就是通过工厂来创建想要的对象,而不是自己去创建对象。这样降低了代码间的耦合度。比如一个服饰工厂可以生产衣服,裤子,鞋子,袜子,帽子等等。我们想要用衣服,不用自己来造了,而是和工厂说你想要什么,那工厂就给你生产什么。这就是工厂模式。主要是将创建对象的实例交给工厂类来完成,并进行管理。
工厂分为简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂模式:是工厂模式最简单的一种,实例有工厂直接创建。也就是上面的例子中,你想要衣服,那这个服饰工厂就给你做一件衣服给你。
工厂方法模式 :就是工厂本身不进行实体对象的创建,而是通过对应的下游工厂来创建然后在返回,有点总工厂子工厂的意思,总工厂管理着所有的子工厂,每个子工厂只生产指定的商品,当通知总部想要什么东西时,总部就通知对应的子工厂生产,拿到产品后再返回客户。
抽象工厂:就是一个抽象工厂类,里面只是声明可以实现哪些对象,但是具体的实现就交给具体的工厂完成。抽象工厂就好比包皮公司,它告诉你他可生产服饰,也就可以生产食品。那比如你想要衣服,它就给你一个服饰工厂的联系方式,你通过这个服饰工厂来获取到衣服。想要辣条,那他就给你推一个食品公司的联系方式,让这个食品公司给你做。
工厂方法模式和抽象工厂模式的区别:在于工厂方法模式主要是生产某一类商品。而抽象工厂,我不关心你怎么实现,只要你说你能做这个商品,我就可以为你代言。
5.3 构建者模式
将构建一个复杂的对象可以进行拆分成构建一个个简单的对象。然后组装成复杂的对象,就是构建者模式。比如mybatis 中为了构建Configuration 这个复杂对象,就先构建了Dadasource、MapperStment、等很多的对象组成,在解析SqlMapConfig配置文件的时候,就会先将各种配置文件封装到对应的对象中,最后组装成 Configuration 对象
5.4 代理模式
代理模式就是我们想要执行的方法通过代理对象来完成,就好比我们要抢票回家,我们不想自己盯着抢,所以就找代理帮我们抢从而达到目的。代理模式分为静态代理和动态代理。
静态代理:就是具体的代理类,在编译阶段就知道这个代理能做什么事情。就好比抢票的代理一样,它只能做抢票这个代理,而不能代你抢钱哈哈。
动态代理:动态代理就不同了,它在编译阶段也没有具体的实现。而是在程序运行期间根据JVM的反射机制动态生成的。比较出名的就JDK 动态代理 和cglib 动态代理。
JDK 动态代理:主要实现InvocationHandle 接口并实现其 invoke 方法。
Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
// 前置增强
....
// 调用原有业务逻辑
result = method.invoke(obj,args);
//后置增强
...
return result;
}
})
cglib 动态代理:需要引入其依赖,使用方法和JDK动态代理差不多。实现MethodInterceptor 接口并实现 intercept 方法。
public Object getCglibProxy(Object obj) {
return Enhancer.create(obj.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object result = null;
//前置增强
...
result = method.invoke(obj,objects);
//后置增强
...
return result;
}
});
}
JDK 动态代理是java 语言自带的功能,提供了稳定的支持。是通过拦截器和反射实现的,jdk 动态代理只能代理继承接口的类。
cglib 动态代理是第三方提供的工具,基于ASM 实现的,性能比较高,cglib 动态代理无需通过接口实现,它是通过实现子类的方式来完成调用的。
5.5 适配器模式
6. 数据结构和算法
6.1 数据结构
常见的数据结构如下:
6.1.1 线性表
6.1.1.1数组
有限个相同类型的变量组成的集合。物理地址连续(连续的内存空间存储)。
删除和插入元素的时候,后续的元素需要进行位移。
扩容数组的时候,需要先创建一个新数据,然后将原数组的值填充到新数组中。
优点:具有高效的随机访问能力。只需要给出下标,就可以常量时间找到对应元素。
缺点:插入和删除,会导致其他元素发生位移。影响效率。
6.1.1.2链表
链表是物理上不连续的节点组成。每个节点包含两部分。存储数据元素以及指向下个节点的指针。
常见的链表有:单链表,双链表,循环链表。
优点:插入,删除、更新的效率高。
缺点:查询的效率低,不能随机访问。在查询节点的时候,只能从头结点开始一个个的往后找。
6.1.1.3 栈
栈是一种线性结构,栈中的元素只能先入后出。最早进入的元素为栈底,最后进入的元素叫栈顶。
栈可以用数组实现也可以用链表实现。
6.1.1.4 队列
队列也是一种线性结构,只能先入先出。队列的出口端叫做队头,队列的入口端加做队尾。
队列可以用数组实现也可以用链表实现。
6.1.2 散列表
散列表也是 hash 表。这种数据结构提供了key 和value 的映射关系。只要给出 key ,就可以高效的查询出它所匹配的 value 。
6.1.3 树
树又分为二叉树和多叉树。
6.1.3.1 二叉树
二叉树每个节点最多只有两个子节点。
6.1.3.2 满二叉树
一个二叉树所有的非叶子节点都存在左右子节点。且所有的叶子节点都在同一层,就是满二叉树。
6.1.3.3 完全二叉树
如果二叉树的深度为k。k-1 层为满二叉树。第k 层,所有节点都连续集中在最左边,则是一颗完全二叉树。
6.1.3.4 平衡二叉树
左子树和右子树的深度差的绝对值不超过1。并且左子树和右子树也为平衡二叉树。
6.1.3.5 二叉查找树
在二叉树的基础上。如果有左节点,左节点的的值都小于根节点。如果有右节点,右节点的值都大于根节点。二叉查找树又叫二叉排序树。
6.1.3.6 红黑树
一种自平衡的二叉查找树。有以下特征:
1、每个节点不是红色节点就是黑色节点。
2、根节点是黑色。
3、每个叶子节点都是黑色的空节点。
4、如果一个节点是红色的。那么它的子节点必须为黑色。
5、每个节点到各个叶子节点的包含相同数量的黑节点。
6、新加入的节点为红色,然后校验是否满足红黑树的规则,。如果不满足则需要进行颜色翻转、左旋、右旋等操作。
6.1.3.7 B 树
B 树为一种多叉树。一颗 M 阶的B 树 特征如下:
1、树的每个节点至多有M 个子节点。
2、根节点至少有两个子节点。
3、除根节点外,其他非叶子节点至少有 m/2 个子节点。
4、所有叶子节点都在同一层。
6.1.3.8 B+ 树
B+ 树是B树的变种。如下特征:
1、多路平衡树
2、只有叶子节点保存数据
3、搜索时 也相当于二分查找
4、增加了相邻叶子节点指针
6.2 算法
常见的算法如下:
6.2.1 排序
6.2.1.1 冒泡
相邻的两个元素比较,左边大于右边则进行交换。时间复杂度O(n^2)。是稳定排序
6.2.1.2 快排
快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。每次选择一个基准元素,将比基准元素大的元素放到右边,比基准小的元素放到左边。然后分别对左右进行快排。是不稳定的排序
6.2.1.3 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。是不稳定的排序。
7. 消息队列
主流的消息队列有RabbitMQ、kafka、RocketMQ。三者各有优劣。RocketMQ 支撑高并发高可用的。
7.1 消息队列的使用场景
比如 创建订单减少库存、以及秒杀场景就可以使用消息队列。消息队列的使用,主要有三大优点:降低耦合、流量削峰填谷、异步。在一些不需要同步处理的场景,可以先把请求保存到消息队列中,直接返回结果给前端。提升响应速度。后续再进行消息的处理。如我们感知项目中,对没有接口日志都要进行日志入库的操作。我们之前是返回结果之前,将请求信息写入日志文件,后台线程定期执行入库。这样做将业务和日志入库进行了强耦合,进行了磁盘io ,接口的整体响应效率受到影响。后面引入 RocketMQ,接口再响应之前,向RocketMQ 中发送一条请求的消息后就返回给前端了。然后单独启用一个日志入库的服务消费RocketMQ 中的日志消息,进行日志入库。
7.2 RocketMQ 的角色
-
Producer:消息的发送者;Producer与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
-
Consumer:消息接收者;Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还 是Slave拉取。
-
Broker:暂存和传输消息;Broker分为Master与Slave,一个Master可以对应多个Slave,Master与Slave 的对应关系通过指定相同的BrokerName,不
同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。
-
NameServer:管理Broker;是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
-
Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息
-
Message Queue:相当于是Topic的分区;用于并行发送和接收消息
7.3 RocketMQ 的执行流程
1、启动 NameServer,NameServer 起来后监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心。
2、Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP+端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系。
3、收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker上,也可以在发送消息时自动创建 Topic。
4、Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。
5、Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息
7.4 RocketMQ 消息过滤
RocketMQ 的消息过滤主要是在 consumer 端订阅消息时再做消息过滤的。主要基于 Tag 进行过滤。consumer 在订阅消息时,除了指定 topic 还可以指定 tag 。根据 tag 来过滤消息。
7.5 零拷贝
所谓的零拷贝就是数据在项目的虚拟内存和服务器的物理内存之间没有发生数据拷贝。正常的数据和磁盘的交互流程是。数据首先在项目的虚拟内存中,然后拷贝到物理内存,最后拷贝到磁盘上。读取的话先从磁盘到物理内存,再到项目的虚拟内存。使用零拷贝技术可以减少拷贝的次数。内存中不发生数据拷贝,提升数据传输效率。
7.6 同步复制异步复制
如果一个broker 组中存在master 和slave .消息需要从 master 同步到 slave.有同步复制和异步复制两种方式。
同步复制:是等 master 和 slave 都写入成功后才返回客户端写成功状态。
异步复制:只等 master 写入成功就返回。
Master 角色的broker 支持读和写。slave 角色的broker 仅支持读。consumer 可以连接master 或者slave 进行消息的读取。
7.7 刷盘机制
RocketMQ 的所有消息都是持久化的,消息会先写入物理内存,然后刷盘保存到磁盘上。刷盘有同步刷盘和异步刷盘
同步刷盘:同步刷盘等待消息保存到了磁盘后才返回。
异步刷盘:消息保存到了物理内存就返回了。
7.8 延时消息
延时消息是指消息发送到 broker 后,不会立即被消费。而是等待特定的时间投递给真正的topic .一共有18个等级。可以通过messageDelayLevel 配置。最久延时2小时。
7.9 事务消息
RocketMQ 采用的是两阶段提交的方式实现事务消息。
流程如下:
1、发送方向RocketMQ发送“待确认”消息。
2、RocketMQ将收到的“待确认”消息持久化成功后,向发送方回复消息已经发送成功,此时第一阶段消息发送完成。
3、发送方开始执行本地事件逻辑。
4、发送方根据本地事件执行结果向 RocketMQ 发送二次确认(Commit或是Rollback)消息。RocketMQ收到Commit状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;收到 Rollback 状态则删除第一阶段的消息,订阅方接收不到该消息。
7.10 顺序消息
实现顺序消息的话,只需要生产者只往一个队列中发送消息,并且发送模式调整为同步发送。这样就可以保证消费中从这个队列中消费的消息都是有序的。
8. mysql
8.1 mysql 体系架构
mysql server 架构分为:网络连接层、服务层、存储引擎层、和系统文件层。
网络连接层:客户端连接器提供与 mysql 服务端连接的支持。java、c、typhon 通过各自的 api 与 mysql 进行连接。
服务层:系统管理和控制工具、连接池、SQL 接口、解析器、查询优化器、缓存。
8.2 mysql 运行机制
1、建立连接。mysql 客户端和服务端通信方式为半双工。也就是某一时刻,只能接受数据或者发送数据。
2、查询缓存。先在缓存中查询是否有完全一致的 sql ,如果存在就直接将结果返回。
3、解析器解析。解析器将 sql 进行语法解析,检测语法是否正确。
4、查询优化器根据解析树生成最优的执行计划。
5、查询存储引擎执行计划返回结果。
8.3 mysql 存储引擎InnoDB
InnoDB 支持事务、有行锁。支持多种索引。主要分为内存结构和磁盘结构两大部分。
8.3.1 内存结构
内存结构包含:buffer pool 、change buffer、log buffer、Adaptive Hash index 四部分。
1、buffer pool(缓冲池)。以page 为基本单位,在InnoDB 访问表和索引时会在 Page 页中缓存,从而下次查询的时候从 缓冲池中查找,从而减少 磁盘的IO ,提升效率。
2、change buffer(写缓存)。主要在进行增删改的操作是,如果 buffer pool 中没有对应的 Page 数据。并不会立刻从磁盘页中加载数据到缓存池,而是先在 change buffer 中记录下来,等下次数据读取时,再将数据合并到buffer pool 中。减少IO ,提升效率。
3、Adaptive Hash Index (自适应哈希索引),用户优化对缓存池的查询。
4、Log buffer(日志缓冲区)。用来保存要写入磁盘的 undo/redo 数据 。
8.4 mysql 索引
mysql 索引可以提升查询效率。有如下索引类型
从应用层次划分:普通索引、唯一索引、主键索引、复合索引
从数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)、非聚集索引(非聚簇索引)
8.4.1 普通索引
最普通的索引类型,基于普通字段简历的索引,没有任何限制。
8.4.2 唯一索引
和普通所以的区别,索引字段的值必须唯一,但是可以为空值。
8.4.3 主键索引
它是一种特殊的索引,不允许有空值,且字段值必须唯一。一张表只能有一个主键,也只能有一个主键索引。
8.4.4 复合索引
复合索引是对表的多个字段创建的联合索引。
如果表已经建立了(col1,col2),就没有必要再单独建立(col1);如果现在有(col1)索引,如果查询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。
8.4.5 聚集索引
B+树叶子节点存放的索引值和行记录就属性聚集索引,如果叶子节点只存储了索引值和主键 那就是非聚集索引。
简单说不需要回表查询的就是聚集索引,需要回表查询的就是非聚集索引。主键索引就是聚集索引。
8.4.6 索引原理
Mysql 索引采用的是B+ 树的数据结构。
1、非叶子节点不存储数据,值存储索引值。
2、叶子节点包含在同一层,包含所有的索引值和数据。
3、相邻的叶子节点通过指针连接,提高区间访问性能。
8.4.7 like 查询
MySQL 在使用 Like 模糊查询时,索引是可以被使用的,只有把%字符写在后面才会使用到索引。
8.4.8 explain 有哪些字段
1、select_type 表示查询类型。常用值有SIMPLE
2、type 表示存储引擎查询数据时采用的方式。ALL 全表查询、index 基于索引的全表扫描、range 范围查询、ref 索引查询。
3、possible_keys 表示查询时能够使用到的索引。
4、key:表示查询时真正使用到的索引,显示的是索引名称。
8.4.9 慢查询优化
先分析慢查询的原因:
1、全表扫描
2、全索引扫描
3、索引过滤性不好
4、频繁的回表查查询
如果没有使用索引就创建所以,如果索引效率不高,就优化索引,最好使用覆盖索引。避免回表查询。
8.5 mysql 锁
8.5.1 锁分类
在 MySQL中锁有很多不同的分类
-
从操作的粒度可分为表级锁、行级锁和页级锁。
- 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB 等存储引擎中。
- 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。
- 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。应用在BDB 存储引擎中。
-
从操作的类型可分为读锁和写锁
- 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。
- 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。
-
从操作的性能可分为乐观锁和悲观锁。
- 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
- 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。
8.5.2 悲观锁
悲观锁(Pessimistic Locking),是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁范畴。
表锁:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。
共享锁:是行锁-读锁,事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。
排它锁:是行锁-写锁。事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录
8.5.3 乐观锁
乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时,想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁,而是在进行事务提交时再去判断是否有冲突了。
乐观锁实现的关键点:冲突的检测。
悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要求高的选择乐观锁;对于并发率要求低的可以选择悲观锁。
实现方式:添加版本和时间戳
8.5.4 死锁
死锁产生的四个必要条件:
互斥条件:一个资源同一时刻只能被一个进程占用。
不可剥夺:一个资源除非被占用的进程主动释放,否则不能被剥夺
请求保持:一个进行请求资源时,没有获取到则保持请求,直到获取资源
循环等待:进程a 等待进程b 持有的资源,进程b 等待进程a 持有的资源
破坏其中一个条件就可以避免死锁。
8.6 mysql 事务
8.6.1 事务特性
mysql 事务有4个特性,即所谓的 ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
原子性:事务是一个原子操作单元,对数据的修改,要么全都执行,要么全都不执行。
一致性:是指事务在开始前和事务结束后,数据库的完整性限制没有被破坏。
隔离性:是指一个事务的执行,不会被其他事务干扰,一个事务的内部操作及使用的数据对其他的并发事务是隔离的。
持久性:指事务一旦提交,它对数据库中的数据修改时永久性的,后续操作不会对其有任何影响,不会丢失。
8.6.2 事务隔离级别
数据库系统提供了以下 4 种事务隔离级别
-
读未提交:解决了回滚覆盖类型的更新丢失,但可能发生脏读现象(一个事务读取到了另一个事务修改但未提交的数据),也就是可能读取到其他会话中未提交事务修改的数据。
-
已提交读:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生不可重复读现象(一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致),也就是可能在一个事务中两次查询结果不一致。
-
可重复度:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读(一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录),简单的说幻读指的的当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
-
可串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下
8.6.3 MVCC
多版本控制,支持读读、读写、写读并行。但为了保证一致性,写写是无法并行的。
在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影响其他事务对此记录的读取,实现写和读并行。
8.7 mysql 分库分表
使用分库分表时,主要有垂直拆分和水平拆分两种拆分模式,都属于物理空间的拆分。
垂直拆分:由于表数量多导致的单个库大。将表拆分到多个库中。
水平拆分:由于表记录多导致的单个库大。将表记录拆分到多个表中。
8.7.1 主键策略
1、uuid
2、雪花片算法。ID 按照时间有序生成。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0
3、redis 生成id.利用 redis 的原子操作 INCR 来实现。
8.7.2 分片策略
1、范围分片
2、hash 取模分片
3、一致性hash 分片
8.8 mysql 主从同步
8.8.1 适用场景
MySQL 主从模式是指数据可以从一个 MySQL 数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,从节点可以复制主数据库中的所有数据库,或者特定的数据库,或者特定的表。
8.8.2 主从同步作用
1、实时灾备,用于故障切换
2、读写分离,提供查询服务
3、数据备份,避免影响业务(高可用)
8.8.3 主从同步实现原理
主要分为三个步骤
1、主库将数据库的变更记录到Binlog 日志文件中
2、从库读取主库的 Binlog 日志文件信息写入到从库的 Ready log 日志中。
3、从库读取 Ready Log 日志信息在从库中 Replay ,更新从库信息
异步复制:master 不会等待slave 节点返回,直接提交事务,会导致数据不一致。
半同步复制:master 会等待slave 节点的ack 消息后才会进行事务提交。
9. redis
9.1 跳跃表
跳跃表是有序集合的底层实现。就是将有序集合的部分节点进行分层。每一层都是有序集合,并且层次越高,节点数量就越少。最底层的包含所有节点数据。典型的空间换时间。
9.2 字典
字典是 redis 用来存储键值对的一种数据结构。
hash 函数: 把redis 的key(字符串、整型、浮点型)统一转换成统一长度的整数。
hash 冲突:采用单链表解决。
字典扩容:新申请的容量为原来的一倍,将老的数据迁移到新字典中需要rehash。比较缓慢。
9.3 压缩列表
由一系列特殊编码的连续内存块组成的顺序型数据结构。没有压缩列表包头部、entry数据和尾部。头部信息包含压缩列表的字节长度、压缩列表尾部元素的偏移量、压缩列表元素个数;尾部为一个字节,为结束标志。
entry 数据包含:前一个元素的字节长度、当前元素的编码、以及当前元素的数据内容。
9.4 快速列表
quicklist 是列表的底层实现。quicklist 是一个双向链表,同时每个节点是一个压缩列表。这样quicklist 的每个节点都可以存储多个元素。
9.5 缓存过期和淘汰策略
9.5.1 过期删除策略
redis 淘汰策略有三种。定时删除、惰性删除和主动删除。
Redis 目前采用惰性删除和主动删除。
9.5.1.1 定时删除
在设置键的过期时间时,创建一个定时器,让定时器在该键过期时,执行对键的删除操作。(需要创建定时器,消耗cpu)
9.5.1.2 惰性删除
在访问 key 的时候发现它失效了,就删除这个key
9.5.1.3 主动删除(定期删除)
每隔一段时间,我们就对一些key 进行检查,删除过期的 key
9.5.2 淘汰策略
9.5.2.1 LRU
LRU (Least recently used )最近最少使用。基于链表实现,每次新数据插入到链表头部,如果链表中的节点有被用到,就将该节点移到链表头部。这样链表尾部的元素就是最近最少使用的了。删除的 时候从尾部开始删除。
volatile-lru:从已经设置过期的数据集中挑选最近最少使用的进行数据淘汰。
allkeys-lru:从所有数据集中挑选最近最少使用的数据进行淘汰。
9.5.2.2 随机
从数据集中任意选择一个数据淘汰
9.5.2.3 volatile-ttl
从已经设置过期时间的数据集中挑选将要过期的数据进行淘汰。从过期时间表中随机挑选几个键值对,取出其中ttl 最小的键值对进行淘汰。
9.6 缓存穿透、缓存雪崩、缓存击穿
9.6.1 缓存穿透
缓存穿透是指在高并发下查询 key 不存在数据,会穿透缓存查询数据库,导致数据库压力过大而宕机。
解决方案:
一:设置一个短暂的随机延时,避免同一时刻所有的请求都穿过缓存增大数据的压力。这样先从数据库查询的数据查询出后,添加到缓存中,其他的请求就不会从数据库中请求了。
二:设置布隆过滤器,在缓存之前再加一个层布隆过滤器,在查询的时候先去布隆过滤器中查询key 是否存在,如果不存在就直接返回,如果存在再查询缓存和DB.
布隆过滤器的原理是一组hash函数。当一个元素被加入集合中时,通过k个 hash 函数将这个元素映射成一个数据中的k个点,把他们置为1。检索是,我们只要看看这些点是不是1就知道集合中存不存在这个元素了。只要有一个点为0,则被检测的元素一定不存在集合中。
9.6.2 缓存雪崩
大量缓存集中在某一时间段内失效,这样在失效的时候,大量请求达到数据库,造成数据库崩溃。
解决方案:
1、key 的失效期分散开,不同的key 设置不同的有效期。
2、设置二级缓存
3、搭建集群。单节点故障后,由从节点补上。
9.6.3 缓存击穿
主要针对热点数据,当key 在某一时间点过期的时候,正好有大量的并发请求访问这个key。从而请求都达到数据库,加大的数据库压力。
解决方案:
1、针对热点 key.不设置超时时间。定期的充数据库同步到redis 中。
2、用分布式锁控制访问的线程。使用redis 的setnx互斥锁进行判断,只有获取锁的线程才能访问数据库,其他的线程进行等待。
9.7 单线程的Redis 为什么这么快
1、正常情况下都是内存操作,不会和磁盘交换,从而访问速度更快。
2、单线程没有锁,没有多线程的切换和调度,不会死锁,没有性能损耗。
3、采用I/O 多路复用模型,非阻塞io 。效率更高
4、数据结构简单,压缩处理。
9.8 redis 分布式锁实现
加锁:使用setnx 加锁,如果执行成功,然后设置过期时间。
解锁:删除加锁的key
10. 中间件
10.1 zookeeper
10.1.1 zookeeper 数据结构
zookeeper 将所有的数据存储在内存中,数据模型是一个Znode tree。用斜杠分割路径,每个znode 上还保留自己的数据内容,同时保留一系列的属性信息。
可以根据需求创建如下四种类型的节点:
持久节点:在节点创建后,会一直存在服务器上,直到删除操作主动删除。
持久顺序节点:和持久节点一样,创建后就一直存在,并且多了一个特性,在创建节点的时候,会加上一个数字后缀,来表示顺序。
临时节点:它的生命周期和客户端会话绑定在一起,客户端会话结束,节点就会被删除掉。如持续节点不同的是,临时节点不能创建子节点。
临时顺序节点:有顺序的临时节点,相对于临时节点而言,在创建的时候,会加一个数字后缀,表示顺序。
10.1.2 监听器
Watcher(事件监听器)是zookeeper 中一个重要的特性,zookeeper 允许用户在指定的节点上注册一些watcher。并在一些特定的事件触发的时候,zookeeper 服务端会将事件通知到感兴趣的客户端。
10.1.3 zookeeper 应用场景
1、数据发布订阅
2、集群管理
3、分布式锁。利用zookeeper 可以实现分布式锁的排它锁和共享锁。
排他锁:利用zk 的强一致性,以及临时节点的特性。首先定义一二锁节点。然后多线程在锁节点下创建一个临时节点,哪个节点创建成功了,就说明该线程获取到了分布式锁,没有获取到锁的线程,就注册一个所节点的子节点监听事件,以便监听到锁节点的变化情况。如果一个线程执行完成或者执行异常,都会关闭连接,表示释放了锁,这样其他节点就可以通过监听事前来重新获取锁了。
共享锁:一样的是利用zk 节点的强一致性。不过创建的是顺序的临时节点。分别表示读锁和写锁。对于读请求,如果没有比自己小的节点或者所有比自己小的节点都是读请求,那么表明自己已经成功的获取到了共享锁,开始执行读取逻辑。对于写请求,如果自己不是序号最小的节点,就需要等待。
4、分布式队列
10.1.4 ZAB 协议
在 zookeeper 中主要是利用原子广播协议来保证数据一致性的。
ZAB 协议的核心是定义了对于那些会改变 Zookeeper 服务器数据状态的事务请求的处理方式
即:所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将一个客户端事务请求转化成一个事务Proposal(提议),并将该Proposa1分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
10.2 dubbo
在集群负载均衡时,dubbo 提供了多种均衡策略。包含随机、轮询、最小活跃调用数、一致性hash。 默认为随机调用。
11. 分布式
11.1 分布式下读写一致性
用户读取自己结果的一致性。保证用户永远能够第一时间看到自己更新的内容。
如:我们发朋友圈,别人能不能第一时间看到没关系,但是需要我们自己第一时间就能看到,这个应该怎么实现?
解决方案:
1、我们设置一个更新时间窗口,在刚刚更新的这段时间内,我们默认都从主库读取,过了这个窗口后,挑选最近有从主库更新的从库读取。
2、我们直接记录用户操作的时间戳。在请求的时候,直接把这个时间戳带上,凡是最后更新时间小于这个时间戳的分库就不予以响应。
11.2 单调一致性
本次读取到的数据不能比上次读取到的数据旧。
解决方案:
根据用户id 计算hash 值,映射到对应的服务器上,这样同一个用户不管怎样刷新,都会被映射到同一台机器,保证不会出现数据变旧的情况。
11.3 CAP 理论
一个分布式系统不能同时满足一致性、可用性、分区容错性这三个基本需求,只能满足其中两个。
11.4 BASE 理论
BASE 由三个单词组成。基本可用(Basically Available)、软状态(Soft state)、最终一致性(Eventually consistent)。核心是想是:即使无法做到强一致性,但是每个应用都可以根据自己的特点,采用适当的方式来使得系统达到最终一致性。
11.5 2PC 和 3PC
两阶段提交协议:分为准备阶段和提交阶段。在准备阶段,事务管理器会给每个参入者发送prepare 消息,每个参数者在自己本地执行事务,并写入undo/redo 日志(undo 是修改前的日志,用于事务回滚,redo是修改后的日志,用来数据提交),这个时候事务还没有提交。在提交阶段,如果事务管理器收到了参入者执行失败或者超时的消息时,会发送回滚的消息给所有参入者进行回滚。如果都成功了,就发送提交的消息给所有参入者进行数据提交操作。
三阶段提交协议:是将2PC 的提交阶段一分为二。precommit 阶段发送预提交请求,各个参入者向协调者反馈执行结果。docommit 阶段执行事务提交。
11.6 paxos 一致性算法
paxos 算法核心就是提案。
1、proposer 选择一个提案为N ,向半数以上的Acceptor 发送编号为N 的 prepare 请求。
2、如果 Acceptor 收到一个编号为N的 prepare请求,并且N为当前Acceptor 所响应的请求中编号最大的,它就接收这个提案,并且不再接收比这个编号更小的提案了。
3、如果proposer 收到了半数以上Acceptor 对其发出的编号为N 的prepare请求响应。就会给半数以上的 Acceptor 发送提案的accept 请求。
4、如果acceptor 收到一个编号为N的accept 请求,如果当前acceptor 没有对编号大于N 的prepare 请求做出响应,它就接收该提案。
11.7 Raft 一致性算法
Raft 一致性算法分为两个阶段:首先是选举阶段,然后通过选举出来的领导人带领进行正常操作。
选举的过程:
1、集群中所有的节点开始的时候都是 follower 跟随者。当发现没有领导者的时候,
2、部分的跟随者会变成候选者,候选者会向跟随者发起投票请求。如果当前候选者获得了超过半数的选票,就自动变成了领导者。
3、如果集群中存在领导者,其他的候选者也会变成跟随者,所有的跟随者都会从领导者中进行数据同步,保证数据一致性。
12. mybatis
12.1 mybatis 初始化
1、通过jdk 文件流加载器读取xml配置文件。
2、通过XmlConfigBuilder 来解析配置文件,并封装到configuration 对象中。
3、创建SqlSessionFactory 对象,并传递configuration
4、通过SqlSessionFactory.openSqlSession() 来创建sqlSession 对象并返回。
12.2 sql 执行过程
1、通过调用Sqlsession 对象的query()或者update() 方法。
2、通过statmentid 从configuration 中获取到对应的mapperstatement
3、将configuration 和mapperstatement 对象传递给Executor 执行器来执行sql 操作。
4、Executor 执行器用来处理缓存,以及动态sql 的生成,并将sql 传递给statementhandle
5、statementHandle 将调用parameterHandle 进行参数的解析
6、statementHandle 执行sql 得到结果。
7、statementHandle 通过ResultSetHandle 将返回的结果进行对象映射,最终返回结果。
12.3 mybatis 插件
mybatis 作为应用广泛的优秀的ORM开源框架,具有强大的扩展性,在mybatis 中的 Executor、StatementHandle、ParameterHandle、ResultSet 这四大组件提供了简易的拓展支持。mybatis 支持插件对上面这四大对象进行拦截,从而实现前置增强和后置增强。
自定义插件只需要实现mybatis 的interceptor 接口,并通过注册来声明拦截的类和方法。