《Java面试题汇总》

编程知识 更新时间:2023-04-26 03:21:10

《面试题汇总》之基础篇

一 Java基础
1 基础知识点
1.1 int和Integer的区别
int是整型,是java8中的基本数据类型之一;

Integer是int对应的包装类,有一个final修饰的int字段,并提供了数学运算、字int和字符串之间转换等常用的方法

Integer和String一样,也是不可变类型

查看源码可知,在java5之后,valueOf方法使用了一个缓存机制,默认缓存是-128~127;在创建这个范围中的整数时,不需要new新对象,而是使用缓存,提高性能,缓存在Boolean、Short、Byte和Character中同样存在;

把基本数据类型转换成包装类型时装箱,把包装类型转换为基本类型时拆箱;

java中有自动拆装箱功能,但在性能敏感场合中,尽量避免使用无意义的拆装箱行为;

原始数据类型int不能和java泛型结合使用,原始数据类型是线程不安全的

1.2 String,StringBuffer,StringBuilder区别
String是一个final修饰类,所有属性也是final的;所以String具有不可变性,也就是对字符串的操作,如拼接、剪切都是产生新的对象;

StringBuffer本质是一个线程安全的可修改字符串序列,因为保证了线程安全,所以会带来额外的性能开销;

StringBuilder本质和StringBuffer没有区别,但是StringBuilder去掉了线程安全部分提高了效率,是绝大部分情况下字符串拼接的首选。

如果确定拼接字符串会发生多次,并且长度可以预计,name可以在开始指定合适的大小,避免数组扩容带来的开销。

1.3 & 和 && 区别
与:a与b 全真得真 有假即假

&: 没有短路,按位与,前后两个表达式都进行运算;

&&:短路功能,逻辑与,当第一个表达式为false时,就不计算第二个表达式了。当第一个表达式为true,再计算第二个表达式。

1.4 方法重写与方法重载的特点(区别)
方法重载的特点:

同名不同参,与返回值无关。

父类原方法:
public int test(int a,int b){}

子类方法重载:
public 返回值类型 test(float a,float b){}//参数类型不同的重载
public 返回值类型 test(int a,int b,int c){}//参数个数不同的重载
方法重写的特点:

遵循两同,两小,一大原则

两同:方法名和参数列表相同

两小:返回值,抛出异常比父类小

一大:访问修饰符比父类大

父类原方法:
public float test(int a,int b){}

子类重写方法:
public float test(int a,int b){}
public int test(int a,int b){}
private float test(int a,int b){}//私有的话修饰符访问权限比父类小,不是重写
1.5 Java三大特点:封装、继承、多态
封装:隐藏对象内部细节

优点

提高代码复用性

隐藏实现细节,对外只提供一个公共的访问方式

提高安全性

继承:子类对象拥有父类对象的全部属性跟方法。提高代码的复用性,可扩展性,是多态的前提条件

优点:

提高代码复用性

让类与类产生关系,提供了多态的前提

缺点:

类与类之间产生了耦合,不符合OOP的开发原则

多态:只同一事物表现出来的多种形态。【父类引用指向子类对象】

应用场景

通过参数传递形成多态

直接在方法体中使用抽象类的引用指向子类类型的对象

优点:

隐藏了子类的类型,提高代码的扩展性

缺点:

只能使用父类共有的内容,无法使用子类特有的功能

抽象

1.6 类加载过程
类初始化 -》装载 -》 验证 -》 准备

Class文件由类装载器装载后,在JVM中形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class结构信息:构造函数、属性、方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能

虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,即虚拟机的类加载机制

工作机制

类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过的步骤如下:

装载:查找和导入Class文件;

链接:把类的二进制数据合并到JRE中

校验:检查载入Class文件数据的正确性

准备:给类的静态变量分配存储空间

解析:将符号引用转成直接引用

初始化:对类的静态变量,静态代码块执行初始化操作

1.类初始化
1).遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的java代码场景有:
*使用new关键字实例化对象
*读取或设置一个类的静态字段(被final修饰,已在编译期将结果放入常量池的静态字段除外)
*调用一个类的静态方法
2).使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3).初始化一个类时,如果发现其父类还没有进行初始化时,则需要先触发其父类初始化
4).虚拟机启动时,用户需要指定一个要执行的主类(包含main),虚拟机会先初始化这个主类
2.装载
1).通过一个类的全限定名获取定义此类的二进制字节流
2).将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3).在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
3.验证
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃
4.准备
正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,
说明:
此时进行内存分配的仅包括类变量(static修饰),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆。
1.7 String底层使用char数组还是byte数组
JDK8以及之前是char数组

JDK9以及之后是byte数组

1.8 类加载顺序
首先加载父类的静态内容:静态字段、静态语句块;

再加载子类静态字段、静态语句块;

父类的普通变量初始化和语句块;

父类的构造方法;

子类的普通变量初始化和语句块;

子类的构造方法;

1.9 五大基本原则
单一职责原则

每一个类功能要求单一,只负责一件事

开放封闭原则

对修改关闭,对扩展开放

里氏替换原则

子类能够替换父类,能够出现再父类出现的任何地方

依赖倒置原则

具体依赖抽象,上层依赖下层

接口分离原则

接口中的方法要尽量细化,同时接口中的方法尽量少

2 Java关键字
2.1 final修饰的对象是否可变
final修饰值类型时,变量一旦赋值就不能改变;

final修饰引用类型时,被引用的对象属性值可以改变,但是该引用不能指向新的对象

2.2 this和super的区别
*this与super区别:
-属性:
-this访问本类属性,如果本类没有则访问父类
-super访问父类属性
-方法:
-this访问本类方法,如果没有则访问父类
-super访问父类方法
-构造:
-this调用本类构造,必须放在构造方法首行
-super调用父类构造,必须放在子类构造方法首
-其他区别:
-this表示当前对象
-super不能便是当前对象

*this.变量 和 super.变量的区别
-this.变量:调用当前对象的变量
-super.变量:直接调用父类的变量

*this(参数) 和 super(参数)区别:
-this(参数):调用(转发)的是当前类中的构造器;
-super(参数):用于确认要使用父类中的哪一个构造器。

*注意点:
-在对拥有父类的子类进行初始化时,父类的构造方法也会执行,且优先于子类的构造函数执行;因为每一个子类的构造函数中的第一行都有一条默认的隐式语句super();

  • this() 和super()都只能写在构造函数的第一行;
  • this() 和super() 不能存在于同一个构造函数中。
  • this()和super()都必须写在构造函数的第一行;
  • this()语句调用的是当前类的另一个构造函数而这个另一个构造函数中必然有一个父类的构造器,再使用super()又调用一次父类的构造器, 就相当于调用了两次父类的构造器,编译器不会通过;
  • this和super不能用于static修饰的变量,方法,代码块;因为this和super都是指的是对象(实例)。
    2.3 this可以出现在static修饰的方法中吗?
    不可以,因为静态方法时随着类加载就加载了,而this是当前对象,加载的时候,还没有创建当前对象。

2.4 volatile
可见性:指线程之间的可见性,即一个线程修改的状态对于另一个线程是可见的。

实现原理

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

禁止指令重排序

指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。

JDK1.5之后,可以使用volatile变量禁止指令重排序。针对volatile修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏

2.5 final、finally、finalize区别
final:关键字,修饰的对象不可变

finally:try catch里面用于最终执行的

fianlize:jvm回收垃圾前会调用一次

2.6 synchronized关键字
修饰方法,可防止多个线程同时访问被修饰的方法

修饰静态方法,可防止多个线程同时访问该类的静态同步方法,对类中所有对象都起作用

修饰方法中的代码块,只对当前代码块实行互斥访问,同一时间,只有一个线程能独占访问,其他的来了只能等待阻塞

不能被继承,如果父类的的synchronized在继承时并不自动声明,需要显示的声明

修饰this时,会得到当前对象锁,当其他线程来访问时,会被阻塞

2.7 volatile关键字作用
保证可见性

当一个共享变量被volatile修饰时,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,会去内存读取新值

实际使用常与CAS结合来保证原子性

CAS操作主要有三个参数:内存地址V,预期的旧值A,将要替换的目标值B,只有当V地址对应的值是预期的A时才做替换,否则什么都不做,整个比较并替换的过程是原子操作;

2.8 static关键字
在类中,用static声明的成员变量升级成为类变量;生命周期与类相同,在整个应用程序执行期间都有效

用途:可以在没有创建对象的情况下调用静态成员方法/变量

使用方式:

静态成员被所有对象共享

静态成员方法中只能访问静态成员,不能访问非静态成员

3 常见编程题
3.1 编程实现两个大数的加法
/**
String a = “123456…” a.size大于1000位
String b = “3456999…” b.size大于1000位
*/
public void test(){
String a =“10001”;
String b = “999999”;
char[] large = null;//大
char[] small = null;
if(a.length() > b.length()){
large = a.toCharArray();
small = b.toCharArray();
}else{
large = b.toCharArray();
small = a.toCharArray();
}
int[] sums = int[large.length + 1];//最终的结果位数最高位可能为0
for(int i=0;i<large.length;i++){
sums[i] += large[large.length -i - 1]-‘0’;
}
for(int i=0;i<small.length;i++){
sums[i] += small[small.length -i - 1]-‘0’;
}
for(int i=0;i<sums.length - 1;i++){
if(sums[i] > 9){
sums[i+1] += sums[i]/10;
sums[i]%= 10;
}
}
StringBuilder sb = new StringBuilder();
for(int i = sums.length-1;i>=0;i–){
sb.append(sums[i]);
}
String result = sb.toString();
if(result.startsWith(“0”)){
result = result.substring(1);
}
System.out.println(result);

}
3.2 编程实现两个大数的乘法
/**
String a = “123456…” a.size大于1000位
String b = “3456999…” b.size大于1000位
*/
public void test(){
String a =“10001”;
String b = “999999”;
char[] large = null;//大
char[] small = null;
if(a.length() > b.length()){
large = a.toCharArray();
small = b.toCharArray();
}else{
large = b.toCharArray();
small = a.toCharArray();
}
int[] multi = new int[a.length() + b.length()];
for(int j=small.length-1; j>0;j–){
for(int i=large.length-1;i>0;i–){
int num1= small[j]-‘0’;
int num2 = large[i]-‘0’;
multi[large-1-i + small.length-1-j] += num1 * num2;
}
}
for(int i=0;i<multi.length-1;i++){
if(multi[i]>9){
multi[i+1] += multi[i] /10;
multi[i] %= 10;
}
}
StringBuilder sb = new StringBuilder();
for(int i=multi.length-1; i>=0;i–){
sb.append(multi[i]);
}
String result = sb.toString();
if(result.startsWith(‘0’)){
result = result.substring(1);
}
System.out.prinln(result)
}
3.3 字符串处理问题
找出字符串出现最多的字符

给定一个字符串,先声明两个变量,一个res表示下标为0位置的元素,一个max表示最多出现的次数;

遍历字符串,声明字符temp代表下边为i的元素,count初始赋值为0,再双重for循环,判断第j个元素是否等于temp,等于则count++;

判断得到的count跟max比较,如果大于等于,则将count赋值给max,对应的temp值赋值给res;

即res出现次数max次最多。

image-20200912170649906

image-20200912170953930

找出第一次重复出现的字符

image-20200912171443276

找出第一个只出现一次的字符

image-20200912171659444

image-20200912171751711

统计手机号各个数字个数,按照升序输出

image-20200912172121847

输入为一个字符串和字节数,输出为按字节截取字符串,但是要保证汉字不能被截半个

例如:“人ABC” 4,应该截取为“人AB”,输入“人ABC们DEF” 6,应该截取为“人ABC”,而不是“人ABC + 们的半个”。

image-20200912173305577

截取指定字符串

image-20200912173528858

设计一种反转字符串的算法

image-20200912175644719

image-20200912175732242

image-20200912175854771

写函数将句子按照一定的分隔符分割后返回

image-20200912184028454

给定一个字符串,反转字符串中每个字符顺序,同时保留单词和空格的初始位置

image-20200912192944900

判断字符串是不是合法的ipv4地址

image-20200912193632685

image-20200912193949971

3.4 数组问题
求和最大的子序列

给定一整数序列A1,A2,…An(可能有负数),求A1An的一个子序列AiAj,使得Ai到Aj的和最大,并输出子序列的内容。

image-20200912195222778

image-20200912200820261

数组去重排序

有一个数组中有100个数字,请输出:没有重复并按照升序排序的数字

image-20200912200923070

有一个正整数数组,存在重复元素,非降序排列。要求将数组元素打印到屏幕上,请手写程序并满足一个条件:

按照升序输出数组元素

重复元素仅打印一次

时间复杂度O(n)

实例:原始数据[2,3,3,6,6,9,10,10,16,18],依次在屏幕上打印2,3,6,9,10,16,18

image-20200912201015402

image-20200912201105702

奇偶调换

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数与偶数之间的相对位置不变

image-20200912202036656

image-20200912202307498

给出随机的100个数,需要为1~100,按从小到大顺序输出,并输出相应的序号。

image-20200912201734804

3.5 手写一个单例模式
定义:类有且仅有一个实例对象存在

特点:

只有一个实例

必须自己创建自己的唯一实例

必须给所有其他对象提供这一实例

构建单例三步骤

静态实例变量

构造方法私有化

提供一个公共的访问方式

/**

  • 饿汉式1
  • 类加载的时候就完成实例化(初始化)
  • 缺点:在类加载时就完成初始化,如果没有呗用到,就回造成内存浪费
    /
    class Singleton01 {
    //1.创建静态的实例变量
    private static final Singleton01 INSTANCE = new Singleton01();

    //2.私有化构造
    private Singleton01(){};

    //3.获取实例方法
    public static Singleton01 getInstance(){
    return INSTANCE;
    }
    }

    /
    *
  • 饿汉式2
  • 枚举方式
    /
    enum Singleton02{
    //枚举构造方法都是私有的
    Single; // 单例实例对象
    }

    /
    *
  • 懒汉式 线程不安全
  • 优点:使用时才加载,延时创建对象
    /
    class Singleton03{
    //1.new一个空对象,用时才实例化
    private static Singleton03 singleton03 = null;

    //2.私有化构造
    private Singleton03(){};

    //3.
    public static Singleton03 getInstance(){
    //判断单例是否已经创建
    if(singleton03 == null){

    return singleton03 = new Singleton03();
    }
    return singleton03;
    }
    }

    /
    *
  • 懒汉式 适用于多线程
  • 1.属性和构造 私有化
  • 2.获取实例的方法 双重检查锁
    */
    class Singleton04{
    //1.new一个空对象,用时才实例化
    private static Singleton04 singleton04 = null;

    //2.私有化构造
    private Singleton04(){};

    //3.
    public static Singleton04 getInstance(){
    //判断单例是否已经创建
    if(singleton04 == null){

    //此处添加一个同步锁,锁对象使用挡墙的class对象
    synchronized (Singleton04.class){
    if(singleton04 == null){
    singleton04 = new Singleton04();
    }
    }

    }
    return singleton04;
    }
    }
    3.5.1 饿汉式

(线程安全、反射不安全、反序列化不安全)

优点

类加载时就生成了,执行效率高

缺点

如果当前对象在生成时需要浪费很多内存

调用单例的方法可能不是getInstance方法,这样就造成内存产生垃圾,造成内存浪费

对于反射或反序列化是不安全的

image-20200912152840716

3.5.2 登记式

(线程安全、防止反射供给、反序列化不安全)

image-20200912153539977

3.5.3 枚举式

(线程安全、防止反射攻击、反序列化安全)

image-20200912154125242

3.5.4 懒汉式

(线程不安全、延迟加载)

线程不安全的

image-20200912154602948

加synchronized到同步方法的线程安全(每一次都需要同步,效率低)

image-20200912154723855

加synchronized到同步代码块(每一次都需要同步,效率低)

image-20200912154811206

3.5.5 双检索的线程安全

(减少同步的次数,提高效率)

image-20200912154910648

为防止指令重排需要添加volatile关键字

image-20200912155136277

3.5.6 ThreadLock

(不加锁,以空间换时间,为每个线程提供变量的独立副本,可以保证各自线程中是单例的,但是不同线程之间不保证)

image-20200912155349387

3.5.7 CAS实现

(无锁乐观策略,线程安全)

image-20200912155905449

4 JVM指令规范代码题
image-20200910194251275

image-20200910194608902

image-20200910195439852

5 写一个单例Singleton
image-20200910195727359

饿汉式,懒汉式

image-20200910195757632

2.1 直接实例化饿汉式

image-20200910200710841

2.2 枚举式饿汉式

image-20200910200807534

2.3 静态代码块饿汉式

image-20200910201320702

2.4 线程不安全的懒汉式(适合单线程)

image-20200910201832563

2.5 线程安全的懒汉式(适合多线程)

image-20200910202139222

image-20200910202234070

2.6 静态内部类的方式线程安全懒汉式

image-20200910202418221

6 继承类的问题
image-20200910202716994

/*考点分析
1 类初始化过程
- 父类的初始化:
– (1)j = method();
– (2)父类的静态代码块
- 子类的初始化:
– (1)j = method();
– (2)子类的静态代码块
main所在的类先执行,因此main的入口方法先输出:(5)(1)(10)(6)

2 实例初始化过程
- 父类的实例化方法
– (1)super() ==>最前面执行
– (2)父类非静态方法:i = test();
// 非静态方法前面其实有一个默认的对象this,this表示构造器()正在创建的对象,所以本题中执行的是子类重写的test方法(面向对象多态)
– (3)父类非静态代码块
– (4)父类无参构造 ==>最后面执行
- 子类的实例化方法
– (1)super() ==>最前面执行
– (2)子类非静态方法:i = test();
– (3)子类非静态代码块
– (4)子类无参构造 ==>最后面执行

3 方法的重写

结果如下:
*/
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)

image-20200910205046006

image-20200910205733727

image-20200910210727275

image-20200910210845567

7 Java传参问题
image-20200910211042278

image-20200910212422997

image-20200910212437073

8 递归与循环迭代
//青蛙跳台阶青蛙可以一次跳1级/2级台阶请问跳上第n级台阶有多少种方法?


// 使用递归:
if(target == 1){
return 1;
}
if(target == 2){
return 2;
}
//第一次有两种选择,然后根据不同的选择,然后开始不同的下一步,但是下一步还是一样有两种选择
return JumpFloor(target - 1) + JumpFloor(target - 2);

//非递归,使用循环迭代:
int x=1, y=2, sum;
if(target == 1){
return 1;
}
if(target == 2){
return 2;
}
for(int i = 3; i <= target; i ++){
sum = x + y;
x = y;
y = x;
}
return sum;
image-20200910213759409

9 就近原则、变量分类、非静态代码块执行、方法调用
image-20200910214144762

image-20200910214407318

image-20200910214827219

image-20200910214948758

image-20200910215632081

10 常用的线程安全的类?
stack:线程安全,先进后出

hashtable:比hashmap多了个线程安全,每个方法都加了synchronized关键字

concurrentHashmap:引入“分段锁”概念,把一个大的map拆分为多个Hashtable,根据key.hashcode()来决定把key放入那个Hashtable中

cHashmap中把map分为N个segment,put跟get元素时,先根据key.hashcode()算出放入哪个Segment中

在根据key.hashcode()算出放入Hashtable的位置,扩容也是针对单个segment的hashtable而言的

放入相同索引位置的segment时会阻塞,放入不同的索引位置的segment时,不会阻塞,所以性能明显提升

二 Java集合
1 HashMap与Hashtable区别
Hashtable继承自Dictionary,HashMap继承自AbstractMap,二者都实现了Map接口;

Hashtable不允许null key和null value,HashMap允许;

HashMap的使用几乎与Hashtable相同,不过Hashtable是synchronized同步的。

2 HashMap扩容机制
HashMap的初始大小(哈希桶)为16,加载因子是0.75,当集合存储的数据达到12(当前容量*0.75)时进行扩容;扩容为当前容量的2倍;同时将之前的元素再次进行一次hash运算,填充到新的hash桶中;

问:为啥是0.75?
答:从扩容、空间利用率共同决定的
1.7中的HashMap底层是数组+链表存储结构,链表采用头插法,可能造成环形引用,进入死循环;

1.8中的HashMap底层采用数组+链表+红黑树的存储结构,链表采用尾插法

扩容时机:当一个链表的长度大于8时,hash桶的容量大于等于64时,会进行转换成红黑树的操作(如果总容量小于64先扩容);—基于泊松分布统计而来的

当红黑树中元素个数小于6时,再从红黑树转回链表,多线程进行put操作时,可能会出现数据被覆盖的情况。

追问:HashMap扩容为啥是乘以2?

因为扩容的底层是2进制的(n - 1) & hash与位运算,扩容按照2的n次幂是,(n-1)的2进制全部是1111…1,这样与添加元素的哈希值进行位运算是可以充分的散列,减少hash碰撞。

问:线程不安全问题怎么解决?
答:可以使用hashtable 、 concurrentHashMap.

3 ArrayList与LinkedList区别
ArrayList底层是动态数组,查询快,增删改慢

因为是数组,通过下标查询,效率高,但是插入或者删除需要移动后面的所有元素,导致效率降低;

初始化时长度是0,当add元素长度默认变成10,扩容机制是每次扩容1.5倍;

LinkedList底层是双向链表,查询慢,增删改快

采用的是链表,插入或删除只需要更改节点的next跟pre指向,不涉及其他元素位置的变更,效率高,但是查找需要从当前节点通过next跟pre进行循环遍历,效率降低

对于随机访问get和set,ArrayList优于LinkedList;

在线绘B+树

4 Iterator迭代器
hasNext()

next()

remove()

5 ArrayList源码解析
底层是一维数组,内存空间连续

new ArrayList()时声明的是一个空数组,即没有申请内存空间

调用add方法添加元素时,申请内存初始化数组长度为10的数组

当数组长度不够时,动态扩容1.5倍

6 LinkedList源码解析
底层是双向链表,内存空间不连续

其他类似ArrayList

7 Stack类源码
后进先出

底层是动态数组

8 Vector类
底层是动态数组

线程安全,效率低

扩容为2位

9 泛型底层原理
Java5开始增加的新特性

将数据类型参数化,泛型即形参用来占位,实际数据类型是实参

10 HashSet底层源码
底层是哈希表,有序不重复

存放元素进HashSet的原理

使用HashCode方法获取元素hash值,使用hash算法计算出在数组中的索引位置

若该位置没有元素,直接放入

若有元素,则将该元素与已有元素一次比较hash值,hash值不同,则直接放入

hash值相同,则使用新元素调用equals方法比较,相等则添加失败,否则直接放入

问题:为啥要求重写equals方法后要重写hashCode方法?

解析:当两个元素调用equals方法相等时证明这两个元素相同,重写hashCode方法后保证这两个元素得到的哈希码值相同,由同一个哈希算法生成的索引位置相同,此时只需要与该索引位置已有元素比较即可,从而提高效率并避免重复元素的出现。

11 TreeSet底层源码
底层是红黑树,有序(默认从小到大)

排序功能

自然排序(comparable):让元素类型实现comparable接口重写comparaTo方法定义比较规则(0相等,负数小于,整数大于)

比较器排序(comparator):构造集合时传入comparator接口

12 HashMap底层源码
JDK1.7以及之前为数组+链表;JDK1.8以及之后底层是数组+链表+红黑树

new对象时加载因子为0.75

添加元素时,默认初始容量是16

容量达到12时,进行扩容

链表的长度达到8时转换为红黑树

13 HashMap遍历的方式
使用For_Each遍历Entries

使用For_Each遍历Keyset跟values

使用Iterator遍历entries

14 HashMap在jdk1.7下头插法为甚会产生循环链表?
使用头插法假设初始的hashmap目前已经处于临界状态,且有一个位置n下有单链表,链表元素有1跟3,且1指向3的单链表;

此时put一个元素时会发生扩容,假设扩容后处于n位置的1跟3在复制到扩容后的hashMap中仍然存在hash碰撞;

由于jdk1.7采用头插法,此时3会指向1,而初始map中1又是指向3的,所有产生了循环链表;

此时如果get(1)或者get(3)时,仍然可以正常获得value值;但是如果get(5),而5也是在位置n上,就会遍历n位置的循环链表,遍历的线程在循环链表上没有尽头,导致卡死;

15 Hashmap中存放自定的key时,为什么要重写hashcode方法跟equals()方法?
HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地 址,这样即便有相同含义的两个对象,比较也是不相等的

例如,生成了两个“羊”对象,正常理解这两个对象应该是相等的,但如果你不重写 hashcode()方法的话,比较是不相等的!
HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等 的。若equals()不相等则认为他们不相等。如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为 同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。HashMap用来判断key是否相等的方法,其实是调用了HashSet判断加入元素 是否相等。

三 IO
四 线程与多线程
0 线程的生命周期
image-20201010113044023

新建 - 就绪 - 阻塞 - 运行 - 终止

新建:创建对象后即进入就绪状态,如:Thread t = new MyThread();

就绪(Runnable):线程对象调用start()方法即进入

运行状态(Running):当CPU开始调度处于就绪状态的线程,线程才真正运行,即进入运行状态【注意:就绪状态是进入运行状态的唯一入口】

阻塞状态(Blocking):运行中的线程由于某种弄原因,暂时放弃对CPU的使用权,停止执行,即进入了阻塞状态。

死亡状态(Dead):执行完了,或者因异常退出了run()方法,线程结束生命周期。

导致线程阻塞的原因

等待阻塞–wait方法

同步阻塞–synchronized同步锁失败

其他阻塞–调用线程的sleep或join方法,或者发出IO请求,线程会进入阻塞

当sleep状态超时、join等待线程终止或者超时、IO处理完毕,线程重新进入就绪状态

1 实现多线程的几种方式
继承Thread类,重写run方法。

实现Runnable接口,重写run方法。【可以避免由于Java的单继承特性而带来的局限,适合多个线程去处理同一资源的情况】

实现Callable接口,重写call方法。【有返回值,允许抛异常】

使用线程池【减少创建新线程的时间,重复利用线程池中线程,降低资源消耗,可有返回值】

run方法与start方法的区别:

  • run方法就是普通的方法,按照顺序执行;
  • start方法代表线程进入就绪状态,是否执行取决于当前线程是否抢占了时间片;
    继承Thread类

