review



basic

面向对象和面向过程的区别

面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。

面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。

面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。

基本数据类型

在这里插入图片描述

String相关

String 底层就是一个 char 类型的数组,比如“你好” 就是长度为2的数组 char[] chars = {'你','好'};

字符型常量和字符串常量的区别

形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符

什么是字符串常量池?

字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

String有哪些特性

  • 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
  • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。

String真的是不可变的吗?

  • 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子:

1 String不可变但不代表引用不可以变

1
2
3
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);
  • 结果:

    str=Hello World

  • 解析:

  • 实际上,原来String的内容是不变的,只是str由原来指向”Hello”的内存地址转为指向”Hello World”的内存地址而已,也就是说多开辟了一块内存区域给”Hello World”字符串。(实际上都是创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容。

2.通过反射是可以修改所谓的“不可变”对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建字符串"Hello World", 并赋给引用s
String s = "Hello World";

System.out.println("s = " + s); // Hello World

// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");

// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);

// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);

// 改变value所引用的数组中的第5个字符
value[5] = '_';

System.out.println("s = " + s); // Hello_World

String str=”i”与 String str=new String(“i”)一样吗?

不一样,因为内存的分配方式不一样。String str=”i”的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。

String str=new String(“i”) 两个对象,一个是静态区的”i”,一个是用new创建在堆上的对象。

String初始化相关问题

1
2
3
4
5
6
public class foo{
public static void main (String[] args){
String s;
System.out.println("s=" + s); // Variable 's' might not have been initialized
}
}

由于String s没有初始化,代码不能编译通过

在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。(String对象内部已经重写了equals和hashcode,避免了不必要的hash碰撞

String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的?

  • String是字符串常量,创建的字符内容是不可改变。不可变是因为String类中定义的char数组是final
  • StringBuffer和StringBuilder是字符串变量,创建的字符内容是可以改变的。
  • StringBuffer是线程安全的,而StringBuilder是非线程安全的。

String常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
length() 字符串的长度
charAt() 截取一个字符
toCharArray() String转换成char数组
equals()和equalsIgnoreCase() 比较两个字符串
startsWith()和endsWith() startsWith()方法决定是否以特定字符串开始,endWith()方法决定是否以特定字符串结束
equals()和 ==: equals()方法比较字符串对象中的字符,==运算符比较两个对象是否引用同一实例。
substring()
concat() 连接两个字符串
replace() 替换
trim() 去掉起始和结尾的空格
valueOf() 转换为字符串
toLowerCase() 转换为小写
toUpperCase() 转换为大写

自动装箱与拆箱

Integer a= 127 与 Integer b = 127相等吗

  • 对于对象引用类型:==比较的是对象的内存地址。
  • 对于基本数据类型:==比较的是值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果整型字面量的值在-128127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false
public static void main(String[] args) {
Integer a = new Integer(3); // 这个是创建堆对象
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true

Integer a1 = 128; // 相当于 new Integer()
Integer b1 = 128;
System.out.println(a1 == b1); // false

Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}

变量

静态变量和实例变量区别

  • 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
  • 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

静态变量与普通变量区别

  • static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
  • 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。

静态方法和实例方法有何不同?

静态方法和实例方法的区别主要体现在两个方面:

  • 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

值传递

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

  • 值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的(可以改变传过来的对象的属性值,但是更改引用是不会影响调用者)

为什么 Java 中只有值传递

  • 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
  • Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;

swap(num1, num2);

System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}

public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;

System.out.println("a = " + a);
System.out.println("b = " + b);
}
  • 结果:

    a = 20 b = 10 num1 = 10 num2 = 20

  • 解析:

在这里插入图片描述

  • 在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身

举例2:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}

public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
  • 结果:

    1 0

  • 解析:

在这里插入图片描述

  • array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {

public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}

public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
  • 结果:依旧没有影响调用者的对象

    x:小李 y:小张 s1:小张 s2:小李

  • 解析:

  • 交换之前:

在这里插入图片描述

  • 交换之后:

在这里插入图片描述

  • 通过上面两张图可以很清晰的看出:方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

举例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 因为在Java里没有引用传递,只有值传递
public class Example {

char[] ch = { 'a', 'b', 'c' }; // 拷贝地址后你可以通过它修改这个地址的内容(引用不变)
String str = new String("good"); // 但是你不能改变这个地址本身使其重新引用其它的对象

public static void main(String args[]) {
Example ex = new Example();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.print(ex.ch);
}

public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'g';
}
}

输出:good and gbc。

值传递和引用传递有什么区别

  • 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
  • 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

重写与重载

构造器(constructor)是否可被重写(override)

  • 构造器不能被继承,因此不能被重写,但可以被重载。

重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

  • 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
  • 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
  • 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

多态的实现

Java实现多态有三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
People p=new Stuent();

class People{
public void eat(){
System.out.println("吃饭");
}
}
class Stu extends People{
@Override
public void eat(){
System.out.println("吃水煮肉片");
}
}

构造方法

在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

  • 帮助子类做初始化工作。

一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?

  • 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性?

  • 名字与类名相同;
  • 没有返回值,但不能用void声明构造函数;
  • 生成类的对象时自动执行,无需调用。

对象的初始化顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class HelloB extends HelloA {

public HelloB() {
System.out.println("HelloB");
}

{
System.out.println("I'm B class");
}

static {
System.out.println("static B");
}

public static void main(String[] args) {
new HelloB();
}
}

class HelloA {

public HelloA() {
System.out.println("HelloA");
}

{
System.out.println("I'm A class");
}

static {
System.out.println("static A");
}

}

输出结果:

1
2
3
4
5
6
static A
static B
I'm A class
HelloA
I'm B class
HelloB

