文章目录
- 基础
- String、StringBuffer、StringBuilder的区别
- jvm
- 堆和栈的区别
- 垃圾回收
- 标记阶段
- 清除阶段
- 异常类型
- 双亲委派机制
- hashmap和hashtable concurrentHashMap 1.7和1.8的区别
- java的数据结构
- 排序算法,查找算法
- 堆排序
- ThreadLocal
- 单例模式
- 常量池
- synchronized
- synchronize和lock的区别和使用场景
- LOCK的实现类
- 如何避免死锁
- hashMap 存储大量数据
- 为什么HashMap中的键往往都使用String?
- 线程池
- if,else嵌套优化
- 数组和链表的区别
- hashMap和hashtable的区别
- 常用注解
- 线程的状态
- volatile 如何保证可见性和指令重排
- 异步开发
- set如何保证不重复
- 深拷贝和浅拷贝
- 优化链表查询速度
- 数据库
- 如何优化联表查询
- sql优化方案
- 如何查看有没有走索引
- mysql
- oracle
- 如何优查询速度
- 建表
- 索引的建立规则
- 索引以及实现方式
- 索引的类型
- 聚簇索引和非聚簇索引
- 联合索引的命中规则
- 数据库事务
- select for update
- 什么情况下会发生锁表
- 幻读
- mysql redo和undo
- mysql和oracle的区别
- myisam和InnoDB的区别
- mybatis的条件语句
- parameter 入参类型
- mybatis的缓存机制
- mybatis如何封装返回结果
- 数据库死锁
- 写sql
- spring
- springboot
- springcloud
- SpringBoot启动过程
- spring.factories文件
- 设计模式
- IOC和AOP
- JDK动态代理和CGLIB代理有什么区别
- Spring AOP 和 AspectJ AOP 有什么区别?
- spring事务
- Spring事务失效的场景
- spring对象生命周期
- SpringBoot启动原理
- Spring Factories
- 拦截器和过滤器
- 如何解决循环依赖
- Spring SpringBoot SpringCloud的区别
- Spring Filter 怎么写
- 获取Spring容器对象
- SpringAOP
- redis
- 哪里用到的redis,时效
- redis数据结构
- Stirng 底层实现
- redis的淘汰策略
- 缓存击穿,缓存雪崩
- redis的锁机制
- redis分布式锁
- redis线程模型
- 部署方式
- kafka
- 使用消息队列的原因
- kafka 如何解决消息重复
- kafka如何保证消息不丢失
- kafka什么时候进行rebalance
- kafka 消息积压怎么办
- kafka分区和消费者的关系
- 分区数量
- 消费者分区分配策略
- 其他微服务组件
- eureka注册中心机制
- 限流算法
- 一致性哈希
- 无状态的服务,消费降级
- linux命令
- 其他
- 数据结构-树
- 树
- 特点
- 概念
- 树的表示
- 二叉树
- 概念
- 特点
- 特殊的二叉树
- 二叉树的性质
- 二叉树的存储结构
- 二叉树的遍历
- 堆
- 优先级队列
- 捕获异常
- 设计模式
- 工厂模式
- 抽象工厂模式
- 数据格式标准化
基础
String、StringBuffer、StringBuilder的区别
jvm
概念
1.JVM是java虚拟机,用来执行字节码文件(二进制 class文件)的虚拟计算机。除了java,Scala,Groovy和Python等其他语言经过处理也可以转换成字节码文件。
2.JVM运行在操作系统上,和硬件没有任何关系。
跨平台原理:编译后的字节码文件和平台无关,在java虚拟机上运行。统一的class文件结构,就是jvm的基石。
JVM分类:
- 类加载器子系统
- 运行时数据区
- 执行引擎
- JIT编译器(主要影响性能):编译执行
- 解释器(负责响应时间):逐行解释字节码
程序执行方式有三种,静态编译执行,动态编译执行,动态解释执行。
在java中,程序的执行以动态解释为主,动态编译为辅。(静态编译如C,直接编译成可执行文件exe)
机器码和字节码的区别:
机器码是CPU直接读取,速度快;字节码需要直译器转译后才能变成机器码。
JDK包括了编译器等开发工具和JRE
JRE包括了运行类库和JVM
JVM有两种运行方式 client和server Client启动快,运行慢。Server启动慢,运行快。
JVM流程 .java文件编译器解释为class文件,交给jvm执行引擎执行,执行时会用空间存储数据,就是JVM内存。
JVM内存主要为:堆,栈,方法区,本地方法区,程序技术器。
- 程序计数器
当前线程执行字节码的行数指示器,用来记录虚拟机字节指令地址,线程私有。执行本地方法时为空。也称为PC寄存器。
字节码解释器在工作时,通过改变计数器的值来选取下一跳执行的代码,分支,循环,跳转,异常处理,线程恢复等功能都依赖程序计数器完成。
Java虚拟机的多线程的实现方式:通过轮流切换并分配处理器执行时间实现。 - 本地方法栈
和虚拟机栈作用类似,区别是一个执行java方法,一个执行native方法。线程私有。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。 - 方法区(1.8为元数据区)
主要是存储类信息,静态变量,编译后的代码(字节码)等数据,常量池。线程共享 - 栈
栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表:用来存储方法参数和方法中定义的局部变量
操作数栈:用于保存计算中的临时变量和中间结果,是JVM执行引擎的一个工作区,通过入栈和出栈进行数据访问。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
动态链接:指向方法区的运行时常量池
在Hotspot的演变过程中:
- Java6及之前:方法区存在永久代,保存有静态变量
- Java7:进行去永久代工作,虽然还保留着,但静态常量池,如字符串常量池,已经移动到堆中
- Java8:移除永久代,类型信息、域(Field)信息、方法(Method)信息存放在元数据区;字符串常量池、静态变量存放在堆区
堆中分为老年代和年轻代,年轻代中分为eden区和存活区,区中分为s0和s1
新生成的对象在Eden区
触发Minor GC后幸存的对象存入s0,再次触发Minor GC后,eden区和s0的对象存入s1中,s0清空。
每次移动,递增计数器,超过默认值15 (通过 -XX:+MaxTenuringThreshold 设置),移动到老年代中,eden中没有足够内存分配,也会分配到老年代。
老年代靠major GC。
新生代的回收机制采用复制算法,老生代采用的回收算法是标记整理算法。
堆和栈的区别
栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。栈中数据生命周期短,出栈即失效。栈超过虚拟机允许最大深度StackOverflow
堆:存储对象,线程共享。堆中数据声明周期长,由垃圾回收机制不定期回收。空间不够扩展申请不到足够的内存,oom
垃圾回收
参考【Java】垃圾回收
作用区域:频繁发生在年轻代,较少发生在老年代,极少发生在方法区(永久代/元空间)
引用类型才需要垃圾回收,基本数据类型不需要。
内存泄漏:这个对象不再使用,但是GC没法回收。
垃圾回收分为标记和清除阶段
标记阶段
引用计数法
引用对象+1,引用失效-1。为0则认为可以进行回收。
优点:实现简单,垃圾容易辨识;判定效率高,回收没有延迟
缺点:
1.需要单独的字段存储计时器,增加空间开销
2.每次赋值都需要进行加减法,增加时间开销
3.无法处理循环引用的情况
可达性分析算法
通过被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,走过的路径被称为引用链。当一个对象到GC roots没有任何引用相连时,证明该对象不可用。
同样具备实现简单和执行高效的特点,能有效解决循环依赖的问题,防止内存泄漏的发生。
可达性分析必须在一个能保证一致性的环境下进行。这点也是导致GC必须进行"stop the world"的一个重要原因。
所谓GC roots根集合就是一组必须活跃的引用。可以是:
1.虚拟机栈中引用的对象。
2.静态变量引用的对象,除非类卸载,否则他的引用对象一直存在。
3.所有被同步锁持有的对象。(同步锁要是被销毁,同步就失效了)
清除阶段
JVM中常见的清除方法:1.标记清除法 2.标记复制法 3.标记压缩法
标记清除法:
把存活的对象进行标记,清除死亡对象。
当堆中有效空间被用完,就会stw。然后进行标记和清除。要把用户线程停止保持一致性,防止用户线程产生垃圾。
缺点:
1、效率不高,需要遍历
2、进行GC时需要停止整个应用程序,用户体验差。
3、清理出来的空闲空间不是连续的,会产生碎片。
标记复制法:
内存分为两块,每次只用其中的一块。垃圾回收时,将存活的对象复制到未使用的一块,清除不可达的对象。
年轻代S0和S1也是用的复制算法。
优点:1.没有标记和清除的过程,实现简单,运行高效;2.复制后能保证空间的连续性
缺点:损失一半空间。
适合回收对象多的场景,复制少。适用于年轻代。
标记压缩算法
1.第一阶段和标记清除算法相同,从根节点开始标记被引用对象。
2.第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间。
标记压缩算法等同于标记清除算法加压缩。
优点:1.没有碎片 2.不会内存减半
缺点:
1.效率低,因为会进行整理压缩
2.移动对象时,如果对象被其他对象引用,需要调整引用的地址。
3.移动过程中会stw
分代收集算法:
不同生命周期对象采用不同的算法,提高效率。
1.年轻代:区域小,生存周期短,回收频繁。
采用复制算法,内存利用率不高,hotspot中两个survivor的设计得以缓解
2.老年代:区域大,生命周期长,回收不频繁。
采用标记清除或者标记清楚整理算法。
Old GC: 只收集 old gen 的 GC。只有垃圾收集器 CMS 的 concurrent collection 是这个模式
Mixed GC: 收集整个 young gen 以及部分 old gen 的 GC。只有垃圾收集器 G1 有这个模式
年轻代的gc称为Minor GC。新生代Eden区满的时候触发Minor GC
Full GC是回收整个堆。触发条件为:
1.System.gc
2.老年代空间不足
3.方法区空间不足
4.Minor GC后进入老年代的平均大小大于老年代的可用大小
5.由Eden区,from 区向to区复制,对象大于to的内存,也大于老年代的内存。
单例模式是静态的,生命周期长,如果中间引用了别的对象,那么这个对象一直不会被回收。
Major GC通常是跟full GC是等价的,收集整个GC堆,但也有说法是old GC。
异常类型
为了及时有效的处理异常,java引入了异常类。所有的异常都是Thorwable的子类。
Throwable下有两个分支Excepiton和Error。
异常类主要分为三种类型:
-
系统错误Error
系统错误是由虚拟机抛出的,用户无法处理,如
OutOfMemoryError :内存耗尽 ;
NoClassDefFoundError :无法加载某个Class ;
StackOverflowError :栈溢出 -
编译时异常:Exception (除了其子类RuntimeException)
在编译时期抛出的异常,在编译期间检查程序可能出现的问题,如果有提前防范,捕获处理
应用逻辑处理:
- NumberFormatException :数值类型的格式错误;
- FileNotFoundException :未找到文件;
- SocketException :读取网络失败。
编写逻辑造成:
- NullPointerException :对某个 null 的对象调用方法或字段;
- IndexOutOfBoundsException :数组索引越界
-
运行时异常 RuntimeException
-
java虚拟机正常运行期间抛出的异常。这类异常只有在运行时才能发现是否有异常。
RuntimeException,Error以及他们的子类都被称为免检类,其他异常被称为必检类(Checked Exception),编译器会强制程序员检查并try-catch处理,或者在方法头进行声明。如数组越界和空指针。
-
双亲委派机制
参考【Code皮皮虾】带你盘点双亲委派机制【原理、优缺点】,以及如何打破它?
双亲委派机制是在JDK1.2后才引入的。
加载类时不直接加载,委托给自己的父类加载器,递归直到加载成功。否则自己加载。
目的:1.防止类的重复加载。2.避免核心类遭到修改
Java提供四种类加载器:
- BootStrap 启动类加载器:加载java核心类库 ,javahome/lib下的jar包,rt.jar等
- Ext 扩展类加载器:加载java_home/ext/lib
- Application 应用程序类加载器:主要用来加载当前应用claspath下的所有类。
- User 用户自定义类加载器:用户自定义,加载指定路径下的类
什么时候破坏这个机制?
JDBC
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "0000");
获取连接时的DriverManager因为处于rt下,会被启动类加载器加载。
类加载时,会执行静态方法。其中会加载所有实现了Driver接口的实现类,但是这些实现类都是第三方提供的,启动类加载器无法加载,因此引入了ThreadContextClassLoader(线程上下文类加载器,默认情况下是AppClassLoader)来使用应用程序加载器,破坏双亲委派机制。
tomcat
比如tomcat web容器里面部署很多应用程序,但是每个应用依赖的第三方类库版本不同,但是类的全路径名可能相同。
双亲委派无法加载多个相同的class文件,因此tomcat给每个web容器单独同一个webAppClassLoader加载器。实现隔离性,优先加载Web应用自己定义的类,加载不到再交给CommonClassLoader加载,这和双亲委派机制恰好相反。
如何打破双亲委派机制?
1.自定义类加载器:继承ClassLoader,不想打破,只需要重写findClass,想打破,重写整个loadClass方法,设定自己的类加载逻辑。
2.使用线程上下文类加载器
public class Main {
public static void main(String[] args) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
}
}
hashmap和hashtable concurrentHashMap 1.7和1.8的区别
hashmap
1.7 数组+链表
1.8 数组+链表+红黑树
ConcurrentHashMap简介
hashmap线程不安全。
hashtable线程安全,方法直接加synchronize锁,性能低下。
concurrentHashMap
对数组增加了voliate关键字
1.7 segment+hashentry 分段锁
1.8 cas+synchronized
getSize
1.7获取三次,两次一致返回,三次拿不到加锁进行计算
1.8 获取basecount
put方法
计算哈希值;
当前map是否为空,为空先初始化;
判断哈希值所在位置是否有值,没值直接cas替换
有值判断key是否相等,相等不变
不相等判断是红黑树还是链表,进行循环,key相同替换,未找到相同的进行增加
java的数据结构
16 张图带你搞懂 Java 数据结构
排序算法,查找算法
常见面试的查找和排序算法
面试高频考点 – 常见的排序算法(7种)
- 插入排序:从第二个元素开始,每选择一个元素,这个元素前面就是有序区间,后面就是无序区间。在有序区间选择合适位置插入。
O(n^2) 稳定 - 希尔排序:将数据分成n组,进行排序,主键缩小n值
O(n^1.3~1.5) 不稳定 - 选择排序:每次将最大或者最小放到最后,知道所有都排完
O(n^2)不稳定 - 冒泡排序:在无序区间,通过相邻数的比较,将最大的数据放入一侧,持续整个过程,直到数组整体有序。
时间复杂度:
最坏情况 O(N^2)
最好情况 O(N) (一趟就有序了)
空间复杂度: O(1)
稳定性: 稳定
堆排序
将数组放入堆,调整每一颗子树,使之变成大根堆。
O(n * logN) O(1) 不稳定
package com.ln.mybatis.sort;
import java.util.Comparator;
import java.util.PriorityQueue;
//求前k个最小的元素
public class sortTest {
public static void main(String[] args) {
int test[]={10,11,12,32,434,1,2,3,4,5,6,7,8,9,10,11,12,32,434,54645,6565,757,323,32};
int[] topk=topK(test,3);
for (int i = 0; i < topk.length; i++) {
System.out.println(topk[i]);
}
// System.out.println(topK(test,3));
}
// 找出数组中最小的元素,建立大根堆,然后对根节点进行比较,大则不放,小则替换
public static int[] topK(int[] arr,int k){
// 创建一个大小为k的大根堆
PriorityQueue<Integer> maxHeap =new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return 02-01;
}
});
for (int i = 0; i < arr.length; i++) {
if(i<k){
maxHeap.offer(arr[i]);
}else {
if(maxHeap.peek()>arr[i]){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] ret=new int[k];
for (int i = 0; i <k ; i++) {
ret[i]=maxHeap.poll();
}
return ret;
}
}
- 冒泡排序:
最坏 O(n^2) 最好 O(n) 稳定 - 快速排序(重要)
1.找一个基准值,存储
2.比较左边和右边,小于的放到右边,大于的放到左边
3.然后对左右区间按同样的方式处理
nlogn 最坏n^2
ThreadLocal
线程局部变量,在多并发的情况下使用。
在使用完成之后需要remove掉,避免内存泄漏。ThreadLocal变量的key为弱引用,使用完成后TheadLocal没有使用的强引用后会释放,但是value是强引用,只要线程存活,一直存在强引用,需要通过remove删除Entry.线程池尤为严重。
单例模式
public class SingleTonObj {
private static volatile SingleTonObj singleTonObj;
private SingleTonObj() {
}
public static SingleTonObj getObj() {
if (singleTonObj == null) {
synchronized (SingleTonObj.class) {
if (singleTonObj == null) {
singleTonObj = new SingleTonObj();
}
}
}
return singleTonObj;
}
}
- volatile
其中volatile关键字是为了防止指令重排
jvm创建对象分三步:1.分配空间,2.实例化对象,3.将对象指向空间
如果不使用volatile,jvm会优化,将3移动到2前面,这样a线程,走到了3,还没2。b线程第一个判null已经不成立,返回了没有实例化完成的对象 - 第二个判空的原因是 a,b都经过了第一次判空,但是a先拿到了class的锁,进行了实例化,然后进行释放锁,b拿到锁之后需要在进行判空防止重复实例化破坏单例
- Happens-Beforene内存模型和程序模型顺序
程序A在B前,线程中A就会在B前执行
Happens-Beforene不会破坏代码中的先后顺序,但是在不同代码或者不同线程中的顺序无关
常量池
深度剖析Java常量池
通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class
Class常量池可以理解为Class文件中的资源仓库。Class文件中除了包含版本,字段,方法,接口,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
int a=1 1为字面量,a为符号引用
字面量是指由字母,数字构成的字符串和数值常量,字面量只可以右值出现。
符号引用是编译原理中的概念,是相对于直接引用来说的,主要包括了以下三大类:
- 类和接口的全限定类名
- 字段的名称和描述符
- 方法的名称和描述符
运行时常量池:只有运行时被加载到内存中,这些符号才有对应的内存地址,那么这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用会转变为加载到内存区域的代码的直接引用,也就是动态链接
字符串常量池:
jdk1.6以及之前,运行时常量池在永久代,运行时常量池包含字符串常量池。
jdk1.7: 有永久代,但是逐渐去永久代,字符串常量池从永久代的运行时常量池分配到堆中。
jdk1.8:无永久代,运行时常量池在元空间,字符串常量池依然在堆中。
intern
1.6 在常量池中寻找equal()相等的字符串,存在则返回常量池中的引用。不存在就在永久代的常量池中新建一个实例,放入常量池中并返回。
1.7:不存在永久代,常量池存在返回,不存在可以直接指向堆上的实例。
> String s0="zhigan";
String s1="zhigan";
String s2="zhi" + "gan";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 );//true
字面量声明的字符串常量在编译期就能确定,会存储在常量池中,地址相同。
String s0="zhigan";
String s1=new String("zhigan");
String s2=“zhi”+new String("gan");
System . out . println ( s0 == s1 ); // false
System . out . println ( s0 == s2 ); // false
System . out . println ( s1 == s2 ); // false
new String() 的字符串不是常量,不能在编译期确定,不放入常量池,他们有自己的地址空间。
String a="a3.4";
String b="a"+3.4;
System . out . println ( a == b ); // true
jvm对于加号连接,在编译器就会进行优化,将常量字符串连接。
String a="ab";
String bb="b";
String b="a"+bb;
System . out . println ( a == b ); // false
在+中带有引用,JVM无法优化,因为引用无法在编译期确认,只能在程序运行是动态分配,并将连接后的新地址赋值给B,因此为false(如果bb是一个方法的返回结果,同样的原因)如果bb用final修饰,那么他在编译期会被解析为常量,比较的结果为true。
String s = "a" + "b" + "c" ; // 就等价于 String s = "abc";
String a = "a" ;
String b = "b" ;
String c = "c" ;
String s1 = a + b + c ;
s1这个就不一样,可以通过观察器JVM指令码发现s1的"+"操作会变成如下:
StringBuilder temp=new StringBuilder();
temp.append(a).append(b).append( c );
String s=temp.toString();
Java八大基本对象的包装类型除了两个浮点型,其他都有常量池。另外Byte,Short,Int,Long,Character这五种整形的包装类也只是对应值小于127才可以使用对象池。
synchronized
参考 面试官:请详细说下synchronized的实现原理 - 知乎
概念:在多线程情况下,多个线程访问共享资源会出现问题,而synchronized关键字则是用来保证线程同步的
synchronized解决可见性的方式是,每次都清除工作内存,从主内存中重新获取。
synchronized可以保证并发编程的三大特性:原子性,可见性,有序性。
synchronized可以实现悲观锁,非公平锁,可重入锁,独占锁或者排它锁。
实现原理:
Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。
jDK1.6对synchronized做了哪些优化?
引入偏向锁和轻量级锁,随着竞争的激烈而升级。
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
使用CAS操作将当前线程的ID记录到对象的Mark Word中。
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。
锁消除是指java编译时,消除不可能发生共享资源竞争的锁。
锁粗化是指java编译时,将不必要的重复加锁粗化到整个操作的外部。
for(int i=0;i<n;i++){
synchronized(lock){
}
}
//粗化后
synchronized(lock){
for(int i=0;i<n;i++){
}
}
synchronize和lock的区别和使用场景
参考粗谈synchronize和Lock锁的区别及使用场景
区别
- synchronize是java关键字,内置特性。Lock是一个接口,通过这个接口的实现类可以实现同步访问。
- synchronize 在代码执行结束后或者代码执行异常后会自动释放;而lock必须要用户手动去释放锁,否则会造成死锁。
- synchronize可以锁住代码块,类,对象,lock只能锁代码块
- synchronize只能是非公平锁,而lock可以是公平锁,也能是非公平锁。
- synchronize等待不中断,而lock可中断。
- synchronize不知道线程有没有获得锁,但是lock可以知道。
- synchronize是隐式锁。Lock是显示锁。
- synchronize是悲观锁的一种实现.lock的实现类ReentrantLock主要用到unsafe的CAS和park两个功能实现锁,乐观锁的一种实现。
- 性能比较:竞争不激烈时,synchronize的性能优于ReentrantLock,但是在竞争激烈的情况下,synchronize性能下降几十倍,ReentrantLock的性能可以维持常态。
重入锁提供多样化的同步,如时间限制的同步,被打断的同步。
synchronize的释放: - 占有锁线程代码执行完成
- 占有锁线程出现了异常
- 占有锁线程调用wait方法,进入waiting状态需要释放锁
- 执行完成后可以通过notifyAll或notify等object对象的api来唤醒其他等待线程立马执行。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
接口方法
- lock();用来获取锁,如果锁被其他线程获取,则进行等待
Lock lock = ...; //声明锁
lock.lock(); //获得锁
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
- tryLock();尝试获取锁,立即返回
- tryLock(long time, TimeUnit unit); 尝试在一定时间内获取锁
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
- lockInterruptibly()
它是对于那些未竞争的到锁,而 可以被外部调用interrupt()来中断,从而达到不在等候锁资源,不再去竞争锁
synchronize和reentrantlock都是可重入锁。
就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。synchronize不用手动释放。
实现
- ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
- Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止
LOCK的实现类
Lock定义了标准Lock的API
AQS(AbstractQueuedSynchronizer)
ReentrantLock:重入锁,支持公平和非公平
ReentrantReadWriteLock:在ReentrantLock上基础上支持读写分离是和多读少写的场景CountDownLatch:
Semphore:
Java的Lock实现类介绍
如何避免死锁
1.死锁预防
1.1.破坏占有并且等待
1.一次性申请运行过程中需要的所有资源
2.允许只获得初期资源就开始运行,运行后逐步释放使用完毕的资源,然后再去请求新的资源。
1.2.破坏不可抢占条件
当获取锁失败,释放之前获取的资源
1.3.破坏循环等待的条件
定义资源的线性顺序来预防
2.避免死锁,在使用前进行判断,只允许不会产生死锁的进程申请资源.
一般采用银行家算法来避免。需要知道进程请求资源的最大数目。
hashMap 存储大量数据
参考准备用HashMap存1W条数据,构造时传10000还会触发扩容吗?存1000呢?
不指定容量,会随着数据的增加不断扩容,影响性能。
在指定调用容量的构造方法时,会重新调用另一个构造方法,传入默认的负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
构造方法中初始化了两个成员变量。threadHold 扩容阈值和 loadFactor 负载因子。
tableSizeFor就是找到大于入参的2的整数次方,如传入10,会得到16.
设置容量为2的整数次方是为了减少哈希冲突。
推荐在集合初始化的过程中指定集合初始化大小为 ((需要存储的元素个数)/0.75)+1
为什么HashMap中的键往往都使用String?
参考 为什么HashMap中的键往往都使用String?
1.String重写了hashCode,两个不同引用的String类型,只要值相等,hashcode就相等,而非地址相等。(设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。)
2.String不可变,每当创建一个字符串对象,他的hashcode就被缓存下来,所以存储hashMap不用重新计算,相比于其他对象更快。
线程池
参考并发编程(三):线程池基本面试题(必背题目)
1. 作用:限制系统中执行线程的数量。
1.降低资源消耗:复用线程。
2.提高效率,提前创建,使用从中获取节省创建时间。
3.增加线程的可管理型。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。
2. 常见线程池:
1.SingleThreadExecutor
只有一个线程,保证任务按顺序执行(FIFO,LIFO,优先级)
任务队列为链表结构的有界队列
2.FixedThreadPool
定长线程池,超出等待。只有核心线程,执行完成后回收。
任务队列为链表结构的有界队列。
3.CachedThreadPool
超出回收空闲线程,没有则创建线程。核心线程固定,非核心线程无限。使用完成后闲置10分钟回收。任务队列为延时阻塞队列。
4.ScheduledThreadPool
定时执行任务线程池
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
5.WorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
3. 线程池中的几个重要参数
- corePoolSize 核心线程数量,用不到也不会回收。allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize 最大线程数量,活跃线程数量达到该值,阻塞新任务。
- keepAliveTime 非核心线程最长存活时间
如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 - unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory 线程工厂,指定线程池创建新线程的方式。
- handler 拒绝策略,达到最大线程要执行的饱和策略。
//TreadPoolExecutor(自定义参数线程池)(推荐使用)
public class ThreadPoolDemo {
public static void main(String[] args) {
//1. 使用ThreadPoolExecutor指定具体参数的方式创建线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2, //核心线程数
5, //池中允许的最大线程数
2, //空闲线程最大存活时间
TimeUnit.SECONDS, //秒
new ArrayBlockingQueue<>(10),//被添加到线程池中,但尚未被执行的任务
Executors.defaultThreadFactory(), //创建线程工厂,默认
new ThreadPoolExecutor.AbortPolicy()//,如何拒绝任务
);
//2. 执行具体任务
poolExecutor.submit(new MyRunnable());
poolExecutor.submit(new MyRunnable());
//3. 关闭线程池
poolExecutor.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
}
4. 拒绝策略
任务不断过来,系统无法及时处理,就要拒绝。
- AbortPolicy(默认) 抛出RejectedExecutionException
- CallerRunsPolicy :由调用线程处理该任务。
- DiscardOleddestPolicy: 该策略将丢弃最早的未处理任务,并尝试再次提交当前任务
- DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。
可以通过实现RejectedExecutionHandler接口自定义接口。
5. execute和submit的区别
execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。
submit方法适用于需要关注返回值的场景
6. 线程池的关闭
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
7. 线程数的选择
计算密集型:应为 cpu核数+1 减少上下文切换
即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。
IO密集型:
线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
IO比cpu慢,设置过少线程数会造成cpu资源的浪费。
等待时间越长,线程越多。
8. 线程池都有哪几种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,在未指定容量时,容量默认为 Integer.MAX_VALUE.此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5、DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
9. 线程工作流程
提交任务,线程是否达到核心线程数,未达到,创建核心线程,否则下一步
查看任务队列是否已满,未满,放入任务队列,否则,进入下一步
线程是否到达最大线程数,未到,创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。
随着任务的增加增加活跃线程数,活跃线程数=核心线程数
10.线程池优化
- ThreadPoolExecutor自定义线程池,任务量不大,使用无界队列。任务量大,使用有界队列,防止OOM。
- 如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
- 最大线程数一般设为2N+1最好,N是CPU核数
- 核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
- 如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果。
if,else嵌套优化
参考Java—优化 if-else 代码的 8 种方案
1.提前return,去除不必要的else
if(xx){
}
else{
return
}
--------
if(!xx){
return
}
2.使用三元表达式
3.使用枚举类
String OrderStatusDes;
if(orderStatus==0){
OrderStatusDes="订单未支付";
}else if(OrderStatus==1){
OrderStatusDes="订单已支付";
}else if(OrderStatus==2){
OrderStatusDes="已发货";
}
---------------------
String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc();
4.合并条件表达式
结果相同,合并表达式
5.使用Optional优化if,else
String str = "jay@huaxiao";
if(str != null) {
System.out.println(str);
} else{
System.out.println("Null");
}
-----------------------------
Optional<String> strOptional = Optional.of("jay@huaxiao");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
6.表驱动法
又称之为表驱动、表驱动方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或case)来把它们找出来的方法。以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。
if(param.equals(value1)) {
doAction1(someParams);
} else if(param.equals(value2)) {
doAction2(someParams);
} elseif(param.equals(value3)) {
doAction3(someParams);
}
---------------
// 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
Map<?, Function<?> action> actionMappings = newHashMap<>();
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);
7.使用策略模式
例如支付场景下,支持多种支付方式
public class PaymentService {
CreditService creditService;
WeChatService weChatService;
AlipayService alipayService;
public void payment(PaymentType paymentType, BigDecimal amount) {
if (PaymentType.Credit == paymentType) {
creditService.payment();
} else if (PaymentType.WECHAT == paymentType) {
weChatService.payment();
} else if (PaymentType.ALIPAY == paymentType) {
alipayService.payment();
} else {
throw new NotSupportPaymentException("paymentType not support");
}
}
}
enum PaymentType {
Credit, WECHAT, ALIPAY;
}
作者:小黑说Java
链接:https://juejin.cn/post/7030976391596212255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这种不满足开闭原则(对修改关闭,对扩展开放),修改后需要对其他支付方式进行测试。
策略设计模式是一种行为设计模式。当在处理一个业务时,有多种处理方式,并且需要再运行时决定使哪一种具体实现时,就会使用策略模式。
抽象支付方式为一个策略接口
public interface PaymentStrategy {
public void payment(BigDecimal amount);
}
针对具体的支付方式做实现
public class CreditPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用银行卡支付" + amount);
// 去调用网联接口
}
}
public class WechatPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用微信支付" + amount);
// 调用微信支付API
}
}
重新实现支付服务paymentservice
public class PaymentService {
/**
* 将strategy作为参数传递给支付服务
*/
public void payment(PaymentStrategy strategy, BigDecimal amount) {
strategy.payment(amount);
}
}
策略模式优化后
public class StrategyTest {
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
// 使用微信支付
paymentService.payment(new WechatPaymentStrategy(), new BigDecimal("100"));
//使用支付宝支付
paymentService.payment(new AlipayPaymentStrategy(), new BigDecimal("100"));
}
}
在使用了策略模式之后,在我们的支付服务PaymentService中便不需要写复杂的if…else,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好。
数组和链表的区别
数组:随机查询快,增删慢;内存连续;
链表:随机查询慢,增删快;空间分散,不需要连续;
数组固定大小,在编译期间分配内存;链表动态灵活,在执行或者运行时分配内存;
链表因为要存储上一个和下一个的引用元素,因此需要更多的内存;
链表内存利用率更高;
对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组。
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表。
ArrayList,LinkedList,Vector的区别
ArrayList动态数组,默认容量10,扩容为1.5倍,新建数组,复制数据。
LinkedList双向链表
LinkedList还实现了Deque接口,所以LinkedList还可以用作队列
Vector也是数组,线程安全,每次扩容一倍。
hashMap和hashtable的区别
参考HashMap和Hashtable的区别
1.hashMap线程不安全
hashtable线程安全,用synchronized关键字实现
2.hashMap允许null作为键或者Value,HashTable会抛出异常
3.hashtbale使用的是key的hashcode,hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
4. HsahMap在数组+链表的结构中引入了红黑树,Hashtable没有
5.HashMap初始容量为16,Hashtable初始容量为11
5. HsahMap扩容是当前容量翻倍,Hashtable是当前容量翻倍+1
6. HsahMap只支持Iterator遍历,Hashtable支持Iterator和Enumeration
使用场景:
- 非并发场景(单线程)使用HashMap,并发场景(多线程)可以使用Hashtable,但是推荐使用ConcurrentHashMap(锁粒度更低、效率更高)。
- 另外使用在使用HashMap时要注意null值的判断,
Hashtable也要注意防止put null key和 null value。
常用注解
@Autowired @Component @RestController @Cacheable @RequestMapping @Value @Bean @Import
@Autowired和@Resource的区别
A默认byType,可以通过@Qualify指定bean名称。是spring的注解,默认必须存在bean,可以用required=false设置
R默认ByName,有name和type两种属性,先找name,name没有匹配type。是java的注解。
线程的状态
新建 就绪 运行 阻塞 死亡
wait和sleep的区别
wait释放锁,需要notify唤醒,sleep不释放锁,自动唤醒。
run和start的区别
run直接运行,start是进入就绪状态。
volatile 如何保证可见性和指令重排
造成可见性的原因是JAVA内存模型JMM,在java内存模型中,共享变量存放在主内存中,每个线程都有自己的工作内存,操作共享变量需要从主内存中获取,但是何时写回主内存不可预知,这就导致每个线程变量的操作是封闭的,其他线程不可见的。
CPU快内存慢,一般都用寄存器解决。
可以加synchronized关键字,进入synchronize代码块,会清除缓存,从主内存中获取共享变量,进行操作,刷新回主内存,然后释放锁。
效率低下,出现了缓存一致性协议,有MSI,MESI,MOSI等,最出名的是Intel的MESI协议,保证了每个缓存中使用的共享变量的副本是一致的。
使用volatile等于告诉CPU需要MESI协议和嗅探机制来保证可见性。
MESI机制:
1.Modify:当缓存中的数据被修改时,该缓存设置为M状态
2.Eclusive(独占):当只有一个缓存使用某行数据时,设置为E状态
3.Share(共享):当多个CPU有数据的缓存,该数据的缓存设置为S状态
4.Invalid(无效):当某个数据的缓存修改时,其他持有该数据的缓存更新为I状态
核心思想:CPU修改数据,发现该数据是共享变量,会发出通知让其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量的时候,发现自己的缓存行是无效的,那么他就会重新读取。
监听和通知基于总线机制。总线嗅探机制就是一个监听器。
2.有volatile修饰的共享变量在写之前会多出一条lock指令
lock前缀会触发
1.将当前缓存行的数据写会主内存
2.这个写回操作会使其他CPU中缓存了该内存地址的数据无效。
JMM:
1.lock前缀会将线程工作内存中的缓存数据写回主内存
2.通过缓存一致性协议,其他线程如果工作内存中使用了该变量的值,就会失效
3.其他线程会重新从主内存获取新的值、
大量使用volatile会导致总线风暴。
volatile保证数据的可见性,但不保证数据操作的原子性。在多线程环境下,使用volatile变量是线程不安全的,可以使用锁机制或者原子类。
禁止指令重排
编译器不会对volatile读以及volatile后面的任务内存操作重排序。
通过内存屏障来实现。
异步开发
- 线程异步,可以使用线程池
- CompletableFuture异步,它是基于异步函数式编程。相对阻塞式等待返回结果,CompletableFuture 可以通过回调的方式来处理计算结果,实现了异步非阻塞,性能更优。
- SpringBoot @Async 异步
set如何保证不重复
1.通过hashcode方法获取hash值
2.在hash表中查找,如果不存在则添加成功。如果hash表中含有该值,则进行equals比较,相同添加失败,不同添加到已有对象链末尾。
深拷贝和浅拷贝
浅拷贝 对象的引用变量还是指向原对象地址
深拷贝 对象的引用变量指向不同,会新建引用对象
一般都是浅拷贝,深拷贝的是实现方式:1.重写clone方法,克隆引用成员变量;2.字节流写入文件再读出来;3.构造函数传参等。
优化链表查询速度
数据库
如何优化联表查询
1.在条件字段加索引
2.创建临时表
3.在a表中加入b表的字段,单查a表
sql优化方案
- 不使用select * ,避免多余的网络开销和回表操作
当以主键索引为查询条件时,select * 不会触发回表,因为已经主键索引的B+树已经包含了所有数据;当以非主键索引作为查询条件时,查询结果只有查询条件和主键,还需要根据主键再去查询其他字段,就是回表查询。
覆盖索引是指select到from的查询列都是主键或者索引。 - 能union all,就不用union。去重的过程中需要遍历,排序和比较
- 小表驱动大表: in 左大右小 exist 左小右大
- 用连接查询代替子查询
- 联合索引的使用遵循左前缀法则
如何查看有没有走索引
mysql
参考MySQL如何查看SQL查询是否用到了索引?
explain+sql
EXPLAIN select * from tb_brand where id='1';
type 性能
由好到坏:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
至少range,最好ref
possible_keys 查询用到的索引,没有的话值为null
key 实际决定查询结果使用的索引,没有的话值为null
rows为执行查询时必须检查的行数
oracle
参考Oracle通过执行计划查看查询语句是否使用索引
explain plan for +sql
select * from table(dbms_xplan.display)
TABLE ACCESS FULL为全表扫描;
index range scan为索引范围扫描;
常见的索引类型扫描:
- index unique scan 索引唯一扫描
当查询条件可以用到主键,唯一键,具有外键约束的键,或者只是访问索引所在的数据的时候,优化器会选择这种扫描类型 - index range scan 索引范围扫描
当优化器发现在唯一列上使用了>,<,between会使用范围扫描,在组合列上只使用部分进行查询,导致查询出多行数据。 - index full scan 全索引扫描
查询的数据可以全部从索引中获取,则使用全索引扫描 - index fast full scan 索引快速扫描
与全索引扫描类型,区别是返回的数据是不排序的
如何优查询速度
慢查询SQL
是指超过指定时间的sql。在mysql中默认关闭,手动开启
show variables like 'slow_query%';
show variables like 'long_query_time';
参数说明如下:
slow_query_log:慢查询开启状态
slow_query_log_file:慢查询日志存放的位置(一般设置为 MySQL 的数据存放目录)
long_query_time:查询超过多少秒才记录
mysql> set global slow_query_log=ON;
Query OK, 0 rows affected (0.05 sec)
mysql> set global long_query_time=0.01;
Query OK, 0 rows affected (0.00 sec)
超过指定时间就会记录在sql中
建表
尽量避免用text类型,采用es或者oss存储
字段尽量设置非null,会影响索引稳定
记得写注释comment
数据重要的情况下采用innodb,而且支持事务操作
减少索引大小可以采用前缀索引,但是前缀索引不能消除group by ,order by带来排序开销
经验:新核心迁移,编码不一致,扩容 gbk 到utf-8
索引的建立规则
建立索引常用的规则
索引以及实现方式
索引是帮助数据库高效获取数据的数据结构。
数据库除了维护数据,还维护着满足特定查找算法的数据结构,数据结构以某种方式指向引用数据,称为索引。
MySQL索引的数据结构
MySQL索引的数据结构
Oracle是B树,Mysql是B+树
参考面试官:你知道多少种索引?
索引,目录,提高查询效率。
索引常用的实现方式 B树,B+树 。hash也可以。
B树就是平衡树,时间复杂度为logn
- 多路查找树,叶子节点位于同一层
- 每个节点不仅包含数据的键值,还包括data值
- 每个节点相当于一个磁盘块
B+树基于B树实现,是有序的。
- 每个叶子节点存储字段键值以及对应的数据
- 非叶子节点只存储索引键值以及指向子节点的指针,不存储数据
- 每个节点相当于一个键盘块
- 同一层级的叶子节点之间以双向链表的形式相连
为什么有B树,还要有B+树?
B+树的叶子节点会指向下一节点,遍历查找更快。
非叶子节点不存储数据,key更紧密,数据查询更稳定和迅速,因为更好的利用空间局部性原理。
为什么使用B+树?
数据库访问通过页,尽量减少IO操作次数,因此树的层级要尽可能的少。
B树相比于二叉树,B树的非叶子结点可以有多个子树,因此B树高度远远小于AVL树和红黑树,磁盘IO数大大减少。
B+树相比于B树
1.非叶子节点不存储数据,存储的数据更多,因此B+树的高度更低,更少的磁盘IO操作。由于每个节点存储的记录更多,对局部性原理的利用更好,缓存的命中率更高。
2.更适合范围查找:B+树只需要对链表进行遍历,但是B树需要找到查询下限,然后进行中序遍历,直到找到查询的下限。
3.更稳定的查询效率:B+树的查询复杂度稳定为树高,因为所有数据都在叶子节点。
哈希也可以,但是会有两个缺点:1.哈希冲突。2.哈希计算的是个值,无法进行范围查询。
Mysql的innodb使用的是B+树。
索引的类型
1,普通索引:普通索引是最基本的索引,它没有任何限制,值可以为空;仅加速查询。
2,唯一索引:唯一索引与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
3,主键索引:主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。
4,组合索引:组合索引指在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
5,全文索引:应用场景为百度和淘宝的搜索框。关键字查找,不支持大小写,索引创建慢。mysql中的MyIsam支持,innnodb不支持。不推荐使用,一般用es实现。
6.聚集索引
7.辅助索引:根据条件查询出主键,通过主键返回聚集索引,根据聚集索引的主键排序,找到条件对应的数据
聚簇索引和非聚簇索引
mysql默认引擎innodb分为两种索引,聚簇索引和非聚簇索引,每个索引对应一颗b+树,两者的区别主要是叶子节点存储的数据不同,聚簇索引存储行数据,非聚簇索引存储聚簇索引,因此需要进行第二次查询,称为回表查询。
聚簇索引一般是指主键索引,一张表只能有一个。查询效率高。
非聚簇索引也称二级索引,数量不限制,查询效率低。
聚簇索引数据和索引都在一块存储。非聚簇索引叶子节点指向存储节点的位置,数据和索引分开存储。
在innodb中,在聚簇索引上创建的索引称为辅助索引,像非聚簇索引,复合索引,前缀索引,唯一索引。辅助索引叶子结点不存储数据,存储聚簇索引,总是需要二次查找。
聚簇索引具有唯一性,因为索引和数据在一块存储。
表中行的物理顺序和索引中的行物理顺序是相同的。
聚簇索引默认是主键,如果没有主键,会选择一个非空的列来做聚簇索引(类似oracle中的rowid)。如果想指定非主键做聚簇索引,删除主键,设置聚簇索引,然后重新设置主键即可。
myisam使用非聚簇索引,主键叶子结点存储主键,辅助键索引存储辅助键,都指向表数据,对数据来说这两种键没有区别。索引树独立,无法通过辅助键访问主键。
聚簇索引优势
1.数据和索引叶子结点存储在一起,每页有多行数据,查询其中一条会加载到buffer(缓存器)中,访问同页其他行就不会访问磁盘直接返回,查询更快。
(一次iO读写会获取16k的资源,读取到的数据区域称为Page,b+树,一个叶子节点上有多条数据和索引值,因此数据在叶子节点上不需要重复查询,走缓存。只有页分裂,即数据不存在才重新申请IO,)
2.当行数据发生变化,只需维护聚簇索引。非聚簇索引叶子节点存储主键,更节约空间。
3.myisam使用非聚簇索引,地址凌乱,拿到地址,按照合适的算法进行IO读取,聚簇索引只需一次。
4.涉及大数据量的排序,全表扫描,count,非聚簇索引更快,因为索引小,这些操作都是在内存中完成的。
聚簇索引中物理顺序和索引顺序一致,因此建议使用自增主键,不用uuid。默认会在索引树的末尾增加主键值,对索引树的结构影响最小。索引紧凑,磁盘碎片少,效率高。
主键大小也会影响,因为辅助索引中存储的主键,导致索引存储内存增多。影响IO操作读取的数据量。
优点:
1.查询快,索引和数据在一块
2.主键排序和范围查找很快
缺点:
1.插入速度依赖于插入顺序,一般主键自增
2.更新主键代价高,需要移动行
3.二级索引访问需要进行两次索引查找
mylsam查询比innodb速度快的原因
1.innodb不会压缩索引 查询时需要缓存数据块,而mylsam数据和索引是分开的,可以压缩索引,在相同容量的内存加载更多的数据
2.innodb寻址要映射到块,再到行(个人猜想主要是查询行的版本号),MYISAM记录的直接是文件的OFFSET,定位比INNODB要快
(注释: SELECT InnoDB必须每行数据来保证它符合两个条件: 1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除)
3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护MVCC (Multi-Version Concurrency Control)多版本并发控制
联合索引的命中规则
在test表上对列a,b,c建立联合索引
联合索引采用左前缀命中规则
从左到右匹配直到匹配终止条件。
终止条件为范围操作符,函数等不能应用索引的情况。
和顺序无关,mysql优化器会进行优化。
a‹› range索引
or 不使用索引
order by 没有影响
a= and b‹› 走索引
数据库事务
数据库事务是一个不可分割的数据库操作序列,也是数据库控制并发的基本单位,其执行结果为使数据库从一种状态到另一种状态
事务的特性 ACID
原子性 Atomicity:事务中的操作要么全部成功,要么全都失败
一致性 Consistency:事务执行前后,数据库中的数据具有一致性
隔离性 Isolation:多个事务之间不会相互影响
持久性 Durability:事务提交后,对数据库的改变是永久性的
脏读:读到了未提交的数据
不可重复读:两次读取到的数据内容不一致,这是update引起的
幻读:两次读取到的数据数量不一致,这是insert或者delete引起的
事务的隔离性
读取未提交:会发生脏读、不可重复度、幻读
读取已提交:避免脏读 Oracle默认
可重复读:避免脏读,不可重复读 Mysql默认
可串行化:最高隔离级别,事务顺序执行,不会相互影响
注意:
- Mysql默认采用MyIsam,不支持事务
- 事务隔离机制的实现基于锁机制和并发调度。
- 隔离级别越低,事务请求的锁越少。MySQL的innodb默认使用可重复读并不会有性能损失。
- innoDB存储引擎在分布式事务下一般会用到可串行化隔离级别
--mysql
start transaction; -- 开启事务
savepoint abc;--设置回滚点
rollback to abc;--回滚到abc
commit; -- 提交事务
rollback; -- 回滚事务
--查看MYSQL中事务是否自动提交
show variables like '%commit%';
--关闭自动提交
set autocommit = 0;-- 0:OFF 1:ON
set session transaction isolation level --隔离级别;
--eg: 设置事务隔离级别为:read uncommitted,read committed,repeatable read,serializable
set session transaction isolation level read uncommitted;
--查询当前事务隔离级别
select @@tx_isolation;
select for update
对行加锁,不能增删改,其他人也不能select for update
for update 使用场景:高并发情况下对数据有很强的要求
排他锁的申请前提,没有线程对结果集的数据进行排他锁和行级锁,否则会申请阻塞
共享更新锁的释放条件:1.commit 2.推出数据库 log off 3.程序停止运行
数据一致性
悲观锁:先锁后更 乐观锁:先更后比较 悲观锁适合频繁更新的情况,乐观锁适合频繁查询的情况
行锁和表锁
innoDb默认行级锁,指定主键是行级锁,否则是表级锁
for update仅适用于InnoDB,必须在实务块中才能生效(Begin Commit)
总结
1.innodb行锁是根据索引项加锁实现的,只有通过索引查询数据才会产生行锁,否则只会产生表锁
2.由于mysql的行锁是针对索引加的锁,不是对记录进行加锁,因此即便查询不同行的记录,但是索引相同也会发生锁冲突
3.当表有多个索引,不同的事务可以用不同的索引锁定不同的行。不论是使用主键索引,普通索引还是唯一索引,innodb都会使用行锁来进行加锁
4.即便查询条件中用到了索引,但是因为sql执行计划不一定采用索引,因此分析锁冲突时,需要检查sql执行计划,判断是否真的走了索引
oracle
select for update 当发现有人修改时,等待修改完成之后再去查询
for update nowait 发现有人修改,直接异常
for update wait 3 超过三秒未更新结束,抛出异常
什么情况下会发生锁表
insert,delete,update,select for update 未提交事务的情况下
alter table 修改表结构
truncate table清空表数据
mysql手动加锁
GET_LOCK()和RELEASE_LOCK()函数来获取和释放表锁。
锁表发生在并发而非并行。
减少锁表概率:缩短数据库事务开启和提交时间:具体:批量执行改为单个执行,优化sql执行速度。
幻读
什么时候发生幻读,如何解决?
幻读产生的条件:
在可重复读的隔离界别下,普通的查询都是快照读,查询不到别的事务新插入的数据,所以幻读是在当前读的状态下产生。
快照读:读取的是快照数据,不加锁的查询读取的都是快照数据
当前读:读取数据库最新数据,加锁的查询和增删改都会进行行当前读
解决幻读,innodb加入了间隙锁,间隙锁不仅可以锁住数据行的实体,也可以锁住数据行之间的间隙。
会造成并发下降。
解决方案:
1.降低为读取已提交,但是为了解决数据和日志不一样的问题,需要把binlog格式设置为row。
–日志不一样的原因:binlog日志是以commit顺序为准,如果第一个事务更新数据但未提交,第二个事务更新数据满足第一个事务中的更新条件但是提交了,日志中第二个日志在前,第一个日志在后。看着就是把第一个事务中的数据也更新了
RC情况下默认是statement,记录顺序是以commit方式提交的。
row会记录每一列之后的值。
2.判断有无数据,有则删除,无数据不删除,因为删除不存在的数据一定要加间隙锁
3.间隙锁只针对写锁,只要两个线程锁定的区间有交叉就会出现死锁。可以根据条件查询出所有主键,根据主键删除数据,这样只会加行锁。
间隙锁产生的条件:
执行当前读,where条件没有命中索引(命中索引加行锁,没有索引加表锁)
innodb_locks_unsafe_for_bin_log 默认false 启用间隙锁。
间隙锁和行锁的组合称为netxt-key lock 临间锁,每个next-key lock 临间锁 是前开后闭区间,n行数据,产生n+1个next-key 锁
数据 1,2,3 select * from table for update 产生四个next-key锁,(-∞,1],(1,2],(2,3],(4,+∞],此时另一个事务进行插入是无法插入的,避免了幻读的产生
间隙锁是在可重复读的隔离级别下生效。
只有可重复读的隔离界别的当前读才会出险幻读。
增加排他锁时,会对结果集的行增加间隙锁。
mysql共享锁 sql + lock in share mode
1.允许其他事务增加共享锁
2.不允许其他事务增加排他锁
3.多个事务共同添加,必须等待先执行的事务commit后才行。
排他锁 sql + for update
1.事务之间不允许其他排他锁和共享锁读取,修改更不允许
2.所有事务只有一个排他锁执行commit后,其他事务才可以执行
mvcc
多版本并发控制,解决读写时的线程安全问题,线程不用去争抢读写锁。
隔离性:通过加锁(当前读)和MVCC(快照读)实现。
一致性:通过undolog,redolog,隔离性共同实现。
mvcc的实现,基于undolog,版本链,readView。
在mysql存储过程中,会隐式的定义几个字段:
trx_id:事务id,每次进行一次事务操作,自增1
roll_pointer:回滚指针,用于找到上一个版本的数据,结合undolog进行回滚
使用select读取数据,这是时刻的数据会有多个版本,通过readview来判断能够读取哪个版本
readview中包含以下字段:
m_ids:活跃的事务id列表 活跃的事务是指还没有commit的事务
min_trx_id_:活跃事务中的最小值
max_trx_id_:下一个事务id
ctreator_trx_id:执行select读这个操作的事务id
readview如何判断哪个版本可用
trx_id==ctreator_trx_id 可以访问这个版本 :读取自己创建的记录
trx_id<min_trx_id_ :可以访问这个版本 :要读取的事务已提交,可以访问
trx_id>max_trx_id_:不可访问 :读取的事务id已经不在该版本链中,故无法访问
min_trx_id_<=trx_id<=max_trx_id_: trx_id在当前事务在活跃事务中不可以访问这个版本,反之可以 :读取的事务id不在活跃列表可以读取
mvcc如何实现RC和RP的隔离级别
1.rc时,每个快照读,都会生成并获取最新的readview
2.rr时,只有在同一个事务的第一个快照读才会创建readview
幻读问题:
快照读:通过mvcc,rr的隔离界别解决了幻读问题,因为每次都是同一个readview
当前读:通过临键锁,rr隔离级别并不能解决幻读问题
mysql redo和undo
mysql redo和undo
事务的原子性,隔离性由锁机制实现;
事务的一致性和持久性由redo和undo日志来保证
redo log是重做日志,保证持久性。
undo log是回滚日志,保证事务的一致性。
redo是记录尚未完成的操作,数据库崩溃则用其重做。
redo log分为两部分
- 内存中的重做日志缓冲,易失的
- 硬盘中的重做日志文件,持久的
innodb采用write ahead log (预先日志持久化策略),先写入日志,在写入磁盘
第一步:InnoDB 会先把记录从硬盘读入内存
第二部:修改数据的内存拷贝
第三步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第四步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第五步:定期将内存中修改的数据刷新到磁盘中(注意注意注意,不是从redo log file刷入磁盘,而是从内存刷入磁盘,redo log file只在崩溃恢复数据时才用),如果数据库崩溃,则依据redo log buffer、redo log file进行重做,恢复数据,这才是redo log file的价值所在
在commit时写入redo日志
为什么不直接写,而是先写日志?
磁盘写入时间长,先写入日志,日志只需要考虑修改的数据。
如何保证每次都写入redo log file?
没开启0_direct 直接写磁盘操作。每次写到内存,在刷到磁盘。
每次将redo buffer写入os cache 文件缓存,innodb都调用fsync操作将缓存写入redo log file。
buffer pool中的数据未刷新到磁盘,称为脏页。
redo log满时,会把脏页刷入磁盘。
除了redo满时,什么时候刷脏页?
系统内存不足,淘汰数据页为脏页时。
mysql认为空闲时。
mysql正常关闭前,会把所有的脏页刷到磁盘。
脏页刷入会带来性能问题吗?
在生产环境中,如果我们开启了慢 SQL 监控,你会发现偶尔会出现一些用时稍长的 SQL。**这是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,**导致数据库操作抖动。
2.5 参数innodb_flush_log_at_trx_commit
上面提到的Force Log at Commit机制就是靠InnoDB存储引擎提供的参数innodb_flush_log_at_trx_commit来控制的
该参数控制 commit提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。
1、当设置参数为1时,(默认为1,建议),表示事务提交时必须调用一次 fsync 操作,最安全的配置,保障持久性
2、当设置参数为2时,则在事务提交时只做 write 操作,只保证将redo log buffer写到系统的页面缓存中,不进行fsync操作,因此如果MySQL数据库宕机时 不会丢失事务,但操作系统宕机则可能丢失事务
3、当设置参数为0时,表示事务提交时不进行写入redo log操作,这个操作仅在master thread 中完成,而在master thread中每1秒进行一次重做日志的fsync操作,因此实例 crash 最多丢失1秒钟内的事务。(master thread是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性)
undo log
用于记录更改的前一份copy
undo log的存储位置
在InnoDB存储引擎中,undo存储在回滚段(Rollback Segment)中,每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo 页的申请,在5.6以前,Rollback Segment是在共享表空间里的,5.6.3之后,可通过 innodb_undo_tablespace设置undo存储的位置。
purge线程的作用,清除undo页和清除page中带delete_bit的标识。数据删除只是打标记,并不是真的删除。
undo日志两种,insert和update
insert执行后就删除。
update对应delete和insert操作,需要提供mvcc操作,执行并不删除,等待purge线程操作。
undo log是逻辑日志。redo log是物理日志。
undo和rollback的区别,undo会使用rollback。
mysql和oracle的区别
- mysql插入’‘就是’’
oracle插入’'就是null - 隔离界别 mysql 可重复读 oracle读取已提交
- 在linux下,mysql区分大小写,oracle不区分
myisam和InnoDB的区别
- InnoDB支持事务,MyISAM不支持,InnoDB每条sql语句都默认封装成事务进行提交,影响速度,优化方式是将多条sql放入begin和commit之间,组成一个事务。
- InnoDB支持外键,而MyISAM不支持
- innodb支持行锁,表锁,myisam只支持表锁
- innodb必须有主键,myisam可以没有
- innodb聚簇索引,myisam非聚簇索引
- innodb不会保留表的行数,myisam会保存表的行数
一张表修改要求比较高的事务处理选择InnoDB,查询要求比较高,选择MyISAM
mybatis的条件语句
MyBatis动态SQL 多条件查询(if、where、trim标签)
if where 判断非空
choose、when、otherwise标签 多条件选择
foreach 拼接list
parameter 入参类型
1.基本数据类型:int,string,long,Date
#{value}或${value} 获取参数中的值
2.复杂数据类型:类或者Map
#{属性名}或{属性名} ,map中则是#{key}或
{key}
for each的collection属性
单个List<Long> idList,为list
单个array数组,属性为array
map中放array或者list时,属性为数组的key
java对象中放array或者list时,属性为对象的属性名
resultType
1.基本数据类型
2.pojo类型
mybatis的缓存机制
一级缓存 sqlsession共享
二级缓存 同一个命名空间共享
mybatis如何封装返回结果
resultType设置为java对象:
1.反射创建对象
2.同名的列赋值给同名的属性(通过反射,找到set方法赋值)
3.得到java对象,如果是集合放入list中
当列名和java对象属性名不一致时,使用resultMap建立结果映射
数据库死锁
出现原因:
1.外键未加索引
在更新从表会对主表加写锁,在更新主表时,如果从表没有索引,会对整个表加锁。
容易发生死锁
2.用户1锁A,请求锁B,用户2锁B请求锁A
多表操作尽量按照相同的顺序进行处理。
避免方法
1.所有的update和delete必须走唯一索引
schema的操作
2.sql语句中不要有太复杂的关联操作,使用explain sql检查sql,全表扫描尽量优化为索引
3.把select放在update之前
4.避免事务中的用户相互等待。
现场解决方法:
1.重启系统
2.撤销进程,剥夺资源。终止参与死锁的进程,收回资源,从而解除死锁。
3.进程回退策略
级联删除和更新
ON DELETE CASCADE ON UPDATE CASCADE
Oracle不支持,只能用触发器实现
写sql
1.商品id 单价 数量 按照销售额排序 前十
PRODUCTIDNUMBER 商品数量
PRICE 商品单价
productid 商品id
select sum(PRODUCTIDNUMBER*PRICE) ,productid from TESTPRODUCT where rownum<11 Group BY productid ORDER BY sum(PRODUCTIDNUMBER*PRICE) ;
spring
springboot
- 内嵌servlet服务器,Tomcat,Jetty等,不需要打成war包部署到容器中,只需要打成可执行的jar包即可
- 简化配置:添加对应功能的starter依赖简化maven配置
springcloud
springcloud是一系列架构的有序集合。基于springboot简化分布式基础设置的开发,如服务发现,配置中心,路由zuul,消息总线,负载均衡fegin+ribbon,断路器hystrix,数据监控等。
SpringBoot启动过程
1.创建springApplication对象,运行run方法;
通过类加载器加载classpath下所有的spring.factories配置文件,创建初始化对象;
创建环境对象environment,读取环境配置,如application.yml
2.创建程序上下文createApplicationContext,创建bean工厂对象
3.刷新上下文(启动核心),refreshContext(工厂对象配置,bean处理器配置,类的扫描,解析,bean的定义,bean类信息缓存,tomcat创建,bean实例化,动态代理对象创建)
4.通知监听者,启动程序完成
spring.factories文件
当需要将类加载到spring中,而类并不在spring启动类的包下,除了在启动类@Import,还可以在resource中新建META-INF目录,新建spring.factories。通过这种方式来加载非默认扫描路径,自己写的或者第三方的类。 通过Spring的spi扩展实现
参考springboot核心基础之spring.factories机制
设计模式
- 单例模式:spring bean 默认单例
- 工厂模式:spring主要通过BeanFactory和ApplicationContext来生产对象
- 代理模式:AOP的实现方式是通过代理实现,Spring主要使用JDK动态代理和CGLIB代理
- 模板模式方法:对数据库类的操作,JDBCtemplate,数据库建立连接,执行查询,关闭连接几个过程非常适合模板方法
redisTemplate,restFulTemplate
IOC和AOP
IOC:控制反转,自己new对象改为由Spring注入,解耦。
AOP:面向切面编程,提取公共代码,进行增强处理,解耦。
JDK动态代理和CGLIB代理有什么区别
参考 java动态代理
静态代理:代理类和被代理类实现相同的接口,代理类持有被代理对象,调用接口方法中进行前置和后置操作,调用被代理类的方法。
类已经写好,编译后就能生成class文件。
public interface Person {
//租房
public void rentHouse();
}
public class Renter implements Person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
}
public class RenterProxy implements Person{
private Person renter;
public RenterProxy(Person renter){
this.renter = renter;
}
@Override
public void rentHouse() {
System.out.println("中介找房东租房,转租给租客!");
renter.rentHouse();
System.out.println("中介给租客钥匙,租客入住!");
}
}
public class StaticProxyTest {
public static void main(String[] args) {
Person renter = new Renter();
RenterProxy proxy = new RenterProxy(renter);
proxy.rentHouse();
}
}
动态代理:
代理类在程序运行时创建的代理方式被称为动态代理。动态代理相较于静态代理可以对代理类的所有函数进行统一管理,不需要对每个方法都写一遍。
通过reflect包下的proxy类和invocationHandler
生成jdk动态代理类和代理对象。
public interface Person {
//租房
public void rentHouse();
}
public class Renter implements Person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
}
创建代理类实现invocationHandler接口,需要实现invoke方法,代理类中存在被代理对象。
调用代理类的方法都会调用invoke方法,在invoke中通过反射调用被代理对象的方法。
在执行被代理对象的方法调用前后增加自己的处理,这就是spring aop的主要原理。
public class RenterInvocationHandler<T> implements InvocationHandler{
//被代理类的对象
private T target;
public RenterInvocationHandler(T target){
this.target = target;
}
/**
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//代理过程中插入其他操作
System.out.println("租客和中介交流");
Object result = method.invoke(target, args);
return result;
}
}
public class ProxyTest {
public static void main(String[] args) {
//创建被代理的实例对象
Person renter = new Renter();
//创建InvocationHandler对象
InvocationHandler renterHandler = new RenterInvocationHandler<Person>(renter);
//创建代理对象,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
Person renterProxy = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class<?>[]{Person.class}, renterHandler);
renterProxy.rentHouse();
//也可以使用下面的方式创建代理类对象,Proxy.newProxyInstance其实就是对下面代码的封装
/*try {
//使用Proxy类的getProxyClass静态方法生成一个动态代理类renterProxy
Class<?> renterProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[]{Person.class});
//获取代理类renterProxy的构造器,参数为InvocationHandler
Constructor<?> constructor = renterProxyClass.getConstructor(InvocationHandler.class);
//使用构造器创建一个代理类实例对象
Person renterProxy = (Person)constructor.newInstance(renterHandler);
renterProxy.rentHouse();
//
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
}
}
JDK动态代理生成的代理类中的方法都会调用RenterInvocationHandler中的invoke方法,而invoke方法中调用了被代理对象的指定方法。
没有实现接口采用cglib代理
通过继承目标类,在子类中采用方法拦截的方式拦截所有父类方法的调用,然后加入自己需要的操作。因为使用的是继承,因此类不能为final。
创建被代理类
public class UserService {
public void getName(){
System.out.println("张三!");
}
}
创建代理工厂类ProxyFactory
public class ProxyFactory<T> implements MethodInterceptor {
private T target;
public ProxyFactory(T target) {
this.target = target;
}
// 创建代理对象
public Object getProxyInstance() {
// 1.cglib工具类
Enhancer en = new Enhancer();
// 2.设置父类
en.setSuperclass(this.target.getClass());
// 3.设置回调函数
en.setCallback(this);
return en.create();
}
//拦截方法
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
System.out.println("开始事务...");
// 执行目标对象的方法
Object result = method.invoke(target, args);
System.out.println("提交事务...");
return result;
}
}
JDK和CGLib动态代理都是实现SpringAOP的基础。如果加入容器的目标对象有实现接口,用动态代理,如果目标对象没有实现接口,用CGlib代理。
Spring AOP 和 AspectJ AOP 有什么区别?
Spring AOP是动态代理,运行时增强
AspectJ属于编译增强
Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢,而 AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。
spring事务
Spring事务基于数据库事务
Spring隔离级别和数据库事务隔离级别一样,多了个默认,采用数据库默认的隔离界别
Spring事务的两种方式,编程式事务,声明式事务
编程式事务精确到代码级别,声明式事务精确到方法级别。
Spirng事务的传播机制
a调用b
- propagation_required a没有事务,b就开启事务,a有事务,b就和他合并到一起。
- propagation_requires_new a没有事务,b新建事务,a有事务,挂起,b新建事务。
b影响a,a不影响b - propagation_nested 有事务就加入,没有事务就新建。a require b nested b异常,a不回滚,a影响b,b不影响a
- propagation_supports 如果a有事务就加入事务,没有事务,就以非事务的方式进行
- propagation_not_support 有事务挂起,以非事务运行
- propagation_mandatory 有事务就加入,a没有事务就报错
- propagation_never 以非事务的方式运行存在事务报错
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
//事务超时时间,超时回滚
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//如果事务只读,可以利用事务的只读属性开启优化措施
boolean readOnly() default false;
}
//事务的回滚策略,指定异常回滚
@Transactional(rollbackFor= MyException.class)
//事务的回滚策略,指定异常不回滚
@Transactional(noRollbackFor= MyException.class)
Spring事务失效的场景
- 作用于非public方法中,声明式事务基于aop,会进行判断是否是公共方法
- 数据库不支持事务
- 事务方法未被Spring管理
- 同一个类中的方法调用 a调用b,b添加事务注解,a不添加事务注解
- 未配置事务管理器 DataSourceTransactionManager
- 方法的事务传播类型不支持事务
- 不正确的捕获异常,在执行sql时,即使发生异常但是由于捕获异常而不会回滚
- 错误的标注异常类型,Spring事务回滚的异常类型为RuntimeException,如果在事务方法中捕获并抛出Exception异常,回滚会失效
spring对象生命周期
ApplicaitonContext代表spring的IOC容器,负责实例化,配置,装配bean。
- 生产
- 加载BeanDefinition:
Spring启动,调用loadBeanDefinition方法,通过注解,xml等方式扫描BeanDefinition到beanDefinitionMap,然后遍历map中的对象: - createBean创建Bean对象
- 实例化:createBeanInstance 通过反射获取构造方法进行构造实例,优先选择使用@Autowired注解和无参构造方法。参数从单例池中查找,先根据类型查找,多个实例用参数名匹配。
- 填充属性:populateBean方法 通过三级缓存进行属性注入;
- 初始化Bean对象:initializeBean方法进行初始化:
1.初始化容器相关信息,invokeAwareMethods方法,为实现了各种aware接口的Bean设置注入BeanName,beanFactory等容器信息。
2.invokeInitMethods方法执行bean的初始化方法,是通过实现initializingBean接口而实现的afterPropertiesSet方法,之后执行Bean上自定义的initMethod方法,
3.在执行执行初始化方法initMethod之前,执行BeanPostProcessor方法进行初始化前后的处理,包括负责AOP处理的AnnotationAwareAspectJAutoProxyCreator,负责构造后@PostContstruct和销毁前@PreDestory方法系统级处理器。通过实现PriorityOrdered接口来执行顺序。 - 注册销毁方法registerDisposableBean,在销毁时调用destory方法
- addSingleTon:将完整的bean对象放入单例池singleTonObject中。
- 加载BeanDefinition:
- 使用
- 销毁
close,销毁前执行postProcessorBeforeDestruction 会执行@preDestroy方法;
通过destoryBeans方法逐一销毁所有的Bean,会执行destory方法;
destroy执行之后,通过客户销毁方法 invokeCustomDestoryMethod方法,来执行丁自定义的Destroy方法
SpringBoot启动原理
有@SpringBootApplication注解的启动类
该注解有@EnableAutoConfiguration(会导入AutoConfigurationImportSelector,这个类会将所有符合条件的@Configuration配置都进行加载),@SpringBootConfiguration(等同于@Configuration),@ComponmentScan(扫描,加载)这三个注解构成。不需要增加配置内容和扫描路径,只使用@EnableAutoConfiguration即可。
执行run()
-
服务构建
服务指SpringApplication对象,1.首先将资源加载器,主方法类放入内存中
2.逐一判断服务类是否存在,来确定web服务的类型,默认servlet(即基于servlet的web服务,如tomcat),还有响应式非阻塞服务reactive,如spring-webflux,还有什么都不用用的none。
3.加载初始化类,读取所有META-INF/spring.factory文件中的注册初始化(bootstrapRegistryInitializer),上下文初始化(ApplicationContextInitializer)和监听器(ApplicationListener)三种配置。
没有默认的注册初始化配置,spring-boot和spring-boot-autoconfiguration这两个工程中配置了7个上下文初始化和8个监听器。也可以自定义这三个配置。
4.通过运行栈stackTrace判断main方法所在的类,大概率是启动类本身 -
环境准备
2.1 new一个BootStrapcontext,逐一调用启动注册初始化器(bootstrapRegistryInitializer)中的初始化initialize方法
2.2 将java.awt.headless这个设置为true,表示缺少显示器,键盘等输入设备,也可以正常启动
2.3 启动运行监听器SpringApplicationRunListeners,发布启动事件,获取并加载spring-boot工程springfactories配置文件中的EventPublishingRunListener,在启动时会将8个监听器引入
2.4 prepareEnvironment
构造可配置环境ConfigurationEnvironment,根据不同的Web服务类型会构造不同的环境,默认servlet,构造之后会加载很多系统环境变量,如systemEnvironment,jvm系统属性systemProperties等在内的4组配置信息,将配置信息加载到propertySource的内存集合中。通过配置环境configureEnvironment方法将我们启动时传入的参数args进行设置,例如启动时传入的“开发/生产”环境哦配置都会在这一步进行加载,同时在propertySource集合的首个位置添加一个值为空的配置内容,configurationProperties后续使用
发布环境准备完成这个事件,8个监听器监听到事件,部分会进行相应处理注入环境配置后置处理监听器会加载spring.factories配置文件中的“环境配置后处理器”(监听器通过观察者模式设计,逐一串行执行)
可配置环境在过程中可能会有变化,通过更新保证匹配
将spring.beaninfo.ignore设为true,表示不加载Bean的元数据信息,同时打印Banner图 -
容器创建
createApplicationContext来创建容器
根据服务器类型创建容器ConfigurableApplicaitonContext,默认是Servlet,创建注解配置的servlet-web服务器容器,即AnnotionConfigServletWebServerApplicationContext,在这个过程中,会构造存放和构建bean实例的Bean工厂 DefaultListableBeanFactory
将这三个都放入容器中,通过prepareContext方法对容器中部分属性进行初始化了。
先用postProcessApplicaitonContext方法设置Bean名称生成器,资源加载器,类型转换器等,接着执行上下文初始化ApplicationContextInitializer,默认加载7个。发布容器准备完成监听事件后,陆续为容器注册启动参数,banner,bean引用策略和懒加载机制等等,通过bean定义加载器将启动类在内的资源加载到bean定义池beanDefinitionMap中,以便后续根据bean定义创建bean对象,然后发布资源加载完成事件 -
填充容器
生产自身提供的和自定义的bean对象,放入容器,也就是自动装配
Spring Factories
在META-INF下,META-INF目录是提供jar文件描述信息的文件目录。
SpringFactories模仿java SPI机制
SPI机制是service provice interface,面向对象基于接口编程,如JDBC,java定义接口,数据库公司实现接口,换数据库不需要修改java代码。类似还有xml解析,日志模块的方案。
java SPI 就是为某个接口寻找服务的实现的机制。
对于在maven中引用的其他外部包加入容器的过程,需要用到spring.factories。
在SpringBoot启动时,SpringFactoriesLoader类会通过loadFactories或者loadFactoryNames寻找每个jar包下的META-INF/spring.factories,
AutoConfigurationImportSelector的selectImports方法返回的类名,来自spring.factories文件内的配置信息。通过该方法加载外部的Bean
拦截器和过滤器
- 实现原理:过滤器基于回调函数;拦截器基于java反射
- 使用范围:Filter在servlet中定义,依赖servlet,只能在web中使用。拦截器可以在web,application,swing
- 触发时机不同:Filter在请求进出servlet前后。拦截器可以深入到方法前后,异常抛出前后。
- 范围不同:过滤器过滤所有请求,拦截器只对action请求起作用。(只会对Controller中请求或访问static目录下的资源请求起作用。)
- 进入Bean的情况不同:拦截器先于ApplicationContext加载,所以拦截器无法注入Spring容器管理的bean。
解决办法:拦截器不使用@Component加载,改为使用@Configuration+@Bean加载。 - 执行顺序不同:过滤器用@Order控制级别,级别越小越先执行;拦截器默认执行顺序为注册顺序,也可以通过@Order手动执行,级别越小越先执行;
Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码、做一些业务逻辑判断等。
SpringMVC 中的Interceptor 拦截器的主要作用就是拦截用户的 url 请求,并在执行 handler 方法的前中后加入某些特殊请求,比如通过它来进行权限验证,或者是来判断用户是否登陆。
拦截器的实现方式:实现HandlerInterceptor 或者WebRequestInterceptor
过滤器:用于属性甄别,对象收集(不可改变过滤对象的属性和行为)
拦截器:用于对象拦截,行为干预(可以改变拦截对象的属性和行为)
过滤器是JavaWeb的三大组件之一,是实现Filter的java类。
过滤器实现对请求资源的过滤功能,在请求资源前,响应前进行操作。
过滤器主要用来如参数过滤、防止SQL注入、防止页面攻击、过滤敏感字符、解决网站乱码、空参数矫正、Token验证、Session验证、点击率统计等。
过滤器 Filter
init():该方法在容器启动初始化过滤器时被调用,它在Filter的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。
doFilter():容器中的每一次请求都会调用该方法,FilterChain用来调用下一个过滤器Filter。
destroy():容器销毁时被调用。一般在方法中销毁或关闭资源,也只会被调用一次。
拦截器核心API
SpringMVC拦截器提供三个方法分别是preHandle、postHandle、afterCompletion,我们就是通过重写这几个方法来对用户的请求进行拦截处理的。
preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行,在整
个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
如何解决循环依赖
一级缓存:实例化,初始化的对象
二级缓存:实例化,还未初始化的对象
三级缓存:对象工厂,用来创建二级缓存中的对象
AB相互依赖
实例化A,放入三级缓存,属性注入时发现需要B,去实例化B,发现需要依赖A,从缓存中依次查找,从三级缓存中删除,放入二级缓存,完成初始化后,将B放入一级缓存。接着初始化A,完成后删除二级缓存中的A,放入一级缓存。
使用三级缓存的原因是:保证使用的是同一个对象。
单用二级缓存,在二级缓存中放入一个普通Bean之后,BeanPostPorcessor生成代理对象覆盖,多线程环境,取不到一致的对象。
保证获取的是同一个对象。A3和A2都依赖A1,A1依赖A2,A3,在进行A1的依赖注入时,创建A2时,给A2注入了A1的代理对象,但是A3进行依赖注入时,如果不缓存A1的代理对象,aop会重新生成代理对象,导致单例被破坏
Spring SpringBoot SpringCloud的区别
Spring IOC和AOP
SpringBoot 约定大于配置,快速开发
SopringCloud 微服务治理,提供解决方案
SpringBoot可以依赖SpringCloud开,SpringCloud不能离开SpringBoot,属于依赖关系。
Spring Filter 怎么写
参考 SpringBoot——SpringBoot使用过滤器Filter
1.注解实现
编写一个过滤器,在启动类上添加ServletComponentScan注解扫描过滤器的对应包
2.Spring Boot 的配置类实现
新增过滤器不添加注解,通过FilterRegistrationBean注册自己的过滤器
获取Spring容器对象
实现BeanFactoryAware ,ApplicationContextAware,ApplicationListener接口
获取spring容器对象
SpringAOP
参考:Spring AOP代码实现:实例演示与注解全解
Aspect Oriented Programming 面向切面编程,用于处理代码中公共非业务逻辑,如日志,鉴权等。
- PointCut:在什么时候切入 分为execution和annotation方法。前者用路径表达式指定那些类织入切面,后者指定被那些注解修饰的代码织入切面。
- Advice: 处理时机和处理内容,在什么时间做什么事。
- Aspect:切面,即PointCut和Advice
- JointPoint:连接点,是程序执行的一个点。例如一个方法的执行或者一个异常的处理
- Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();//拿到方法
Object[] allArgs = joinPoint.getArgs();//拿到参数
@Aspect
@Component
public class LogAdvice {
// 定义一个切点:所有被GetMapping注解修饰的方法会织入advice
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
// @Pointcut("execution(* com.example.demo.controller..*(..))")
private void logAdvicePointcut(){}
@Before("logAdvicePointcut()")
public void logAdvice(){
// 这里只是一个示例,你可以写任何处理逻辑
System.out.println("get请求的advice触发了");
}
}
redis
哪里用到的redis,时效
- 数据库存储 存储登录信息 token 30分钟 实现value的序列化,存储的对象
- 缓存 路由转发,接口匹配 redismanager默认30天
工作流暂存 10分钟:为了缓解数据库压力,开始的流程是异步进程,但是要全量进程的结果 - 幂等性的实现 在消费消息时,将消息的uuid存储到redis中,防止重复消费
redis数据结构
String List Hash Set ZSet
Stirng 底层实现
动态字符串sds
sds结构一共有五种header定义,目的是为了满足不同长度字符串使用,节省内存。
header主要包括:
- len长度
- alloc字符串最大容量,不包括header和最后的空终止字符
- flags标志位,第三位表示类型,其余五位未使用
- buf:字符数组
- encoding
- int:可以使用long类型的整数表示的会用long型来存储
- raw:长度大于44字节的字符串,使用sds存储
- embstr:长度小于等于44字节的字符串,效率比较高,且数据都保存在一块内存区域。
List底层是链表
hash底层是dict
ZSet的实现方式:
ziplist压缩链表:
- 元素数量小于128
- 每个元素长度小于64
skiplist跳跃链表:不满足上述条件就采用跳表,具体来说组合了map和skiplist
- map用来存储 member到score的映射, 时间复杂度为O(1)
- skiplist按照从小到大存储分数,每个元素的值都是[score,value]对
跳表就是在有序节点上增加多级索引
n个节点,第一层索引为n/2,第二层索引为n/4,第k层索引为n/2^k
空间换时间的折半查找
redis排行榜 zset
添加
zadd key score value
ZADD broadcast:20210108231 1 lisi
加分
zincrby key increment member
ZINCRBY broadcast:20210108231 2 lisi
排名
ZRANGE broadcast:20210108231 0 -1 WITHSCORES
redis的淘汰策略
过期键淘汰策略是定期删除和惰性删除
定期删除是指:redis服务器定期操作redis.c/serverCron函数执行时,redis.c/activeExpireCycle会被调用。actvieExpire函数在规定时间内,分多次遍历服务期内的多个数据库,从过期字典中检查一部分key的过期时间并删除。
current_db记录当前检查数据库,函数处理2号数据库时间超限,返回后下次检查会从3号数据库开始检查。所有数据库检查完毕current_db重置为0,然后再次开启一轮的检查工作。
惰性删除是指用户请求,此时会检查过期,过期清除不会返回
大量key堆积,内存耗尽如何处理?
内存淘汰机制:
缓存击穿,缓存雪崩
- 缓存穿透:缓存和数据库中都没有数据,多次重复访问导致数据库崩溃
解决:
业务层校验:对于不合规的入参,直接返回
不存在数据设置短过期时间
布隆过滤器 - 缓存击穿:热点key到点失效,大量请求进入,从而全部到达数据库,压垮数据库
解决:
永不过期
定时更新:过期时间1h,每到59min去更新
互斥锁:redis根据key获取到的value为空时,先加锁,去数据库加载,加载完毕,释放锁。其他线程请求发现获取锁失败,则睡眠一段时间。 - 雪崩:缓存大面积失效,或者Redis宕机,大量请求进入数据库,压垮数据库
解决:
设计有效期均匀分布:避免缓存设置相近的有效期,可以设置有效期时增加随机值,或者统一规划有效期,使得过期时间均匀分布。
缓存预热:流量大时,提前访问一遍,将数据存储到redis中并且设置不同的过期时间
保证Redis的高可用
redis的锁机制
redis一般用作缓存,多读少写,只支持乐观锁
Redis事务命令主要包括 WATCH, EXEC, DISCARD, MULTI。
事务使用MULTI开启,这时可以执行多条命令,Redis在这些事务中加入命令,当用户执行Exec命令时才真正的执行队列当中的命令。执行discard,丢弃队列中的命令。
Watch命令是Exec执行的条件,watch的key没有修改则执行事务,否则事务不会被执行。
Watch命令可以被调用多次,一个watch命令可以监控多个key。watch命令调用则开启监视功能,直到exec命令终止。
Redis的watch命令给事务CAS机制,如果key在执行exec前有变动,则整个事务被取消。
事务中,采用watch加锁,unwatch解锁,执行EXEC命令或者Discard命令后,锁自动释放,不需要进行unwatch操作。
redis分布式锁
参考:Redis实现分布式锁的7种方案
分布式锁需要保证:
1.互斥性:只能有一个客户端获得锁
2.安全性:锁只能被持有该锁的客户端删除
3.死锁:锁超时释放,避免死锁
4.容错:当redis部分节点宕机,客户端仍能获取锁和释放锁
1.一种实现
setnx k v; 成功返回1 失败返回0;
getset k v;返回key的旧值;
expire k secondsl;给key设置超时时间
del key [key …]
1.1setnx key 当前时间+过期时间
1.2. 失败获取锁失败,成功 expire 超时时间
1.3.执行业务 del key
问题:如果获取锁成功未设置超时时间的时候进行重启,会产生死锁。
(如果不是kill线程,而是shutdown可以用springBean的predestory注解,进行删除key操作)
优化:获取锁失败后,查看当前key的value值,如果不为空,并且当前时间大于该值,说明当前锁失效,通过getset获取值。如果getset获取的值,和一开始的旧值相同,则获取锁成功。
问题:多节点时间需要尽可能的保持一致。getset即便失败也会延长之前锁的时间。
2.使用lua脚本
保证setnx和expire的原子性
3.set扩展命令
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
问题:业务没执行完,锁就释放了;锁被别的线程误删。
4.SET EX PX NX + 校验唯一随机值,再删除
还是会存在业务没有执行完成,锁释放的问题。
5.Redisson框架
开源框架Redisson
加锁后启动一个watchdog线程,每隔10s检查是否持有锁,持有锁就延长,防止锁过期提前释放。
6.redlock+redisson
redis节点全为master,避免master加锁后未同步到slave然后宕机导致加锁失败的情况
按顺序向5个master节点请求加锁
根据设置的超时时间来判断,是不是要跳过该master节点。
如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
redis线程模型
参考 深入学习redis 的线程模型
redis内部采用事件处理器是单线程的,因此redis叫做单线程模型。采用IO多路复用机制同时监听多个socket,将产生的事件的socket压缩到内存队列中,事件分派器根据事件不同的类型选择对应的事件处理器进行处理。
文件事件处理器的结构
- 多个socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)
线程模型
多个socket可能并发产生不同的操作,每个操作对应不同的事件,但是IO多路复用程序会监听多个socket,将产生事件的socket放入队列中排队,事件分派器每次从队列中取出一个socket,根据socket对应的事件类型交给对应的事件处理器进行处理。
建立连接
- redis服务端进程初始化时,将server socket 的AE_READABLE事件与连接应答处理器关联。
- 客户端socket01向redis进程serverSocket请求建立连接,此时server socket 产生一个AE_READBLE事件,IO多路复用程序监听到server socket产生的事件后,将socket压入队列。
- 文件事件分派器从队列中获取socket,交给连接应答处理器。
- 连接应答处理器会创建一个能与客户端通信的socket01,并将socket01的AE_READABLE事件与命令请求处理器关联
执行一个set请求 - 客户端发送了一个set key value请求,此时redis中的socket01会产生AE_READABLE事件,IO多路复用程序,将socket01压入队列
- 此时事件分派器从队列中获取到socket01产生的AE_READABLE事件,由于前面socket01的AE_READABLE事件已经和命令请求处理器关联
- 事件分派器将命令交给请求处理器来处理。命令处理器读取k v并在自己内存中完成设置
- 操作完成后,将socket01的AE_WRITEABLE事件与命令回复处理器关联
- 如果此时客户端准备好了接受数据,那么redis中的socket01会产生一个AE_READABLE事件,同样压入队列中
- 事件派分器找到相关联的命令回复处理器,由命令回复处理器对socket01输入本次操作的一个结果,比如ok,之后解除socket01的AE_READABLE事件与命令回复处理器的关联。
为什么redis效率高?
1.纯内存操作
2.核心是基于非阻塞的IO多路复用机制
3.C语言实现,语言更接近操作系统,执行速度相对会快
4.单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
redis的吞吐量:
单点TPS达到8万/秒,QPS达到10万/秒
qps是指每秒最大能接受的用户访问量,tps是指每秒钟最大能处理的请求数。
部署方式
单机,主从,哨兵
kafka
使用消息队列的原因
1.流量削峰(消息过多,处理有限) 落库采用异步消息的方式,减缓数据库压力
2.服务解耦(接口故障不影响生产和消费)消息发出去,发消息的应用down了,也不会影响后序微服务的消费
3.异步(提高响应速度,用户无感知)
kafka 如何解决消息重复
-
生产者:生产者发送的消息没有收到正确的broker响应,导致producer重试。
启动kafka的幂等性要启动kafka的幂等性,设置: enable.idempotence=true ,以及 ack=all 以及 retries > 1 。ack=0,不重试。可能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集
幂等生产者的原理:每个生产者都有唯一Pid,发次发送消息会累加seq。broker为每个topic的每个分区都维护了一个当前当前写成功的消息最大PID-seq,消息落盘+1,当收到小于当前最大PID-seq时就会丢弃该消息。
-
消费者:
offset手动提交,业务成功处理后,提交offset
幂等性:多次操作结果一致
获取消息的唯一id,会将id存入redis,记录为处理中,以后的消息就不会进行重复处理
kafka如何保证消息不丢失
1.生产者发送给服务器
acks机制 0 发送就成功,1 生产者收到leader分区的响应则认为成功 -1 当所有ISR中的副本全部收到消息,生产者才认为是成功的
2.服务器通过副本保存消息
3.消费者,关闭自动提交,在接收到消息,进行业务处理完毕后再提交偏移量
kafka什么时候进行rebalance
参考Kafka的Rebalance机制可能造成的影响及解决方案
每当有新的消费者加入或者订阅的topic数发生变化,会触发rebalance(再均衡:在同一个消费组当中,分区的所有权从一个消费者转到另一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。
过程如下:
1.所有消费者向coordinator发送请求,请求加入comsumer group。一旦所有成员都发送了请求,Coordinator会从中选择一个consumer作为leader,并将组员信息发给leader
2.leader分配消费方案,指定哪个消费者消费哪些topic的哪些分区。发给coordinator,coordinator发送给消费者,组内成员知道自己该消费哪些分区了。
coordinator:每个consumer group会选择一个服务器作为自己的coordinator,负责监控整个消费者组内各个分区的心跳,以及判断是否宕机和开启rebalance的
partition:每个topic分区,备份在不同的服务器上
如何选择coordinator?
对groupid进行hash,然后对_consumer_offsets的分区数量进行取模,默认分区数量为50,可以配置。
发生的时机:
1.分区数量增加。
2.对topic的订阅发生变化
3.消费者组成员的加入或者离开。
影响:1.重复消费;2.集群不稳定;3.影响消费速度
kafka 消息积压怎么办
参考
1.修复消费者,然后停掉所有消费者
2.临时建立10倍或者20倍的queue数量(新建topic,分区是原来的10倍)
3.写一个临时分发消息的consumer,xiaofei消费挤压数据不作处理,均匀写入临时建好10数量的queue中
4.征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的消息
5.这种做法相当于临时将queue资源和consumer资源扩大10倍,以10倍速度来消费消息
6.消费完成之后,回复原有的部署架构,重新用原来的consumer机器来消费消息。
kafka分区和消费者的关系
1.生产者中的分区合理消费,消费者的线程对象和分区保持一致,多余的线程不会进行消费
2.消费者默认即为一个线程对象
3.消费者服务器数*线程数 = partition个数
分区数量
分区多可以增加吞吐量
分区过多:
- 客户端服务端需要的内存变多,客户端生产者有个参数batchsize,默认16kb。为每个分区缓存消息,满了打包发送。
分区越多,消费者获取数据所需的内存就增多。同时消费者线程数要匹配最大分区数,线程切换开销很大。
服务器端维护许多分区界别的缓存。 - 文件句柄的开销:每个分区在底层文件系统都有属于自己的目录。分区越多,要同时打开的文件句柄数就越多。
- 降低高可用性:kafka通过副本来保证高可用。分区多,服务器上的副本就多,服务器挂掉,在副本之间进行重新选举的消耗就大。
目标吞吐量TT,消费者吞吐量TC,生产者吞吐量TP,分区数= TT/max(TP,TC)
分区和消费者的关系是多对一
一个分区只能对应一个消费者的原因是保证消息顺序性。
因此消息挤压,部署多台消费者实例是不能加快消费的,最多增加到和分区数量一致,超过的组员只会占有资源二不起作用。
消费者分区分配策略
1.range策略
- 将消费者按名称排序,分区按照数字排序
- 分区总数/消费者,除得尽均匀分配,除不尽位于前面的消费多负责分区
2.roundrobin 轮询
为了保证均匀分配,需要满足两个条件
1.同一个消费者组里每个消费者订阅的主题必须相同
2.同一个消费者里面所有消费者的num.streams必须相等
3.sticky分配策略
主要实现两个目的,如果发生冲突,优先实现第一个
1.分区分配尽可能均匀
2.分区的分配尽可能的与上次分配的保持相同
0.11.X版本引入,最复杂最优秀
区别于轮询,在消费者挂掉之后,不会全部分区重新轮询分配,而是将挂掉消费者消费的分区进行重新分配。
其他微服务组件
eureka注册中心机制
参考高频面试之Eureka
注册中心,包括服务发现,治理等功能。
@EnableEurekaServer作为注册中心,@EnableEurekaClient作为服务的提供者或消费者。
- 注册服务
EurekaClient将服务信息封装成Intanceinfo对象,通过EurekaHttpClient调用register方法发送post请求执行服务注册;
EurekaServer提供了基于Jersey的Rest风格接口,在ApplicationResource类中提供了addInstance方法来接受注册信息。如果注册信息通过校验,将服务信息保存到本地注册表。数据结构为双层HashMap,key为应用名,内层map,Key是应用实例信息编号,value是InstanceInfo
EurekaClient接受并解析注册结果,判断httpResponse的statusCode,如果是204则代表注册成功
调用replicateToPeers方法将此次注册信息复制到对等的Eureka节点 - 定时任务
- 拉取服务器注册实例
任务通过ScheduledExecutorService来实现任务调度,执行周期默认为60秒一次
获取方式有两种,全量获取和增量获取。第一次全量获取,后序增量获取;获取到服务器注册实例信息后,保存或更新到本地
- 续约
任务通过ScheduledExecutorService来实现任务调度,执行周期默认为30秒一次
通过Renew方法发起续约请求。将appname,appid以及intanceinfo作为参数,通过EurekaHttpClien发送Http请求
EurekaServer通过renewLease()方法接受续约请求
根据AppName从注册表获取对应的服务信息,并更新一些属性如renewsLastMin,lastUpdateTimestamp
EurekaServer返回结果200或者204
EurekaClient接受续约结果;如果是404重新发起,如果是200则表示续约成功,更LastSuccessHeartBeatTimestamp变量- 剔除服务
通过 JDK 自带的 Timer 来实现任务调度,通过 evict() 方法执行具体的操作
首先判断是否开启了实例自我保护机制,如果开启自我保护,则不做任何操作
如果未开启,根据 lastUpdateTimestamp 收集已过期的服务,加入到List集合中
通过 internalCancel() 方法,在该方法中从 registry 中剔除已经过期的实例。具体的剔除过程会通过打乱过期服务列表,并通过 Random 随机剔除,保证服务器剔除的均匀性
- 剔除服务
- 拉取服务器注册实例
限流算法
一致性哈希
一致性哈希相较于普通哈希具有更好的可扩展性和容错性。
哈希是对节点个数取模,在进行扩容或者节点下线时,需要重新映射所有数据。
一致性哈希是对2^32取模,将哈希值空间映射到虚拟的闭环上,称为哈希环。取模所得值,进行顺时针寻找到的第一个节点为哈希环中的位置。节点扩容和下线时只需要迁移相邻节点的数据。
当分布不均衡时,采用虚拟节点来解决问题。
无状态的服务,消费降级
linux命令
cat 文件 | grep 关键字 | wc -l
参考 Linux系统中统计文件中某个字符出现次数命令详细教程
- 不分大小写统计
grep -o -i ‘a’ aaa.txt | wc -l - 统计多个文件中某个字符出现次数总和
grep -o -i ‘a’ aaa.txt bbb.txt | wc -l
其他
数据结构-树
参考树、二叉树、完全二叉树、满二叉树的概念和性质
树
特点
只有一个根节点;每个子树根节点只有一个前驱,可以有0或者多个后驱。
注意:子树之间不能有交集
概念
- 节点的度:一个节点含有子树的个数称为该节点的度
- 叶节点:度为为0
- 树的度:最大节点的度
- 节点的层次:根为1层,往下2层,以此类推
- 树的高度或者深度:最大层数
- 兄弟节点:同根
- 堂兄节点:同层不同跟
树的表示
二叉树
概念
一颗二叉树是一个节点的有序集合,该集合为空,或者由一个根节点加上两颗别称为左子树和右子树的二叉树组成。
特点
1.二叉树不存在子节点大于2的度
2.二叉树有左右之分,次序不能颠倒,因此二叉树是有序树。
对于任意二叉树都是由以下几种情况复合而成的。
特殊的二叉树
满二叉树
所有叶子结点都在最后一层。所有分支节点都有两个孩子。节点为2^k-1。k为层数。
完全二叉树
前n-1层是满的。不满的那层子从左往右是连续的。
二叉树的性质
①若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点。\n②若规定根结点的层数为1,则深度为h的二叉树的最大结点数为2h-1个。\n③对任何一棵二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0 = n2+1。(常用这个性质解选择题)\n④若规定根结点的层数为1,则具有N个结点的满二叉树的深度h = log2(N+1)。\n⑤对于具有N个结点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点:\n1、若 i > 0,则该结点的父结点序号为:( i - 1) / 2;若 i = 0,则无父结点。\n2、若2i + 1 \u003C N,则该结点的左孩子序号为:2i + 1;若2i + 1 >= N,则无左孩子。\n3、若2i + 2 \u003C N,则该结点的右孩子序号为:2i + 2;若2i + 2 >= N,则无右孩子。
二叉树的存储结构
顺序存储一般用来存储完全二叉树,否则会造成空间浪费。现实只有堆会用数组存储。
链式存储,用链表表示二叉树。每个节点为左右指针域和数据域。
二叉树的遍历
深度遍历
- 前序 根左右
- 中序 左根右
- 后序 左右跟
广度遍历:层次遍历
堆
参考堆的应用 – Top-K问题(巨详细)
堆是二叉树,一般的二叉树用链表存储,用数组存储会浪费空间,但是堆是完全二叉树,用数组存储。
已知父节点下标n, 他的右节点为2n+2,左节点2n+1
已知子节点n,他的父节点为(n-1)/2
大根堆和小根堆
大根堆:根节点大于左右孩子节点
小根堆:根节点小于左右孩子节点
优先级队列
java提供这种数据结构,每次添加或者删除元素,都会变成小跟堆。
// 默认得到一个小根堆
PriorityQueue<Integer> smallHeap = new PriorityQueue<>();
smallHeap.offer(23);
smallHeap.offer(2);
smallHeap.offer(11);
System.out.println(smallHeap.poll());// 弹出2,剩余最小的元素就是11,会被调整到堆顶,下一次弹出
System.out.println(smallHeap.poll());// 弹出11
// 如果需要得到大根堆,在里面传一个比较器
PriorityQueue<Integer> BigHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
top-K问题
数组,找出前三个最小元素
1.排序
2.放入小跟堆
3.在大根堆中放三个元素,循环数组每次往里放一个数据,弹出最大的数据,最后堆里就是结果。
堆的向下调整算法、堆的向上调整算法、堆的实现、Topk问题
捕获异常
exception.printStackTrace打印到流,流转换为字符,log对象输出到log文件里
提取业务处理类共性,抽象出父类,公共模块根据名称获取子类对象,向上转型为父类对象,进行调用,捕获异常
设计模式
工厂模式
参考工厂模式
工厂模式属于创建型模式。
意图:定义一个创建接口的接口,让其子类字节决定实例化哪个类,工厂模式使其创建过程延迟到子类执行
主要解决:接口选择的问题
如何解决:让子类实现工厂接口,返回的也是也是一个抽象的产品
关键代码:创建过程在其子类实现
在任何需要生成复杂对象的地方,都可以使用工厂方法模式。简单对象,只需要new就能完成创建的对象,无需工厂模式。使用工厂模式,需要引入一个工厂类,增加系统的复杂度。
举例,抽象一个形状接口,圆,方块实现这个接口,定义一个工厂类提供给获取形状的方法,根据入参判断实例化圆/方块
抽象工厂模式
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");
//调用 Circle 的 draw 方法
shape1.draw();
//获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("SQUARE");
//调用 Square 的 draw 方法
shape3.draw();
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
//调用 Red 的 fill 方法
color1.fill();
//获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("GREEN");
//调用 Green 的 fill 方法
color2.fill();
//获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");
//调用 Blue 的 fill 方法
color3.fill();
}
}
数据格式标准化
xml到标准对象 thoughtworks.xstream 流
json到标准对象 hutools工具,底层反射调用set方法
javaBean
java语言中的可重用组件。
满足:1.类是公共的;2.有一个无参的公共构造器;3.有属性,且有对应的get和set方法。
其他开发者通过JSP,Servlet 其他JavaBean,applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地复制粘贴的功能。
javaBean的任务就是一次性编写,任何地方执行,任何地方重用。
难点:堆排序,redis跳表
八股文我背了第一层,技术hr问第二层,给我讲解,完事推荐我可以看书:
深入理解java虚拟机 (周志明)
MySQL实战宝典(姜承尧)