image-20200913115017303

实现Runnable接口

image-20200913115051999

callable接口实现,测试类中需要用到FutureTask

image-20200913115324565

image-20200913115450478

线程池

image-20200913115726779

2 wait和sleep的区别
相同点

sleep()、wait()都是使线程暂停执行的方法。

不同点:

原理不同。

sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,他会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒。例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加一个sleep()方法,以便让自己每隔一秒执行一次,该过程如同闹钟一样。

wait()方法是object类的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法或者notifyAll()时才醒来,不过开发人员也可以给他指定一个时间,自动醒来。

notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许他去获得锁。

notifyAll()方法唤醒所有等待这个对象的线程并允许他们去竞争获得锁。

对锁的处理机制不同。

sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用sleep()方法并不会释放锁。

wait()方法调用wait()方法后,线程会释放掉他所占用的锁,从而使线程所在对象中的其他synchronized数据可以被其他线程使用。

使用区域不同。

wait()方法必须放在同步控制方法和同步代码块中使用;wait()、notify()、notifyAll()不需要捕获异常。

sleep()方法则可以放在任何地方使用;sleep()方法必须捕获异常,sleep的过程中,有可能被其他对象调用他的interrupt(),产生InterruptedException。

小总结:由于sleep不会释放锁标志,容易导致死锁问题的发生,因此一般情况下,推荐使用wait()方法。

3 线程池
3.1 定义
一个线程集合,在需要执行的时候从池中取出已有的线程,而不是创建新线程。

执行完后放回线程池,等待下次任务分配

3.2 线程池的创建与使用
image-20201010112707941

image-20201010112720079

3.3 四种线程池的创建
newCachedThreadPool:创建一个可缓存线程池

newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数

newScheduledThreadPool:创建一个定长线程池,支持定时以及周期性任务执行

newSingleThreadExecutor:创建一个单线程化的线程池,只会用唯一的工作线程来执行任务

3.3 线程池的优点
重用存在的线程,减少对象创建销毁的开销

可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多的资源竞争,避免堵塞

提供定时执行、定期执行、单线程、并发数控制等功能

4 Runnable与Callable/Future区别
Callable重写call方法,Runnable重写run方法

Callable任务执行后可返回值,返回值被Future拿到,Runnable任务不返回值

call方法可抛异常,run方法不可以

运行Callable任务可以拿到一个Future对象,表示异步计算结果,提供给了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可以取消任务的执行,还可以获取执行结果。

Future接口表示异步任务,没有完成的任务给出的未来结果,即Callable用于产生结果,Future用于获取结果

5 FutureTask?ExecutorService?
FutureTask表示一个可以取消的异步运算,有启动运算、查询运算是否完成,取回运算结果的方法。

只有当运算完成时,才能取出结果,尚未运行结束,get方法会被阻塞。

一个FutureTask对象可以调用了Callable和Runnable对象进行包装,由于FutureTask也是调用Runnable接口,所以可以提交到Executor执行

6 阻塞队列实现原理
Blocking Queue:一个支持两个附加操作的队列

队列为空时,获取元素的线程会等待队列变为非空;

队列为满时,存储元素的线程会等待队列可用

阻塞队列常用于生产者与消费者模式

JDK7提供了7个阻塞队列:

ArrayBlockingQueue:数组结构组成的有界阻塞队列

LinkedBlockingQueue:链表结构的有界阻塞队列

PriorityBlockingQueue:支持优先级排序无界阻塞队列

DelayQueue:使用优先级队列实现的无界阻塞队列

SynchronousQueue:不存储元素的阻塞队列

LinkedTransferQueue:链表结构的无界阻塞队列

linkedBlockingDueue:链表结构的双向阻塞队列

Java 5 之前实现同步存取主要使用普通集合,配合wait,notify,notifyAll,synchronized关键字的使用

Java 5 之后可以使用阻塞队列实现,减少了代码量

BlockingQueue是Queue接口的子类,主要用作线程同步的工具,控制线程之间的通信【生产者消费者模型】

7 进程与线程的区别
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位

8 线程池的参数
1、corePoolSize
核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。
核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
2、maxPoolSize
当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
3、keepAliveTime
当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
4、allowCoreThreadTimeout
是否允许核心线程空闲退出,默认值为false。
5、queueCapacity
任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。

线程池按以下行为执行任务:
1、当线程数小于核心线程数时,创建线程。
2、当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3、当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务

四 并发编程

  1. 并发编程的三要素
    原子性

可见性

有序性

1.1 实现可见性的方法
synchronized或者Lock:保证同一时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性

1.2 常用的并发工具类
ConuntDownLatch:类似计时器,做减法

countDown() 发通知后,开始执行

只计数一次

await() 来了等待

CyclicBarrier:类似收集器,做加法

计数可以重置,reset()

await() 来了等待

Semaphore:类似停车场,有增邮件

acquire:增加

release:减少

Exchanger:交换器???

  1. synchronized、volatile关键字
    synchronized关键字:

修饰方法,可防止多个线程同时访问被修饰的方法

修饰静态方法,可防止多个线程同时访问该类的静态同步方法,对类中所有对象都起作用

修饰方法中的代码块,只对当前代码块实行互斥访问,同一时间,只有一个线程能独占访问,其他的来了只能等待阻塞

不能被继承,如果父类的的synchronized在继承时并不自动声明,需要显示的声明

修饰this时,会得到当前对象锁,当其他线程来访问时,会被阻塞

volatile关键字:

保证可见性,禁止指令重排序;常比喻成轻量级“synchronized”

当一个共享变量被volatile修饰时,会保证修改的值会立即被更新到主内存,当有其他线程需要读取时,会去主内存读取新值

实际使用常与CAS结合来保证原子性

只能用来修饰变量,无法修饰方法或者代码块

  1. 什么是CAS?
    Compare And Swap:比较并交换

一种基于乐观锁的操作

乐观锁:采取乐观的态度,默认不会有其他线程来争抢资源,性能高

悲观锁:采取悲观的态度,默认会有其他线程来争抢资源

CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。

如果内存地址的值与A相同,那么就将内存里面的值更新为B。

CAS是通过无限循环来获取数据的,如果第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有机会执行

JUC并发包下的类大多都是使用CAS操作实现的

CAS的常见问题

1.CAS易造成ABA问题
一个线程a将数值改成b,接着又改成了a,此时CAS认为没有变化,其实是改变过了
解决方案:使用版本号标识,每操作一次version加1.java5中提供了AtomicStampedReference来解决

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized来解决

3.CAS造成CPU利用率增加
之前说过CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用
4 什么是Future?
并发编程,常用到非阻塞模型,在之前的多线程的三种实现中,不管是继承thread类还是实现Runnable接口,都无法保证获取到之前的执行结果,通过实现Callback接口,并用Future可以来接收多线程的执行结果。

Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便任务执行成功或者失败后作出相应的操作。

5 什么是AQS?
AbstractQueueSynchronizer,抽象队列同步器。

AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。

用一个int类型的变量表示同步状态,并提供一系列的CAS操作管理同步状态。

AQS是一个用来构建锁和同步器的框架,使用AQS能简单高效的构造出应用广泛的大量的同步器,如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS实现的。

6 AQS支持的两种同步方式
独占式

共享式

方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式如ReentrantReadWriteLock。AQS为使用提供了底层支撑,如何组装未完成,使用者可以自由发挥。

7 ReadWriteLock是啥?
ReentrantLock,是为了防止线程A在写线程,线程B在读数据造成数据的不一致,但是如果两个线程都是在读数据,并不会对数据造成修改,不需要加锁,但是ReentrantLock还是加锁了,这就造成了程序性能下降。

ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是其具体实现,实现读写分离,读锁共享,写锁独占,读读共享不互斥,读写、写读、写写均互斥,可以提高读写的性能。

8 FutureTask是啥?
表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否可以完成、取消任务等操作,当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask可以放入线程池中。

9 synchronized与ReentrantLock的区别
本质区别:synchronized是JVM层面的关键字,不需要释放锁;ReentrantLock是java类,需要在finally里手动释放锁;

类可以提供更加灵活的特性,可以有继承,可以有方法,可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在一下几点:

ReentrantLock可以对获取锁的等待时间进行设置,避免死锁

ReentrantLock可以获取各种锁的信息

ReentrantLock可以灵活的实现多路通知

两种锁机制不一样,synchronized是非公平锁;ReentrantLock可以通过构造方法实现公平锁或非公平锁;

ReentrantLock可以绑定多个Condition。通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.通过await(),signal();

并发量比较小,优先使用synchronized

并发量大,优先使用ReentrantLock

synchronizezd与ReentrantLock实现原理


Synchronized原理:
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

ReenTrantLock实现的原理:
CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似。

CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

CLH队列:带头结点的双向非循环链表

10 线程B怎么知道线程A修改了变量
volatile修饰变量

synchronized修饰修改变量的方法

wait/notify

while轮询

11 synchronized、volatile、CAS比较
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞

volatile提供多线程共享变量可见性和禁止指令重排

CAS是基于冲突检验的乐观锁(非阻塞)

12 ThreadLocal是啥?有啥用?
ThreadLocal是本地线程副本变量工具类。

主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,高并发时,可以实现无状态的调用。

特别适用于各个线程依赖不同的变量值完成操作的场景。

空间换时间的做法,每个Thread里面维护一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全的问题了

13 为啥wait方法与notify/notifyAll方法要在同步块中被调用
JDK强制行为,wait与notify/notifyAll方法在调用前都必须先获得对象的锁

14 多线程同步的方法有哪些?
Synchronized关键字

Lock锁实现

分布式锁

15 线程的调度策略
线程调度器选择优先级最高的线程运行,但是如果发生以下情况会终止线程运行:

线程体中调用了yield方法让出了CPU的占用权

线程体中调用了sleep方法使线程进入睡眠状态

线程由于IO操作受到阻塞

另一个优先级更高的线程出现

在支持时间片的系统中,该线程的时间片用完了

16 ConcurrentHashMap的并发度是什么?
ConcurrentHashMap的并发度是segment的大小,默认是16,意味着最多可以同时有16条线程操作ConcurrentHashMap;

17 Linux环境下如何查找哪个线程使用CPU最长?
获取项目的Pid,jps 或者ps -ef |grep java

top -H -p pid,顺序不能改变

18 死锁如何避免?
两个及两个以上线程被永久阻塞的现象

产生死锁的根本原因:申请锁的时候发生了交叉闭环

19 唤醒一个阻塞线程的方法?
如果线程因为调用了wait(),sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出interruptedException来唤醒它;

如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

20 synchronized与lock有啥区别
21 ReentrantLock的tryLock()、 lock()、 unLock()方法
tryLock():

不会阻塞,能获取锁则立即返回true;不能获取锁,则立即返回false;

lock()

会阻塞,能获取锁则立即返回true;不能获取锁,则阻塞,直到获取锁;

unLock()

释放锁,需要手动释放。

22 CountDownLatch的使用场景
23 造成死锁的原因?如何预防?
24 加锁会带来的性能问题?如何解决?
25 ConcurrentHashmap
jdk1.7下的ConcurrentHashmap
Segment:默认是16,初始化后不可扩容,集成了ReentrantLock

segment内部有hashentry[] table:扩容针对Segment数组对象,只是初始化时的segment内部数组长度一致,扩容不影响其他Segment数组大小

第0个位置会计算出来table长度

存放数据到链表中时,通过CAS保证并发性

hashEntry:

load-factor:加载因子

ConcurrentHashmap的key值不能为null

初始化一个ConcurrentHashmap时需要申明三个参数,并初始化一个segment[0],算出了每个segment内部数组的长度
当存放对象进ConcurrentHashmap时,会对key进行hash运算得到segment的数组索引位置,如果此位置为null,则先需要new Segment[],调用segment的put方法,再将key-value放进segment对象内部的hashEntry数组的位置,采用头插法
jdk1.8下的ConcurrentHashmap
Node[]:

put方法:hash算法获取索引下标位置,利用CAS的方法判断位置,加上自旋锁的机制,直到CAS将元素存入成功

存放数据到链表中时,通过synchronized保证并发性

五 异常机制
1 Exception/Error的区别
两者都是继承自Throwable,在Java中只有Throwable类型的实例才可以被抛出或者捕获。

Error指正常情况下不太可能出现的情况,绝大部分的Error会导致程序崩溃,处于非正常的不可恢复的状态,如OOM、StackOverflowError。是程序中不应该试图捕获的严重问题。

Exception是程序正常运行中可以预料的意外情况,可以捕获并处理。

2 运行时异常与一般异常的区别
受检查异常:在编译时被强制检查的异常。在方法的声明中声明的异常。如ClassNotFoundException、IOException

不受检查异常:不受检查异常通常是在编码中可以避免的逻辑错误,根据需求来判断如何处理,不需要再编译器强制要求。

3 写出几种常见的运行时异常
运行时异常RuntimeException是所有不受检查异常的基类

NullPointerException

ClassCastException

NumberFormatException

IndexOutOfBoundException

4 ClassNotFoundException与NoClassDefFoundError区别
ClassNotFoundException:当应用程序运行的过程中尝试使用类加载器去加载class文件时,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当使用Class.forName()时会出现这种异常情况,比如加载JDBC驱动类

NoClassDefFoundError:并不需要应用程序去关心catch问题。当JVM加载一个类时,如果这个类在编译时可用,但在运行时找不到这个类的定义时,JVM就会抛出该错误。一般情况下,在使用框架时,如果框架组件一来了某个包,而没有导入,就会出现这个问题,或者由于版本问题导致

5 throw/throws的区别
throw是在方法体内,手动抛出的异常,一次只能抛出一个异常对象,由方法体处理,如果方法体内不处理,则需要在方法上声明throws;

throws是在方法声明时,表明该方法可能产生的所有异常,不做任何处理直接向上层传。

6 谈谈对异常的了解
尽量不要捕获类似Exception这样的通用异常

不要生吞异常,即catch到之后不处理

在实际产品中,使用日志

try-catch会产生额外的性能开销,不要一个大的try包住大段代码。

六 新特性
1 Java8函数式接口
1.1 java.util.function
1.2 java内置核心四大函数式接口
image-20200907200926476

// 1 功能型接口 Function<T,R> method: R apply(T t)
Function<String,Integer> function = s -> {return s.length();};
sout(function.apply(“abc”));
//=>输出结果 : 3