对象的初始化顺序:(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;(2)当static语句执行完之后,再执行main方法;(3)如果有语句new了自身的对象,将从上到下执行代码块、构造器(两者可以说绑定在一起),如果没有就不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class People {
String name;

public People() {
System.out.print(1); // 子类的构造过程中,必须调用其父类的构造方法。
}

public People(String name) {
System.out.print(2);
this.name = name;
}
}

class Child extends People {
People father;

public Child(String name) {
// super(name + ":F"); 如果我们在这调用了父类的构造方法,就直接输出 23,不会调用父类无参构造方法了。
father = new People(name + ":F"); // 如果在子类构造方法中我们并没有显示的调用基类的构造方法,如:super(); 这样就会调用父类没有参数的构造方法。
System.out.print(3);
this.name = name;
}

public Child() {
System.out.print(4);
}

}

输出结果:123

总之,一句话:子类没有显示调用父类构造函数,不管子类构造函数是否带参数都默认调用父类无参的构造函数,若父类没有则编译出错。

反射

获取反射机制的三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 方式一、new对象:getClass方法来获取
Student student = new Student();
Class classObj1 = student.getClass();
System.out.println(classObj1.getName());

// 方式二、Class.forName方法(路径-相对路径)
Class classObj2 = Class.forName("com.aop8.reflect.Student");
System.out.println(classObj2.getName());

// 方式三、类名.class
Class classObj3 = Student.class;
System.out.println(classObj3.getName());

Collection

怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

1
2
3
4
5
6
List<String> list = new ArrayList<>();
list. add("x");
// 增删改都会throw new UnsupportedOperationException();
Collection<String> clist = Collections.unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

1
2
3
4
5
6
7
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的StudentList
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的StudentSet
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Studentname查找对应StudentMap

img

List 接口

List:有序的,可重复的

ArrayList

底层是数组实现的,数组没变,初始容量为10,(空间不够)创建新数组,复制原来的数组的值 ,增加新的值进去,变得是数组引用指向(指向了新的数组)

  • 特性:
    • 空间连续
    • 有序的
    • 数据可重复(只能存 引用型)
    • 对中间节点,查询快,但是做增删操作,效率低下。
  • 优点:单链表,查询效率快
    img

注意:ArrayList list = new ArrayList(20);中的list扩充几次? 答案:0次。

解析:这里有点迷惑人,大家都知道默认ArrayList的长度是10个,所以如果你要往list里添加20个元素肯定要扩充一次(扩充为原来的1.5倍),但是这里显示指明了需要多少空间,所以就一次性为你分配这么多空间,也就是不需要扩充了。

LinkedList

底层是双向链表
适合存储大量数据,

  • 优点:查询慢,但中间节点增删快,
Vector

类似于ArrayList ,但较之于它,Vector是线程安全的

Set 接口

img

HashSet

底层是Hash表,且其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
无序集合、不能重复
相比set接口,HashSet 多了一个clone()方法。

特点:

  • 不能保证元素的排列顺序,顺序有可能发生变化
  • 不是同步的
  • 集合元素可以是 null,但只能放入一个 null

一般操作 HashSet 还是调用 Collection 的 add / remove 等方法进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HashSetTest {

public static void main(String[] args) {
//增加
Set<String> hashSet = new HashSet<String>();
hashSet.add("1");
hashSet.add("2");
hashSet.add("3");
hashSet.add("4");
hashSet.add("5");

//删除
hashSet.remove("1");

//查询 无法获取某个元素
System.out.println("是否包含1元素:" + hashSet.contains("2"));

//迭代
Iterator<String> it = hashSet.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
}
}

HashSet 的实现原理?

当向 HashSet 结合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在 HashSet 中存储位置。根据这种方式可以看出,HashSet 的数据存取其实是通过哈希算法实现的,因为通过哈希算法可以极大的提高数据的读取速度。通过阅读 JDK 源码,我们知道 HashSet 是通过 HashMap 实现的,只不过是HashSet 的 value 上的值都是 null 而已

简单的说,HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等,并且两个对象的hashCode() 方法返回值相等

注意,如果要把一个对象放入 HashSet 中,重写该对象对应类的 equals() 方法,也应该重写其 hashCode() 方法。其规则是如果两个对 象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

HashSet 中的add ()方法会使用HashMap 的put()方法

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

源码:

1
2
3
4
5
6
7
8
9
10
// value值统一默认
private static final Object PRESENT = new Object();

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
LinkedHashSet

LinkedHashSet 在迭代访问 Set 中的全部元素时,性能比 HashSet 好,但是插入时性能稍微逊色于HashSet(因为 HashSet 直接采用哈希算法,而 LinkedHashSet 还需要维护链表结构)。

TreeSet

SortedSet 接口的唯一实现类,TreeSet 可以确保集合元素处于排序状态,这也是 TreeSet最大的特征之一。

底层是最优二叉树
img

Map集合

  • Map中的元素是两个对象,一个对象作为键,一个对象作为值。键不可以重复,但是值可以重复。
  • Map存储元素使用put方法,Collection使用add方法
类型 说明
Map(顶层接口) 将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
HashMap 采用哈希表实现,所以无序。底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。
要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
Hashtable 底层是哈希表数据结构,线程是同步的,不可以存入null键,null值。
效率较低,被HashMap 替代。
LinkedHashMap 该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。
TreeMap 底层是二叉树数据结构。可以对map集合中的键进行排序。需要使用Comparable或者Comparator 进行比较排序。return 0,来判断键的唯一性。
ConcurrentHashMap 线程安全。,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了。

因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。

因此,正确使用Map必须保证:作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:如果两个对象相等,则两个对象的hashCode()必须相等;如果两个对象不相等,则两个对象的hashCode()尽量不要相等。即对应两个实例a和b:如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode();如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。

HashMap

我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,而在 JDK 1.8 之前是由“数组+链表”组成。改成“数组+链表+红黑树”,主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。转回成链表节点是用的6而不是复用8,因为我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。

注意:红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。

img

总结:

  1. 转红黑树:节点>阈值8,且数组长度 >= 64
  2. JDK1.8后底层是由“数组+链表+红黑树”,之前是数组+链表
扩容机制

resize()使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

1.8前是头插法,并发下容易导致链表成环了,这就会导致:Infinite Loop。而尾插法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题

HashMap的扩容机制—resize()

HashMap 链表插入方式 → 头插为何改成尾插 ?

HashMap学习

ConcurrentHashMap

JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment

1
2
3
4
5
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}

JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

1
2
3
4
是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在
ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的
结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个
Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)

Java8中ConcurrentHashMap是如何保证线程安全的

面试必问之 ConcurrentHashMap 线程安全的具体实现方式

网络编程

网络模型

这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

常用协议

TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。

UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。

选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。

WebSocket 和HTTP的区别及原理

http协议是用在应用层的协议,他是基于tcp协议的,http协议建立链接也必须要有三次握手才能发送信息

http链接分为短链接,长链接,短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response,在一个TCP连接上可以传输多个Request/Response消息对。长链接是在一定的期限内保持链接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的

WebSocket他是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮询问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息

websocket的持久连接只需建立一次Request/Response消息对,之后都是TCP连接,避免了需要多次建立Request/Response消息对而产生的冗余头部信息。

WebSocket 和HTTP的区别及原理

三次握手过程理解

简单

详细

img

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户端的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

TCP的三次握手与四次挥手理解及面试题

四次挥手 - 待补充

从浏览器发出请求到响应都经历了什么

从 URL 输入到页面展现到底发生什么?

多线程

死锁

如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

死锁检测

1、Jstack命令:java虚拟机自带的一种堆栈跟踪工具。

2、JConsole工具:Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。

什么是自旋锁?

自旋锁是SMP架构中的一种low-level的同步机制

当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。

目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyWaitNotify3{

MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;

public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}

public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}

线程生命周期

新建状态(New):当线程对象创建后,即进入新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法时,线程即进入就绪状态。处于就绪状态的线程只是说明此线程已经做好准备,随时等待CPU调度执行,并不是说执行了start()方法就立即执行。

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。

死亡状态(dead):线程执行完毕或者是异常退出,该线程结束生命周期。

阻塞状态分类

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  2. 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态;
  3. 其他阻塞:通过调用线程的sleep()或join()或发出I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

线程遇见异常会怎么?

① 如果该异常被捕获或抛出,则程序继续运行。

② 如果异常没有被捕获该线程将会停止执行。

③ 运行时异常在编译阶段是无法检测出来的,非运行时异常在编译阶段是可以检测出来的,比如你使用了一个sleep()函数,会强制要求你throws,并且try catch。

④ 当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler处理方法。

线程池

Java面试经典题:线程池专题

线程池

常见知识

switch

switch是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上

在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从Java 7开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

float f=3.4;是否正确

  • 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。

short运算类型转换

short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗

  • 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
  • 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换

总结:运算结果是 int型,所以第一个需要通过s1 = (short) (s1 + 1)强转成short类型

final finally finalize区别

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。

抽象类和接口

抽象类只能被继承,一个抽象方法可以被子类们继承不断增强,但是实现接口只能一次性完成功能

抽象类

1
2
3
4
5
6
7
8
9
10
11
a、抽象类不能通过构造方法被实例化只能被继承,一个抽象方法可以被子类们继承不断增强

b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public

d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;

e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

f、非抽象方法需要方法体,抽象方法(abstract)没有方法体

接口

1
2
3
4
5
6
7
a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);

b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;

c、一个类可以实现多个接口;

d、JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰

例子:

选项中哪一行代码可以替换题目中//add code here而不产生编译错误?()

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class MyClass {
public int constInt = 5;
//add code here
public void method() {
}
}

// 答案:A
A: public abstract void method(int a); // abstract关键字只能修饰类和方法,不能修饰字段。
B: constInt = constInt + 5;
C: public int method();
D: public abstract void anotherMethod() {} // 抽象类中的抽象方法(加了abstract关键字的方法)不能实现。

== 和 equals 的区别是什么

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

为什么要有 hashCode

1
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
  • 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

BIO,NIO,AIO 有什么区别?

  • 简答
    • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
    • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
    • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非阻塞 IO ,异步 IO 的操作基于事件和回调机制。

字符串常量Java内部加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {
public static void main(String[] args) {
/*
(1).str1
str1 会有4个对象
一个StringBuilder、
一个coder ldc、
一个blue ldc、
String
这个时候常量池中没有coderblue这个ldc在
str1.intern():在jdk7后,会看常量池中是否存在,如果没有,它不会创建一个对象,
如果堆中已经这个字符串,那么会将堆中的引用地址赋给它
所以这个时候str1.intern()是获取的堆中的
* */
String str1 = new StringBuilder("coder").append("blue").toString();
System.out.println(str1);
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());//true
System.out.println();

/*
sum.misc.Version类会在JDK类库的初始化中被加载并初始化,而在初始化时它需要对静态常量字
段根据指定的常量值(ConstantValue)做默认初始化,此时sum.misc.Version.launcher静态常
量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池 - StringTable
里了
str2对象是堆中的
str.intern()是返回的是JDK出娘胎自带的,在加载sum.misc.version这个类的时候进入常量池
*/
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2);
System.out.println(str2.intern());
// false
System.out.println(str2 == str2.intern());//false
}
}

根据以上代码判断输出结果。