//2 判断型接口 Predicate method: boolean test(T t)
Predicate predicate = s -> {return s.isEmpty();};
sout(predicate.test(“abc”);
//
=>输出结果 : false

//3 消费型接口 Consumer method: void accept(T t)
Consumer consumer = s -> {sout(s);};
consumer.accept(“abc”);
//===>输出结果 : abc

//4 供给型接口 Supplier method: T get()
Supplier supplier = () - > {return “abc”;};
sout(supplier.get());
//===>输出结果 : abc
2 流式计算(Stream)
2.1 是啥?
流式数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

2.2 为啥?
特点:

Stream自己不会存储元素;

Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream;

Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

2.3 咋玩?
分几个阶段:

创建一个Stream:一个数据源(数组、集合)

中间操作:一个中间操作,处理数据源数据;

终止操作:一个终止操作,执行中间操作链,产生结果

七 网络编程基础(TCP/UDP)
1 TCP三次握手
标志位

【SYN包:请求建立连接的数据包,SYN=1表示要建立连接】

【ACK包:回应数据包,表示接收到了对方的某个数据包,仅当ACK=1时,确认号字段才有效】

【seq序列号:用来标记数据包的顺序】

【ack确认号:表示序号为确认号减去1的数据包以及以前的所有数据包已经正确接收,也就是说他相当于下一个准备接收的字节的序号】

三次握手过程:

第一次握手:建立连接时,客户端发送数据包,标志位SYN=1,随机seq=x到服务器;

第二次握手:服务器接收SYN=1包,知道客户端要建立连接,返回SYN=1和ACK=1、ack=x+1、和随机seq=y;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK=1,ack=y+1。

三次握手目的是确认客户端和服务器端建立了正常的连接,可以发送数据。

2 TCP 与 UDP的区别:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付;
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP对系统资源要求较多,UDP对系统资源要求较少。

3 为什么UDP有时比TCP更有优势?
UDP以其简单、传输快的优势,在越来越多场景下取代了TCP,如实时游戏。
(1)网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。
(2)TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于TCP内置的系统协议栈中,极难对其进行改进。

4 UDP和TCP编程区别
步骤区别

TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();
  4、开启监听,用函数listen();
  5、接收客户端上来的连接,用函数accept();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接; closesocket(SocketListen);closesocket(SocketWaiter);
  8、关闭监听;

TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind(); * 可选
  4、设置要连接的对方的IP地址和端口等属性;
  5、连接服务器,用函数connect();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接;

UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();
  4、循环接收数据,用函数recvfrom();
  5、关闭网络连接;

UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind(); * 可选
  4、设置对方的IP地址和端口等属性;
  5、发送数据,用函数sendto();
  6、关闭网络连接;

传输协议区别

TCP协议是传输控制协议,提供可靠无差错的数据传输,效率较低—【点对点打电话,必须回复】

UDP协议是用户数据报协议,不可靠的数据传输,效率高-------【发信息,发出即可】

5 HTTP协议,请求格式,响应格式
请求格式 : 请求行、请求头、空白行和请求体;

响应格式 : 响应行、响应头、空白行和响应体。

6 HTTP状态码
HTTP状态码也是有规律的

1** 请求未成功

2** 请求成功、表示成功处理了请求的状态代码。

3** 请求被重定向、表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

4** 请求错误这些状态代码表示请求可能出错,妨碍了服务器的处理。

5**(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

200 OK 资源请求成功
301 永久重定向
302 暂时重定向
400 客户端请求语法错误,服务器无法解析
401 没有权限的用户,访问了需要权限的资源
403 服务器收到请求,但是拒绝服务
404 请求的资源不存在(url错误,资源不存在)
500 服务器内部错误
503 服务器当前不可用,当前不能处理客户端请求
7 RESTful
REST:Representational State Transfer(表象层状态转变)

8 计算机7层网络结构
应用层----最高层

表示层

会话层

传输层

网络层

数据链路层

物理层----最低层

9 URL是啥?有哪些组成部分
统一资源定位符

<传输协议>://<主机名>:<端口号>/<资源地址>

八 反射机制
1 反射的概念
编写代码时不确定要创建什么类型的对象,也不确定调用什么方法,通过在运行时传递参数决定,即动态编程技术----反射机制

动态创建对象动态调用方法的机制

2 通过反射获取类的私有属性
反向获取class对象:Class.forName(“xxx全类名”)

将对象实例化:newInstance()

获取对象所有的私有属性数组:getDeclaredFields()

设置私有属性允许访问:setAccessible(true)

遍历得到属性值

九 设计模式
1 设计模式的分类:

  • 创建型:单例、工厂方法、抽象工厂、建造者、原型,共五种
  • 结构型:装饰器、代理、适配器、外观、桥接、组合、享元,共七种
  • 行为型:策略、模板方法、观察者、迭代子、责任链、命令、备忘录、状态、访问者、中介者、解释器,共十一种

1.1 单例模式:
定义:保证一个类只有一个实例,并且提供给一个访问该全局访问点

应用场景:

网站的计数器

应用程序的日志应用

多线程的线程池应用

Windows的任务管理器

Windows的回收站

优点

单例,确保所有的对象都访问一个实例

具有伸缩性,类自己控制实例化进程,因此可以改变实例化进程

提供对唯一实例的受控访问

内存只有一个对象,可以节约资源

允许可变数目的实例

避免对共享资源的多重占用

缺点

不适合变化的对象

没有抽象层,不易扩展

职责过重,违反了单一职责原则

注意事项

不能使用反射构造单例,反射会创建新实例

懒汉单例模式需要注意线程安全问题

饿汉式跟懒汉式单例构造方法都是私有的,不能被继承;登记者模式的单例可以被继承

单例创建方式

饿汉式:类初始化是,就立即加载该对象,线程安全,调用效率高

懒汉式:类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具有懒加载功能

静态内部类:结合了懒汉式跟饿汉式各自优点,真正需要对象时加载,加载类是线程安全的

枚举单例:实现简单,调用效率高。本省枚举式JVM 提供了保障。可以避免通过反射跟反序列化的漏洞。但是没有延迟加载

双重检测锁方式:(JVM 指令重排序,可能会初始化多次,不推荐使用)

单例创建方式的选择

不需要延迟加载,就使用枚举或者饿汉式,枚举性能好于饿汉式

需要延迟加载,使静态内部类或者懒汉式,及静态内部类好于懒汉式

无特殊要求,尽量使用饿汉式

1.2 工厂模式
定义:提供一种创建对象的最佳方式。创建对象时不会对客户端暴露创建逻辑,通过一个共同接口来指向创建的新对象。实现了创建者与调用者的分离。主要分为简单工厂、工厂方法、抽象工厂三种

好处:

代替new操作

降低程序耦合,方便维护

实现类、创建对象统一管理控制,将调用者与实现类解耦

Spring开发中的工厂模式

Spring IOC

Spring IOC容器创建Bean就是使用的工厂设计模式

Spring中无论是XML配置,还是配置类,还是注解配置进行创建Bean,基本都是使用简单工厂来创建的

当容器拿到BeanName和 class 类型后,通过动态反射机制创建具体的对象,最后将对象放进Map(容器)中

为啥Spring IOC 要使用工厂模式?

解耦类与类、方法与方法的耦合

减少new操作,降低代码重复量,减低耦合

由工厂创建Bean,需要时直接找工厂即可,不需要管其他的逻辑处理

IOC 容器工厂有个静态Map 集合,生产出来的对象就存进Map 集合中,保证了实力不会重复影响程序效率

//

工厂模式分类

简单工厂

生产同一等级结构中的任意产品(不支持扩展增加产品)

工厂方法

生产同一等级结构中的固定产品(支持拓展增加产品)

抽象工厂

生产不同产品族的全部产品(不支持拓展增加产品,支持增加产品族)

1.3 代理模式:
定义:

通过代理控制对象访问,可以在对象调用方法之前,调用方法之后去处理/添加新功能(AOP)

代理在原代码乃至原本的业务代码都不改变的情况下,直接在业务流程中切入新代码,增加新功能

应用场景

Spring AOP 、 日志处理 、 异常处理、事务控制、权限控制等

分类

静态代理

动态代理(JDK自带动态代理)

Cglib、javaassist(字节码操作库)

三种代理的区别

静态代理:简单代理模式,是动态代理的理论基础

JDK 动态代理:使用反射完成代理。

需要有顶层接口,Mybatis的Mapper文件是单例

Cglib动态代理:使用反射完成代理。

可以直接代理类(JDK 不行),使用字节码技术,不能对final 类进行继承。(需要导入jar包)

  • 静态代理:

    • 程序员创建或工具生成代理类的源码,再编译代理类。
    • 静态:程序运行前就已经存在代理类的字节码文件,代理类与委托类的关系在运行前就已经确定了
    • 缺点:每个需要代理的对象都需要自己重复编写代理
    • 优点:可以面向实际对象或者接口的方式实现代理
  • JDK 动态代理
    -也叫JDK代理、接口代理
    -动态代理对象,利用JDK的API,动态的在内存中构建代理对象(根据被代理接口来动态生成代理类的class文件,并加载运行的过程),即动态代理

    • 缺点:必须面向接口、目标业务类必须实现接口
    • 优点:不用关心代理类,只需要在运行阶段才指定代理哪个对象
  • Cglib 动态代理
    -原理:利用asm 开源包,对代理对象类的class文件加载进来,通过修改字节码文件生成子类来处理
    -定义:跟JDK一样利用反射。不同在于Cglib可以直接代理类。Cglib动态代理底层是字节码技术,不能对final类进行继承(需要导入jar包)

1.4 建造者模式
1.5 装饰器模式
给一个对象动态的增加一些新功能,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例;

1.6 模板方法模式:
定义:一个抽象类中封装一个固定流程,流程中的具体步骤可由不同子类进行不同的实现,通过抽象类让固定流程产生不同的结果

应用场景:

数据库访问的封装

Junit单元测试

Servlet关于doGet / doPost 方法的调用

1.7 外观模式
定义:

也叫门面模式,隐藏系统的复杂性,并向客户端提供一个可以访问的接口

向现有的系统添加一个接口,用这个接口来隐藏实际的系统给的复杂性

对外只是一个接口,内部很多的复杂接口逻辑已经被实现

1.8 原型模式
定义

克隆

提供一个样板实例,原型可以定制。

应用场景

类初始化过程需要消耗非常多的资源,包括数据、硬件资源等。可以通过原型拷贝避免消耗

通过new产生一个对象需要非常复杂的数据准备或者权限,使用原型模式

一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以使用原型模式拷贝多个对象;即保护性拷贝

Spring框架的多例就是原型

使用方式

实现Cloneable接口。该接口作用是:在运行时通知虚拟机可以安全的在实现了此接口的类上使用clone方法。在JVM中,只有实现了Clone接口的类才可以被拷贝,否则会报CloneNotSupportException异常

重写Object类的clone方法。作用是返回对象的一个拷贝,但是作用域是protected类型的,一般的类无法调用,因此Prototype类需要将clone方法的作用域改为public类型

浅复制与深复制

浅复制:只是拷贝了基本类型的数据,对于引用数据类型数据,只拷贝一份引用地址

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象

1.9 策略模式
定义:

定义了一堆算法 或 逻辑 或 相同意义的操作,并将每一个算法、逻辑、操作封装起来,而且使他们还可以互相替换

目的是为了简化 if…else… 所带来的的复杂和难以维护

应用场景

针对一组算法或逻辑,将每一个算法或者逻辑封装到具有共同接口的独立类中,从而使得他们之间可以互相替换

优点:

算法可以自由切换

避免使用多重条件判断

扩展性良好

缺点:

策略类会增多

所有策略类都需要对外暴露

1.10 观察者模式
定义

行为性模型:关注系统中对象之间的相互交互,解决系统在运行时对象之间的互相通信和协作,进一步明确对象的职责

观察者模式,又叫发布-订阅模式,定义对象之间一对多的依赖关系,使得一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新

实现方式

推:每次都会把通知以广播方式发送给所有观察者,所有观察者只能被动接收

拉:观察者只要知道有情况即可,至于什么时候获取内容,获取什么内容,都可以自主决定

应用场景

关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系,事件多级触发场景

跨系统的消息交换场景,如消息队列,事件总线的处理机制

观察者设计模式的关键角色

subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合中,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象;

ConcreteSubject:具体主题(具体观察者),该角色将有管状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知;

Observer:抽象观察者,是观察者的抽象类,定义了一个更新接口,使得在得到主题更改通知时更新自己;

ConcreteObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

优缺点

优点

降低了目标与观察者之间的耦合关系,两者之间时抽象耦合关系

被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】

缺点

如果观察者非常多的话,那么所有观察者收到被观察者发送的通知会耗时

如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

2 设计模式的六大原则
开放封闭原则

尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码

里氏代换原则

使用的基类可以在任何地方使用继承的父类,完美的替换基类

依赖倒转原则

面向接口编程,依赖抽象

接口隔离原则

使用多个隔离接口,比使用单个接口要好。降低类之间的耦合度

迪米特法则(最少知道原则)

一个对象应当对其他对象尽可能少的了解,简称类间解耦

单一职责原则

一个方法负责一件事情

十 MySQL
考点

多表联查(必考)

聚合函数和分组(重点)

子查询

1 inner join / left join / right join三者区别?
inner join查询的是交集,左右两表数据对应存在;

left join查询的是包左不包右,左表数据全部显示,如果右表没有对应的则显示null;

right join与left join相反。

2 where / having的区别?
where在group by前面执行,having 在之后执行

where后面不可以跟聚合函数(count,sum,avg,max…),having可以

asc—升序(默认) desc–降序

4 行转列
select name,
max(CASE when 科目=语文 then 成绩 end )语文,
max(CASE when 科目=数学 then 成绩 end )数学,
from score
group by 姓名

5 脏读、不可重复读、幻读?
脏读(读未提交数据)

事务A读取前,期间事务B对数据做了修改,事务A开始读取的是事务B刚修改的数据,基于此事务A继续操作,但是事务B因为出错进行回滚事务,导致事务A读取的数据不纯粹,此时事务A已经基于开始读到的数据进行了操作,即产生了脏读。

经典案例:A转账,B取款,B取一半又回滚了,导致最显示的最终余额比实际少了。

不可重复读(前后多次读取,数据内容不一致)

事务A读取时间较长,中间事务B对某条数据刚好做了修改,导致事务A两次读取的数据不一致,即系统无法读取重复的数据。

幻读(前后多次读取,数据总量不一致)

事务A需要多次读取数据的总量,中间事务B对数据进行了插入,导致事务A前后读取的数据总数变化,产生幻读。

6 事务隔离级别
读未提交:无法防止任何并发问题

读已提交:可以防止脏读(Oracle默认隔离级别)

可重复读:可以防止脏读,不可重复读(MySQL默认隔离级别)

串行化:可以防止脏读、不可重复读、幻读(效率低,不建议使用)

7 嵌套查询和嵌套结果的区别?
嵌套结果: 是一次查询 然后映射到对应属性

嵌套查询是多次查询: 有N+1问题 解决方法就是使用懒加载和嵌套结果

8 事务四大特性(ACID)
原子性(Atomicity):

指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

一致性(Consistency):

指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

隔离性(Isolation):

当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

持久性(Durability):

指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

9 分页limit偏移量过大优化和删除重复数据中id小的数据
分页limit偏移量过大,会造成查询速度变慢
如:select * from user_address limit 100,10 //查询用时0.011S
select * from user_address limit 100000,10 //查询用时0.618S
结果可看的出来,偏移量大起来时查询速度就会变慢,那么优化一下写法。

select * from user_address where id >= (select id from user_address order by id limit 100000,1) limit 10 //查询用时0.068S
但这种写法适用于偏移量大的结果,实际使用要根据业务场景选择相应策略。


######查找重复的proId及个数
select proId,count()
from test
group by proId
having count(
)>1

######删除重复数据中id最小的那条
1创建临时表
create temporary table temp as
select min(id) as minid from test group by proId;

delete from test where id in (select id from temp);

2直接在原表操作
delete from test where id in (
select min(id) from test group by proId
);
执行报错:更新数据时使用了查询,而查询数据又做了更新的条件,mysql不支持这种方式.

可以再加一层封装,如下:
delete from test
where id in (
select minid from (
select min(id) minid from test group by proId
)
)
10 MySQL的两种引擎(Innodb/MyISAM)的区别
Innodb:

支持数据库事务,ACID,实现了sql标准的四种隔离级别

支持行锁和外键约束,写操作(insert、update)时不需要锁定全表,高并发时,效率高

不保存表的行数,select count(*)时需要扫描全表,效率低

设计目标是处理大容量数据库系统,当MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。

使用数据库事务时,首选Innodb。

当写操作多于读操作时选择Innodb引擎

MyISAM:

MyISAM是MySQL默认的引擎,但是不支持数据库事务

不支持行级锁跟外键,写操作(insert、update)时需要锁定全表,效率低

保存表的行数,select count(*)时直接读取,不需要扫描全表,效率高

当读操作远远多于写操作时选择MyISAM引擎

应用场景:

1、MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM
2、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB
11 MySql的底层索引区别
Innodb

B+Tree索引结构

使用聚集索引

Innodb数据文件本身就是索引文件,索引的key是数据表的主键

InnoDB的辅助索引data域存储相应记录主键的值而不是地址

MyISAM

B+Tree索引结构,叶节点的data域存放的是数据记录的地址(即索引文件与数据文件分离)

主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复

辅助索引也是B+Tree,data域保存数据记录的地址

追问:为什么不建议使用过长的字段作为主键?
答案:因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大

追问:索引为啥用B+树而不使用红黑树?
答:
1.B+树特点是只在叶子节点上存放数据,减少磁盘IO次数;
B+数叶子节点之间有指针连接,遍历叶子就能获取全部数据;
B+数一个节点可以存储多个值,深度小,查询次数少
2.红黑树是二叉树,叶子节点之间没有指针,因此一个节点只能取出一个值,深度大,查询次数多
12 order by 后面有多个字段时,排序顺序
order by 多个字段时,用逗号分隔每一个字段,如果字段不指明排序方式,默认是增序。

排序的方法是首先按照第一个字段排序,如果第一个字段相同的,再按照第二个字段来再次排序,

从而产生排序结果。

13 索引问题解析
是啥?

帮助MySQL高效获取数据的数据结构

能干啥?

当表中数据量越来越大时,索引对于性能的影响愈发重要,通过索引可以提升查询性能

索引分类

按存储结构划分

Hash索引:基于hash表实现,只有精确匹配索引所有列的查询才有效,对于每行数据,存储引擎都会对所有引擎列计算一个hash码,并且Hash索引将所有的hash码存在索引中,同时在索引表中保存指向每个数据行的指针;

B+tree索引:数据库索引的的存储结构。数据都在叶子节点上,并添加了顺序访问指针,每个叶子节点都指向了相邻的叶子节点的地址

B-Tree索引:加快数据访问。不需要全表扫描获取数据,数据分布在各个节点中

按应用层次划分

普通索引:一个索引只包含单列,一个表可以有多个单列索引

create index <索引名> on tablename(字段名);
alter table tablename add index [索引名] (字段名);
create table tablename ([…],index[索引名] (字段名));
唯一索引:索引列的值必须唯一,但允许有空值

create unique index <索引名> on tablename(字段名);
alter table tablename add unique index [索引名] (字段名);
create table tablename ([…],unique index [索引名] (字段名));
复合索引:多列值组成一个索引,专门用于组合搜索,效率大于索引合并

create index <索引名> on tablename(字段名1,字段名2…);
alter table tablename add index [索引名] (字段名1,字段名2…);
create table tablename ([…],index[索引名] (字段名1,字段名2…));
按物流顺序与键值逻辑顺序

聚集索引:不是一种单独的索引类型,而是一种数据存储方式。一般都不能修改,修改后需要重写排序。----InnoDB底层用的是B+Tree结构存储

非聚集索引—MyISAM

索引结构为啥使用B+Tree,而不是其他的?

BTree:因为B树不管是叶子节点还是非叶子节点都会保存数据,导致在非叶子节点中能保存的指针数量变少,指针少的情况下,要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低

Hash:虽然可以快速定位,但是没有顺序,IO复杂度高

二叉树:数的高度不均匀,不能自平衡,查询效率与树高有关,并且IO代价大

红黑树:树的高度随着数据量的增加而增加,IO代价高

为啥官方建议自增长主键作为索引?

结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且减少数据的移动,每次插入都是插入到最后,总之就是减少分裂和移动的频率

索引在什么情况下会失效?

条件中有or关键字

对于多列索引,不是使用的第一部分,则不会使用索引

如果列类型时字符串,那一定要在条件中将数据使用引号引用起来,否额不会使用索引

如果使用全表扫描要比使用索引快,则不会使用索引

唯一索引比普通索引快?为啥?

查询效率上比较:效率相差不大

普通索引查询流程

根据索引查找到第一条满足条件的记录

找到下一条记录

判断下一行是否满足条件

满足重复步骤2 ,不满足则返回条件的结果集

唯一索引查询流程

根据索引找到第一条满足条件的记录

返回该行记录

从更新角度看,普通索引可以利用change buffer 更新操作的性能比唯一索引要更好

14 数据库三范式
建表符合数据库的三范式(时间换时间)

第一范式:【原子性】列具有原子性,拆分到不能拆分为止

第二范式:【唯一性】在满足第一范式基础上,非主键列完全依赖于主键,而不能依赖于主键的一部分

第三范式:【数据冗余】在第二范式基础上,非主键列只依赖于主键,不依赖与其他非主键

反三范式(空间换时间)

通过添加冗余代码或重复数据提高数据库的读性能

15 数据库查询优化
对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描

尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。

尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描

in 和 not in 也要慎用,否则会导致全表扫描

select id from t where name like ‘%abc%’
尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描

不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引

16 并发事务会带来哪些问题?
脏读:一个事务读取到了另一个事务尚未提交的数据

不可重复读:一个事务中两次读取的数据内容不一致,要求是在一个事务中多次读取的结果是一致的。

进行update操作时引发的问题

幻读:一个事务中,某一次的select操得到的结果所表征的数据状态,无法支撑后续的操作。

查询得到的数据状态不准确,导致幻读。

17 数据库的锁机制
按数据的操作类型分

读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。

写锁(排他锁):当前写操作没有完成前,会阻断其他写锁和读锁。

按照操作的粒度分

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高;

页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般;

按照操作性能分

乐观锁:一般实现方式时对记录数据版本进行比对,在数据更新提交时才会进行冲突检测,如果出现冲突了,则提示错误信息;

悲观锁:在对一条数据修改时,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁时悲观锁的不同实现,但都属于悲观锁范畴。

十一 JDBC
1 Statement与PreparedStatement区别
前者会出现SQL注入问题,使用预编译处理可以防止SQL注入

前者在执行SQL时才会编译,后者会进行预编译,使用占位符,提高批量操作执行效率

第一次执行SQL时,前者效率高,后者因为需要预编译,耗费资源

十二 JavaWeb
1 Servlet生命周期
加载–> 实例化 --> 初始化 --> 服务 --> 销毁

Web容器加载Servlet类并实例化(默认延迟,一次)

运行init方法进行初始化(一次)

用户请求该Servlet,请求到达服务器时,运行其service方法(每次)

service方法运行与请求对应的 doGet() | doPost() 方法(每次)

销毁实例时调用destroy方法(一次)

image-20200913152948926

image-20200913153132111

2 转发(forward)和重定向(redirect)区别
本质区别:转发是服务器行为,重定向是客户端行为

转发地址栏的URL不会改变,重定向URL会改变

转发是一次请求,重定向是二次请求

转发不可以访问其他项目工程,重定向可以访问

转发过程发生在同一个web应用,可以共享转发前的Request对象,重定向会将前面的Request对象销毁,创建新的对象发出请求

重定向成功的状态码:302

3 数据库连接池的工作机制
服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接;

客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其标为忙;

如果当前没有空闲连接,池驱动程序会新建一定数量的连接,新建连接的数量由配置参数决定;

当使用池连接调用完成后,池驱动程序将此连接标为空闲,其他调用就可以继续使用这个池连接了。

4 JSP内置对象
request:向客户端请求数据

response:封装了JSP产生的响应,然后被发送到客户端以响应客户的请求

pageContext:为JSP页面包装页面的上下文,管理对属于JSP中特殊可见部分中已经命名对象的访问

session:用来保存每个用户的信息,以便跟踪每个用户操作状态

application:应用程序对象

out:向客户端输出数据

config:表示Servlet的配置,当一个Servlet初始化时,容器把某些信息通过此对象传递给这个Servlet

page:JSP实现类的实例,是JSP本身,通过这个可以对它进行访问

exception:反应运行的异常

5 Get和Post区别
get请求回退是无害的,post请求回退会重新发送请求

get请求URL可以保存书签,post不可以

get请求只能进行url编码,post请求支持多种编码方式

get请求会被完整保存在浏览记录里,post不会呗保存

get请求提交的数据大小(一般是1024字节),post请求在请求体中提交数据,取决与服务器

get不安全,数据暴露在url中,post较安全

image-20201010110225861

Get将表单数据按照param=value的形式,添加到action指向的URL页面,并且两者使用?连接,而各个变量之间使用&连接;Post是将表单中的数据放在form数据体中,按照变量和值相对应的方式,传递到action指向的URL

Get是不安全的,因为在传输过程中,数据被放在URL中;Post的所有操作对用户来说都是不可见的,是安全的;

Get传输的数据量小,这主要因为受URL长度限制;而Post可以传输大量数据,所以在文件上传只能使用Post;

Get限制Form表单的数据集必须为ASCII字符,而Posst支持整个ISO10646字符集。

6 JSP四大域对象
pageContext:代表与一个页面相关的对象和属性;

request:代表与Web客户机发出的一个请求相关的对象和属性,一个请求可能跨越多个页面,涉及多个Web组件(由于forward指令和include动作的关系);

session:代表与用于某个Web客户机的一个用户体验相关的对象和属性。一个Web会话可以也经常会跨越多个客户机请求;

application:代表与整个Web应用程序相关的对象和属性。实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。

7 MVC设计思想(分层控制)
基于Java的Web应用系统采用MVC架构模式,即model、view、controller分离设计;

Model:负责业务逻辑的模块,每一种处理一个模块;

View:负责页面展示,显示Model处理结果给用户,主要实现数据到页面转换过程;

Controller:负责每个请求的分发,把Form表单数据传递给Model处理,把处理的数据传递给View;

注意:SpringMVC 是一种控制层的框架,与MVC设计思想不一样;

8 Session和Cookie区别
session保存在服务器,客户端不知道其中的信息;cookie保存在客户端,服务器能够知道其中的信息;

session中保存的是对象,cookie中保存的是字符串;

session不能设置路径,同一个用户在访问一个网站期间,所有的session在任何一个地方都可以访问到;而cookie中如果设置了路径参数,name同一个网站不同路径下的cookie互相是访问不到的;

session需要借助cookie才能正常,如果客户端完全禁止cookie,那么session将失效。

服务器关闭Session会消失,cookie不会

session存储容量没有上限,cookie只有4K

9 如何知道是哪一个客户端的机器正在请求你的Servlet?
答案:ServletRequest类可以找出客户端机器的IP地址或者是主机名。

getRemoteAddr()方法获取客户端主机的IP地址,

getRemoteHost()可以获取主机名,

getRemotePort()方法获取客户端口。

10 tomcat服务器是如何创建Servlet实例的?
答案:load-on-startup标签

A.先到缓存中寻找有没有这个对象

如果没有:

1、通过反射去创建相应的对象(执行构造方法)

2、tomcat会把对象存放到缓存中

3、执行初始化方法init

如果有该对象,直接获取到这个对象

B. 执行服务方法

C.返回响应的数据到客户端(浏览器)

11 简述request.getAttribute()与requset.getParameter()的区别
getParameter 是用来接受用post个get方法传递过来的参数;

getAttribute是获取属性的,获取Object对象;

request.setAttribute() 和 getAttribute() 方法传递的数据只会存在于Web容器内部。

12 监听器的作用与用法
监听Servlet容器产生的事件并进行相应的处理。主要是监听request、application、session的事件

容器的事件分类:

生命周期相关事件

属性状态相关事件

存值状态相关事件

底层原理:采用接口回调方式实现

13 JSP的静态包含与动态包含的区别
静态包含是编译时包含,若包含的页面不存在就会报错,而且两个页面的contentType属性应保持一致,因为两个页面会合二为一,编译成一个class文件

动态包含是运行时包含,可以向被包含页面传递参数,包含页面和被包含页面是独立的,会编译成两个class文件,若被包含的页面不存在,不会编译报错,不影响其他部分执行

14 Servlet是单例还是多例?
单例多线程

好处

单例减少了servlet的实例化开销

通过线程响应多个请求,提高请求的响应时间

缺点:

高并发时会有线程安全问题,主要由于成员变量引起的,因此在servlet实例中应避免使用成员变量;

如果无法避免,要使用同步来保护要使用的变量,为了保证系统的最佳性能,应同步可用性最小的代码路径。

15 JSP与servlet区别
JSP是前台展示,也是servlet的一种简化,编译之后是一种java类

servlet是做后台代码的逻辑控制,没有9大内置对象

《面试题汇总》之进阶篇
一 ORM – MyBatis
0 Mybatis简介
优点

基于SQL编程,相当灵活,将程序代码与SQL解耦,支持动态SQL

相比传统JDBC,减少了大量代码

底层是JDBC,因此能与各种数据兼容

与Spring框架完美集成

提供映射标签,支持对象与数据库的ORM,提供对象关系映射标签,支持对象关系组件维护

缺点

SQL编写量大

SQL依赖于数据库,可移植性差

适应场景

适合对性能要求高,需求灵活的互联网项目

如何获取自动生成的主键值?

insert方法总是返回一个int值,表示插入的行数
如果是自增长策略,自动生成的键值在方法执行完后被设置进参数对象中
可以在标签中配置
< insert id=‘insert’ usergeneratedkeys = “true” keyproperty=‘id’>
insert into names (name) values (#{name})

这样就把自动增长的键id赋值到user对象中,可以取出
在mapper中如果传递多个参数

使用#{0}、#{1}…分别对应第1,2…个参数

在接口方法中使用@Param(“username”)参数,在mapper中用#{username}对应接收

将多个参数封装成map集合,

动态SQL的作用?执行原理?常见的有哪些?

使用标签形式编写

执行原理是根据表达式的值,完成逻辑判断并动态拼接SQL功能

有九种SQL标签:trim|where|set|foreach|if|choose|when|otherwise|bind

Mybatis为半自动ORM映射工具,与全自动的区别?

Hibernate是全自动,不需要手动编写SQL,直接根据对象关系模型获取数据;

Mybatis需要手动编写SQL,所以是半自动的

Mybatis实现一对一查询的方式?怎么操作?

第一种:联合查询。多表联合查询,只查询一次,使用resultMap里面配置association节点配置实现

第二种:嵌套查询。根据表的结果外键id,再去另一个表中也是使用association配置,但另一个表是通过select属性配置的

Mybatis实现一对多的方式?怎么操作?

第一种:联合查询。多表联合查询,只查询一次,使用resultMap里面配置collection节点配置实现

第二种:嵌套查询。根据表的结果外键id,再去另一个表中也是使用collection配置,但另一个表是通过select属性配置的

Mybatis接口绑定实现方式?

第一种:注解绑定。在接口方法上加上@Select,@Update等注解,包含sql语句来绑定

第二种:xml配置。在xml映射文件的namespace必须是接口的全路径名

Mybatis插件运行原理?手动编写插件?

执行原理:可以编写parameterHandler、ResultSetHandler、StatementHandler、Executor四种接口插件,使用JDK动态代理,为需要拦截的接口生成代理对象实现拦截功能,当执行这四种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,只会拦截指定的需要拦截的方法

插件编写:实现Mybatis的Interceptor接口并复写intercept()方法,然后在插件编写注解,指定要拦截哪一个接口的哪些方法即可,配置文件中需要配置编写的插件

存储过程与函数的区别

存储过程关键字是procedure,函数关键字是function

函数必须有返回值,存储过程没有返回值,但是有传出参数

函数注重结果,存储过程注重过程

函数可以在select语句中直接使用,存储过程不能

1 $ 和# 区别
#{ }:

预编译处理(带引号,能避免sql注入)

“#{ } ” 被解析为一个参数占位符 “?”,调用PreparedStatement的set方法来赋值

	eg:(select * from user where name = #{name}) 

	解析为:(select * from user where name = ?)

${ }:字符串替换(不带引号,可能引发sql注入)

	eg:(select * from user where name = ${name})

	解析为:(select * from user where name = Jay)

能使用#{} 的地方就用#{},相同的预编译sql可以重复利用,能避免SQL注入;

为啥还要保留${}?

原因:

表名、字段名需要动态替换的时候就用 , 其 他 情 况 不 用 {} ,其他情况不用 ,可能会被SQL注入。

order by 后面可以用于排序

sql注入问题:

name = ‘jack’ or 1=1

若使用${}会引发sql注入:select * from user where name = ‘jack’ or 1=1 ------- 对替换的值不加引号

若使用#{}不会引发sql注入:select * from user where name = " ‘jack’ or 1=1 " -----对替换的值加了引号

2 Mybatis缓存与懒加载
2.1 缓存
一级缓存:

SqlSession级别缓存,默认开启;

参数与SQL一致情况下,使用同一个SQLSession对象调用一次Mapper方法,只执行一次SQL,结果会被存在一级缓存中,以后再查询时,只要session没有flush或者close,就可以直接获取缓存中的数据,不会再次查询数据库

基于PerpetualCache的HashMap本地缓存,当session flush或者close之后,该Session中的所有Cache就清空;

二级缓存:

nameSpace级别缓存,(跨sqlSession)的缓存,默认不开启;

要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可以用来保存对象状态),可以在映射文件中配置

与一级机制相同,默认采用perpetualCache,HashMap存储,不同在于作用域是Mapper(NameSpace),并且可以自定义存储源,如Ehcache

2 .2 懒加载
局部延迟加载即懒加载

就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。

  • 优点:

先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表

速度要快。

  • 缺点:

因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时

间,所以可能造成用户等待时间变长,造成用户体验下降。

fetchType=“lazy” 懒加载策略
fetchType=“eager” 立即加载策略

association和collection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策略。
例如:

配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的
equals、clone、hashCode、toString方法时也会触发关联对象的查询。
我们可以在配置文件中使用lazyLoadTriggerMethods配置项覆盖掉上面四个方法。




全局懒加载

在Mybatis的核心配置文件中可以使用setting标签修改全局的加载策略




延迟加载的原理

使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()发现a.getB()是null,就单独发送事先保存好的查询关联B对象的SQL,把B查询出来,然后调用a.setB(),于是a对象的对象b属性就有值了,接着完成a.getB().getName()方法的调用。即延迟加载原理
3 MyBatis里面有哪些执行器?
ExecutorType.SIMPLE

这个类型不做特殊的事情,它只为每个语句创建一个PreparedStatement。

ExecutorType.REUSE

这种类型将重复使用PreparedStatements。

ExecutorType.BATCH

这个类型批量更新,且必要地区别开其中的select 语句,确保动作易于理解。

4 嵌套查询与嵌套结果
嵌套结果: 是一次查询 然后映射到对应属性

嵌套查询是多次查询: 有N+1问题 解决方法就是使用懒加载和嵌套结果

5 ORM框架思想
Java是面向对象语言,对象模型,数据库是关系模型

Object Relation Mapping对象关系映射

查找跟插入数据为例:

传统的JDBC查询数据,需要先从查询的结构集ResultSet中读出数据,在封装成对象;将关系模型转换为对象模型;插入数据需要先将对象模型转换为关系模型

6 Hibernate与Mybatis对比
H是面向POJO的全ORM框架,而M是面向SQL的半ORM框架

H基本不需要编写SQL就可以通过映射关系来操作数据库,是全表映射;M需要提供Sql去操作POJO,灵活度高

管理系统Hibernate是主流,移动互联网时代,考虑性能,首选Mybatis,(因为可以优化SQL)

映射层(Mapper)的配置Hibernate不需要配置接口和SQL,Mybatis需要,因此工作量稍大;Hibernate提供了HQL(Hibernate Query Language)对POJO进行操作,

Hibernate多表查询时会有性能丢失,Mybatis可以自由书写SQL,支持动态SQL,支持存储过程

对于性能要求低的管理系统、ERP系统可以使用Hibernate;对于性能要求高、响应快、灵活的系统推荐Mybatis;

7 JDBC编程不足有哪些?Mybatis是如何解决的?
数据库连接创建,释放频繁造成系统资源浪费从而影响性能,如果使用数据库连接池可以解决此问题;

解决:在mybatis-config.xml中配置数据连接池,使用连接池管理数据库连接

Sql语句写在代码中会造成代码不易维护,实际应用sql变化较大,sql变动需要改变java代码

解决:将sql语句配置在XXXMapper.xml文件中与java代码分离

向sql语句传参比较麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应

解决:Mybatis自动将java对象映射到sql语句中

对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果将数据库记录封装成pojo对象,解析比较方便

Mybatis自动将sql执行结果映射到java对象

8 Mybatis接口绑定有几种方式
通过注解绑定:如@select

通过xml文件里面的sql绑定,namespace

ORM – Hibernate
1 基本概念跟CRUD方法的区别
Hibernate在调用configure方法时,会解析hibernate.cfg.xml文件,配置文件会指定有哪些映射文件xxx.hbm.xml文件

1.1 get与load区别
get方法:根据组件(整型Id)立刻从数据库获取数据

如果没有找到符合条件的记录,会返回null

返回的是实体类对象

在Hibernate3之前,get方法只在一级缓存中查找,如果没有找到则越过二级缓存,直接发出SQL查找数据库

Hibernate3开始,get方法也可以访问二级缓存,不再是只写不读

load方法:懒加载,延迟获取数据。

如果没有找到符合条件的记录,会抛异常

返回的是实体类对象的代理

load方法先在一级缓存中查找,如果没有找到则查找二级缓存,仍找不到则发出SQL查找数据库

1.2 save、update、merge、lock、saveOrUpdate、persist区别
瞬时态实例可以通过调用save、persist或者saveOrUpdate方法变成持久态

游离态可以通过调用update、saveOrUpdate、lock或者replicate变为持久态

save、persist会引发SQL的插入语句;update跟merge会引发SQL的更新语句

save与update的区别:

save是将瞬时态对象变成持久态,update是将游离态对象编程持久态

merge可以完成save跟update的功能,将新的状态合并到已有的持久化对象上或者创建新的持久化对象

persist与save区别:

persist方法把一个瞬时态的实例持久化,但并不保证标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时间;保证在一个事务外部被调用时并不触发一个插入语句

save需要保证返回标识符,因此立即执行插入语句,不管是事务内部还是外部

lock与update的区别:

update把一个已经更改过得脱管状态的对象变成持久状态

lock方法把一个没有更改过的脱管状态的对象变成持久状态

saveOrUpdate与merge方法区别:

根据Id和Version值来确定是save还是update,调用saveOrUpdate后对象时持久的

调用merge的对象还是脱管

、、、、、、、、、、、、、

对象的状态

瞬时(transient):数据库没有与之对应的数据,超过作用域会被JVM回收,一般new出来且与session没有关联的对象

持久(persistent):数据库有与之对应的数据,当前与session有关联,并且相关联的session没有关闭,事务没有提交:持久对象状态发生改变,在事务提交时会影响到数据库(Hibernate能检测到)

脱管/游离态(detached):数据库中有数据与之对应,当之前没有session与之关联;脱管对象发生改变,Hibernate不能检测到

2 Hibernate查询hql/criteria
get方法是面向表查询

HQL是面向对象查询的

image-20201009214605932

占位符?可以使用:xxx 来替代,对应的queryString(“xxx”,zzz)

setFirstResut(m) + setMaxResults(n) 可以用来分页(每页n条记录),具有可移植性

Criteria【条件查询】

eq:等于

Restrictions.eq(“name”,name)//等于
Restrictions.lt(“birthday”,birthday)//小于
gt:大于

lt:小于

支持setFirstResult() / setMaxResults() 分页功能

3 Hibernate中SessionFactory是线程安全吗?Session线程安全吗?
SessionFactory是线程安全的,一般只会在启动时构建,对于应用程序,最好将SessionFactory通过单例模式进行封装以便于访问。

Session是轻量级非线程安全的,线程间不能共享,表示与数据库进行交互的工作单元。session由SessionFactory创建,在任务完成后会被关闭。

解决session非线程安全的办法:

可以使用ThreadLocal将Session与当前线程绑定,同一个线程获得的都是同一个session,

Hibernate3中的SessionFactory的getCurrentSession()就可以办到

4 Session加载实体对象的过程
Session在调用数据库查询功能之前,首先会在一级缓存中通过实体类型和主键进行查找,

如果一级缓存查找命中且数据状态合法,则直接返回

如果一级缓存没有命中,Session会在当前NonExists记录中进行查找,如果NonExists中存在同样的查找条件,就返回null

如果一级缓存查找失败则查找二级缓存,二级缓存没有命中则直接返回

如果之前的查询 都没有命中,则发出SQL语句,如果查询未发现对应记录则将此次查询添加到Session的NonExist

二 Spring
1 Spring Bean生命周期
1.实例化 ----------------【构造器、工厂方法】
2.依赖注入---------------【setter属性】
3.检查Aware接口并设置依赖–【BeanNameAware、BeanFactoryAware】
4.前置处理器--------------【postProcessBeforeInitialization(Object bean,String name)】
5.初始化Bean-------------【init()】
6.后置处理器--------------【postProcessAfterInitialization(Object bean,String name)】
7.使用Bean---------------【使用Bean】
8.销毁Bean---------------【destroy()】
详细过程:

实例化Bean对象【构造方法或工厂方法】

通过setter设置Bean属性【依赖注入】

如果通过各种Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖。Aware接口具体包括BeanNameAware、BeanFactoryAware和ApplicationContextAware,分别注入Bean ID、Bean Factory或者ApplicationContext【检查Aware接口,并设置相关依赖】

如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID

如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身

如果实现BeanPostProcesser,调用BeanPostProcesser的前置方法postProcessBeforeInitialization【前置处理器】

调用Bean自身定义的init方法【初始化】

调用BeanPostProcesser的后置方法postProcessAfterInitialization【后置处理器】

使用Bean【使用Bean】

容器关闭之前,调用Bean的destroy方法【销毁Bean】

单例Bean的生命周期与容器一致,

  • 对象创建:当应用加载,创建容器时,对象就被创建了
  • 对象运行:只要容器在,对象一直活着
  • 对象销毁:当应用卸载,销毁容器时,对象就被销毁了

    多例Bean生命周期:
  • 对象创建:当使用对象时,创建新的对象实例
  • 对象运行:只要对象在使用中,就一直活着
  • 对象销毁:当对象长时间不用时,被 Java 的垃圾回收器回收了
    2 Spring Bean的作用域
    Spring有5个作用域,最基础的有两种:

singleton,这是Spring默认的作用域,每个IOC容器创建唯一一个Bean;

prototype,针对每个getBean请求,容器都会单独创建一个Bean实例;

在Web(WebApplicationContext中)容器还有三种作用域:

request,针对每个HTTP请求都创建单独的Bean实例;

session,同一个Session范围内使用一个Bean;

GlobalSession,用于Portlet容器中,提供了一个全局的HTTP session。

配置单例还是多例,在标签中设置scope属性即可

3 Spring中的 AOP / IOC / DI
AOP(Aspect Oriented Program):

Spring项目中的事务、安全、日志等功能都可以用AOP(aspect oriented program)技术

AOP Proxy底层基于JDK动态代理或者cglib字节码操作技术,运行时动态生成被调用类型的子类,并实例化代理对象,实际的方法会被代理给响应的代理对象。

核心概念:

Aspect切面,横跨多个类,Spring 框架创建Advisor来指代它,内部包括切入时机(Pointcut)和切入动作(Advice);

Joint Point,在Spring中只有方法可以作为切入点;

Advice,定义切面中采取的动作,知名要做什么,通过Before/After/Around指明什么时候做;

Pointcut,具体定义Aspect中使用那些Joint Point。

IOC(Inversion of Control):

控制反转,简单来说就是将全部对象的控制权交给了IoC容器;传统的应用程序中都是我们自己在对象中主动控制去获取依赖对象,反转则是让容器来创建以及注入依赖对象;

华为云,

Spring 的IoC和AOP口头解释:
IOC:是一个容器,可以通过工厂方法将对象存进容器或者取出来,不使用new关键字,达到了控制反转;

DI:依赖注入,得到对象,通过容器得到连接

AOP:把对象放进容器中,可以包装对象,修改源代码,前置增强/后置增强等,制造了一个切面

DI(Dependency Injection):

IOC的具体体现;

把业务层与持久层的依赖关系交给Spring来维护,即通过Spring框架把持久层对象传入业务层,无需自行去获取。

编写程序时,通过控制反转,把对象的创建交给了 Spring,但是代码中不可能出现没有依赖的情

况。IOC 解耦只是降低他们的依赖关系,但不会消除。例如:业务层仍会调用持久层的方法。

4 Spring动态代理
Spring在运行期,生成动态代理对象,不需要特殊的编译器

AOP是基于动态代理实现的,spring默认是采用JDK动态代理

底层的动态代理有两种:

若目标对象实现了若干接口,Spring使用JDK的java.lang.reflect.Proxy类做动态代理【JDK动态代理】

若目标对象没有实现任何接口,Spring使用CGLIB库生成目标对象的子类做动态代理【Cglib动态代理】

使用哪种动态代理取决于代理对象是否实现了接口,是则用Java动态代理,否则用cglib动态代理

追问一:为什么Java动态代理必须是接口?
因为java是单继承,所以被代理对象必须是接口类型。

追问二:两种代理方式那种效率高?

答案:CGLib。

JDK动态代理主要涉及java.lang.reflect包下边的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑贬值在一起。JDK动态代理有一个限制,就是它只能为接口创建代理实例;

CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。

注意点:

对接口创建代理优于对类创建代理,因为会产生更加松耦合的系统,所以spring默认是使用JDK代理。对类代理是让遗留系统或无法实现接口的第三方类库同样可以得到通知,这种方式应该是备用方案。

标记为final的方法不能够被通知。spring是为目标类产生子类。任何需要被通知的方法都被复写,将通知织入。final方法是不允许重写的。

spring只支持方法连接点:不提供属性接入点,spring的观点是属性拦截破坏了封装。 面向对象的概念是对象自己处理工作,其他对象只能通过方法调用的得到的结果

5 Spring中的设计模式
BeanFactory和ApplicationContext应用了工厂模式,用于创建对象实例;

在Bean创建中,Spring提供了单例和原型等模式,默认是单例的;

AOP领域使用的是代理模式、装饰器模式和适配器模式;

事件监听器使用的是观察者模式;

类似JDBCTemplate等模板对象,使用的是模板方法模式。

6 Spring事务
特征:ACID【原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)】

事务管理:

编程式

开发者直接把事务的代码和业务代码耦合到一起,在实际开发中不用。

声明式

开发者采用配置的方式来实现的事务控制,业务代码与事务代码实现解耦合,使用的AOP思想。

隔离级别:

ISOLATION_DEFAULT:使用数据库默认的事务隔离级别;

ISOLATION_READ_UNCOMMITTED:最低级别,允许看到其他事务未提交的数据,会产生脏读、不可重复读、幻读。

ISOLATION_READ_COMMITTED:只能读取已提交的数据,可以防止脏读,会产生不可重复读、幻读;

ISOLATION_REPEATABLE_READ:防止脏读、不可重复读,会产生幻读;

ISOLATION_SERIALIZABLE:最高级别,事务处理为串行,阻塞的,能避免所有情况。

传播机制:

PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。【required】

PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常;

PROPAGATION_NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。(外层事务的回滚可以引起内层事务的回滚,而内层事务的异常并不不会导致外层事务的回滚)

PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。(一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响)

PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起;

PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

声明式事务管理

基于XML的声明式事务管理

配置平台事务管理器

事务通知配置

事务AOP织入配置

基于注解的声明式事务控制

配置平台事务管理器

配置事务通知(@Transactinal注解配置)Service层

事务注解驱动配置tx:annotation-driven/、@

7 Spring创建的对象是单例,怎么存在线程安全问题?
Spring框架里的bean获取实例的时候都是默认单例模式,所以在多线程开发里就有可能会出现线程不安全的问题。

当多个用户同时请求一个服务器时,容器(tomcat)会给每一个请求分配一个线程,这时多个线程会并发执行该请求所对应的业务逻辑(controller里的方法),此时就要注意啦,如果controller(是单例对象)里有全局变量并且又是可以修改的,那么就需要考虑线程安全的问题。

解决方案

设置@scope(“prototype”)为多例模式,为每个线程创建一个controller

使用ThreadLocal。ThreadLocal基本实现思路是:它会为每个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突,因为每个线程都拥有自己的变量副本,从而也就没必要对该变量进行同步啦。

在ssh或ssm框架里的service或dao对象虽然也是单例模式,但正如上面分析的,他们没有可修改的全局变量,所以在多线程环境下也是安全的。

8 Spring框架通过IOC容器创建对象的方式?
在Spring容器中的Bean都是单例的

创建方式

利用默认的无参构造器,如果没有就会报错

静态工厂方法创建

实例工厂方法创建

创建时机:

lazy-init为“default/false”时,在启动Spring容器时创建bean,但是如果bean是prototype时,特殊,这种情况下无效

spring启动会发现错误,

有可能会造成一些数据长时间存在内存中

lazy-init为“true”时,当context.getBean时创建,bean为多例时,必须采用这种方案创建

不能及时发现错误

数据会在需要的时候加载

初始化

由spring容器调用init方法

在构造器之后执行

销毁

如果是单例,则必须返回ClassPathXMLApplicationContext该容器,才能执行销毁

如果是多例,容器不负责销毁

三 SpringMVC
1 SpringMVC的理解
核心组件:

DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性;

Handler:(Controller)后盾控制器,负责处理请求的控制逻辑;

HandlerMapping:映射器对象,用于管理url与对应的controller的映射关系;

HandlerAdapter:适配器,主要处理方法参数、相关注解、数据绑定、消息转换、返回值、调用视图解析器等等;

ViewResolver:视图解析器,解析对应的视图关系。

执行流程:

一个请求匹配前端控制器DispatcherServlet的请求映射路径;

DispatcherServlet接收到请求后,将根据请求提交给处理器映射器(HandlerMapping);

HandlerMapping 根据用户的url请求查找匹配该url的Handler,并返回一个执行链HandlerExecutionChain;

DispatcherServlet 再请求处理器适配器(HandlerAdapter) 调用相应的Handler进行处理并返回ModelAndView给DispatcherServlet;

DispatcherServlet 将ModelAndView请求ViewResolver(视图解析器)解析,返回具体View;

DispatcherServlet 对View进行渲染视图(即将模型数据填充至视图中);

DispatcherServlet 将页面响应给用户。

2 Spring容器与SpringMVC容器以及Web容器的区别?
Spring是根容器,SpringMVC是其子容器。子容器的创建依赖于父容器的创建,父容器先于子容器创建。子容器(SpringMVC容器)可以访问父容器(Spring容器)的Bean,父容器(Spring容器)不能访问子容器(SpringMVC容器)的Bean。

web容器(tomcat)是管理servlet对象的地方,Spring和SpringMVC是管理bean对象的地方,更进一步的讲,spring是管理service和dao的容器,springMVC是管理controller的容器。

3 SpringMVC拦截器与Servlet过滤器的区别
拦截器是基于Java的反射机制的,而过滤器是基于函数回调。

拦截器不依赖于servlet容器,过滤器依赖于servlet容器。

拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。

拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。

在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。

拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

4 SpringMVC执行流程
请求前端控制器(DispatchServlet)

请求查询Handler(处理器映射器HandlerMapping)

返回处理器执行链到前端控制器

请求执行Handler(处理器适配器HandlerAdapter)

请求处理器Handler

响应回处理器适配器(HandlerAdapter)

返回处理结果(ModelAndView)到前端控制器

请求视图解析器(ViewReslover)

返回视图到前端控制器

渲染视图(视图页面JSP)

响应回客户端

image-20210104154627247

三 SSH与SSM对比
/

struts简介

1 Filter作为控制器
Servlet VS Filter

servlet能做的Filter都能做

Filter能做的Servlet不可以全能做,因为拦截资源不是Servlet的功能,Filter中有个FilterChain,这个API在Servlet中没有

2 Struts2与SpringMVC区别
Struts2 SpringMVC
框架机制 Filter Servlet
拦截机制 a)类级别拦截,每次请求创建一个Action,与Spring整合时注入作用域为prototype(否则会出现线程并发问题),通过setter、getter把request数据注入属性
b)一个Action对应一个request,response上下文,接收参数通过属性接收,这说明属性参数是让多个方法共享的
c)Action的一个方法可以对应一个url,而类属性被所有方法共享,所以无法用注解或其他方式标识其所属方法
d)有自己的拦截Interceptor机制 a)方法级别拦截,一个方法对应一个Request上下文,方法独立,独享request,response数据;一个方法同时对应一个url,参数传递时直接注入方法的,方法所独有。处理结果通过ModeMap返回给框架
b)Spring整合时,默认作用域是单例的,默认对所有请求只创建一个Controller,因此没有共享属性,线程安全;可以通过@Scope注解修改作用域
c)独立的AOP方式,零配置,
性能 配置文件大,类级别的拦截,每次请求对应一个新的action,需要加载所有属性值注入,性能低 零配置,因为是基于方法的拦截,加载一次单例模式注入,性能高
配置 Struts2需要通过xml配置才能有一样的效果 SpringMVC与Spring无缝连接,零配置
设计思想 OOP思想 基于servlet进行扩展
集成 集成了Ajax,在Action处理必须安装插件或者手写代码集成,不方便 集成了Ajax,只需有个@ResponseBody注解就尅实现直接返回响应文本到前端页面
3 SSH与SSM框架对比
定义

SSH:Struts2 + Spring + Hibernate

SSM:SpringMVC + Spring + Mybatis

区别

相同点:

Spring依赖注入(DI)来管理各层组件,使用AOP切面编程来管理事务、日志、权限等

Hibernate与MyBatis都是可以通过SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory生成Session,最后由Session来开启事务和SQL语句。其中SessionFactoryBuider,SessionFactory,Session 的生命周期都差不多

Hibernate和MyBatis都支持JDBC和JTA事务处理

不同点(一):Struts2 与 SpringMVC

Struts2与SpringMVC控制视图与模型的交互机制不同;

Struts2是Action级别,SpringMVC是方法级别,更容易实现RESTFul风格

Struts2 的实现原理【嵌入Filter过滤器】:

image-20201009202706003

1.客户端初始化指向Service容器(Tomcat)的请求
2.这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助)
3.接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请求是否需要调用某个Action
4.如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy
5.ActionProxy通过Configuration Manger询问框架的配置文件,找到需要调用的Action类
6.ActionProxy创建一个ActionInvocation的实例
7.ActionInvocation实例使用命名模式来调用,在调用Action的过程前后,涉及到相关拦截器(Intercepter)的调用
8.一旦Action执行完毕,ActionInvocation负责根据Struts.xml中的配置找到1对应的返回结果。返回结果通常是(但不总是,也可能是另外一个Action链)一个需要被表示的jsp或者FreeMarker的模板
9.将处理结果返回

SpringMVC实现原理【Servlet嵌入】:

image-20201009202915295

1.客户端发出一个Http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet的请求映射路径(在web.xml中指定),web容器将请求转交给DispatcherServlet
2.DispatcherServlet接收到这个请求后将根据请求的信息(URL,http,请求报文头和请求参数Cookie等)以及HandlerMapping的配置找到处理请求的处理器(Handler)
3-4.DispatcherServlet根据HandlMapping找到对应的Handler,将处理权交给Handler(Handler将具体的处理进行封装),再由具体的HandlerAdapter对Handler进行具体的调用
5.Handler对数据处理完以后将返回一个ModelAndView()对象给DispatcherServlet
6.Handler返回的ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherServlet通过ViewResolver将逻辑视图转为真正的视图view
7.Dispatcher通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view视图并返回给客户端

不同点(二)Hibernate 与 Mybatis

MyBatis可以进行更为细致的SQL优化,可以减少查询字段

MyBatis容易掌握,而Hibernate门槛较高

Hibernate的Dao层开发比MyBatis简单,MyBatis需要维护SQL和结果映射

Hibernate对 对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便

Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同的SQL

Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳,更新操作不能指定刷新指定记录,会清空整个表,但是也可以使用第三方缓存

Hibernate 封装性好 屏蔽了数据库差异,自动生成SQL语句。应对数据库变化能力较弱,SQL语句优化困难

MyBatis仅实现了SQL语句和对象的映射,需要针对的数据库写SQL语句,应对数据库变化能力较强,SQL语句优化比较方便

总结:

SSM和SSH不同主要在MVC实现方式,以及ORM持久化方面不同(Hiibernate与Mybatis)
SSM越来越轻量级配置,将注解开发发挥到极致,且ORM实现更加灵活,SQL优化更简便;
SSH较注重配置开发,其中的Hiibernate对JDBC的完整封装更面向对象,对增删改查的数据维护更自动化,但SQL优化方面较弱,且入门门槛稍高。

四 Dubbo +Netty+ Zookeeper
0 Dubbo的工作流程
客户端调用

客户端存根将方法、参数等数据序列化

客户端存根Netty的NIO发送消息(发送前必须序列化)

服务端存根将消息反序列化

服务端存根调用本地服务

本地服务处理

服务端存根返回处理结果

服务端存根将结果序列化

客户端存根通过Netty的NIO返回消息

客户端存根反序列化

返回调用结果给服务消费方

1 Dubbo的节点角色
Provider:服务提供方(洗浴中心)

Consumer:服务消费方(客人)

Registry:服务注册与发现的注册中心(商铺注册登记中心)

Monitor:监控服务的统计中心(统计服务被调用的次数)

Container:服务运行容器(烧烤一条街,洗浴一条街)

image-20200929155120794

2 Dubbo调用关系(链路)
服务容器负责启动,加载,运行服务提供者;

服务提供者在启动时,向注册中心注册自己提供的服务;

服务消费者在启动时,向注册中心订阅自己所需的服务;

在注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者;

服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用;

服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心;

Dubbo注册中心宕机的话,消费者还可以去访问到服务提供者吗?

可以,因为服务消费者一开始就会从注册中心保留一份服务提供者的地址信息表,如果消息注册中心宕机了,可以通过缓存中的地址信息去访问到服务提供者服务器,只要服务提供者的服务器没有做改动,就能正常访问到。

3 Zookeeper的工作机制/功能/应用场景
3.1 工作机制

Zookeeper从设计模式角度来理解:是一个基于观察者模式(一个人干活,有人盯着他)设计的分布式服务管理框架

负责 存储 和 管理 大家都关心的数据

然后接受观察者的注册,

一旦这些数据的发生变化,Zookeeper就将负责通知已经注册的那些观察者做出相应的反应

从而实现集群中类似Master/Slave管理模式

Zookeeper = 文件系统 + 通知机制

3.2 功能

数据发布/订阅

负载均衡

命名服务

分布式协调/通知

集群管理

分布式锁

分布式队列

3.3 应用场景

统一配置管理

将每个子系统都需要配置的文件统一放置到zk的ZNode节点中

统一命名服务

通过ZNode进行统一命名,各个子系统可以通过名字获取节点上资源

分布式锁

通过创建共享资源的顺序临时节点和动态监听机制,从而控制多线程对共享资源的并发访问

集群状态

通过动态感知节点的增、删、从而保证集群下相关节点数据主、副本的一致性

4 分布式事务
CPA理论

C:Consistency【一致性】

服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。

P:Partition Tolerance【分区容忍性】

服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。

A:Availability【可用性】

分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CAP的组合方式

CA:放弃分区容错性;关系型数据库按照CA进行设计;

AP:放弃一致性,追求最终一致性;NoSql数据库按照AP设计;

强一致性:写入成功立刻要查出最新数据

最终一致性:允许暂时的数据不一致,只要最终在用户接受的时间内数据一致即可

CP:放弃可用性;一些强一致性要求的系统按CP进行设计,比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。

说明:由于网络问题的存在CP系统可能会出现待等待超时,如果没有处理超时问题则整理系统会出现阻塞

总结: 在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性。

分布式事务的解决方案

基于XA协议的2PC(二阶段提交)

XA:分布式事务协议;大致分为两个部分:事务管理器(全局的调度者,负责提交回滚)和本地资源管理器(数据库)

2PC算法思路:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各个参与者是否要提交数据或者终止操作

缺点:不仅要锁住参与者的所有资源,而且要锁住协调者资源,开销大;(即效率低,对高并发不友好)

基于XA协议的3PC(三阶段提交)

基于2PC同时在参与者与协调者中都引入超时机制

在第一阶段和第二阶段之间插入一个准备阶段

CanCommit

PreCommit

DoCommit

分布式事务的TCC(事务补偿)

TCC:Try、 Confirm、 Cancel,将业务逻辑分成三块,

分布式事务对事务怎么做统一管理?

5 分布式锁
定义:指分布式环境下对共享资源加锁,避免多线程访问时出现并发安全问题

原因:由于分布式部署的服务实例部署在不同的机器上,资源共享不再是传统的线程共享,而是跨JVM进程之前的资源共享

解决方案:

基于数据库实现分布式锁

基于“乐观锁”实现

乐观锁总是乐观的认为每次从操作数据库时不会有其他线程对数据进行修改,所以不会上锁,但在更新时会判断其他线程在这之前有没有对该数据进行修改,通常采用“版本号version”机制实现

“版本号version”机制:

当前线程取出数据时,顺带把版本号的值取出,最后在更新数据记录时,将取值作为更新条件。更新成功后加1,其他线程发现更新时版本号不是当初获取的记录,所以更新失败,即避免了并发多线程访问共享数据时出现数据不一致的现象

性能:

同一时刻只有一个线程能获取锁并成功操作共享资源,其他线程获取失败,所以系统的吞吐性能低

因为要携带version进行匹配,同时执行加1的更新操作,影响数据库性能

乐观锁适合【写少读多】的业务场景

基于“悲观锁”实现

悲观锁总是悲观的认为每次并发线程都有其他线程过来争抢资源并修改,所以每次都会上锁,导致其他线程发生阻塞,最终只有当前线程释放了锁,其他线程才能获取锁再进行修改

悲观锁建立在数据库底层搜索引擎基础上,采用select…for update的方式加锁,高并发多线程请求时,特别是“读”操作时,影响数据库性能

悲观锁可能会导致死锁,适合于【读少写多】的业务场景

基于Redis实现分布式锁

基于Redis原子操作SETNX和EXPIRE实现

原子性:因为redis单线程机制,同时刻同一个部署节点只允许一个线程执行

Redis2.6.X版本格式为:SET Key Value [EX s] [PX ms] [NX|XX]

EX:表示Key的存活时间

NX:表示只有当Key不存在时才会设置值

XX:表示当Key存在时才会设置Key值

使用SETNX命令是实现“分布式锁”核心,使用命令获取锁时,如果返回0,则获取失败,反之成功

为了防止并发线程获取锁后发生异常,从而导致其他线程总是返回0导致死锁,需要给Key设置合理的过期时间EXPIRE

成功获取锁并执行完成相应操作后,需要释放锁,可以通过DEL删除锁,删除时需保证被删除的锁是当前线程获取的,避免误删

基于Zookeeper实现分布式锁

基于临时顺序节点和Watcher机制实现

使用Redisson中间件实现分布式锁

Redisson:

建立在缓存中间件Redis基础上实现的具有高性能且操作便捷的“综合中间件”

作用远远超过Redis,提供分布式远程服务(Remote Service)、分布式实时对象服务(Live Object)以及分布式调度任务服务(Scheduler Service)

底层数据结构采用动态类的形式设计

6 Zookeeper的内部原理
选举机制

半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器