1
2
3
4
5
6
7
coderblue
coderblue
true

java
java
false

原因分析

intern()作用说明: intern() 是一个本地方法。如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,然后返回此String对象的引用。
简单来说就是:如果常量池中已经存在了这个字符串,则直接返回池中的引用,如果常量池中没有这个字符串,则新建然后返回池中的引用。

那么问题来了,为什么同样的操作,字符串coderblue返回的结果是true,而java返回的记过的false呢?

原因: 这是因为在JDK初始化的时候,有一个初始化的”java“字符串(JDK出娘胎自带的),在加载sun.musc.Version这个类的时候进入了字符串常量池。

源码 sun.misc.Version:

1
2
3
4
5
6
7
8
9
10
package sun.misc;

import java.io.PrintStream;

public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_201";

...
}

可重入锁的代码验证

1、ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author Lauy
* @date 2021/1/29
*/
public class ReentrantLockMain {
public static void main(String[] args) {
new Thread(new SynchronizedTest2()).start();
}
}

class SynchronizedTest2 implements Runnable {

private final Lock lock = new ReentrantLock();

public void helloA() { //方法1,调用方法2
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " helloA()");
helloB();
} finally {
lock.unlock();
}
}

public void helloB() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " helloB()");
} finally {
lock.unlock();
}
}

@Override
public void run() {
helloA();
}
}

2、synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SynchronizedMain {
public static void main(String[] args) {
new Thread(new SynchronizedTest()).start();
}
}

class SynchronizedTest implements Runnable {
private final Object obj = new Object();

public void helloA() { //方法1,调用方法2
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + " helloA()");
helloB();
}
}

public void helloB() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + " helloB()");
}
}

@Override
public void run() {
helloA(); //调用helloA方法
}
}

均输出:

Thread-0 helloA()
Thread-0 helloB()

==&equals&final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
String s1 = new String("blue");
String s2 = new String("blue");
System.out.println(s1 == s2); // false
System.out.println(Objects.equals(s1, s2)); // true
String s3 = "blue";
String s4 = "blue";
System.out.println(s3 == s4); // true
System.out.println(s3 == s1); // false
String s5 = "blueblue";
String s11 = "blue" + "blue";
String s6 = s3 + s4;
System.out.println(s5 == s6); // false
final String s7 = "blue";
final String s8 = "blue";
String s9 = s7 + s8;
System.out.println(s5 == s9); // final修饰,true
final String s10 = s3 + s4;
System.out.println(s5 == s10); // false
}
}
  • new (“blue”)不相等,会各自在堆空间创建
  • 常量比较相等
  • 常量 + 常量还是在常量池
  • 对象 + 对象 != 对象,但是重写equals值相等 或者 用final修饰

ThreadLocal

ThreadLocal提供 getset等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此 get总是返回由当前线程在调用 set时设置的最新值。

为什么wait必须写在同步(synchroized)代码块中

https://img-blog.csdnimg.cn/20210226003548328.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDc2NDY1,size_16,color_FFFFFF,t_70)

创建线程的方式

  • 继承Thread
  • 实现Runable接口
  • 实现Callable接口(可以获取线程执行之后的返回值)

https://zhuanlan.zhihu.com/p/58821827

序列化保存到文件

阅读Shape和Circle两个类的定义。在序列化一个Circle的对象circle到文件时,下面哪个字段会被保存到文件中? ( )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {

public String name;

}

class Circle extends Shape implements Serializable{

private float radius; // 将被保存的字段。

transient int color; // 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

public static String type = "Circle"; // 一个静态变量不管是否被transient修饰,均不能被序列化

}

详情

查找算法之哈希查找

哈希查找是通过计算数据元素的存储地址进行查找的一种方法。O(1)的时间复杂度,即所谓的秒杀。哈希查找的本质是先将数据映射成它的哈希值。哈希查找的核心是构造一个哈希函数,它将原来直观、整洁的数据映射为看上去似乎是随机的一些整数。

JUC

JVM由哪些部分组成

在这里插入图片描述

  • JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
    • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
    • Execution engine(执行引擎):执行classes中的指令。
    • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
    • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

Java 8从永久代到元空间

方法区是JVM 的规范,永久代(PermGen space)是HotSpot对这种规范的实现。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之的是 Metaspace(元空间)。

并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

运行流程是什么

首先通过编译器把 Java 代码转换成字节码类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

说一下 JVM 运行时数据区

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:

简单的说就是我们java运行时的东西是放在那里的

在这里插入图片描述

程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

为什么要线程计数器?因为线程是不具备记忆功能

Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

栈帧就是Java虚拟机栈中的下一个单位

本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码

Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

栈先进后出,堆先进先出

详细的介绍下程序计数器?(重点理解)

  1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
  2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

详细介绍下Java虚拟机栈?(重点理解)

  1. Java虚拟机是线程私有的,它的生命周期和线程相同。
  2. 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 解释:虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。

在这里插入图片描述

解析栈帧:

  1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
  2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
  4. 方法出口:方法出口是什么呢,方法出口正常的话就是return 不正常的话就是抛出异常咯

一个方法调用另一个方法,会创建很多栈帧吗?

  • 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面(说明被调用者先进去)

栈指向堆是什么意思?

  • 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址

递归的调用自己会创建很多栈帧吗?

  • 答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去

详细的介绍Java堆吗?(重点理解)

  • java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
  • 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
  • 从内存回收角度来看java堆可分为:新生代和老年代
  • 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  • 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  • 堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定

深拷贝和浅拷贝

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
  • 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
  • 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

JVM如何判断一个对象是否存活

判断方式

  • 垃圾收集器对 Java堆里的对象 是否进行回收的判断准则:Java对象是存活 or 死亡

判断对象为死亡才会进行回收

  • Java虚拟机中,判断对象是否存活有2种方法:
    1. 引用计数法
    2. 引用链法(可达性分析法)

引用计数法

方式描述

  • Java 对象添加一个引用计数器
  • 每当有一个地方引用它时,计数器 +1;引用失效则 -1;

判断对象存活准则

当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。

优点

  • 实现简单
  • 判断高效

缺点

  • 无法解决 对象间相互循环引用 的问题

即该算法存在判断逻辑的漏洞

  • 具体描述
1
2
3
4
5
6
7
8
9
<-- 背景 -->
// 对象objA 和 objB 都有字段 name
// 两个对象相互进行引用,除此之外这两个人对象没有任何引用
objA.name = objB;
objB.name = objA;

<-- 问题 -->
// 实际上这两个对象已经不可能再被访问,应该要被垃圾收集器进行回收
// 但因为他们相互引用,所以导致计数器不为0,这导致引用计数算法无法通知垃圾收集器回收该两个对象

正由于该算法存在判断逻辑漏洞,所以 Java虚拟机没有采用该算法判断Java是否存活。

引用链法(可达性分析法)

img

垃圾收集器

天上飞的理念,要有落地的实现(垃圾收集器就是GC垃圾回收算法的实现)

GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现

GC算法主要有以下几种

  • 引用计数(几乎不用,无法解决循环引用的问题)

  • 复制拷贝(用于新生代)

    复制算法在年轻代的时候,进行使用,复制时候有交换。优点没有产生内存碎片

  • 标记清除(用于老年代)

    先标记,后清除,缺点是会产生内存碎片,用于老年代多一些

  • 标记整理(用于老年代)

简述Java垃圾回收机制

  • 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?

  • 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
  • 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
  • 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

垃圾回收详细过程

图解 Java 垃圾回收算法及详细过程!

虚拟机类加载机制

JAVA类加载器包括几种?