节点类型

持久型

持久目录节点

持久化有序目录节点

短暂型

临时目录节点

临时有序目录节点

监听器原理

在main方法创建ZK同时会创建两个线程,一个负责网络连接,一个负责监听

监听事件会通过网络通信发给ZK

ZK获得注册的监听事件后,立即将监听事件添加进监听列表

监听到数据变化或者路径变化,就会发送消息给监听线程

常见的监听:

监听节点数据的变化:get path [watch]

监听子节点增减的变化:ls path [watch]

监听线程就会在内部调用process方法(需要实现process方法内容)

7 zk的功能
命名服务

配置管理

集群管理

分布式锁

负载均衡

分布式队列

8 zk重要配置(conf)
tickTime=2000ms:心跳间隔,默认是2000ms,每隔2秒会发送一次心跳,用于客户端与服务器或服务器之间维持心跳,还可以控制Follower跟Leader的通信时间,默认是2*tickTime

initLimit=10:用于Follower在启动过程中同步Leader数据的时间,默认10*tickTime

ysncLimit=5:Leader节点和Follower节点进行心跳检测的最大延迟时间

dataDir=/tmp/zookeeper:存储快照文件的默认目录

clientPort=2181:端口号

9 zk的节点Znode类型
PERSISTENT:持久节点

PERSISTENT_SEQUENTIAL:持久顺序节点,创建的节点会自动加上序号;

EPHEMERAL:临时节点,生命周期跟客户端会话绑定,会话失效就自动清除;

EPHEMERAL_SEQUENTIAL:临时顺序节点,创建的节点会自动加上序号;

10 RPC使用哪些技术?
动态代理-----Java动态代理/CGLib动态代理

序列化和反序列化-----Fastjson

NIO通信(异步IO)------Netty

服务注册中心------Redis/Zookeeper

五 Redis
Redis=Remote Dictionary Server

1 Redis与数据库的双写一致性?
Redis不支持事务

先删缓存,再更新数据库,延时后再删一遍缓存 ----- 双删
在做数据的增删改操作时,先删一遍缓存,然后再操作数据库,延时一段时间之后再删一遍缓存,这么做是为了避免脏读;

追问一:问什么要使用双删?
答案:利用反证法
1.只先删
A事务执行更新操作,先删除掉缓存中数据,然后去更新数据库;
B事务在A更新前查询数据,发现缓存没有,去数据库查询到旧的数据,查到并放进缓存;
AB事务结束之后,C事务又来查询,直接查到缓存中的B放入的旧数据;
结果:产生脏读

2.只后删
A事务执行更新操作,直接操作数据,然后删除缓存中数据;
B事务在A事务删除缓存之前,执行查询,拿到缓存中的旧数据;
结果:

追问二:为啥是延时后再删一遍?
答案:确保其他事务都执行完了,可以再次使用反证法
A事务按照双删流程执行操作,没有延时;
B事务在A事务第一次删除缓存后查询数据,没有拿到,去数据库查询,取出旧数据,但是在更新缓存之前A的双删操作结束了,此时缓存数据为空,B事务把取出的旧数据存进缓存中。
2 缓存雪崩、缓存穿透、缓存击穿
缓存雪崩,因为某段时间redis服务器崩了,或者大批数据过期了,所有请求就会直接去找数据库中查询,造成存储层产生很大压力;

解决方案:

基于哨兵模式或者集群模式:redis是一个单级缓存,可以在搭建一个集群分布式部署,避免单机故障,保证redis的高可用性;

限流降级组件:

热点数据缓存不过期:热点key永不失效

过期时间随机化:缓存数据的过期时间随机,防止同一时间大量数据同时过期

互斥锁重建缓存:高并发场景下,为了避免大量的请求同时到达存储层查询数据、重建缓存,可以使用互斥锁控制。比如对某个key只能有一个线程来访问,其他线程来了就等待一段时间后重试。锁的类型,在单机环境下可以使用JUC并发包下的Lock,分布式可以使用Redis的SETNX方法

异步重建缓存:采取异步策略,会从线程池中获取线程来异步构建缓存,从而不会让所有的请求直接到达存储层。

缓存穿透:用户不断请求缓存跟数据库都没有的数据,导致永远越过缓存直接永远的访问数据库

解决方式:

布隆过滤:对所有查询的参数以hash的形式去存储,在控制层就进行校验,不符合就丢弃,避免对底层数据库的访问;

当查询数据库时如果没有查询到数据,则将null返回给前端客户,同时将null数据塞入缓存中,并对对应的key设置过期时间,如果后面再查询就直接从缓存中返回null给客户;

缓存击穿:(缓存中没有,但是数据库中有,一般是过期时间到期了):缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:

设置热点数据,永远不过期

接口限流与熔断降级

布隆过滤器:类似一个hashset,过滤已存在的元素,最终保证集合中的元素是唯一的,即“去重”;判重之前不需要将数据列表加载至内存中

使用互斥锁(mutex key)。对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

布隆过滤器

生产环境中的作用:判断一个元素是否存在于大数据量的集合中

底层算法核心逻辑:

第一部分:设计并构造K个哈希函数以及大小为N的数组,并设置数组每个元素的初始值为0

第二部分:判断一个元素是否存在于大数据量的集合中,将元素经过K的哈希函数计算得出K的哈希值,并判断位数组对应下标的元素取值是否为1,若都为1,则大概率是存在的,只要有一个对应的数组元素取值不为1,则一定不存在于集合中

3 redis常用的数据结构
1、String

 单key,单value,memcached也支持此类型

 Redis最基本的数据类型,一个redis中字符串中最大有512M

2、Hash

 一个键值对集合

3、List

 是个链表,linkedList。

4、Set(集合)

 无序集合,无重复。

5、Zset(sorted set:有序集合)

 游戏里面用这个数据结构多

 带个double分数。

4 Redis的淘汰策略
当内存空间用满时,就会自动回收老的数据。

redis唯一支持的回收算法是LRU(),用于限制最大内存使用量的maxmemory指令

maxmemory用于指定Redis能使用的最大内存,既可以在redis.conf文件中设置,也可以在运行过程中使用CONFIG SET命令动态修改

将maxmemory设置为0,表示不进行内存限制,对于32位系统有个隐性限制:最多3GB内存

redis的6种汰策略:

volatile-lru:

只限于设置了过期时间(expire)的数据;

优先删除最近最少使用的key;

allkeys-lru(less recently used,LRU):

所有key通用;

优先删除最近最少使用的key;

volatile-random:

从设置了expire过期时间的key中,随机删除一部分;

allkeys-random:

所有key通用,随机删除一部分key

volatile-ttl:

只限于设置了expire的部分

优先删除剩余时间(TTL)短的key

noeviction(默认):不删除策略,如果内存满了直接报错。

5 Redis持久化策略
Redis数据如何持久化
为什么要对redis进行持久化
redis是一个内存数据库,当redis服务器重启,获取电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中

1.Redis持久化思想
说明:redis操作时,首先操作的都是内存中的数据.redis根据自身的配置选择不同的持久化方式.定期将内存中的数据保存到本地磁盘中. 当redis重启服务时,首先会根据配置文件中指定的持久化文件恢复内存数据.

2.RDB模式
特点:
1.定期实现数据备份,但是可能丢失数据.
解释:如果实现用户操作1000次进行一次备份,当redis宕机时,会有未完成的数据丢失
2.该操作的执行的效率最高.
3.该持久化方式是redis的默认策略.
4.RDB模式做的是内存的快照,能够有效的节省磁盘空间,控制持久化文件的大小!!!
解释:相当于照相机,每次快照redis内存然后覆盖之前的.rdb文件
5.RDB模式持久化文件是加密的

3.AOF模式
特点:
1.AOF模式默认是关闭的.(需人为开启)
2.AOF模式可以实现数据的实时备份.

解释:AOF模式就是备份记录的是用户全部执行过程!!!(select等等)
然后存到持久化文件中,redis宕机时,读取持久化文件中每一行命令执行的结果,然后把结果给redis

3.AOF的执行的效率相对RDB模式低.
4.AOF模式做持久化操作时,对原有的持久化文件做追加操作.

5.AOF的持久化文件,内容明文保存
解释:既然需要读取就不能加密

持久化条件配置
save 开头的一行就是持久化配置,可以配置多个条件(每行配置一个条件),每个条件之间是“或”的关系。
“save 900 1”表示15分钟(900秒钟)内至少1个键被更改则进行快照。
“save 300 10”表示5分钟(300秒)内至少10个键被更改则进行快照。

扩展
如果redis宕机,恰好持久化文件又损坏怎么办?
配置redis主从,redis宕机后可以从从机复制持久化文件,一运行就可以了
(具体步骤看其他文档)
6 Redis五种数据结构的使用场景
字符串:

存储用户信息,使用RedisTemplate组件将对象序列化写入缓存,可以从缓存中读取;

列表(list)

适用于“排名”“排行榜”,“近期访问数据列表”等场景

集合(set)

适用于解决重复提交,剔除重复ID的场景

有序集合(sortedset)

适用于充值、积分排行榜

哈希(hash)

适用于具有映射关系的数据对象存储

7 redis是单线程的嘛?如何支持高并发请求?
Redis内部使用了一个文件事件处理器,这个核心处理器是单线程的,但是redis采用IO多路复用机制来同时监听多个Socket,根据Socket的事件类型来选择对应的时间处理器来处理这个事件。

8 Redis高可用方案具体怎么实施?
使用官方推荐的哨兵(sentinel)机制就能实现,当主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。

它有四个主要功能:

集群监控,负责监控redis master和slave进程是否正常工作。

消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。

故障转移,如果master node挂掉了,会自动转移到slave node上。

配置中心,如果故障转移发生了,通知client客户端新的master地址。

9 Redis哨兵机制的原理吗?
通过sentinel模式启动Redis后,自动监控master/slave的运行状态,基本原理是:心跳机制+投票裁决。

每个sentinel会向其它sentinal、master、slave定时发送消息,以确认对方是否活着,如果发现对方在指定时间内未回应,则暂时认为对方宕机。

若哨兵群中的多数sentinel都报告某一master没响应,系统才认为该master真正宕机,通过Raft投票算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。

10 部署Redis哨兵要注意哪些问题?
哨兵至少需要3个实例,来保证自己的健壮性。

11 Redis主从架构数据会丢失吗,为什么?
有两种数据丢失的情况:

1)异步复制导致的数据丢失:因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。

2)脑裂导致的数据丢失:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。

12 Redis主从复制的工作原理?(读写分离,主机写,从机读)
1)一个Slave实例,无论是第一次连接还是重连到Master,它都会发出一个SYNC命令;

2)当Master收到SYNC命令之后,会做两件事:(a) Master执行BGSAVE,即在后台保存数据到磁盘(rdb快照文件);(b) Master同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);

3)当Master在后台把数据保存到快照文件完成之后,Master会把这个快照文件传送给Slave,而Slave则把内存清空后,加载该文件到内存中;

4)而Master也会把此前收集到缓冲区中的命令,通过Reids命令协议形式转发给Slave,Slave执行这些命令,实现和Master的同步;

5)Master/Slave此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致;

13 由于主从延迟导致读取到过期数据怎么处理?
1)通过scan命令扫库:当Redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥Redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。

2)Redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了key是否过期以及对主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null。

14 Redis Key的过期策略有哪些?
1)惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key,很明显,这是被动的。

2)定期删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的key。

3)主动删除:当前已用内存超过maxMemory限定时,触发主动清理策略。主动设置的前提是设置了maxMemory的值。

15 Redis Key、value存储内容大小
当个Key最大存储512MB

value大小不超过512MB

一个单实例的redis最多支持232个键,差不多是2.5亿个,每个Key中的值value可以存232行数据

16 Redis与Memcache区别
适用的业务场景

Redis Memcache
数据结构 哈希、列表、集合、有序集合、字符串 只有KV结构
持久化 AOF、RDB持久化策略 无法满足持久化
高可用 原生支持集群功能,可实现主从复制、读写分离 原生不支持,需要二次开发
存储大小 Redis的Value最大存储512MB Value最大为1MB
底层实现机制

Redis Memcache
内存分配 临时申请内存空间,会导致碎片 预分配内存池管理内存,可节省内存分配时间
虚拟内存使用 有VM机制,能存储比物流内存更多的数据,当超量会引发Swap 存放在物理内存中,无VM内存
线程模型 单线程,无锁冲突,但吞吐量小 多线程,主线程监听,子线程接收请求执行读写。可能存在锁冲突;吞吐量大
17 Redis实现幂等性
幂等性

通俗的说,就是一个接口,多次发起同一个请求,必须保证操作只能执行一次

比如:订单接口,不能多次创建订单;支付接口,不能重复支付同一笔订单

解决方案

唯一索引,防止新增脏数据

token机制,防止页面重复提交

悲观锁,获取数据的时候加锁

乐观锁,基于版本号version实现,更新数据时刻校验

分布式锁,redis或者zookeeper

状态判断

实现思路

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:

如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示。

如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。

18 redis实现分布式锁
将redission注入spring容器中,配置单个或者集群的redis地址

设置redis的key值,比如字符串

传入key值获取redission Lock锁:RLock rLock = redisson.getLock(productKey);//底层是基于redis的setnx指令

上锁,设置过期时间:rLock.lock(30, TimeUnit.SECONDS);

操作后释放锁,可解决高并发

六 RabbitMQ
0 基础知识
定义

采用AMQP高级消息队列协议的一种消息队列技术,最大的特点是消费并不需要确保提供方存在,实现了服务之间的高度解耦

为啥用?

在分布式系统下具备异步、削峰、负载均衡等功能

拥有持久化机制、进程消息,队列中的消息也可以保存下来

实现消费者与生产者之间的解耦

高并发场景下,利用消息队列可以使得tongue访问变为串行访问达到一定量的限流,利于数据库操作

可以利用消息队列达到异步下单效果,排队等待后台逻辑下单

使用场景

服务间异步通信

顺序消费

定时任务

请求削峰

  1. 怎么确保消息正确的发送到RabbitMQ?如何确保?
    发送方确认模式

将信道设置成confirm模式,所有在信道上发布的消息都会被指派一个唯一的ID

一旦消息被投递到目的队列后,或者消息被写入磁盘后,信道会发送一个确认给生产者(包含消息唯一ID)

如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来确认消息。

接收方确认机制

消费者接收每一条消息后都必须进行确认,只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除

并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。即只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。保证数据最终一致性。

image-20201010152944714

  1. 如何避免消息重复投递或重复消费?或者说如何保证消息消费时的幂等性?
    在消息生产时,MQ内部针对每条生产者发送的消息生产一个inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列

在消息消费时,要求消息体中必须要有一个bizid(对于同一业务全局唯一,如订单ID)作为去重依据,避免同一条消息被重复消费

3.消息基于什么传输?
TCP连接的创建跟销毁开销大,且并发数收到系统资源限制,会造成性能瓶颈

RabbitMQ使用信道方式传输数据,建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制

  1. 消息如何分发?
    若该队列至少有一个消费者订阅,消息将以循环的方式发送给消费者,每条消息只会分发给一个订阅的消费者(前提是消费者能正常处理消息并确认消息)

通过路由可以实现多消费的功能

  1. 消息怎么路由
    消息提供方 --> 路由 --> 一至多个队列

消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定

通过队列路由键,可以把队列绑定到交换器上

消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同路由规则)

常用的交换器

fanout:如果交换器收到消息,将会广播到所有绑定的队列上

direct:如果路由键完全匹配,消息就被投递到响应的队列

topic:可以使来自不同的源头的消息能到达同一个队列,使用topic交换器时,可以使用通配符

  1. 如何确保消息不丢失?
    三种情况:

生产者丢失消息

可以选择rabbitmq的事务功能,生产者发送数据前开启事务,消息没被成功接收则生产者异常报错,回滚事务,然后重新尝试,知道成功后提交事务

缺点:事务已开启,就会变成同步阻塞操作,生产者会阻塞等待是否发送成功,消耗性能造成吞吐量下降

可以开启confirm模式,再生产者那里开启confirm之后,每次写的消息都会分配一个唯一ID,然后写入rabbitmq中,rabbitmq会回传一个ack消息,响应发送OK了;如果没能处理该消息,会回调一个nack接口,响应发送fail了,可以进行重试;可以结合这个机制知道自己内存中维护的每个消息的ID,如果超过一定时间没有接收道这个消息的回调,那么可以重发;

两者不同之处:

事务机制是同步的,提交一个事务就会阻塞,但是confirm机制是异步的,发送消息之后可以继续发下一条,然后rabbitmq会回调告知成功与否

一般在生产者这块避免丢失,都是使用confirm机制

rabbitmq自己丢失消息

设置消息持久化到磁盘

创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里面的数据

发送消息的时候将消息的deliveryModel设置为2,这样消息就会被设为持久化方式,此时rabbitmq会将消息持久化到磁盘上;

持久化可以跟生产的confirm机制配合起来,只有消息持久化到磁盘,才会通知生产者ack,这样就算是再持久化之前rabbitmq挂了,数据丢失了,生产者收不到ack回调也会进行消息重发;

消费者丢失消息

使用rabbitmq提供的ack机制,首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动ack,这样就可以避免消息还没有处理完就ack。

消息持久化,前提是队列必须持久化

RabbitMQ确保持久性消息能从服务器重启中恢复的方式是:将他们写入磁盘上的一个持久化日志文件,当发布一条持久化消息到持久交换器上时,RabbitMQ会在消息提交到日志文件后才发送响应

一旦消费者从持久队列中消费一条持久化消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么RabbitMQ会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列中。

  1. 使用RabbitMQ的好处?
    服务间解耦

异步通信性能高

流量削峰

  1. RabbitMQ集群
    镜像集群模式

含义:创建的queue,无论元数据还是queue里的消息都会存在多个实例上,然后每次写消息到queue时,会自动把消息到多个实例的queue里进行消息同步

好处:任何一个机器宕机了,其他机器仍然可以使用

坏处:

性能开销大,网络带宽压力跟消耗很大

没有办法线性扩展,因为一个queue负载很重,其他机器都会同步包含,无法扩展

  1. MQ的缺点
    系统可用性降低:MQ挂了,整套系统跟着挂

系统复杂性提高:硬塞一个MQ进来,需要保证消息没有重复消费、防止消息丢失、保证消息传递的顺序等等

一致性问题:

  1. MQ的优点
    异步处理:相比传统的串行、并行方式,提高了系统吞吐量

应用解耦:系统间通过消息通信,不用关心其他系统的处理

流量削峰:可以通过队列长度控制请求量,可缓解短时间的高并发请求

日志处理:解决大量日志传输

消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯消息通讯,比如实现点对点消息队列或者聊天室

image-20201010160329886

  1. 消息中间件的选型问题
    image-20201010161051928

Kafka、ActiveMQ、RabbitMQ、RocketMQ有什么优缺点?

image-20201010161151925

  1. RabbitMQ工作模式
    simple模式

image-20201010161402855

work工作模式(资源的竞争)

image-20201010161430248

publish/subscribe发布订阅模式(共享资源)

image-20201010161504350

routing路由模式

image-20201010161529252

image-20201010161540894

topic主题模式(路由模式的一种)

image-20201010161611084

七 SpringBoot
0 Spring Boot核心概念
起步依赖

spring-boot-starter启动器

自动配置

1 Spring与SpringBoot的区别?
Spring框架为开发Java应用程序提供了全面的基础架构支持。

它包含一些很好的功能,如依赖注入和开箱即用的模块,如:

Spring JDBC

Spring MVC

Spring Security

Spring AOP

Spring ORM

Spring Test

这些模块可以大大缩短应用程序的开发时间。

Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的复杂例行配置。

它的目标和Spring的目标是一致的,为更快,更高效的开发生态系统铺平了道路。

以下是Spring Boot中的一些功能:

通过starter这一个依赖,以简化构建和复杂的应用程序配置

可以直接main函数启动,嵌入式web服务器,避免了应用程序部署的复杂性

Metrics度量,Helth check健康检查和外部化配置

自动化配置Spring功能 - 尽可能的

SpringBoot开发步骤

创建Maven初始项目

导入pom依赖包,父项目依赖跟启动器(starter)

编写主启动类,添加@SpringBootApplication注解,在类中写main方法,调用SpringApplication.run(xxx.class,args)

编写Controller、Service层方法,注意添加注解

启动主程序启动类的main方法即可启动SpringBoot的web项目

简化部署,导入spring-boot-maven-plugin依赖包,即可打包,在打好的jar包目录下使用java -jar jar包名即可运行web项目

2 SpringBoot自动配置的原理
自动配置原理:

简单来说就是:

SpringBoot启动时会扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包。

根据spring.factories配置加载EnableAutoConfiguration
其中给容器中自动配置添加组件的时候,会从propeties类中获取配置文件中指定这些属性的值。xxxAutoConfiguration:⾃动配置类给容器中添加组件。xxxProperties:封装配置⽂件中相关属性。

根据@Conditional注解的条件,进行自动配置并将Bean注入Spring容器

SpringBoot启动都是通过主配置类启动的,在启动类上有个@SpringBootApplication注解,(进入@SpringBootApplication里面有7个注解,前面4个是元数据注解,重点关注后面核心3个注解)就表示这个是启动类,相当于main方法;这是一个组合注解,主要包含@SpringBootConfiguration(表示是配置类)、@ComponentScan(开启组件扫描)、@EnableAutoConfiguration(开启自动配置)

@SpringBootConfiguration里面有个@Configuration,通过javaConfig的方式添加组件到IOC容器。

进入这个@EnableAutoConfiguration注解里面,又有两个重要的注解:

一个是@AutoConfigurationPackage(也就是自动配置包),点进去是一个@Import(AutoConfigurationPackage.Registrar.class),导入一个Registrar组件,作用是将主配置类所在的包以及下面的所有子包里的组件全部扫描进IOC容器;这也说明了主配置包以及其子包以外的组件,IOC扫描不到;

另一个注解是@Import(AutoConfigurationImportSelector.class),通过导入自动配置导入选择器类,这个类的selectImports方法会通过SpringFactoriesLoader得到大量的配置类。每个配置类根据条件配置类作出决策,实现自动配置的功能。

点进AutoConfigurationImportSelector的selectImports方法,可以知道最核心的loadFactoryNames方法,主要实现由三大步:

1.从classpath(类路径)下获取所有META-INF/spring.factories这个文件下的信息;

2.将获取的信息封装成Enumeration返回;

3.遍历Enumeration,然后获取key为EnableAutoConfiguration下的所有值。

@ComponentScan注解用于组件扫描

spring-boot-starter(web启动器):spring-boot场景启动器;帮我们导入了web模块正常运行所依赖的组件;

@SpringBootApplication(主程序类,入口): Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;

@SpringBootConfiguration:Spring Boot的配置类;

@EnableAutoConfiguration:开启自动配置功能;

3 Spring、SpringMVC、SpringBoot常用注解
3.1 Spring注解
@Autowired自动装配

作用是为了消除代码Java代码里面的getter/setter与bean属性中的property。 getter看个人需求,如果私有属性需要对外提供的话,应当予以保留

@Autowired默认按类型匹配的方式,在容器查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。

@Resource

@Resource后面没有任何内容,默认通过name属性去匹配bean,找不到再按type去匹配

指定了name或者type则根据指定的类型去匹配bean

指定了name和type则根据指定的name和type去匹配bean,任何一个不匹配都将报错

@Autowired与@Resource区别:

@Autowired默认按照byType方式进行bean匹配,@Resource默认按照byName方式进行bean匹配

@Autowired是Spring的注解,@Resource是J2EE的注解,这个看一下导入注解的时候这两个注解的包名就一清二楚了。

Spring属于第三方的,J2EE是Java自己的东西,因此,建议使用@Resource注解,以减少代码和Spring之间的耦合。

@Service

该注解声明被标注的类(.java)是一个bean,其他的类可以使用@Autowired将这个类作为一个成员变量注入;

可以指定被标注的类在spring容器中的id(如:@Service(“Xxx”)),不指定的话,默认是该类名且首字母小写;

@Scope

spring默认的bean都是单例的(singleton),如果使用多例,可以配置(@Scope(“prototype”))表示表示原型,即每次都new一个新的实例

@Component | @Repository | @Service | @Controller

通过在applicationContext.xml配置文件中注册<context:component-scan base-package=”pagkage1[,pagkage2,…,pagkageN]”/>,可以指定一个包名,指定多个包名时用“,”隔开;

这些注解都会讲被标注的.java文件(对象)作为Bean注册进spring容器中

@Component

所有受Spring 管理组件的通用形式,@Component注解可以放在类的头上,@Component不推荐使用。

@Controller

用于对应表现层的Bean,即Action类上

类似于@Service,@Controller标注的Bean注册进spring容器默认名字是类名且首字母小写,可以指定value【比如:@Controller(value=“UserAction”)或@Controller(“UserAction”)】来指定Bean的名字;

@Repository

对应数据访问层Dao的Bean

@Repository(value=“userDao”)注解是告诉Spring,让Spring创建一个名字叫"userDao"的UserDaoImpl实例。

Sping注解小汇总

<context:component-scan base-package=“cn.test”/>
@Configuration把一个类作为一个IoC容器,它的某个方法头上如果注册了@Bean,就会作为这个Spring容器中的Bean。
@Scope注解 作用域
@Lazy(true) 表示延迟初始化
@Service用于标注业务层组件、
@Controller用于标注控制层组件(如struts中的action)
@Repository用于标注数据访问组件,即DAO组件。
@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
@Scope用于指定scope作用域的(用在类上)
@PostConstruct用于指定初始化方法(用在方法上)
@PreDestory用于指定销毁方法(用在方法上)
@DependsOn:定义Bean初始化及销毁时的顺序
@Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
@Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用。如下:
@Autowired @Qualifier(“personDaoBean”) 存在多个实例配合使用
@Resource默认按名称装配,当找不到与名称匹配的bean才会按类型装配。
@PostConstruct 初始化注解
@PreDestroy 摧毁注解 默认 单例 启动就加载
@Async异步方法调用
3.2 Spring MVC注解
@Controller

@Controller 用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。

@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器。单单使用@Controller 标记在一个类上还不能真正意义上的说它就是SpringMVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式:

在SpringMVC 的配置文件中定义MyController 的bean 对象。

在SpringMVC 的配置文件中告诉Spring 该到哪里去找标记为@Controller 的Controller 控制器。

@RequestMapping

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。

@RequestBody

作用: 该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

使用时机:返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用;

@ModelAttribute | @SessionAttributes

该Controller的所有方法在调用前,先执行此@ModelAttribute方法,可用于注解和方法参数中,可以把这个@ModelAttribute特性,应用在BaseController当中,所有的Controller继承BaseController,即可实现在调用Controller时,先执行@ModelAttribute方法。

@SessionAttributes即将值放到session作用域中,写在class上面。

@PathVariable

用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。

@requestParam

@requestParam主要用于在SpringMVC后台控制层获取参数,类似一种是request.getParameter(“name”),它有三个常用参数:defaultValue = “0”, required = false, value = “isApp”;defaultValue 表示设置默认值,required 铜过boolean设置是否是必须要传入的参数,value 值表示接受的传入的参数类型。

3.3 Spring Boot注解
@SpringBootApplication

此注解是个组合注解,包括了@SpringBootConfiguration,@EnableAutoConfiguration和@ComponentScan注解。

@SpringBootConfiguration

@SpringBootConfiguration继承自@Configuration,二者功能也一致,标注当前类是配置类,并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中,并且实例名就是方法名。

@EnableAutoConfiguration

这个注解告诉Spring Boot根据添加的jar依赖猜测你想如何配置Spring。

@AutoConfigurationPackage:将主配置类所在的包作为自动配置的包进行管理

@Import:导入一个类到IoC容器里,具体的类是根据mete-inf下满的spring.factories的配置进行导入

@ComponentScan

@ComponentScan会扫描指定路径下的的类,并将其加入到Ioc容器中。在springboot中,@ComponentScan默认扫描@SpringBootApplication所在类的同级目录以及它的子目录。

@RestController

@RestController 是Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。

@RequestMapping

@GetMapping 等同于 @RequestMapping(method = RequestMethod.GET)
@PostMapping 等同于 @RequestMapping(method = RequestMethod.POST)
@PutMapping 等同于 @RequestMapping(method = RequestMethod.PUT)
@DeleteMapping 等同于 @RequestMapping(method = RequestMethod.DELETE)
@PatchMapping 等同于 @RequestMapping(method = RequestMethod.PATCH)

@Profile

Spring Profiles提供了一种隔离应用程序配置的方式,并让这些配置只能在特定的环境下生效。任何@Component或@Configuration都能被@Profile标记,从而限制加载它的时机。

全局异常处理:

@ControllerAdvice

包含@Component。可以被扫描到。统一处理异常。

@ExceptionHandler

用在方法上面表示遇到这个异常就执行以下方法。

4 SpringBoot注解运行机制
SpringBoot通过根据配置文件自动装配所属依赖的类,再用动态代理的方式注入到Spring容器中;

八 微服务
image-20201010104056006

服务治理:dubbo框架用的是zookeeper,SpringCloud用Eureka

服务路由:负载均衡,Nginx,SpringCloud内置的Ribbon实现负载均衡

服务容错:Hystrix(豪猪)实现服务的熔断,降级,限流

服务网关:Zuul,Spring Cloud Gateway

服务配置:Spring Cloud Config

服务安全:Spring Security,Spring Cloud Security

服务监控: Spring Cloud Sleuth

image-20201010104658520

Spring Cloud微服务框架使用Eureka做服务注册与发现,通过内置的Ribbon实现负载均衡;

Spring Cloud Gateway用做服务的网关,也可以使用Netflix的Zuul做网关,一个域名对应一组IP地址,请求进来之后通过负载均衡策略有序调度后台服务;

Spring Cloud Config用作统一配置中心,因为一个个的微服务如果都有自己的配置,那不好管理,所有我们通过配置中心做到配置统一管理

Spring Cloud Security服务安全框架,基于流行的OAuth2 协议的授权机制,以及基于 Token的资源访问保护机制。

Spring Cloud Sleuth服务链路跟踪,通过HTTP的请求,可以

分布式与微服务的理解

分布式
就是将不同业务分散到不同的地方:比如Mysql还有web项目部署在不同的服务器上
微服务框架
在同一个系统中,把不同的业务分成一个个单独的服务,每个服务可以进行单独的技术选型,独立部署,独立运维;
微服务部署方式
蓝绿部署

不停用老版本前提下,部署新版本,然后进行测试;确认新版本没问题,将流量切换到新版本,同时老版本也升级到新版本

0 downtime(零停机)

原理:通过冗余解决新老版本部署难题

蓝配置(inactive):

绿配置(active):

滚动发布

停止一个或多个服务器,执行更新,并重新投入使用。周而复始,知道集群中的所有实例都更新成功

相比蓝绿部署节省资源,但是缺点较多

没有一个确定可行的环境,蓝绿部署,绿版本是可行的

修改了现有的环境

回滚困难

灰度发布(金丝雀发布)

故事背景:矿井工人发现,金丝雀对瓦斯敏感,旷工下井前,先放一只金丝雀探路

灰度发布开始,先启动一个新版本应用,但是并不是直接将所有流量切过来,而是让测试人员对新版本进行线上测试,即金丝雀。没问题就将少量流量导入,再进行观察,收集数据,新旧版本进行数据比对(A/B测试),运行良好在逐步切换新版本,调整新旧版本运行的服务器副本数量,直到100%切换。

灰度发布过程中,发现新版本有问题可以将流量切回老版本,将负面影响控制在最小

0 SpringCloud核心组件
基本的五大组件:
1.使用SpringCloud开发,会产生很多个独立的微服务,这时候就需要一个服务治理,可以使用Eureka或者阿里的Nacos,把服务注册进去统一管理
2.服务调用的时候可能会有同一个服务被调用多次,这时就需要负载均衡,可以使用Ribbon或者Feign实现;作用是从注册中心拿到多个服务时,可以按照负载均衡策略来进行调度,减轻服务器的压力【常用的策略有轮询、权重、随机等】
3.调用服务时可能会出现某个服务宕机了,为了保证服务的健壮性,需要用到Hystrix(豪猪哥)来实现服务熔断、降级或者限流功能;
4.一个项目里有很多的配置,如果配置也分散到不同的微服务中,就不好管理,这时就需要用到配置中心,一般是用SPring Cloud Configure;
5.还有一个就是网关Zuul,我们有很多个微服务,这些服务最终都是要暴露给前台去调用,在前端如果一个服务就有一个地址,这样不好管理,使用网关就可以统一地址,另外还可以对使用的微服务做统一鉴权管理
REST:Representational State Transfer(表象层状态转变)

0.1 两代Spring Cloud核心组件比较
第一代Spring Cloud(Netflix) 第二代Spring Cloud(Alibaba)
服务注册中心 Netflix Eureka 阿里巴巴Nacos
客户端负载均衡 Netflix Ribbon 阿里巴巴Dubbo LB、Spring Cloud Loadbalancer
熔断器 Netflix Hystrix 阿里巴巴Sentinel
网关 Netflix Zuul 官方Spring Cloud Gateway
配置中心 官方Spring Cloud Config 阿里巴巴Nacos、携程Apollo
服务调用 Netflix Feign 阿里巴巴Dubbo RPC
消息驱动 官方Spring Cloud Stream
链路跟踪 官方Spring Cloud Sleuth/Zipkin
阿里巴巴Seata分布式事务方案
微服务各个核心组件相互配合,才能支持一个完整的微服务架构:

注册中心负责服务的注册与发现,将微服务链接起来

API网关组件负责转发外来的请求

断路器负责监控各服务之间的调用情况,连续多次失败进行熔断保护

配置中心提供统一的配置信息管理服务,实时通知各个微服务获取最新的配置信息

0.2 Spring Cloud 与Dubbo对比
Dubbo是阿里开源的高性能服务框架,基于RPC调用;Spring Cloud Netflix是基于HTTP调用的,效率没有Dubbo高;

Dubbo体系的组件不全,不能提供一站式解决方案,比如服务注册中心需要借助ZK实现;而SpringCloud Netflix提供了一站式解决方案,且有Spring家族支持;

0.3 Spring Cloud 与 Spring Boot的关系
SpringCloud 利用了SpringBoot的特点,快速地进行微服务组件开发,不适用SpringBoot,每个组件的相关Jar包都需要自己导入配置并考虑兼容性问题,不利于快速开发。

1 服务注册与发现(Eureka / Nacos)
1.1 服务信息的存放方式
硬编码:存放在服务消费者,通过配置调用;缺点是地址变化时需要重新维护配置;

数据库存储:存放在数据库,避免了硬编码方式;但是消费者还是需要感知服务提供者的地址信息;

服务注册中心:保存跟管理服务提供者的地址信息,以及服务发布的相关属性信息。

Pull模式获取服务列表:提供者将服务地址信息跟属性写入注册中心,消费者通过注册中心获取提供者列表缓存到本地;

Push模式获取服务列表:当服务提供者列表发生变化时,服务注册中心会主动的推送更新后的列表给消费者,消费者只需要从本地缓存的服务提供者路由表获取即可,提高可靠性;即使注册中心宕机了,还是可以通过缓存获取地址通信。

1.2 服务注册中心几个重要概念
注册中心Registry:

注册中心客户端Registry Client:服务提供者跟消费者都是注册中心的客户端

注册中心管理端Registry Console:注册中心数据管理

服务Service:包含至少一个接口

服务提供者Provider:暴露监听端口,提供服务

服务调用者Consumer:连接端口,发起远程调用

服务注册Service Registry:服务启动,将服务相关配置注册进服务注册表

服务发现Service Discovery:从服务注册表获取服务配置

1.3 Nacos = Eureka + Config + Bus(消息总线)
1.4 Eureka与Zookeeper的区别
Zookeeper是CP,Eureka是AP

Zookeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用

Eureka各个节点是平等关系,只要有一台Eureka就可以保证服务可用,而查询的数据可能不是最新的

Eureka的自我保护机制不再从注册列表移除因长时间没收到心跳而应该过期的服务

Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点(高可用)

当网络稳定时,当前实例新的注册信息会被同步到其他节点中(最终一致性)

Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会想Zookeeper一样使得整个注册系统瘫痪

Zookeeper有Leader和Follower角色,Eureka各个节点平等

Zookeeper采用过半数存活原则,Eureka采用自我保护机制解决分区问题

Eureka本质上是一个工程,Zookeeper只是一个进程

1.5 Eureka/Nacos/Zookeeper/Consul
组件名 语言 CAP 对外暴露接口
Eureka Java AP(自我保护机制,可用性) HTTP
Consul go CP HTTP/DNS
Zookeeper Java CP 客户端
Nacos Java 支持CP/AP切换 HTTP
1.6 Eureka如何保证高可用?
Eureka包含两个组件:Eureka Server 和Eureka Client

每个Eureka Server都是一个集群

Eureka Client微服务启动后会周期性向注册中心发送心跳(默认30S,默认90S后将未续约的服务剔除)以续约自己的信息

Eureka Server同时也是Eureka Client,多个Server之间通过复制的方式相互注册,实现同步

Eureka Client会缓存Eureka Server中的信息,即使Eureka Server节点全部宕掉,服务消费者仍能从本地缓存中找到消息提供者

总结:Eureka 通过心跳检测、健康检查、和客户端缓存机制,提高了系统灵活性、可伸缩性和高可用性

服务端自我保护机制

工作机制:如果在15分钟之内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制,此时出现以下几种情况:

Eureka Server 不再从注册列表中移除因为长时间没有收到心跳而应该过期的服务

Eureka Server仍然能够接受新服务的注册和查询请求,但是不会同步到其他节点上,保证当前节点依旧可用

当网络稳定时,当前Eureka Server新的注册信息会被同步到其他节点中。

1.7 Nacos数据模型(领域模型)
Namespace:命名空间,对不同环境进行隔离

Group:分组,将若干个服务或者若干个配置归集为一个组,通常一个系统归一个组

Service:某一个服务

DataId:配置集或者可以认为是一个配置文件

Namespace + Group + Service 如同Maven中的GAV坐标,GAV

2 负载均衡(Ribbon)
2.1 负载均衡实现方式
基于软件实现-----Nginx

基于DNS域名解析实现:一个域名对应一组web服务器IP地址,通过DNS服务器算法将一个域名请求分配到合适的真实服务器上

优点:省去了维护负载均衡服务器的麻烦;支持基于地理位置的域名解析,加快访问速度;稳定性高;

缺点:控制权在域名服务商手中,不易改善和管理;多级解析,每一级DNS都有缓存,可能某一台服务器已经下线了,但是缓存还有记录,导致访问失败;轮询算法,不能根据服务器的实际差异分配负载;

基于硬件实现:F5 Network Big-IP网络设备,类似于网络交换机,烧钱;

优点:性能好,每秒处理请求数达百万级;支持灵活的策略;具有防火墙安全功能;

缺点:烧钱(十几万到上百万)

2.2 负载均衡算法
轮询法

加权轮询法

随机法

加权随机法

源地址hash法:

根据获取的客户端IP地址,通过哈希函数计算得到一个数值,用该数值对服务器列表大小进行取模,得到的结果就是客户端要访问的服务器的序号,因此在后端服务器数量不变时命中率高;缺点是数量变化时命中率降低;

一致性hash法

解决源地址hash的缺点,通过虚拟节点,均匀分担机器负载;广泛用于分布式系统

底层通过hash环实现,环的起点是0,终点是2^32-1,起点跟终点相连形成环,中间的整数逆时针分布。

2.3 Ribbon可以使用一致性哈希算法的策略嘛?
可以使用,采用guava的一致性哈希算法

Ribbon内置的负载均衡策略

轮询:RoundRobinRule

随机:RandomRule

重试:RetryRule

最小连接数:BestAvailableRule

可用过滤:AvailablityFilteringRule

区域权衡(默认):ZoneAvoidanceRule

2.4 负载均衡分类
客户端负载均衡:Ribbon,服务消费客户端有一个服务器地址列表,调用方请求前通过负载均衡算法选择一个服务器进行访问,负载均衡算法执行是在请求客户端进行;

服务器端负载均衡:Nginx、F5,请求到达服务器之后由负载均衡器根据一定的算法将请求路由到目标服务器中处理。

2.5 Ribbon源码剖析
启动类中注入RestTemplate的Bean,添加@LoadBalanced注解;

经过SpringBoot自动装配原理,会为restTemplate对象设置一个LoadBalancerInterceptor定制器,即添加了负载均衡注解的RestTemplate对象会被添加一个拦截器loadBalancerInterceptor,此拦截器会对后续的拦截请求进行负载均衡处理;

3 API网关组件(SpringCloudGateway)
3.1 简介
性能高于Zuul,速度约为Zuul的1.6倍

提供统一的路由方式(反向代理),基于Filter链的方式提供网关基本功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等

3.2 网关控制具体方式
路由(Route):由一个ID、一个目标URL、一系列断言和Filter过滤器组成

断言(Predicates):参考Java8中的java.util.function.Predicate,开发人员匹配Http请求中的所有内容,断言为true,则匹配路由成功

过滤器(filter):标准的Spring webFilter,可以在请求之前或者请求之后执行业务逻辑

3.3 Spring Cloud Gateway工作流程
客户端(client)向Spring Cloud Gateway 发出请求;

在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到Gateway Web Handler;

Handler再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回结果给客户端;

发送请求之前(pre)的操作:参数校验、权限检验、流量监控、日志输出、协议转换等

发送请求之后(post)的操作:响应内容或响应头的修改、日志输出、流量监控等

3.4 Gateway路由断言规则
Spring Cloud Gateway 内置了很多Predicates功能,通过RoutePredicateFactory路由断言工厂实现各种路由匹配规则匹配到对应的路由

DateTime时间类断言:根据请求时间再配置时间之前/之后/之间

Cookie类断言:指定Cookie正则匹配指定值

Header请求头类断言:指定Header正则匹配指定值/请求头中是否包含某个属性

Host请求主机类断言:请求Host匹配指定值

Method请求方式类断言:请求Method匹配指定请求方式

Path请求路径类断言:请求路径正则匹配指定值

QueryParam请求参数类断言:查询参数正则匹配指定值

RemoteAddr远程地址类断言:请求远程地址匹配指定值

3.5 Gateway动态路由
yml配置文件中设置cloud.gateway.routes.uri时,uri是以lb://开头(lb代表从注册中心获取服务),后面是需要转发到的服务名称。

3.6 Gateway过滤器分类
分类

从过滤器生命周期分:

生命周期时机点 作用
pre 在请求被路由之前调用。可以利用此过滤器实现身份认证、在集群中选择请求的微服务、记录调试信息等
post 在路由到微服务之后执行。可以利用此过滤器为响应添加标准的HTTP、Header、收集统计信息和指标、将响应从微服务发送给客户端
从过滤器类型分:

过滤器类型 影响范围
GatewayFilter 应用到单个路由上
GlobalFilter 应用到所有路由上
3.7 Gateway高可用
启动多个Gateway实例实现高可用,使用Nginx等负载均衡设备进行负载转发以达到高可用目的

#配置多个GateWay实例
upstream gateway {
server 127.0.0.1:9002;
server 127.0.0.1:9003;
}
location / {
proxy_pass http://gateway;
}
4 服务熔断、降级、限流、容错(Hystrix / Sentinel)
引入:微服务的雪崩效应:因为服务提供者不可用,导致服务调用者不可用,并逐渐放大的现象;

服务雪崩的原因:

服务提供者不可用

重试加大请求流量

服务调用者不可用

解决服务雪崩的方案:

服务熔断:切断对下游服务的调用

服务降级:把不关紧要的服务停掉,返回一个兜底数据

服务限流:限制并发/请求量

限制总兵发数(如线程池、数据库连接池)

限制瞬时并发数(如Nginx限制瞬时并发连接数)

限制时间窗口内的平均速率

限制远程接口调用速率,限制MQ的消费速率等

4.1 服务限流方式
计数器

从第一个请求进来后的一秒钟内,每来一个,计数器加1,达到限流的QPS,就拒绝后续的请求

漏桶

固定容量的漏桶按照常量固定速率出水

可以按照任意速率流入水滴到漏桶,超出就会溢出(被丢弃)

桶容量不变

可以让突发流量被整型,为网络提供稳定流量

令牌桶

令牌按固定速率放入令牌桶,桶中最多可以存放固定的Token令牌数,装满后就被丢弃或拒绝;

来一个请求,删一个令牌

加令牌的速度决定了数据通过接口的速度,从而控制接口的流量

4.2 漏桶算法与令牌桶算法的区别
前者按照固定速率流出请求,流入速率任意,流入的请求溢出时,新的请求被拒绝

后者是按固定速率放入令牌,请求是否被处理取决于桶里的令牌数量,数量为0则拒绝请求

针对突发流量,令牌桶可以允许一定程度的突发流量;漏桶通过限制流出速率,可以使突发流量速率平滑;

4.3 服务降级方式
服务降级开关

属于人工降级

通过设置分布式降级开关,实现服务降级,然后集中式管理开关配置信息

image-20201008100208928

自动降级

超时降级

超出定义的最大响应时间,且该服务不是系统的核心服务时,可以在超时后自动降级

失败次数降级

失败次数达到阈值自动降级,可以使用异步线程探测服务是否恢复,恢复即取消降级

故障降级

系统出现网络故障、DNS等故障就直接降级;处理方案有:返回默认值、兜底数据、缓存数据等

限流降级

因访问量过大而导致系统崩溃,可以通过限流,达到阈值就降级;处理方案有:使用排队页面、错误页等

读服务降级

对于非核心业务,读接口有问题时,可切换到缓存、走静态化、读默认值、返回友好的错误页

对于前段页面,可以将动态化页面静态化,提升性能

写服务降级

对于写操作频繁的服务,如“双十一”,用户下单、加购物车、结算等

策略:

同步写操作转异步

先缓存,异步写入DB

先缓存,在流量低峰,定时写入DB

应用实例:比如“秒杀系统”,先扣减redis缓存,正常同步扣减DB库存,在流量高峰期DB扛不住时,可以降级为发送一条扣减信息给DB,异步进行扣减库存,实现最终一致性。如果还是有压力,可以直接扣减缓存,等流量低峰时定时写入DB中

4.4 服务容错策略
失败转移(Failover)

失败自动恢复(Failback)

快速失败(Failfast)

失败缓存(FailCache)

4.5 服务雪崩效应
因服务提供者不可用导致服务消费者不可用,并将不可用逐渐放大的过程

4.6 Hystrix(豪猪哥)如何实现延迟和容错
包裹请求:使用@HystrixCommand包裹对依赖的调用逻辑。页面静态化服务方法(@HystrixCommand添加Hystrix控制)

跳闸机制:服务错误率超过阈值时,Hystrix跳闸,停止请求该服务一段时间

资源隔离:Hystrix为每个依赖都维护一个小型的线程池(舱壁模式)。如果线程池满了,请求会被拒绝,而不是排队等候,即快速失败

监控:Hystrix可以实时监控运行指标和配置变化,如成功、失败、超时、被拒绝的请求等

回退机制:当请求失败、超时、被拒绝,或断路器打开时,执行回退逻辑

自我修复:断路器打开一段时间后,自动进入“半开”状态

4.7 Hystrix实现降级/熔断微服务开发步骤
在pom文件引入Hystrix依赖

在入口类添加@EnableHystrix注解

在controller类中的方法上添加@HystrixCommand注解,设置fallbackMethod方法,配置超时时间,超过时间则会执行fallbackMethod方法并返回客户端

4.8 服务优先级调度
系统资源非常有限时,会触发服务优先级策略

服务实例数量调整:

控制服务实例数量,当资源紧张时移除优先级低的实例

资源不紧张时,再调整服务实例数

加权优先级队列

服务接收请求时,根据消息对应的优先级写入不同优先级队列汇总,

没有设置优先级的写入默认队列

线程调度器

将服务优先级映射到线程优先级,通过创建不同优先级的线程,分别调度不同的服务

java中通过Thread的setPriority方法设置线程优先级

4.9 Sentinel
Sentinel与Hystrix区别

Hystrix需要自己搭建DashBoard监控平台

没有提供UI界面进行熔断降级等配置(@HystrixCommand参数配置,属于代码入侵)

Sentinel有独立可部署的DashBoard控制台组件(一个jar文件,可以直接运行)

Sentinel减少代码开发,通过UI界面配置即可完成细粒度控制

Sentinel的两个核心部分

核心库(Java客户端)不依赖任何框架,项目需要引入的依赖文件

控制台(Dashboard)基于SpringBoot开发,打包后就可以直接运行

5 分布式配置中心(Spring Cloud Config)
微服务架构体系因为数量多,服务之间又有互相调用关系,通过地址进行调用,万一地址改动,传统的方式就需要重启解决,微服务配置中心则有三个特点:

可以进行配置集中统一管理,

在运行期间动态调整配置信息,

修改后实时自动刷新,而无需重启。

5.1 Spring Cloud Config简介
解决分布式系统的配置管理方案,包含client和Server部分

Server提供配置文件存储,以接口形式将配置文件提供出去;

Client通过接口获取数据,并以此数据初始化应用;默认使用Git存放配置文件

提供的功能

提供服务端与客户端支持

集中管理配置文件

配置文件修改后,快速生效

版本管理

支持大的并发查询

支持各种语言

5.2 Spring Cloud Config开发步骤
基于远程方式获取配置

服务端开发流程

创建config-server项目

pom中引入依赖

在启动类上添加@EnableConfigServer注解,开启Spring Cloud Config的服务端功能

在application.propertites文件添加配置服务的基本信息以及Git仓库的相关信息(服务器名、服务端口、spring.cloud.config.server.uri/label/username/password)

客户端开发流程

创建config-client项目

pom中引入依赖

再application.properties中添加配置信息

再在资源rescources目录下创建配置文件bootstrap.properties文件配置spring.cloud.config配置的Git信息【在resources目录下创建了两个配置文件:application.properties(默认的)和bootstrap.properties。这两个文件都可以用于Spring Boot的配置,只是bootstrap.properties中的配置优先级高于前者,而bootstrap.properties一般用来加载外部配置。】