(1)启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
(2)扩展类加载器(extensions class loader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。
该类加载器在此目录里面查找并加载 Java类。
(3)系统类加载器(system class loader)也叫应用类加载器:它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader0来获取它。
(4)用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

简述java类加载机制?

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

类加载的机制及过程

  • 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

在这里插入图片描述

1、加载
  • 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
  • Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
  • 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。
  • 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口
2、连接过程
  • 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
  1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
  2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
  3. 解析:虚拟机常量池的符号引用替换为字节引用过程
3、初始化
  • 初始化阶段是执行类构造器<clinit>() 方法的过程。类构造器<clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确加锁和同步

初始化的总结就是:初始化是为类的静态变量赋予正确的初始值

双亲委托机制

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

怎么打破双亲委派机制

  1. 双亲委派模型的第一次“被破坏”是继承ClassLoader重写自定义加载器的loadClass(),将需要特殊对待的类自己先处理,非处理范围的类调用super方法即可。jdk不推荐。

  2. 一般都只是重写findClass(),这样可以保持双亲委派机制。而loadClass方法加载规则由自己定义,就可以随心所欲的加载类了。

  3. 调用defineClass()方法生成 Class 对象

打破双亲委派机制

jvm类加载器,类加载机制详解,看这一篇就够了

https://juejin.cn/post/6844904041131016200

https://zhuanlan.zhihu.com/p/142141937

Volatile

Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

原理

场景:

状态标记量

Java 中的双重检查(Double-Check)

Spring

Spring是一个轻量级的IoC和AOP容器框架,是为Java应用程序提供基础性服务的一套框架。

优点

(1)spring属于低侵入式设计,代码的污染极低;

(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;

(3)Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。

(4)spring对于主流的应用框架提供了集成支持。

常用注解


Spring

@component :标准一个普通的spring Bean类。可以代替@Repository、@Service、@Controller,因为这三个注解是被@Component标注的。

@Repository:标注一个DAO组件类。

@Service、@Controller实质上属于同一类注解,用法相同,功能相同,区别在于标识组件的类型。

@Autowired是Spring的注解,默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许null值,可以设置它的required属性为false。如果想使用名称装配可以结合@Qualifier注解进行使用。

@Resource默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行名称查找。

@Configuration 声明当前类为配置类,相当于xml形式的Spring配置(类上)

@Bean声明当前方法的返回值为一个bean,替代xml中的方式(方法上)


SpringMVC

@RequestMapping:请求路径映射,

@PathVariable:用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。

@RequestParam:主要用于在SpringMVC后台控制层获取参数。

@ResponseBody:该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

@RequestBody:该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上,再把HttpMessageConverter返回的对象数据绑定到controller中方法的参数上。


SpringBoot

@RestController :@ResponseBody和@Controller的合集。

@EnableAutoConfiguration:开启自动配置

@ComponentScan:表示将该类自动发现(扫描)并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。

@SpringBootApplication:相当于@EnableAutoConfiguration、@ComponentScan和@Configuration的合集。

@EnableScheduling 在配置类上使用,开启计划任务的支持(类上)

Spring的IoC理解

(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系、对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。

(2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。

(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入

依赖注入DI和控制反转IOC

https://www.jianshu.com/p/07af9dbbbc4b 控制反转(IoC)与依赖注入(DI)
https://zhuanlan.zhihu.com/p/67032669 理解依赖注入(DI – Dependency Injection)

依赖注入是种设计思想

Spring源码剖析——依赖注入实现原理

自定义注入注解

见代码自己 study-aop 注解学习

实现一个自定义的@Autowired

Spring的AOP理解

OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。

AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式,AOP代理主要分为静态代理动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP和cglib为代表。

(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)硬编码到Java字节码中,运行的时候就是增强之后的AOP对象

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理

  • JDK动态代理只提供接口的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
  • 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

对比:

静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

img

Spirng4和Spring5下的区别

Spring5 2.3.3.后

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.2.8.RELEASE / d07228cc-7d9d-41a7-b345-97208a848829
@After在切点方法后,return前执行
我是环绕通知后
我进入了业务层的queryUser方法

异常

方法前

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
2021-02-02 22:54:24.878 ERROR 66232 --- [nio-8089-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

方法后

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
我进入了业务层的queryUser方法
2021-02-02 22:53:26.961 ERROR 66232 --- [nio-8089-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
Spring5

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.1.5.RELEASE / e973d3ff-8764-46e3-9f1a-d9862fb750b5
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser前:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
9
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:17:13.164 ERROR 31916 --- [nio-8089-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
我进入了业务层的queryUser方法
2021-02-02 14:16:30.185 ERROR 31916 --- [nio-8089-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
Spring4

正常

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:4.3.6.RELEASE / fabcfd3c-7a9a-4290-a7a9-190f60bbf1ca
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser后:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:31:56.791 ERROR 59308 --- [nio-8089-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:35:40.903 ERROR 34152 --- [nio-8089-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

我进入了业务层的queryUser方法

Spring容器的启动流程

(1)初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中:

  • 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象

(2)将配置类的BeanDefinition注册到容器中:

(3)调用refresh()方法刷新容器:

BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

(1)BeanFactory是Spring里面最底层的接口,是IOC的核心,定义了IOC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。

  • BeanFactroy采用的是延迟加载形式来注入Bean时(调用getBean()),才对该Bean进行加载实例化。
  • BeanFactory通常以编程的方式被创建

(2)ApplicationContext接口作为BeanFactory的子类

  • 它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入

  • 相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

  • 通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

  • 框架功能:

    • 继承MessageSource,因此支持国际化。
  • 资源文件访问,如URL和文件(ResourceLoader)。

    • 载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
  • 提供在监听器中注册bean的事件。

AbstractApplicationcontext

AbstractApplicationContextApplicationContext 的抽象实现类,该抽象类实现应用上下文的一些具体操作,也是我们加载xml配置文件的入口类

AbstractApplicationContext是Spring应用上下文中最重要的一个类,这个抽象类中提供了几乎ApplicationContext的所有操作。主要有容器工厂的处理,事件的发送广播,监听器添加,容器初始化操作refresh方法,然后就是bean的生成获取方法接口等。主要还是提供了一些方法,复杂的操作也是没有太多。

Spring Bean的生命周期

简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation –> 属性赋值 Populate –> 初始化 Initialization –> 销毁 Destruction

img

(1)实例化Bean:

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

  • ①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
  • ②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • ②如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
  • ③如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;

(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

Spring中bean的作用域:

(1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。

(2)prototype:为每一个bean请求创建一个实例。

(3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。

(4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。

(5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。

Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

(1)对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。

(2)对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的(对于有状态的Bean)。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。

1
2
3
有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。

无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。

也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

Spring如何解决循环依赖问题

循环依赖问题在Spring中主要有三种情况:

  • (1)通过构造方法进行依赖注入时产生的循环依赖问题。

    在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。

  • (2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。

    每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

  • (3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。

Spring事务的实现方式和实现原理

spring支持编程式事务管理和声明式事务管理两种方式:

①编程式事务管理使用TransactionTemplate。

②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

启动类开启@EnableTransactionManager

(2)spring的事务传播机制:

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘

④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

如果使用try catch捕获异常了,事务还能回滚?

不能,事务是由aop动态代理实现,内部也是使用trycatch进行捕获的。如果代码层捕获了会导致传递不了到后续。

Spring事务原理一探

Spring Boot

什么是 CSRF 攻击?

CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。

Spring和Springboot的区别

Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。

SpringBoot与SpringCloud 区别

SpringBoot是快速开发的Spring框架,SpringCloud是完整的微服务框架,SpringCloud依赖于SpringBoot。

Spring Boot 有哪些优点?

Spring Boot 主要有如下优点:

  1. 容易上手,提升开发效率,为 Spring 开发提供一个更快、更简单的开发框架。
  2. 开箱即用,远离繁琐的配置。
  3. 提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
  4. SpringBoot总结就是使编码变简单、配置变简单、部署变简单、监控变简单等等

Spring Boot 的核心注解是哪个?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 例如:java 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
  • @ComponentScan:Spring组件扫描。

SpringBoot Starter的工作原理

我个人理解SpringBoot就是由各种Starter组合起来的,我们自己也可以开发Starter

在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)

SpringBoot的自动配置原理是什么

主要是Spring Boot的启动类上的核心注解SpringBootApplication注解主配置类,有了这个主配置类启动时就会为SpringBoot开启一个@EnableAutoConfiguration注解自动配置功能。

@EnableAutoConfiguration (开启自动配置) 该注解引入了AutoConfigurationImportSelector,该类中 的方法会扫描所有存在META-INF/spring.factories的jar包。

有了这个EnableAutoConfiguration的话就会:

  1. 从配置文件META_INF/Spring.factories加载可能用到的自动配置类
  2. 去重,并将exclude和excludeName属性携带的类排除
  3. 过滤,将满足条件(@Conditional)的自动配置类返回
1
2
3
4
5
6
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata
, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}

bootstrap.properties 和 application.properties 有何区别 ?

  • bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud 配置就会使用这个文件。且 boostrap 里面的属性不能被覆盖;
  • application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目 的自动化配置。

SpringBoot事物的使用

SpringBoot的事物很简单,首先使用注解@EnableTransactionManagement开启事物之后,然后在Service方法上添加注解@Transactional便可。

Async异步调用方法

在SpringBoot中使用异步调用是很简单的,只需要在方法上使用@Async注解即可实现方法的异步调用。 注意:需要在启动类加入@EnableAsync使异步调用@Async注解生效。

定时任务的使用

在SpringBoot中使用定时任务是很简单的,只需要在方法上使用@EnableScheduling注解即可实现。@Scheduled配合cron表达式使用

Spring Boot 有哪几种读取配置的方式?

Spring Boot 可以通过 @PropertySource, @Value, @Environment, @ConfigurationPropertie注解来绑定变量

@propertysource(“classpath:LoadBeanFromOuter.properties”)

@configurationproperties(prefix = “Spring.data”)

@Value注解标注的bean属性装配是依靠AutowiredAnnotationBeanPostProcessor在bean的实例化、初始化阶段完成的。

Spring Boot读取配置文件的几种方式

springboot中@Value的工作原理

Spring Boot 是否可以使用 XML 配置 ?

Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。

SpringBoot多数据源拆分的思路

先在properties配置文件中配置两个数据源,创建分包mapper,使用@ConfigurationProperties读取properties中的配置,使用@MapperScan注册到对应的mapper包中

在一个项目中多数据源如何划分:1.分包名(业务) 2.注解方式
cn.li.test01—datasource1
cn.li.test02—datasource2

SpringBoot多数据源事务如何管理

  • 第一种方式是在service层的@TransactionManager中使用transactionManager指定DataSourceConfig中配置的事务
  • 第二种是使用jta-atomikos实现分布式事务管理

如何实现 Spring Boot 应用程序的安全性?

为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。

Spring Boot 中如何解决跨域问题 ?

  1. 因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。
  2. 通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}

}

SpringBoot性能如何优化

  • 如果项目比较大,类比较多,不使用@SpringBootApplication,采用@Compoment指定扫包范围

  • 在项目启动时设置JVM初始内存和最大内存相同

    vm option中设置

  • 将springboot内置服务器由tomcat设置为undertow

SpringBoot微服务中如何实现 session 共享 ?

常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。

spring-boot-starter-parent 有什么用 ?

我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:

  1. 定义了 Java 编译版本为 1.8 。
  2. 使用 UTF-8 格式编码。
  3. 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
  4. 执行打包操作的配置。
  5. 自动化的资源过滤。
  6. 自动化的插件配置。
  7. 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。

总结就是打包用的

SpringBoot如何实现打包

  • 进入项目目录在控制台输入mvn clean package,clean是清空已存在的项目包,package进行打包
  • 或者点击左边选项栏中的Maven,先点击clean在点击package

SpringBoot如何优雅的停机

1、获取程序启动时候的context,然后关闭主程序启动时的context。这样程序在关闭的时候也会调用PreDestroy注解。如下方法在程序启动十秒后进行关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StudyAopApplication {

public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(StudyAopApplication.class, args);

try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.close();
}

}

2、写一个Controller,然后将自己写好的Controller获取到程序的context,然后调用自己配置的Controller方法退出程序。通过调用自己写的/shutDownContext方法关闭程序:curl -X POST http://localhhost:8085/shutdown/context。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/shutdown")
public class ShutDownController implements ApplicationContextAware {

private ApplicationContext context;

@PostMapping("/context")
public String shutDownContext() {
ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context;
ctx.close();
return "context is shutdown";
}

@GetMapping("/")
public String getIndex() {
return "OK";
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}

Springboot 优雅停止服务的几种方法

SpringCloud

SpringCloud学习

SpringCloud技术栈使用

CAP原则

QPS、TPS

保证接口的幂等性

幂等性是系统服务对外一种承诺,承诺只要调用接口成功,外部多次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。

在我调用别人服务失败的时候,你应该怎么做?服务降级。

然后每次执行的结果都会发生变化,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。

解决方案:

token机制

1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。

2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。

3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。

4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

关键点 先删除token,还是后删除token。

后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题。

先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。

token机制缺点
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。

乐观锁机制

这种方法适合在更新的场景中,update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

唯一主键
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。注意去重表和业务表应该在同一库中。

唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。

如何保证接口的幂等性

通过open feign怎么绕过spring security的安全拦截认证

思路是这样的,通过feign调用其他服务的时候,调用一个拦截器,在拦截器中把token传入request头的Authorization参数中。这里的拦截器为feign.RequestInterceptor,实现代码举例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SsoFeignConfig implements RequestInterceptor {

public static String TOKEN_HEADER = "authorization";

@Override
public void apply(RequestTemplate template) {
template.header(TOKEN_HEADER, getHeaders(getHttpServletRequest()).get(TOKEN_HEADER));
}

private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (Exception e) {
return null;
}
}

private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}

}

在调用方声明微服务的时候通过@FeignClient注释的configuration属性指定拦截器

1
2
3
4
5
6
7
@FeignClient(name="user-server/userApi", fallback=UserServiceImpl.class, configuration = SsoFeignConfig.class)
public interface UserService {

@GetMapping("/user/name/{username}")
public SimpleResponse getByusername(@PathVariable("username") String username);

}

@FeignClient中的name为微服务的服务名+微服务的contextPath,fallback为服务调用失败的处理类,configuration用来指定拦截器

到此为止,OAuth2+JWT模式下的swagger+feign处理就完成了

Spring Cloud中Feign如何统一设置验证token

springCloud微服务系列 OAuth2+JWT模式下的swagger+feign处理

Redis

redis入门到进阶

什么是Redis?

Redis 是一个使用 C 语言写成的,开源的高性能key-value非关系缓存数据库。它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。Redis的数据都基于缓存的,所以很快,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。Redis也可以实现数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。

Redis有哪些优缺点?

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有

持久化

什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

在这里插入图片描述

优点:

1、只有一个文件 dump.rdb,方便持久化。

2、容灾性好,一个文件可以保存到安全的磁盘。

3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能

4.相对于数据集大时,比 AOF 的启动效率更高。

缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复

在这里插入图片描述

优点:

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。

2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

1、AOF 文件比 RDB 文件大,且恢复速度慢。

2、数据集大的时候,比 rdb 启动效率低。

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

  • 过期策略通常有以下三种:
  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis的内存

Redis的内存淘汰策略有哪些

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis主要消耗什么物理资源?

  • 内存。

Redis的内存用完了会发生什么?

  • 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

  • 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hash),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

Redis事务

Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令

Redis 的事务是总是带有隔离性的。

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段

  1. 事务开始 MULTI
  2. 命令入队
  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。

  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

  • UNWATCH命令可以取消watch对所有key的监控。

如何解决 Redis 的并发竞争 Key 问题

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

redis缓存穿透,缓存击穿,缓存雪崩

缓存穿透

缓存和数据库中都没有的数据,可用户还是源源不断的发起请求,导致每次请求都会到数据库,从而压垮数据库。

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

解决办法

  ①、业务层校验

  用户发过来的请求,根据请求参数进行校验,对于明显错误的参数,直接拦截返回。

  比如,请求参数为主键自增id,那么对于请求小于0的id参数,明显不符合,可以直接返回错误请求。

  ②、不存在数据设置短过期时间

  对于某个查询为空的数据,可以将这个空结果进行Redis缓存,但是设置很短的过期时间,比如30s,可以根据实际业务设定。注意一定不要影响正常业务。

  ③、布隆过滤器**

  采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

  对于缓存穿透,我们可以将查询的数据条件都哈希到一个足够大的布隆过滤器中,用户发送的请求会先被布隆过滤器拦截,一定不存在的数据就直接拦截返回了,从而避免下一步对数据库的压力。

缓存击穿

Redis中一个热点key在失效的同时,大量的请求过来,从而会全部到达数据库,压垮数据库。

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

①、设置热点数据永不过期

  对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。

②、定时更新

  比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。

③、互斥锁

  这是解决缓存击穿比较常用的方法。

  互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

④分级缓存(缓存两份数据,第二份数据生存时间长一点作为备份,第一份数据用于被请求命中,如果第二份数据被命中说明第一份数据已经过期,要去mysql请求数据重新缓存两份数据)

缓存雪崩

Redis中缓存的数据大面积同时失效,或者Redis宕机,从而会导致大量请求直接到数据库,压垮数据库。

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方法:

  1. 保证Redis服务高可用:redis集群,将原来一个人干的工作,分发给多个人干
  2. 缓存预热:对于即将来临的大量请求,我们可以提前走一遍系统,将数据提前缓存在Redis中,并设置不同的过期时间。
  3. 避免缓存设置相近的有效期,我们可以在设置有效期时增加随机值,或者统一规划有效期,使得过期时间均匀分布。

缓存预热

  • 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
  • 解决方案
    1. 直接写个缓存刷新页面,上线时手工操作一下;
    2. 数据量不大,可以在项目启动的时候自动进行加载;
    3. 定时刷新缓存;

缓存降级

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
  • 缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

  • 热点数据,缓存才有价值
  • 对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
  • 对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
  • 数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
  • 那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key

  • 缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

  • 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

如何保证缓存与数据库双写时的数据一致性?

问题场景 描述 解决
先写缓存,再写数据库,缓存写成功,数据库写失败 缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读 这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败 写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据 缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新 指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候 确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

一个字符串类型的值能存储最大容量是多少?

  • 512M

哨兵模式

RabbitMQ

MQ就是消息队列。是软件和软件进行通信的中间件产品

优点有以下几个:

异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。

应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。

流量削峰 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。

日志处理 - 解决大量日志传输。

消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

缺点有以下几个:

  1. 系统可用性降低

    本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;

  2. 系统复杂度提高

    加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。

  3. 一致性问题

    A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

基本概念

Broker: 简单来说就是消息队列服务器实体

Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列

Queue: 消息队列载体,每个消息都会被投入到一个或多个队列

Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来

Routing Key: 路由关键字,exchange根据这个关键字进行消息投递

VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。

Producer: 消息生产者,就是投递消息的程序

Consumer: 消息消费者,就是接受消息的程序

Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务

由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。

Work Queues (工作队列

https://www.coderblue.cn/3491549389/#常用工作模式

也叫公平队列,能者多劳的消息队列模型。队列必须接收到来自消费者的手动ack 才可以继续往消费者发送消息。默认自动应答。

ACK机制

为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。

如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。

1
2
// 返回确认状态:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

但是这样还是不够的,如果rabbitMQ-Server突然挂掉了,那么还没有被读取的消息还是会丢失 ,所以我们可以让消息持久化。 只需要在定义Queue时,设置持久化消息就可以了,方法如下:

1
2
boolean durable = true;
channel.queueDeclare(channelName, durable, false, false, null);

这样设置之后,服务器收到消息后就会立刻将消息写入到硬盘,就可以防止突然服务器挂掉,而引起的数据丢失了。 但是服务器如果刚收到消息,还没来得及写入到硬盘,就挂掉了,这样还是无法避免消息的丢失。

消息的重复问题

消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。

如何保证RabbitMQ消息的顺序性?

拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;

或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

消息如何分发?

若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。通过路由可实现多消费的功能

消息基于什么传输?

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

如何保证消息不被重复消费?或者说,如何保证消息消费时的幂等性?

针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;

在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;

如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?

发送方确认模式

  • 将信道设置成confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID
  • 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。
  • 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。
  • 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

接收方确认机制

  • 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
  • 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;

下面罗列几种特殊情况

  • 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
  • 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。

如何保证队列的消息不被重复消费?

这个需要灵活作答,考察的是思考力,因为消费的场景有很多,有数据库、有缓存、有第三方接口

  • 1.比如针对数据库,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键(或者UUID),那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  • 2.再比如redis缓存,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
  • 3.再比如第三方接口,需要确定两点,第三方接口程序是有去重能力的,那么脏一点直接丢数据过去,如果没有去重能力,还是需要我们来写程序去重,就是第2点的办法。

分发给一个订阅的消费者

  • 消息不可靠的情况可能是消息丢失,劫持等原因;

  • 丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;

  1. 生产者丢失消息:从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来 确保生产者不丢消息; transaction机制就是说:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送 过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务 (channel.txCommit())。然而,这种方式有个缺点:吞吐量下降; confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派 一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后; rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确 到达目的队列了; 如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
  2. 消息队列丢数据:消息持久化。 处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。 这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个 Ack信号。 这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动 重发。 那么如何持久化呢? 这里顺便说一下吧,其实也很容易,就下面两步 1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列 2. 发送消息的时候将deliveryMode=2这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据
  3. 消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可! 消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息; 如果这时处理消息失败,就会丢失该消息; 解决方案:处理消息成功后,手动回复确认消息。

为什么不应该对所有的 message 都使用持久化机制?

首先,必然导致性能的下降,因为写磁盘比写 RAM 慢的多,message 的吞吐量可能有 10 倍的差距。其次,message 的持久化机制用在 RabbitMQ 的内置 cluster 方案时会出现“坑爹”问题

如何解决消息队列的延时以及过期失效问题?

其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢,造成消息堆积了,MQ存储快要爆了,甚至开始过期失效删除数据了。

针对这个问题可以有事前、事中、事后三种处理

  • 事前:开发预警程序,监控最大的可堆积消息数,超过就发预警消息(比如短信),不要等出生产事故了再处理。
  • 事中:看看消费端是不是故障停止了,紧急重启。
  • 事后:中华石杉老师就是说的这一种(https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-time-delay-and-expired-failure.md),需要对消费端紧急扩容 ,增加处理消费者进程,如扩充10倍处理,但其实这也有个问题,即数据库的吞吐是有限制的,如果是消费到数据库也是没办法巨量扩容的,所以还是要在吞吐能力支持下老老实实的泄洪消 费。所以事前预防还是最重要的。否则无法删除过期数据,那就需要再重写生产消息的程序,重新产生消息。

RabbitMQ 的高可用性如何保证?

RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式

队列

死信队列?

DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。

导致的死信的几种原因?

  • 消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
  • 消息TTL过期。
  • 队列满了,无法再添加。

优先级队列?

优先级高的队列会先被消费。

可以通过x-max-priority参数来实现。

当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。

延迟队列?

存储对应的延迟消息,指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

如何使用延时队列

延时队列,不就是想要消息延迟多久被处理吗,TTL则刚好能让消息在延迟多久之后成为死信,另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就万事大吉了,因为里面的消息都是希望被立即处理的消息。

生产者生产一条延时消息,根据需要延时时间的不同,利用不同的routingkey将消息路由到不同的延时队列,每个队列都设置了不同的TTL属性,并绑定在同一个死信交换机中,消息过期后,根据routingkey的不同,又会被路由到不同的死信队列中,消费者只需要监听对应的死信队列进行处理即可。

具体参考

https://segmentfault.com/a/1190000006879700)

延时队列使用场景

那么什么时候需要用延时队列呢?考虑一下以下场景:

  1. 订单在十分钟之内未支付则自动取消。
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 账单在一周内未支付,则自动结算。
  4. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  5. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  6. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。

RabbitMQ面试题必知必会29道(附答案)

Springboot整合RabbitMQ

MySQL

MyISAM索引与InnoDB索引的区别?

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

存储引擎选择

  • 如果没有特别的需求,使用默认的Innodb即可。
  • MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
  • Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。

Mysql的外键约束

由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,class_id仅仅是一个普通的列,只是它起到了外键的作用而已。

采用读锁

注意:删除外键约束并没有删除外键这一列。删除列是通过DROP COLUMN ...实现的。

主键索引与唯一索引的区别

  1. 主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
  2. 主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
  3. 唯一性索引列允许空值,而主键列不允许为空值。
  4. 主键列在创建时,已经默认为空值 ++ 唯一索引了。
  5. 一个表最多只能创建一个主键,但可以创建多个唯一索引。
  6. 主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
  7. 主键可以被其他表引用为外键,而唯一索引不能。

唯一索引比普通索引快吗, 为什么?

唯一索引不一定比普通索引快, 还可能慢.

  1. 查询时, 在未使用 limit 1 的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配 下一条数据, 发现不匹配后返回. 如此看来唯一索引少了一次匹配, 但实际上这个消耗微乎其微.
  2. 更新时, 这个情况就比较复杂了. 普通索引将记录放到 change buffer 中语句就执行完毕了. 而对 唯一索引而言, 它必须要校验唯一性, 因此, 必须将数据页读入内存确定没有冲突, 然后才能继续操 作. 对于写多读少的情况, 普通索引利用 change buffer 有效减少了对磁盘的访问次数, 因此普通 索引性能要高于唯一索引.

索引有哪几种类型?

主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引

普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

  • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
  • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引

Full-text (全文索引) :全文索引也是MyISAM的一个特殊索引类型,主要用于全文索引,InnoDB从 Mysql5.6版本开始支持全文索引。。

索引的数据结构(b树,hash)

  • 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

1、B树索引

  • mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)

在这里插入图片描述

  • 查询方式:
    • 主键索引区:PI(关联保存的时数据的地址)按主键查询,
    • 普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快
  • B+tree性质:
    1. n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
    2. 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
    3. 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
    4. B+ 树中,数据对象的插入和删除仅在叶节点上进行。
    5. B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

2、哈希索引

  • 简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。

在这里插入图片描述

创建索引的三种方式

第一种方式:在执行CREATE TABLE时创建索引

1
CREATE TABLE user_index2 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card) );

第二种方式:使用ALTER TABLE命令去增加索引

ALTER TABLE table_name ADD INDEX index_name (column_list);

  • ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
  • 其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
  • 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

第三种方式:使用CREATE INDEX命令创建

CREATE INDEX index_name ON table_name (column_list);

  • CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)

你怎么看到为表格定义的所有索引?

索引是通过以下方式为表格定义的: SHOW INDEX FROM <table_name>

mysql中btree与hash索引的适用场景和限制

BTree索引是最常用的mysql数据库索引算法,因为它不仅可以被用在=,>,>=, Innodb和MyISAM默认的索引是Btree索引。

Hash索引,其检索效率非常高的一种精确定位索引

Hash索引不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引,它会将计算出的Hash值和对应的行指针信息记录在Hash表中。但是虽然Hash效率很高但是同样也有很多的弊端存在和限制存在。

(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。

(2)Hash 索引无法被用来避免数据的排序操作。

(3)Hash 索引不能利用部分索引键(组合索引)查询。

(4)Hash 索引在任何时候都不能避免表扫描。

(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。

B树和B+树的区别

  • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。
  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。

在这里插入图片描述

使用B树的好处

  • B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

使用B+树的好处

  • 由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间复杂度找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

列设置为 AUTO INCREMENT

如果在表中达到最大值,会发生什么情况?它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。

怎样才能找出最后一次插入时分配了哪个自动增量?LAST_INSERT_ID 将返回由 Auto_increment 分配的最后一个值,并且不需要指定表名称。

事物的四大特性(ACID)介绍一下?

  • 关系性数据库需要遵循ACID规则,具体内容如下:
  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

事务的隔离级别

  1. 读未提交(RU): 一个事务还没提交时, 它做的变更就能被别的事务看到.
  2. 读已提交(RC): 一个事务提交之后, 它做的变更才会被其他事务看到.
  3. 可重复读(RR): 一个事务执行过程中看到的数据, 总是跟这个事务在启动时看到的数据是一致的. 当然在可重复读隔离级别下, 未提交变更对其他事务也是不可见的.
  4. 串行化(S): 对于同一行记录, 读写都会加锁. 当出现读写锁冲突的时候, 后访问的事务必须等前一个 事务执行完成才能继续执行.

从锁的类别上分MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

  • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁就是让多个线程同时获取一个锁。
  • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排它锁也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥,其它事务可以读取,但不能进行更新和插入操作

数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

    1
    2
    // 核心SQL,主要靠for update
    select status from t_goods where id=1 for update;
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:一般会使用版本号机制或CAS算法实现。

    1
    2
    //核心SQL
    update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

悲观锁、乐观锁的区别及使用场景

定义:

悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

适用场景:

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适,之所以用悲观锁就是因为两个用户更新同一条数据的概率高,也就是冲突比较严重的情况下,所以才用悲观锁.

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。

两种锁的使用场景

  • 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
  • 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

行锁和表锁的含义及区别

记录完整的死锁日志show engine innodb status 时,显示的信息不全。

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

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

显式加锁:

上共享锁(读锁)的写法:lock in share mode,例如:

1
select math from zje where math>60 lock in share mode

上排它锁(写锁)的写法:for update,例如:

1
select math from zje where math >60 for update

表锁

不会出现死锁,发生锁冲突几率高,并发低。

MyISAM在执行查询语句(select)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。

有两种模式:

  • 表共享读锁
  • 表独占写锁

行锁

会出现死锁,发生锁冲突几率低,并发高。

在MySQL的InnoDB引擎支持行锁,与Oracle不同,MySQL的行锁是通过索引加载的,也就是说,行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,行锁则无法实现,取而代之的是表锁,此时其它事务无法对当前表进行更新或插入操作。

SQL执行顺序

1
2
3
4
5
6
7
8
9
SELECT DISTINCT <select_list>
FROM <left_table>
<join_type> JOIN <right_table>
ON <join_condition>
WHERE <where_condition>
GROUP BY <group_by_list>
HAVING <having_condition>
ORDER BY <order_by_condition>
LIMIT <limit_number>

sql优化

什么是通用 SQL 函数?

  • CONCAT(A, B) - 连接两个字符串值以创建单个字符串输出。通常用于将两个或多个 字段合并为一个字段。
  • FORMAT(X, D)- 格式化数字 X 到 D 有效数字。
  • CURRDATE(), CURRTIME()- 返回当前日期或时间。
  • NOW() - 将当前日期和时间作为一个值返回。
  • DATE_FORMAT(date,format) - 将日期格式化select date_format(‘2017-10-12’, ‘%Y-%m-%d’)
  • STR_TO_DATE(str,format):将字符串格式化成Date格式
  • MONTH(),DAY(),YEAR(),WEEK(),WEEKDAY() - 从日期值中 提取给定数据。
  • HOUR(),MINUTE(),SECOND() - 从时间值中提取给定数据。
  • DATEDIFF(A,B) - 确定两个日期之间的差异,通常用于计算年龄
  • SUBTIMES(A,B) - 确定两次之间的差异。
  • FROMDAYS(INT) - 将整数天数转换为日期值

优化

做过哪些MySQL索引相关优化

  • 尽量使用主键查询: 聚簇索引上存储了全部数据, 相比普通索引查询, 减少了回表的消耗.
  • MySQL5.6之后引入了索引下推优化, 通过适当的使用联合索引, 减少回表判断的消耗.
  • 若频繁查询某一列数据, 可以考虑利用覆盖索引避免回表.
  • 联合索引将高频字段放在最左边,最左前缀法则.

一千万条数据的表, 如何分页查询

数据量过大的情况下, limit offset 分页会由于扫描数据太多而越往后查询越慢. 可以配合当前页最后 一条ID进行查询, SELECT * FROM T WHERE id > #{ID} LIMIT #{LIMIT} . 当然, 这种情况下ID必须 是有序的, 这也是有序ID的好处之一.

订单表数据量越来越大导致查询缓慢, 如何处理

分库分表. 由于历史订单使用率并不高, 高频的可能只是近期订单, 因此, 将订单表按照时间进行拆分, 根据数据量的大小考虑按月分表或按年分表. 订单ID最好包含时间(如根据雪花算法生成), 此时既能根据订单ID直接获取到订单记录, 也能按照时间进行查询。

MySQL面试题(总结最全面的面试题)

MySQL进阶学习

Mybatis

MyBatis是什么?

Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。

MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

获取数组中指定的位置的数据

1
2
3
4
#{list[0]}

// 如果是对象的话
#{userInfo.name}

Mybatis小笔记

Mybatis的一级、二级缓存

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/>

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

MyBatis编程步骤是什么样的?

  • 1、 创建SqlSessionFactory
  • 2、 通过SqlSessionFactory创建SqlSession
  • 3、 通过sqlsession执行数据库操作
  • 4、 调用session.commit()提交事务
  • 5、 调用session.close()关闭会话

请说说MyBatis的工作原理

img

浅谈 MyBatis 三级缓存

会先查找二级缓存,未命中然后查询一级缓存。清空策略看配置,commit时会清空一级缓存,更新二级缓存。

mybatis的延时加载

Mybatis配置文件中的settings标签中通过两个属性lazyLoadingEnabled和aggressiveLazyLoading来控制延迟加载和按需加载。

lazyLoadingEnabled:是否启用延迟加载,mybatis默认为false,不启用延迟加载。lazyLoadingEnabled属性控制全局是否使用延迟加载,特殊关联关系也可以通过嵌套查询中fetchType属性单独配置(fetchType属性值lazy或者eager)。

aggressiveLazyLoading:是否按需加载属性,默认值false,lazyLoadingEnabled属性启用时只要加载对象,就会加载该对象的所有属性;关闭该属性则会按需加载,即使用到某关联属性时,实时执行嵌套查询加载该属性。

1
2
3
4
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

在一对多的情况下使用延时加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qst.dao.IUserDao">

<resultMap id="UserAndAcc" type="com.qst.domain.User">
<id column="id" property="id" ></id>
<result column="username" property="username"></result>
<result column="gender" property="gender"></result>
<result column="email" property="email"></result>
<result column="did" property="did"></result>
<collection property="accounts" column="id" ofType="com.qst.domain.Account" select="com.qst.dao.IAccountDao.findallByuid"></collection>
</resultMap>

<select id="findAlls" resultMap="UserAndAcc">
select * from employee
</select>

</mapper>

Linux

常用命令

1
2
3
4
5
6
mkdir 用于新建一个新目录
rm— Remove 会删除给定的文件
cp— Copy 命令对文件进行复制
tail 默认在标准输出上显示给定文件的最后10行内容,可以使用tail -n N 指定在标准输出上显示文件的最后N行内容。
grep 在给定的文件中搜寻指定的字符串。
su 用于切换不同的用户。

linux的chmod赋权限命令相关

1
2
3
4
5
6
7
8
chmod 777 file
-rw------- (600) -- 只有属主有读写权限。
-rw-r--r-- (644) -- 只有属主有读写权限;而属组用户和其他用户只有读权限。
-rwx------ (700) -- 只有属主有读、写、执行权限。
-rwxr-xr-x (755) -- 属主有读、写、执行权限;而属组用户和其他用户只有读、执行权限。
-rwx--x--x (711) -- 属主有读、写、执行权限;而属组用户和其他用户只有执行权限。
-rw-rw-rw- (666) -- 所有用户都有文件读、写权限。这种做法不可取。
-rwxrwxrwx (777) -- 所有用户都有读、写、执行权限。更不可取的做法。

nginx操作命令

1、重启

1
2
service nginx restart
systemctl restart nginx

2、刷新配置文件

1
nginx -s reload

3、上传文件大小限制

1
2
3
4
5
6
7
8
9
10
可以选择在http{ }中设置:client_max_body_size   1m; //默认值为1m,推荐值500M

也可以选择在server{ }中设置:client_max_body_size 20m;

还可以选择在location{ }中设置:client_max_body_size 20m;

三者到区别是:
http{} 中控制着所有nginx收到的请求。
报文大小限制设置在server{}中,则控制该server收到的请求报文大小。
配置在location中,则报文大小限制,只对匹配了location 路由规则的请求生效。

docker常用操作命令

1、使用命令进入对应容器的目录

1
docker exec -it a5f02a3e6dde(启动的tomcat容器的容器id) /bin/bash

2、查看正在/所有容器

1
2
docker ps //查看正在运行的容器
docker ps -a //查看所有的容器(包括run、stop、exited状态的)

docker入门

Java8新特性

去重

1、集合对象:根据字段去重

1
2
3
4
5
6
7
8
9
// 根据 projectCode去重 , collectingAndThen:Collectors操作后附加的整理步骤
List<ProjectStageDto> dataList = projectStageDtoList.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getProjectcode()))), ArrayList::new));

// 不能是Map<String, Object>
List<Map<String, String>> filterList = quickWayList.stream().collect(
Collectors.collectingAndThen(Collectors.toCollection(
() -> new TreeSet<>(Comparator.comparing(item -> item.get("id")))), ArrayList::new));

2、集合元素,也可针对String类型的去重

1
2
3
4
5
6
7
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 5, 5, 5, 6, 7);

List<Integer> distinctNumbers =
numbers.stream().distinct()
.collect(Collectors.toList());

System.out.println(distinctNumbers);//1, 2, 3, 4, 5, 6, 7

Java8Stream流Api学习

ElasticSearch

Elasticsearch 的倒排索引是什么

传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 O(1) 时间复杂度的效率检索文章了,极大的提高了检索效率。

学术的解答方式:

倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。

加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。

lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点:

1、空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;

2、查询速度快。O(len(str))的查询时间复杂度。

但是倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segmentmemory 增长趋势。

在 Elasticsearch 中,是怎么根据一个词找到对应的倒排索引 的?

(1)Lucene的索引过程,就是按照全文检索的基本过程,将倒排表写成此文件格式的过程。

(2)Lucene的搜索过程,就是按照此文件格式将索引进去的信息读出来,然后计算每篇文档打分 (score)的过程。

详细描述一下 Elasticsearch 更新和删除文档的过程。

(1)删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以 展示其变更;

(2)磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是 在.del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。

(3)在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档 在.del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

如何读算法复杂度符号?

如O(n), O of n 或者 Big O of n

https://zhuanlan.zhihu.com/p/102500311

https://zhuanlan.zhihu.com/p/139762008

Dubbo

算法

不死兔

古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String args[]) {
int i = 0;
for (i = 1; i <= 20; i++)
System.out.println(f(i));
}

public static int f(int x) {
if (x == 1 || x == 2) // 校验是否前两个月的
return 1;
else
// 排除出生后第一个月第二个月情况,然后通过递归看是3的几倍数,如果有零月份就由前面if判断执行。
return f(x - 1) + f(x - 2);
}

https://github.com/CoderBleu/leetcode/tree/master/Java

设计模式

单例模式

Spring依赖注入Bean实例默认是单例的。spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。

Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。

简单工厂

BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

策略模式 - if else

在SimpleInstantiationStrategy中有如下代码说明了策略模式的使用情况:

装饰器模式

IO流

观察者模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
spring中Observer模式常用的地方是listener的实现。如ApplicationListener。

或者 该类的实现类ApplicationContextEvent表示ApplicaitonContext的容器事件.

设计模式深入学习

设计思路

精准推送

本来想采用redis的发布订阅模式的,但是

数据可靠性无法保证。消息的发布是无状态的,即发布完消息后该redis-cli便在理会该消息是否被接受到,是否在传输过程中丢失,即对于发布者来说,消息是”即发即失”的.

资源消耗较高,发布者不需要独占一个Redis的链接,而消费者则需要单独占用一个Redis的链接。

Redis pub/sub机制在实际运用场景的理解

  1. 用户关联标签,比如商业、金融、科技等,通过中间表关联

  2. 标签表里有router_key,标签名称

  3. 根据标签动态创建交换机,fanout模式的,根据标签来,

  4. 如果用户有多个标签匹配的,我们根据标签去重只要一个

  5. 根据用户和标签关联的list集合遍历创建队列,然后创建后遍历时还要判断根据标签选择对应的广播交换机绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 创建队列
    Queue queue = new Queue(q.getName(), q.getDurable(),
    q.getExclusive(), q.getAutoDelete(), q.getArgs());

    // 动态创建交换机
    Exchange exchange = new TopicExchange(e.getName());

    // 队列,交换机,routerkey
    Binding binding = BindingBuilder.bind(queue)
    .to(SpringBeanUtils.getBean(exchangeConfig.getName(), TopicExchange.class))
    .with(q.getRoutingKey());

    当消费者方法为void方法时,QueueMessageService.convertSendAndReceive方法返回值为null,注意检查消费者监听方法。
  6. 选择一条事项进行推送

  7. 利用根据标签配置多条监听

    1
    2
    3
    4
    5
    6
    @RabbitListener(bindings = {
    @QueueBinding(
    value = @Queue, exchange = @Exchange(type = "topic", name = "枚举同交换机名"),
    key = {"标签名或者routerkey", "user.*"}
    )
    })
  1. 如果后期此用户申报了这个关联的标签事项,那么我们在中间表移除这个。如果都为空了,我们就根据用户浏览事项搜索次数来添加。

  2. 每天凌晨五点定时推送

https://blog.csdn.net/qq_28533563/article/details/107025629

热词搜索

搜索是前端需要的功能,搜索事项政策时需要的。

  1. 用户搜索事项次数的统计,但是存在限制条件:
  • 同ip下一天只能累计一次
  • 相似度超过80%算重复(根据字符串长度和是否短的存在长的里面)
  • 没有超过三天的过期时间,超过则重置
  1. 首先我们会把事项列表数据存入ES

  2. 用户搜索的时候会调用Get查询ES事项列表数据

  3. 我们会将当天下查询的事项名称和查询次数列表形式存入redis,符合上述俩个限制条件的就不累计

  4. redis改之前,对应库数据也要加一(事项名称,查询次数,创建时间,过期时间-三天),如果超过过期时间就重置此事项名称统计数为1,而且redist获取到列表的对应事项名也要如此,不然就原基础加一

  5. 每次查询后都是要把redis中map列表数据拿出来,对应的搜索次数累计加一,然后重新set,过期时间两天啥的都行,

  6. 定时清除redis中的,然后从数据库中查询同步数据到redis

    1
    2
    3
    4
    5
    6
    7
    0 0 5 * * ?

    最近10次运行时间:
    2021-04-05 05:00:00
    2021-04-06 05:00:00
    2021-04-07 05:00:00
    ...

Elasticsearch

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  1. © 2020-2021 Lauy    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信