在Controller类中的配置属性上添加(@Value("$config.name}")注解

基于本地方式获取配置

开发步骤

在原项目基础上修改config-server项目的配置application.properties

spring.cloud.config.server.native.search-locations指定本地配置文件路

spring.profiles.active=native指定使用本地配置方式

resources资源目录下创建shared目录,配置dev.properties和pre.properties配置文件

修改config-client项目的bootstrap.properties配置

spring.cloud.config.uri:指定config-server配置中心地址

config.profile=dev:指定profile。

5.3 实现自动动态刷新配置
实现一次通知,处处生效。结合消息总线(Bus)实现,底层是MQ。

Spring Cloud Bus消息总线对外提供一个HTTP接口,将接口配置到远程的Git上,当项目开发人员修改项目配置后,Git文件内容发生变动,就会自动调用HTTP接口通知config-server。

config-server会发布更新消息到消息队列中,其他客户端服务订阅到该消息就会刷新项目配置

从而实现整个微服务项目的自动刷新配置

5.4 实现config手动刷新配置
不需要重启服务,手动访问一个刷新地址,再访问服务即可

6 Feign远程服务调用组件
6.1 简介入门
Netflix开发的轻量级的RESTFul的HTTP服务客户端,用来发起请求,远程调用的

以java接口方式调用HTTP请求,不需要象Java中通过封装HTTP请求报文直接调用

不需要拼接URL去调用RestTemplate的api,只需要在消费者一端创建一个feign接口,配合注解@FeignClient(name=“服务提供者配置文件中服务名” fallback=“定义的实现类的兜底回退.class文件”) 即可完成

Spring Cloud增强了Feign,使Feign支持SpringMVC注解

本质:封装了HTTP调用流程,更符合面向接口化编程习惯,类似于Dubbo的服务调用

6.2 Feign支持负载均衡
Feign集成了Ribbon,实现负载均衡

通过配置文件设置重试次数,切换服务器重试次数

通过配置文件设置超时时间

6.3 Feign支持熔断器
开启Hystrix,Feign中的方法都会进行管理,一旦出现问题就进入对应的回退逻辑

如果Hystrix跟Feign都设置了超时时间,按照最小值执行

通过自定义回退方法,实现Feign接口,注册进Spring容器进行管理,实现feign接口的实现类实质是提供兜底数据处理

6.4 支持对请求的压缩跟响应压缩
配置文件中配置Compression属性中的request跟response属性配置

6.5 Feign接口的使用步骤
引入openFeign组件依赖

在application启动类上加@EnableFeignClients注解(注意注释掉启动类@EnableCircuitBreaker熔断的注解)

自定义一个Feign接口,编写相关controller接口方法

在对应的controller类中注入Feign接口,直接调用方法

7 服务安全
8 链路跟踪
9 服务发布
9.1 服务发布的方式
注解方式

在业务层加上@Service(version=“版本号”)注解

在Controller层使用@Reference远程消费

XML配置化方式

image-20201008103444179

image-20201008103531404

API调用方式

image-20201008103556989

10 日志框架
10.1 微服务常用的日志类型
访问日志

针对客户端访问,确认客户端的请求是否被服务端接收到的类型

性能日志

针对提供服务的接口,依赖的接口,关键的程序路径等统计响应时间,打印到专用的性能日志中,用来监控跟排查

远程服务调用日志

一个服务依赖于其他服务,通过使用RPC和REST来调用,打印耗时日志,可以排查超时或接口调用错误等问题

业务系统应用日志

Log4j、Slf4j、Logback、log4j2

分为Trace、Debug、Warn、info、Error等级别

VM的GC日志

10.2 ELK日志框架(Elasticsearch、Logstash、Kibana)
Elasticsearch

一个基于Lucene搜索引擎的NoSQL数据库

Logstash

一个基于管道的处理工具,它从不同的数据源接收数据,执行不同的转换,然后发送数据到不同的目标系统

Kibana

工作在Elasticsearch上,是数据的展示层系统

ELK系统通常用于现代服务化系统的日志管理。Logstash用来收集和解析日志,并且把日志存储到Elasticsearch中并建立索引,Kibana通过可视化的方式把数据展示给使用者

11 监控
11.1 微服务监控类型
容器与宿主机的监控

API监控

调用链监控

应用本身监控

11.2 spring boot admin监控系统
UI部分使用AngularJS将数据展示在前端

包括两部分:服务端和客户端。服务端实际上是一个应用程序,用于收集应用程序的监控信息;客户端需要在Spring Boot应用程序中进行相关配置,才能在运行时与服务端建立通信,将自身的应用程序注册到SpringBoot Admin的服务端中

12 存储与解耦
12.1 分布式数据库架构
因为分布式架构数据库的数据量会不断增长,库跟表的数量会越来越多,随着带来跟高的磁盘、IO、系统开销,数据表单达到千万级别后就会降低Sql的性能,所以要进行优化。

分库

含义:根据业务需要将原库拆分成多个库,降低单库的大小来提高单库的性能

垂直分库:根据业务进行划分,将同一类的业务相关数据划分在同一个库中

水平分库:按照一定的规则划分,每个数据库的各个表结构相同,数据存放在不同数据库中

分表

含义:根据业务需要将大表拆分成多个子表,通过降低单表的大小来提高单表的性能

垂直分表:将一个大表按照功能拆分成多个分表;例如:基本信息表跟详细表

水平分表:按照一定规则划分表,每个数据表的结构相同,数据存储在多个分表中

注:实际项目中尽量使用分库,因为分表依旧公用数据库文件,存在磁盘IO的竞争

水平切分的方式

范围法:例如根据用户id范围进行分表

哈希法:例如对用户id进行取模,划分几个数据库就对几取模

垂直切分的方式

考虑的因素长度,访问频率

长度短,访问频率高的放一起

访问频率低,长度长的放一起

因为数据库会以行为单位将数据加载到内存里,在内存容量有限的情况下,长度短且访问频度高的属性,内存能够加载更多的数据,命中率会更高,磁盘IO会减少,数据库的性能会提升。

分组

含义:主从构成的数据库集群成为分组

12.2 Mycat分库分表
Mycat是开源的分布式数据库系统,核心功能是分表分库

12.3 分布式事务
数据库事务的ACID是通过InnoDB日志和锁来保证的。

事务的隔离性时通过数据库锁的机制实现的。

原子性和一致性是通过Undo Log来实现的。Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log),然后进行数据的修改。如果出现了错误或者用户执行了Rollback语句,系统就可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

持久性是通过Redo Log(重做日志)来实现的。和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是Redo Log已经持久化。系统可以根据Redo Log的内容将所有数据恢复到最新的状态。

分布式事务

水平拆分:指由于单一节点无法满足性能需求,需要扩展为多个节点,多个节点具有一致的功能,组成一个服务池,一个节点服务一部分的请求量,所有节点共同处理大规模高并发的请求量。垂直拆分:指按照功能进行拆分,把一个复杂的功能拆分为多个单一、简单的功能,不同的单一功能组合在一起,和未拆分前完成的功能是一样的。由于每个功能职责单一、简单,使得维护和变更都变得更简单、容易、安全,所以更易于产品版本的迭代,还能够快速地进行敏捷发布和上线。

CAP定理

C(Consistency):一致性,分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本。

A(Availability):可用性,非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键:一个是合理的时间;另一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回;合理的响应指的是系统应该明确返回结果并且结果是正确的。

P(Partition Tolerance):分区容错,当出现网络分区后,系统能够继续工作。

如果选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

对于CP来说,放弃可用性,追求一致性和分区容错性,ZooKeeper其实追求的就是强一致。

对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,

BASE理论

BASE理论指的是:(1)Basically Available(基本可用)(2)Soft State(软状态)(3)Eventually Consistent(最终一致性)BASE理论是对CAP中的一致性和可用性进行权衡的结果,理论的核心思想是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

两阶段提交(2PC)

二阶段提交2PC(Two Phase Commit)是一种在分布式环境下,所有节点进行事务提交,保持一致性的算法。它通过引入一个协调者(Coordinator)来统一掌控所有参与者(Participant)的操作结果,并指示它们是否要把操作结果进行真正的提交(Commit)或者回滚(Rollback)。

2PC分为两个阶段:

(1)投票阶段(Voting Phase):参与者通知协调者,协调者反馈结果。

(2)提交阶段(Commit Phase):收到参与者的反馈后,协调者再向参与者发出通知,根据反馈情况决定各参与者是提交还是回滚。

补偿事务(TCC)

TCC其实就是采用的补偿机制,其核心思想是:针对每个操作都要注册一个与其对应的确认和补偿(撤销)操作。

TCC分为3个阶段:

Try阶段:主要是对业务系统进行检测及资源预留。

Confirm阶段:主要是对业务系统进行确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的,即只要Try成功,Confirm一定成功。

Cancel阶段:主要是在业务执行错误,需要回滚的状态下将执行的业务取消,将预留资源释放。

后置提交

image-20201008144535305

扣减余额耗时200ms,提交事务耗时15ms。扣减积分耗时100ms,提交事务耗时15ms。扣减优惠券耗时100ms,提交事务耗时15ms。当扣减积分或者扣减优惠券执行异常的时候(如服务器重启、数据库异常等),就可能导致数据不一致,

image-20201008144610148

改变事务执行与提交的时序,变成事务先执行,最后一起提交

image-20201008144631894

后置提交虽然降低了数据不一致的概率,但是所有库的连接要等到所有事务执行完才释放,这就意味着数据库连接占用的时间增长了,系统整体的吞吐量降低了。

本地消息表(异步确保)

image-20201008144845514

本地消息表的基本思路如下:

消息生产方需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说它们要在一个数据库里面。然后消息会经过MQ(Message Queue)发送到消息的消费方。如果消息发送失败,就会进行重试发送。

消息消费方需要处理这个消息,并完成自己的业务逻辑。此时,如果本地事务处理成功,就表明已经处理成功了;如果处理失败,就会重试执行。如果是业务上面的失败,那么可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

13 微服务解耦Kafka
13.1 同步/异步调用
同步调用

异步调用

在什么场景下可以使用MQ实现异步调用?在什么场景下又必须使用同步调用呢?
答案是:如果上游服务需要实时关注执行结果,就要使用“同步调用”,
如果上游服务不关注执行结果,就可以使用MQ异步调用。

例如用户发博客、发微信、点赞和评论等功能。用户发布一条博客或者帖子,发布服务将博客发布之后,直接告诉用户博客发布成功了,发布服务并不关心接下来的事情:系统要重新统计用户总共发布了多少条博客,统计用户的积分,统计用户目前的排名,统计用户星级,等等。也就是说,上游服务(发布服务)并不实时关心之后统计博客总数、计算用户积分、计算用户排名等一系列结果,这时MQ异步调用就派上用场了
13.2 Kafka
基本概念:一款开源的、轻量级的、分布式的、可分区和具有复制备份的(Replicated)、基于ZooKeeper协调管理的分布式流平台的功能强大的消息系统

流式处理平台三个特性:

能够允许发布和订阅流数据。从这个角度来讲,平台更像一个消息队列(MQ)或者企业级消息系统。

存储流数据时提供相应的容错机制。

当流数据到达时能够被及时处理。

基本体系结构

image-20201008153207065

生产者:负责生产消息,将消息写入Kafka集群。

消费者:从Kafka集群中拉取消息。

详细阐述

主题:将一组消息抽象归纳为一个主题(Topic)

消息:Kafka通信的基本单位,由一个固定长度的消息头和一个可变长度的消息体构成

分区和副本

将一组消息归纳为一个主题,而每个主题又被分成一个或多个分区(Partition),每个分区由一系列有序、不可变的消息组成,是一个有序队列

每个分区又有一至多个副本(Replica),分区的副本分布在集群的不同代理上,以提高可用性

Leader副本和Follower副本

由于Kafka副本的存在,因此需要保证一个分区的多个副本之间数据的一致性,Kafka会选择该分区的一个副本作为Leader副本,而该分区其他副本作为Follower副本,

只有Leader副本才负责处理客户端的读/写请求,Follower副本从Leader副本同步数据。

偏移量

日志段

生产者

负责将消息发送给代理,也就是向Kafka代理发送消息的客户端。

消费者和消费组

消费者(Consumer)以拉取方式获取数据,它是消费的客户端。在Kafka中,每一个消费者都属于一个特定的消费组(Consumer Group),我们可以为每个消费者指定一个消费组,以groupId代表消费组名称,通过group.id配置项设置

ISR

Kafka在ZooKeeper中动态维护了一个ISR(In-Sync Replica),即保存同步的副本列表,该列表中保存的是与Leader副本保持消息同步的所有副本对应的代理节点ID

14 分布式服务Session
Cookie

诞生原因:HTTP是无状态协议,服务器无法仅从网络连接上就知道访问者的身份,因此诞生Cookie技术

实质:小段文本信息

工作原理:

客户端请求服务器,如果服务器需要记录该用户的状态,就使用Response向客户端浏览器颁发一个Cookie。

客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。

服务器检查该Cookie,以此来辨认用户的状态。

服务器还可以根据需要修改Cookie的内容。

Session

出现的原因:Cookie很多时会增加客户端与服务端的数据传输量,影响性能

同一个客户端和服务端交互时,不需要每次都传回所有的Cookie值,只要传回一个ID,即JSESSIONID

Session是服务器上的一种用来存放用户数据的类HashTable结构。浏览器第一次发送请求时,服务器自动生成一个HashTable和一个Session ID来唯一标识这个HashTable,并将其通过响应发送到浏览器。浏览器第二次发送请求会将前一次服务器响应的Session ID放在请求中一并发送到服务器上,服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的HashTable。

Session一致性问题

单机情况下,每次HTTP连接请求都能够正确路由到存储Session的对应Web服务器。

分布式情况下,如果每台服务器都把Session保存在自己的内存中,不同服务器之间就会造成数据不一致问题

九 全文检索引擎Lucence & ElasticSearch
9.1 全文检索
数据分类

结构化数据:即行数据,存储在数据库中,可以用二维表结构来逻辑表达实现的数据。特点:固定格式,固定长度;

非结构化数据:不定长或无固定格式的数据(结构化之外的所有数据)

文本文件:txt、电子表格、ppt、word

电子邮件、社交媒体、媒体

数据库底层的文件存储方式

存储的物理方式:硬盘是块状存储,基本单位是1kb/块

非结构化的数据查询:

顺序扫描法:从头看到尾,例如windows的文件搜索功能,速度慢;

全文检索:计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数跟位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方法,类似字典的目录查字的过程。

9.2 Lucene实现全文检索
简介:apache下的开放源码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。

使用场景:

在应用中为数据库中的数据提供全文检索实现

开发独立的搜索引擎服务、系统

特点

稳定,索引性能高

高效、准确、高性能的搜索算法

跨平台

全文检索的应用场景

数据量大、数据结构不固定的数据可采用全文检索方式检索

单机软件的搜索:word、markdown…

站内搜索:京东、淘宝…

搜索引擎:百度、谷歌…

Lucence使用倒排索引进行检索

用户搜索条件会被切分成词条

将搜索条件分词去索引中查询,匹配词条

通过词条获取倒排列表,通过倒排列表获得Document队列,将document列表封装返回

9.3 ES
功能

分布式搜索引擎

全文检索

数据分析引擎

对海量数据进行近乎实时处理

特点

分布式

JSON

Restful风格

近实时搜索

安装方便

支持超大数据

基本概念

节点-node

集群-cluster

分片-shard

副本-replica

查询

查询所有(match_all)

匹配查询(match)

词条匹配(term)

布尔组合(bool)

must:必须包含

must_not:必须不包含

should:可以包含

范围查询(range)

模糊查询(fuzzy)

十 Docker容器
1 Docker基本概念
开源的应用容器引擎

基于Go语言遵从Apache2.0协议开源

轻量级、可移植容器

完全沙箱机制(sandbox),应用包之间没有任何接口欧,性能开销极低

2 Docker虚拟化的优势
高效利用系统资源:体现在相同配置的主机可以运行更多的应用

快速的启动时间:Docker容器直接运行在宿主内核中,无须启动完整的操作系统,可达到毫秒级的启动时间,节约开发时间

一致的运行环境:传统开发常遇见开发环境、测试环境、生产环境不一致导致的问题,Docker镜像提供完整的运行时环境,确保一致性

持续交付跟部署:使用DockerFile构建透明化镜像,开发跟测试以及运维都能理解应用运行环境

迁移简单:因为一致性,所以在物理机、虚拟机、公有云、私有云甚至笔记本运行结果都是一致的

已维护跟扩展

3 几个基本概念
镜像(Image):类似一个Linux文件系统,提供容器运行时所需的程序、库、资源、配置等文件,还有配置参数

容器(Container):镜像跟容器类似类跟实例的关系;容器时镜像的运行时实体。容器可以被创建、启动、停止、删除和暂停

仓库(Repository)、镜像注册中心(Docker Registry)

一个镜像注册中心对应多个仓库,每个仓库可包含多个标签,每个标签对应一个镜像。

十一 Kubernetes(k8s)
1 基本概念
2014年google的开源项目,是自动化容器操作的开源平台,包括部署、调度、节点集群间扩展;

k8s不仅支持Docker,还支持Rocket,是另一种容器技术;

主要的功能:

自动装箱:自动化容器的部署和复制

水平扩展:随时扩展或收缩容器规模

将容器组织成组,并且提供容器间的负载均衡

滚动更新:轻松升级应用程序容器的新版本

自动修复:提供容器弹性,如果容器失效就替换它

几个重要概念

Cluster:计算、存储和网络资源的集合

Master(主控节点):是Cluster的大脑,决定将应用放在哪里运行

API server:集群统一入口,以restful风格方式,交给etcd存储

Controller-manager:处理集群中常规后台任务,一个资源对应一个控制器

scheduler:节点调度,选择node节点应用部署

etcd:存储系统,保存集群相关的数据

Node(工作节点):运行容器应用,由Master管理,监控并汇报容器状态,按Master要求管理容器的生命周期

kubelet:master派到node节点的代表,管理本机容器

kube-proxy:提供网络代理,负载均衡操作

Pod:k8s的最小工作单元,每个Pod包含一个或多个容器,作为一个整体被Master调度到一个Node上运行;所有容器使用同一个命名空间(nameSpace),即IP跟Port相同;Pod的使用方式有两种:

one-container-per-pod:将单个容器简单封装成Pod,是k8s最常见的模型

many-container-per-pod:将联系紧密且需要直接共享资源的容器封装到Pod

Controller:k8s通过Controller管理Pod。Controller定义了pod 的部署特性:如几个副本、在哪个Node运行等,k8s提供了Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job等多种Controller

确保预期的pod副本数量

有/无状态应用部署

确保所有node运行同一个pod

一次性任务和定时任务

Service:定义了外界访问一组特定Pod的规则

NameSpace:可以将一个物理的Cluster逻辑上划分成多个虚拟的Cluster,每个Cluster就是一个NameSpace,不同的NameSpace资源完全隔离,k8s默认创建3个NameSpace

2 k8s架构
image-20201009082308866

k8s-master节点架构—【也是一个Node节点】:

API Server:API Server提供HTTP/HTTPS RESTful API,即Kubernetes API。APIServer是Kubernetes Cluster集群的前端接口,各种客户端工具以及Kubernetes其他组件可以通过它管理集群的各种资源。

Scheduler:Scheduler在调度时会充分考虑Cluster集群的拓扑结构,负责决定将Pod放在哪个Node上运行。Scheduler会充分考虑当前各个节点的负载,以及应用对高可用、性能、数据亲和性的需求。

Controller Manager:Controller Manager由多种Controller组成,包括Replication Controller、Endpoints Controller、Namespace Controller、Service Accounts、Controller等。Controller Manager负责管理Cluster集群的各种资源,保证资源处于预期的状态。不同的Controller管理不同的资源。例如,Replication Controller管理Deployment、StatefulSet、DaemonSet的生命周期,Namespace Controller管理Namespace资源。

Etcd:负责保存Kubernetes Cluster集群的配置信息和各种资源的状态信息。当数据发生变化时,etcd会快速地通知Kubernetes相关组件。

Pod网络:Pod要能够相互通信,Kubernetes Cluster必须部署Pod网络,Flannel是其中一个可选方案。

Node节点架构:

kubelet:kubelet是Node节点的agent,当Scheduler确定在某个Node上运行Pod后,会将Pod的具体配置信息(image、volume等)发送给该节点的kubelet,kubelet根据这些信息创建和运行容器,并向Master报告运行状态。

kube-proxy:Service在逻辑上代表后端的多个Pod,外界通过Service访问Pod。每个Node都会运行kube-proxy服务,它负责将访问Service的TCP/UPD数据流转发到后端的容器。如果有多个副本,kube-proxy就会实现负载均衡。

3 k8s集群部署应用的流程
kubectl发送部署请求到API Server。

API Server通知Controller Manager创建一个Deployment资源。

Scheduler执行调度任务,将两个副本Pod分发到k8s-node1和k8s-node2。

k8s-node1和k8s-node2上的kubectl在各自的节点上创建并运行Pod(kubectl创建Deployment,Deployment创建ReplicaSet,ReplicaSet创建Pod)。

应用的配置和当前的状态信息保存在etcd中,执行kubectl get pod时APIServer会从etcd中读取这些数据。

Pod网络会为每个Pod都分配IP。因为没有创建Service,所以目前kube-proxy还没参与进来

注意:每个Pod都有自己的IP地址。当Controller用新Pod替代发生故障的Pod时,新Pod会分配到新的IP地址。这样就产生了一个问题:如果一组Pod对外提供服务(比如HTTP),它们的IP很有可能发生变化,那么客户端如何找到并访问这个服务呢?Kubernetes给出的解决方案是Service。

Kubernetes Service从逻辑上代表了一组Pod。Service有自己的IP,而且这个IP是不变的。客户端只需要访问Service的IP,Kubernetes则负责建立和维护Service与Pod的映射关系。无论后端Pod如何变化,对客户端不会有任何影响,因为Service没有变。Service提供了访问Pod的抽象层。无论后端的Pod如何变化,Service都作为稳定的前端对外提供服务。同时,Service还提供了高可用和负载均衡功能,Service负责将请求转发给正确的Pod。

Pod的ip还是Service的Cluster IP 都是集群内可见,对外不可见,所以k8s提供了两种方式让外界可以与Pod通信:

NodePort:Service通过Cluster集群节点的静态端口对外提供服务,外部可以通过:访问Service。

LoadBalancer:Service利用Cloud Provider提供的LoadBalancer对外提供服务,Cloud Provider负责将LoadBalancer的流量导向Service。目前支持的CloudProvider有GCP、AWS、Azure等

image-20201009083059859

4 k8s集群监控的方式
Weave Scope可以展示集群和应用的完整视图。其出色的交互性让用户能够轻松对容器化应用进行实时监控和问题诊断。

Heapster是Kubernetes原生的集群监控方案。预定义的Dashboard能够从Cluster和Pod两个层次监控Kubernetes。

Prometheus Operator可能是目前功能最全面的Kubernetes开源监控方案。除了能够监控Node和Pod外,还支持集群的各种管理组件,比如API Server、Scheduler、Controller Manager等。

5 k8s集群日志管理
Elasticsearch附加组件来实现集群的日志管理。这是Elasticsearch、Fluentd和Kibana的组合。

Elasticsearch是一个搜索引擎,负责存储日志并提供查询接口。

Fluentd负责从Kubernetes搜集日志并发送给Elasticsearch。

Kibana提供了一个Web GUI界面。

6 K8S与Docker的区别
定义:

Docker是一个开源的应用容器引擎,开发者可以打包他们的应用以及依赖到一个可移植的容器中,发布到流行的Linux机器上,也可以实现虚拟化;

k8s是一个开源的容器集群管理系统,可以实现容器集群的自动化部署,自动化扩缩容,维护等功能;

从虚拟化角度

传统的虚拟技术,在将物理硬件虚拟成多套硬件后,需要在每套硬件上都部署一个操作系统,接着在这些操作系统上运行相应的应用程序

Docker容器内的应用程序进程直接运行在宿主机的内核上,Docker引擎将一些各自独立的应用程序和他们各自的依赖打包,相互独立直接运行在未经虚拟化的宿主机硬件上,同时多个容器也没有自己的内核,显然比传统虚拟机更轻便。每个集群有多个节点,每个节点可以部署多个容器,k8s就是管理这些应用程序所在的小运行环境(container)而生

从部署角度:

传统方式是将所有应用直接部署在同一个物理机器上,这样每个App的依赖都是完全相同的,无法做到App之间的隔离,当然,为了隔离,可以通过创建虚拟机的方式来将App部署到其中,但这样太繁重,所以比虚拟机更轻便的Docker容器技术出现,可以通过Container容器的技术来部署应用,全部的Container运行在容器引擎上即可。

k8s去管理Docker集群,可以及将Docker看成k8s内部使用的低级别组件,另外,k8s不仅支持Docker,也支持Rocker,另一种容器技术。

十一 分库分表
1 分库分表的方式
垂直分库:根据业务不同,分别存放在不同的库中,这些库分别部署在不同的服务器。

垂直分表:将一个表按照字段分成多表,每个表存储其中一部分字段。

  • 好处:
    • 解决业务层面的耦合,业务清晰
    • 对不同业务的数据进行分级管理、维护、监控、扩展
    • 高并发下,垂直分库一定程度的提高访问性能
  • 缺点:没有彻底解决单表数据量过大的问题
    水平分库:把一张表的数据按照一定规则,分配到不同的数据库中,每个库只有这张表的部分数据。

水平分表:把一张表的数据按照一定规则,分配到同一个数据库的多张表中,每个表只有这个表的部分数据。

2 Mycat
基本概念

逻辑库(scheam)

逻辑表(table)

分片节点(dataNode)

节点主机(dataHost)

读写分离

主从复制

通过搭建主从架构,将数据库拆分成主库和从库,主库负责处理事务性得增删改操作,从库负责处理查询操作,能够有效避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善;

读写分离

读写分离就是让主库处理事务性操作,从库处理select查询。数据库复制被用来把事务性查询导致的数据变更同步到从库,同时主库也可以select查询。

3 ShardingJDBC
分库分表带来的问题

事务一致性问题

跨节点关联问题

分页查询问题

主键避重问题

公共表问题

简介

轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架使用

适用于任何基于Java的ORM框架:JPA、Hibernate、Mybatis、SpringJDBC Template或直接使用JDBC;

基于任何第三方的数据库连接池:DBCP、C3P0、Druid

支持任意实现JDBC规范的数据库:Mysql、Oracle、SQLServer和PostgreSQL

主要功能

数据分片

读写分离

Mycat和Sharding-JDBC的区别

Mycat是中间件的第三方应用,Sharding-JDBC是一个jar包;

使用Mycat不需要修改代码,使用Sharding-JDBC需要修改代码;

Mycat基于Proxy,复写了MySQL协议,将Mycat Server伪装成一个MySQL数据库,而Sharding-JDBC是基于JDBC的扩展,是以jar包的形式提供轻量级服务的;

分库分表的策略

分库策略:目的是将一个逻辑表 , 映射到多个数据源

分库找的是数据库 db$->{user_id % 2 + 1}

spring.shardingsphere.sharding.tables.逻辑表名称.database-strategy.分片策略.分 片策略属性名 = 分片策略表达式
分表策略: 如何将一个逻辑表 , 映射为多个 实际表

#分表 找的是具体的表 pay_order_$->{order_id % 2 + 1
spring.shardingsphere.sharding.tables.逻辑表名称.table-strategy.分片策 略.algorithm-expression = 分片策略表达式
Sharding-JDBC支持的几种分片策略

standard:标准分片策略

complex:符合分片策略

inline:行表达式分片策略,使用Groovy的表达式.

hint:Hint分片策略,对应HintShardingStrategy。

none:不分片策略,对应NoneShardingStrategy。不分片的策略。

十二 Linux系统常用指令
1 查找命令
which [文件名]

find [路径信息] -name [文件名]

2 查看进程命令
ps -le :查看系统中所有进程,使用linux标准命令

ps aux:查看系统所有进程,使用BS操作系统格式

可选项

a:显示一个终端所有进程

u:显示进程的归属用户以及内存的使用情况

x:显示没有控制终端的进程

-l:长格式显示更加详细的信息

-e:显示所有进程

3 创建和查看文件的方式
创建文件

vi/vim filename: 创建新文件并使用编辑器进入该文件进行编辑

touch filename:直接创建新的空文件,需要编辑器打开编辑

echo ”内容“ > filename:允许在创建一个文件时向其中输入一些文本

查看文件

cat 文件名:从第一行显示全部内容

more 文件名:翻页查看全部内容

tail -10f 文件名: 查看后10行记录

tail -f 文件名: 实时查看

4 ls -a:查看所有文件(包含隐藏文件)
《面试题汇总》之高级篇(一)
一 数据结构与算法
1 数据结构概念
存数据的,而且存在内存中

常见的数据结构

线性表:N个元素组成的有序序列

数组:存储单元连续,用来存储固定大小元素的线性表;Java中集合对应ArrayList

链表:存储单元非连续,非顺序的存储结构,通过指针的链接次序实现;Java中实现常用LinkedList

栈:一种运算受限的线性表,后进先出;Java中的stack

队列:受限制的线性表,先进先出;Java中的Queue

散列表

hash

位图

二叉树

二叉搜索树(二叉查找树,二叉排序树)

若其左子树存在,则左子树中每个节点值都不大于该节点值

若其右子树存在,则右子树中每个节点值都不小于该节点值

没有键值相等的值

平衡二叉树

左右子树的高度之差的绝对值不能超过1,如果插入或删除一个节点是的高度之差大于1,则进行节点之间的旋转,将二叉树重新维持在一个平衡状态。解决二叉树退化为链表的问题

左右子树仍为平衡二叉树

红黑树

一种特殊的平衡二叉树,保证在最坏情况下基本动态集合操作的事件复杂度为O(log(n))

红黑树放弃了追求完全平衡,比平衡二叉树实现起来更为简单

红黑树每个节点遵循下面的规则

每个节点都有红色或黑色

树的根都是黑色的

不可能有连着的红色节点

所有叶子都是黑色

从任一节点到其叶子的所有简单路径都包含相同数量的黑色节点

多路树

有向图

无向图

带权图

2 算法概念
常见的算法

排序

冒泡

快速

插入

归并

计数

选择

其他

LRU

LFU

Hash算法

一致性Hash

算法思维

递归

回溯

分治

贪心

动态规划

冒泡排序

比较相邻的元素,如果第一个比第二个大,就交换两者

对每一对相邻元素做相同操作,从开始第一对到最后一对,保证最后一个数为最大值

重复操作,除了最后一个

重复以上三步

/**

  • 冒泡排序
    */
    public int[] bubbleSort(int arr[]){
    int len = arr.length;
    for (int i = 0; i < len-1; i++) {
    for (int j = i+1; j < len-1-i; j++) {
    if(arr[j]>arr[j+1]){
    int temp = arr[j+1];
    arr[j+1] = arr[j];
    arr[j] = temp;
    }
    }
    }
    return arr;
    }
    选择排序

首先在数组中找到最值,放在序列的起始位置,然后再从剩余的元素中找寻最值,放到已排序数组的末尾,以此类推,直到全部排序结束

/**

  • 选择排序
    */
    public int[] selectSort(int arr[]){
    int len = arr.length;
    int minIndex, temp;
    for (int i = 0; i < len-1; i++) {
    minIndex = i;
    for (int j = i+1; j < len; j++) {
    if(arr[j] < arr[minIndex]){
    minIndex = j;
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    }
    return arr;
    }
    插入排序

通过构建有序序列,对于未排序序列,在已排序序列中从后往前扫描,找到相应位置并插入

/**

  • 插入排序
    */
    public int[] insertSort(int arr[]){
    int len = arr.length;
    int preIndex, current;
    for (int i = 1; i < len; i++) {
    preIndex = i-1;
    current = arr[i];
    while (preIndex >= 0 && arr[preIndex]>current) {
    arr[preIndex +1] = arr[preIndex];
    preIndex–;
    }
    arr[preIndex+1] = current;
    }
    return arr;
    }
    二 并发编程与系统调优
    《面试题汇总》之高级篇(二)
    一 JVM问题
    1 JMM(Java Memory Model)
    image-20201011173308817

方法区跟堆是线程共享的;

栈、本地方法栈、程序计数器为线程私有

方法区

用于存放虚拟机加载的类信息,常量,静态变量等数据

存放对象实例,所有的对象和容器(数组)都是在堆上分配,是JVM所管理的内存中最大的一块区域

Java方法执行的内存模型:存储局部变量表,操作数栈,动态链接,方法出口等信息

生命周期与线程相同

本地方法栈

作用与虚拟机栈类似,不同点本地方法栈为native方法执行服务,虚拟机栈为虚拟机执行的Java方法服务

程序计数器

当前线程所执行的信号指示器。

JVM中内存区域最小的一块。

执行字节码工作时就是利用程序计数器来选取下一条需要执行的字节码指令

2 Java内存分配
寄存器:无法控制

静态域:static定义的静态成员

常量池:编译时被确定并保存在.class文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)

非RAM存储:硬盘等永久存储空间

堆内存:new创建的对象和数组,由Java虚拟机自动垃圾回收器管理,存取速度慢

栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性

3 Java堆结构什么样?永久代是啥?
Java堆是运行时数据区,所有类的实例和数组都是堆上分配内存。

在JVM启动的时候被创建。

对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收

堆内存是由存活和死亡的对象组成功的。

存活的对象是应用可以访问的,不会被垃圾回收。

死亡的对象是应用不能访问,而且还没有被垃圾回收掉的对象。一直在被垃圾回收掉之前,会一直占据内存空间

  1. Java内存泄漏问题?
    内存泄漏:一直不在被程序使用的对象或者变量一直占据在内存中。【无用,但是无法回收】

Java中的垃圾回收机制,可以保证一个对象不再被引用时,即对象变成个了孤儿,对象将自动被垃圾回收器从内存中清除。

由于Java使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如两个对象,相互引用,但是没有GC Root,那么就可以被垃圾回收掉。

Java中的内存泄漏情况:

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期的对象已经不再需要,但是因为长生命周期对象持有它的引用,而导致不能被自动回收。

通俗的说:创建一个对象,以后一直不再使用了,但是对象却一直被其他对象引用,即使这个对象无用,但是底层垃圾收集器却无法回收。

检查内存泄漏:一定让程序的各个分支都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判断这个对象属于内存泄漏。

二 GC
1 GC简介
GC:GarbageCollocation,垃圾收集

Java提供自动垃圾回收机制,GC功能可以自动检测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显式操作方法。

2 Java垃圾回收机制
Java拥有自动垃圾回收机制,不需要程序员手动的去释放一个对象的内存,而是由虚拟机自行执行。

在 JVM 中,有一个垃圾回收线程,优先级很低,在正常情况下不会执行,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收处理。

3 如何判断一个对象是否存活?
引用计数法

给每个对象设置一个引用计数器,每当一个地方引用这个对象时,就将计数器加1,引用失效,就减1 。当一个对象计数器为零时,就说明这个对象没有被引用,即”死对象“,将会被垃圾回收

缺陷:无法解决循环引用问题。

可达性算法(GC Roots/引用链法)

算法思想:从一个被称为GC Roots的对象向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明对象不可用

在Java中可以作为GC Roots的对象有:

虚拟机栈中引用的对象

方法区类静态属性引用的对象

方法区常量池引用的对象

本地方法栈 JNI(Java Native Interface) 引用的对象

虽然这些算法可以判定一个对象是否可以被回收,但是在满足上述条件的情况下,一个对象还是不一定会被立即回收掉。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是处于一个死缓阶段,若要被真正的回收需要经历两次标记。

如果对象在可达性分析中没有与GC Root的引链,那么此时就会被第一次标记并进行一次筛选,筛选的条件是是否有必要执行finalize()方法。

当对象覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没有必要的。

如果对象有必要执行 finalize() 方法,那么这个对象将会放在一个成为F-Queue的队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize方法执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成内存回收系统的崩溃。

GC 对处于 F-Queue 中的对象进行第二次标记,这时,该对象才被确认将被移除,等待被垃圾回收。

4 垃圾回收机制
分代复制垃圾回收

标记垃圾回收

增量垃圾回收

5 垃圾回收器的基本原理?垃圾回收器是马上回收吗?可以主动通知JVM进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址,大小以及使用情况。

GC采用有向图的方式记录和管理堆中的所有对象

通过这种方式可以确定哪些对象是“可达”的,哪些“不可达”。

当GC法确认一些对象是不可达的,就会回收这些内存空间,

程序员可以手动调用System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

6 分布式垃圾回收(DGC)如何工作?
DGC:分布式垃圾回收。

RMI使用DGC来自动垃圾回收。

因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收比较困难。

DGC使用引用计数算法来给远程对象提供自动内存管理。

7 串行(serial)收集器和吞吐量(throughput)收集器的区别?
吞吐量收集器使用并行版本的新生代垃圾收集器,用于中等规模和大规模数据的应用程序

串行收集器对大多数的小应用(100M左右的内存)就足够了

8 在Java中,对象什么时候可以被垃圾回收?
当对象对当前使用这个对象的应用程序变得不可触及时,这个对象就可以被回收了

9 简述Java内存分配和回收策略以及Minor GC和Major GC
对象优先在堆的Eden区分配

大对象直接进入老年代

长期存活的对象将直接存放进老年代

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。

Minor GC通常在发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收频率较快;

Full GC / Major GC发生在老年代。

一般情况下,触发老年代的GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC,这样可以加快老年代的回收速度。

10 JVM中的垃圾收集器以及特点
新生代垃圾收集器

Serial收集器

特点:Serial收集器只能使用一条线程进行垃圾收集工作,并且在进行垃圾收集的时候,所有的工作线程都需要停止工作,等待垃圾收集线程完成以后,其他线程在可以继续工作

算法:复制算法(Copy)

ParNew收集器

特点:ParNew收集器是Serial收集器的多线程版本。为了利用CPU多核多线程优势,ParNew收集器可以运行多个收集线程来进行垃圾收集工作。这样可以提高垃圾收集过程的效率。

算法:Copy

Parallel Scavenge收集器

特点:Parallel Scavenge收集器是一款多线程垃圾收集器,但是它和ParNew不同

Parallel Scavenge收集器和其他收集器的关注点不同个。其他收集器,比如ParNew 、CMS收集器主要关注如何缩短垃圾收集的时间。Parallel Scavenge收集器关注如何控制系统运行的吞吐量。

吞吐量:指CPU用于运行应用程序的时间和CPU总时间的占比,吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)

如果虚拟机运行的总CPU时间是100分钟,而用于执行垃圾收集的时间为1分钟,那么吞吐量就是99%

算法:copy复制

老年代垃圾收集器

Serial Ole收集器

特点:Serial Old收集器是Serial收集器的老年代版本。主要用于客户端应用程序中作为老年代的垃圾收集器,也可以作为服务端应用程序的垃圾收集器

算法:标记-整理

Parallel Old 收集器

特点:Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本,这个收集器在JDK1.6 版本中出现,在JDK6之前,新生代Parallel Scavenge只能和Serial Old 这款单线程的老年代收集器配合使用。Parallel Old 垃圾收集器 和Parallel Scavenge收集器一样,是一款关注吞吐量的垃圾收集器,和Parallel Scavenge收集器一起配合,可以实现对Java堆内存的吞吐量优先的垃圾收集策略

算法:标记-整理

CMS收集器

特点:CMS收集器是目前老年代收集器中比较优秀的垃圾收集器。CMS是Concurrent Mark Sweep,从名字可以看出,这是一款“标记-清除”算法的并发收集器

CMS是一款以获取最短停顿时间为目标的收集器

image-20201011200525656

CMS工作过程分为4个阶段:

初始标记(CMS initial mark)阶段

并发标记(CMS concurrent mark)阶段

重新标记(CMS remark)阶段

并发清除(CMS concurrent sweep)阶段

算法:复制+标记清除

G1 垃圾收集器

特点:主要步骤

初始标记

并发标记

重新标记

复制清除

算法:复制 + 标记整理

11 垃圾收集方法
标记-清除算法

标记哪些要被回收的对象,然后统一回收

缺陷:

效率低,标记跟清除的效率都很低

会产生大量的内存碎片,导致以后的程序在分配较大对象时,由于没有连续的内存而提前触发一次GC

复制算法

将内存划分成两块相等的部分,每次只使用其中的一快,当当前块内存使用完后,就将还存活的对象复制到另一块内存上,然后一次性清除完第一块内存,再将第二块内存复制到第一块,循环往复。

缺陷:代价大,浪费内存。

改进后的复制算法:将内存区域不是按照1:1进行划分,而是改成8:1:1三部分,较大的部分Eden区,较小的两块都叫Surivor区;

每次分配内存只使用Eden跟From Survivor区,满了之后将存活对象复制到To Survivor区,同时存活的对象年龄加1;然后将To 跟 From Survivor调换;

再次分配内存,依旧使用Eden区跟调换后的From Survivor 区,再次满了之后再将存活对象复制到To Survivor区,年龄加1;循环往复…

当对象的年龄涨到默认的15之后就分配到老年代

另外注意:当存活的对象太多时,JVM会有分配担保机制,将多出的直接复制到老年代,保证系统执行

标记-整理算法

解决标记-清除算法产生大量内存碎片问题;

当对象存活率高时,也解决了复制算法的效率问题,不同之处就是在清除对象时先将回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了

分代收集

现用的虚拟机垃圾回收算法基本都是采用分代收集

根据对象的生存周期,将对象分为新生代跟老年代。

在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。

老年代中对象存活率高,当新生代中没有额外的空间分配对象内存时,进行分配担保

三 类加载相关
1 类加载器
Bootstrap ClassLoader:将存放于lib目录中的,或者-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(按文件名识别)类库加载到虚拟机内存中 。启动类加载器无法被Java程序直接引用。

Extension ClassLoader:将lib/ext目录下,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。

Application ClassLoader:负责加载用户类路径(classpath)上所指定的类库,开发者可直接使用。应用程序加载器

用户自定义类加载器:通过继承java.lang.ClassLoader类来实现

2 类加载器双亲委派模型机制
定义

工作过程:如果一个类加载器接收到了类加载的请求,首先将这个请求委托给他的父类加载器去完成,每个层次的加载器都是如此,因此所有加载请求都应该传递到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派机制

当 AppClassLoader 加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委托给父类加载器 ExtClassLoader 去完成

当 ExtClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给 BootstrapClassLoader 去完成

如果 BootstrapClassLoader 加载失败,会使用 ExtClassLoader 来尝试加载

如果 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,就会抛出 ClassNotFoundException

image-20201011204139048

双亲委派机制的作用

通过带有优先级的层次关系可以避免类的重复加载

保证Java程序安全稳定运行,Java核心API定义类型不会被随意替换

3 什么是 class 文件? Class文件主要的信息结构有哪些?
Class文件是一组 8 位字节为基础单位的二进制流。每个数据项严格按照顺序排列

Class文件格式采用一种类似 C语言 结构体的伪结构来存储数据,这样的伪结构仅有两种数据类型:

无符号数:

基本数据类型。以u1、u2、u4、u8 分别代表1个字节,2个字节,4个字节,8个字节的无符号数,能够描写叙述数字、索引引用、数量值和依照UTF-8编码构成的字符串值

由多个无符号数或者其他表作为数据项构成的复合数据类型。都以 _info 结尾

四 JVM调优
1 JVM数据运行区,哪些会造成OOM情况?
除了数据运行区,其他区域均有可能造成OOM情况

堆溢出:java.lang.OutOfMemoryError : Java heap space

可以在启动时通过-Xmx参数指定,

分析:内存溢出,堆内存不够用了;如果程序设计的最大堆内存已经耗尽,不该申请很多内存的逻辑申请了很多内存,该释放的对象没有释放;

解决:

检查程序,看是否有死循环或者不必要的重复创建大量对象。找到原因之后修改程序和算法。

增加java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小

栈溢出:java.lang.StackOverflowError

永久代溢出:java.lang.OutOfMemoryError : PermGen space

-Xms:520M 初始堆内存
-Xmx:1024M 最大堆内存
-Xmn:256M 新生代大小
-XX:NewSize=256M 设置新生代的初始大小
-XX:MaxNewSize=256M 设置新生代最大值内存
-XX:PerSize=256M 设置永久代初始值大小
-XX:MaxPermSize=256M 设置永久待最大值大小
-XX:NewRatio=4 设置老年代和新生代的比值。表示老年代比新生代为4:1
-XX:SurvivorRatio=8 设置新生代Survivor区和eden区的比值,该比值为Eden区比Survivor区为8:2
-XX:MaxTenuringThreshold=7 表示一个对象在Survivor区移动了7次还没有被回收,则进入老年代。该值可以减少fullGC次数
2 线上常用的JVM参数有哪些?
数据区设置

Xms:初始堆大小

Xmx:最大堆大小

Xss:Java每个线程的Stack大小

XX:NewSize = n :设置年轻代大小

XX:NewRatio = n:设置年轻代跟年老代的比值。如3,表示年轻代与年老代比值为1:3

XX: SurvivorRatio = n :年轻代中Eden区与两个Survivor 区的比值。注意Survivor有连个

XX:maxPermSize = n :设置持久代大小

收集器设置

XX:+UseSerialGC:设置串行收集器

XX:+UseParallelGC:设置并行收集器

XX:+UseParallelOldGC:设置并行老年代收集器

XX:+UseConcMarkSweepGC:设置并发收集器

GC日志打印设置

XX:+PrintGC:打印GC简要信息

XX:+PrintGCDetails:打印GC细节信息

XX:+PrintGCTimeStamps:输出GC问题

3 JVM 提供的常用工具
jps:

用来显示本地Java进程,可以查看本地运行着几个java程序,并显示进程号

命令格式:jps

jinfo

运行环境参数:Java System属性和JVM命令行参数,Java class path等信息

命令格式:jinfo 进程 pid

jstat

监视虚拟机各种运行状态信息的命令行工具

命令格式:jstat -gc 123 25020

jstack

可以观察到 JVM 中当前所有线程的运行情况和线程当前状态。

命令格式:jstack 进程 pid

jmap

观察运行中的 JVM 物理内存的占用情况(如:产生哪些对象?数量多少?)

命令格式:jmap [option] pid

4 堆和栈的区别
堆是运行时确定的内存大小,栈在编译时即可确定内存大小

堆内存由用户管理(Java中由JVM管理),栈内存会被自动释放

栈实现方式采用数据结构的栈实现,具有先进后出的特点,堆为一块一块的内存

栈由于其实现方式,在分配速度上比堆快得多,分配一块栈内存不过时简单的移动一个指针

栈为线程私有,堆为线程共享

5 jstack、jmap、jstat工具
jstack(查看线程)

jstack能得到运行java程序的java stack和native stack的信息。

常用指令:

jstack [option] pid(最常用)

jstack [option] executable core

jstack [option] [server-id@] remote-hostname-or-IP

jstack dump日志文件中的线程状态

死锁:Deadlock(重点关注)

Deadlock:死锁线程,一般指多个线程调用间,进入相互资源占用,导致一直等待无法释放的情况

执行中:Runnable

Runnable:一般指该线程正在执行状态中,该线程占用了资源,正在处理某个请求,有可能正在传递SQL到数据库中执行,有可能对某个文件进行操作,有可能进行数据类型的转换

等待资源:Waiting on condition(重点关注)

Waiting on condition:等待资源,或等待某个条件的发生。具体原因需结合stacktrace来分析

如果堆栈信息明确时应用代码,则证明该线程正在等待资源。一般是大量读取某资源,且该资源采用了资源锁的情况下,线程进入等待状态,等待资源的读取。

又或者正在等待其他线程的执行等

如果发现又大量的线程都在处于Wait on condition,从线程stack上看,正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。

一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;

另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达;

另外一种出现 Wait on condition的常见情况是该线程在sleep,等待sleep的时间到了的时候,将贝唤醒。

等待获取监视器:Waiting on monitor entry(重点关注)

Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。从下图1中可以看出,每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。

image-20210116163527737

暂停:Suspended

对象等待中,Object.wait() 或 TIMED_WAITING

阻塞:Blocked(重点关注)

线程阻塞,指当前线程在执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。

停止:Parked

jmap(查看内存)

得到运行java程序的内存分配的详细情况。如实例个数、大小等

常用指令:

jmap [option] pid

jmap [option] executable core

jamp [option] [server-id@]remote-hostname-or-IP

jstat(性能分析)

可以得到classloader、compiler、gc等相关信息。可以实时监控资源和性能

常用命令:

-class:统计classloader行为信息

-compiler:统计编译行为信息

-gc:统计jdk gc时heap信息

-gccapacity:统计不同的generations相应的heap容量情况

-gccause:统计gc情况,(同-gcutil)和引起gc的事件

-gcnew:统计gc时,新生代的情况

-gcnewcapacity:统计gc时,新生代heap容量

-gcold:统计gc时,老年区的情况

-gcoldcapacity:统计gc时,老年代heap的容量

-gcpermcapacity:统计gc时,permanent区heap的容量

-gcutil:统计gc时,heap的情况

输出参数的内容:

S0:heap上的survivor space 0 区已使用空间的百分比

S0C:S0当前容量的带线啊哦

S0U:S0已经使用的大小

S1:heap上的Survivor space 1 区已使用空间的百分比

S1C:S1当前容量的大小

S1U:S1已经使用的大小

E:heap上的Eden space区已使用空间的百分比

EC:Eden space 当前容量的大小

EU:Eden space 已经使用的大小

O:heap上的Old space 区已经使用的空间的百分比

OC:Old space当前容量的大小

OU:Old space已经使用的大小

P:Perm space 区已使用空间的百分比

PC:Perm space当前容量的大小

PU:Perm space已使用的大小

YGC:从应用程序启动到采样时发生YGC的次数

YGCT:从应用程序启动到采样时发生YGC所用的时间(单位秒)

FGC:从应用程序启动到采样时发生FGC的次数

FGCT:从应用程序启动到采样时发生FGC所用的时间(单位秒)

GCT:从应用程序启动到采样时用于垃圾回收的总时间(单位秒),值等于YGCT+FGCT

五 算法合集【每周一道算法题】
1 排序算法题
十大经典排序算法

冒泡排序

选择排序

从未排序的元素中取出最小(大)放在第一个位置,

循环取

插入排序

从后往前依次比较,插入

希尔排序

原理跟插入差不多,多一个增量序列

归并排序

分组排序后,归并到一起

桶排序

把数据分配到桶,再在桶内排序,最后组合

2 斐波那契数列
/**

  • 斐波那契数列问题

  • 0 1 1 2 3 5 8 13 …
    */
    public class FiBoNaQieTest {

    //O(2^n)
    public static int fib1(int n){
    if(n<=1) return n;
    return fib1(n-1) + fib1(n-2);
    }

    //O(n)
    public static int fib2(int n){
    if(n<=1) return n;

    int first = 0;
    int second = 1;
    for (int i = 0; i < n-1; i++) {
    int sum = first + second;
    first = second;
    second = sum;
    }
    return second;
    }
    }
    3 Leetcode_150 逆波兰表达式
    四则运算表达式

前缀表达式:+ 1 2

中缀表达式:1 + 2

后缀表达式:1 2 +

算法思路:

遇见数字,直接入栈

遇见符号

弹出栈顶的右操作数

弹出栈顶的左操作数

使用符号运算,结果放入栈顶

直到栈中剩余一个结果输出

4 Leetcode_21 合并两个有序链表
/**

  • Definition for singly-linked list.
  • public class ListNode {
  • int val;
    
  • ListNode next;
    
  • ListNode() {}
    
  • ListNode(int val) { this.val = val; }
    
  • ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    
  • }
    */
    class Solution {
    public ListNode mergeTwoLists(ListNode k1, ListNode k2) {
    if(k1 == null) return k2;
    if(k2 == null) return k1;

    ListNode head ;
    if(k1.val <= k2.val){
    head = k1;
    k1 = k1.next;
    }else {
    head = k2;
    k2 = k2.next;
    }

    ListNode cur = head;
    while(k1 != null && k2 != null){
    if(k1.val < k2.val){
    cur = cur.next = k1;
    k1 = k1.next;
    }else{
    cur = cur.next = k2;
    k2 = k2.next;
    }
    }


if(k1 == null){
cur.next = k2;
}
else if(k2 == null){
cur.next = k1;
}

return head;
}
}

//使用虚拟头节点
public ListNode mergeTwoLists(ListNode k1, ListNode k2) {
if(k1 == null) return k2;
if(k2 == null) return k1;

ListNode head = new ListNode(0);
ListNode cur = head;
while(k1 != null && k2 != null){
if(k1.val < k2.val){
cur = cur.next = k1;
k1 = k1.next;
}else{
cur = cur.next = k2;
k2 = k2.next;
}
}


if(k1 == null){
cur.next = k2;
}
else if(k2 == null){
cur.next = k1;
}

return head.next;
}

//递归
public ListNode mergeTwoLists(ListNode k1, ListNode k2) {
if(k1 == null) return k2;
if(k2 == null) return k1;
if(k1.val <= k2.val){
k1.next = mergeTwoList(k1.next,k2);
return k1;
}else{
k2.next = mergeTwoList(k2.next,k1);
return k2;
}

}
六 数据结构
1 红黑树
自平衡的二叉树(不是绝对的平衡)

红黑树的定义

每个节点都有颜色(红色或者黑色)

树的根始终是黑色的

没有两个相邻的红色节点

叶子节点都是黑色的,为null

一个节点本身是红色的,其两个孩子节点都是黑色的

从节点(包括根)到其任意子孙节点(null/叶子节点)的每条路径都具有相同数量的黑色节点

红黑树的两大操作

recolor(重新标记颜色)

rotation(旋转,树达到平衡的关键)

插入新节点都是红色节点

插入元素后的几种情景

父节点是黑色的,不需要进行调整

父节点是红色的,

叔叔节点是空,旋转+变色

叔叔节点是红色,父节点+叔叔节点变黑色,祖父节点变红色

叔叔节点是黑色,旋转+变色

更多推荐

《Java面试题汇总》

本文发布于:2023-04-19 19:03:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/805d7492cf80b19647e045cac3b05a03.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:面试题   Java

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 88698文章数
  • 21631阅读数
  • 0评论数