Java面试题-进阶篇(2022.4最新汇总)

Java面试题-进阶篇

1. 基础篇

1.1 基本数据类型和包装类

1. 基本数据类型和包装类

基本类型 大小(字节) 默认值 封装类
byte 1 (byte)0 Byte
short 2 (short)0 Short
int 4 0 Integer
long 8 0L Long
float 4 0.0f Float
double 8 0.0d Double
boolean 1 false Boolean
char 2 \u0000(null) Character

2. int 和Integer所占字节大小一样吗?

int和integer 占用内存一样,都是4个字节。

1.2 Double转Bigdecimal可能会出现哪些问题?怎么解决?

丢失精度
不能直接使用Bigdecimal的构造函数传double进行转换,部分数值会丢失精度,因为计算机是二进制的,Double无法精确的储存一些小数位。

解决方法 Double转BIgdecimal的两种常用方式:

  • 先将Double转换为String再用Bigdecimal构造函数,则不会发生精度丢失问题;
  • 直接调用Bigdecimal的valueOf()函数。其内部实现也是先转String再用Bigdecimal构造函数。

1.3 equals 与 == 的区别?

==

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是同一个对象。比较的是真正意义上的指针操作。

  • 比较的是操作符两端的操作数是否是同一个对象。
  • 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
  • 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:
    int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。

equals

equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断

1.4 Java创建对象有几种方式?

  • new创建新对象
  • 通过反射机制
  • 采用clone机制
  • 通过序列化机制

1.5 Java中的反射机制

1. 定义

反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

2. 反射的实现方式

获取Class对象,有4中方法:

  • Class.forName(“类的路径”);
  • 类名.class
  • 对象名.getClass()
  • 基本类型的包装类,可以使用 包装类.TYPE来获得该包装类的Class对象

3. 获取对象的信息

  • Class:表示正在运行的Java应用程序中的类和接口
  • Field:提供有关类和接口的属性信息,以及对它的动态访问权限。
  • Constructor:提供关于类的单个构造方法的信息以及它的访问权限
  • Method:提供类或接口中某个方法的信息

4. 反射机制的优缺点

优点

  • 能够运行时动态获取类的实例,提高灵活性;
  • 与动态编译结合

缺点

  • 使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
  • 相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)。

性能低的解决方案:

1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;
2、多次创建一个类的实例时,有缓存会快很多
3、ReflectASM工具类,通过字节码生成的方式加快反射速度

1.6 Java中clone方法的使用

1. 对象克隆
使用clone()方法,这个对象必须要实现Cloneable接口,并且重写clone方法

2. 浅拷贝和深拷贝

  • 浅拷贝:被复制对象的所有值都与原对象相同,而所有的对象引用仍然指向原来的对象。

  • 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。

直接使用对象的clone方法,执行的是浅拷贝;

要实现深拷贝,除了调用Object中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。这就要求被引用的对象也必须要实现Cloneable接口,重写clone方法。

1.7 Java中序列化的用法

1. 序列化
在类定义时实现java.io.serializable接口即可,Java会自动序列化。需要注意的,被标为transient和static的属性是不会被序列化的。

2. 序列化对象
ObjectOutputStream类用来序列化一个对象,writeObject()方法 可以将Java对象序列化到一个文件中(.ser)。

3. 反序列化对象
ObjectInputStream类用来反序列化一个对象,readObject()方法 可以将.ser文件反序列化为Java对象。
readObject() 方法中的 try/catch代码块需尝试捕获 ClassNotFoundException 异常。对于 JVM 可以反序列化对象,它必须是能够找到字节码的类

1.8 final关键字的用法

  • final修饰类,不能被继承,没有子类,final类中的方法默认是final的。
  • final修饰方法,不能被子类的方法覆盖,但可以被继承。
  • final修饰成员变量,表示常量,只能被赋值一次,赋值后值不再改变。
  • final不能用于修饰构造方法。
  • 对于被final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象。

使用final修饰变量,要注意:

  • final修饰的变量必须被赋初值(声明时,静态块中,构造函数三种赋值方式);
  • final修饰变量只能被赋值一次;

1.9 static关键字的用法

1. static 关键字的使用场景

  • 静态变量和静态方法,都属于类的静态资源,类实例所共享。
  • 静态块,多用于初始化操作,提升程序性能。
  • 静态内部类。
  • 静态导包,不需要使用类名,可以直接使用资源名。

2. static变量、成员变量、构造函数 三者的初始化顺序

先初始化父类static --> 再初始化子类的static --> 再初始化父类的其他成员变量 -->父类构造方法 -->子类其他成员变量 -->子类的构造方法。

示例:这段代码的输出结果是什么?

public class Test {
    
    
    Person person = new Person("Test");
    static{
    
    
        System.out.println("test static");
    }

    public Test() {
    
    
        System.out.println("test constructor");
    }

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

class Person{
    
    
    static{
    
    
        System.out.println("person static");
    }
    public Person(String str) {
    
    
        System.out.println("person "+str);
    }
}


class MyClass extends Test {
    
    
    Person person = new Person("MyClass");
    static{
    
    
        System.out.println("myclass static");
    }

    public MyClass() {
    
    
        System.out.println("myclass constructor");
    }
}
test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

过程分析:

1.找到main方法入口,main方法是程序入口,但在执行main方法之前,要先加载Test类
2.加载Test类的时候,发现Test类有static块,先执行static块,输出test static结果
3.然后执行new MyClass(),执行此代码之前,先加载MyClass类,发现MyClass类继承Test类,要先加载Test类,Test类之前已加载
4.加载MyClass类,发现MyClass类有static块,先执行static块,输出myclass static结果
5.然后调用MyClass类的构造器生成对象,在生成对象前,需要先初始化父类Test的成员变量,执行Person person = new Person(“Test”)代码,发现Person类没有加载
6.加载Person类,发现Person类有static块,先执行static块,输出person static结果
7.接着执行Person构造器,输出person Test结果
8.然后调用父类Test构造器,输出test constructor结果,这样就完成了父类Test的初始化了
9.再初始化MyClass类成员变量,执行Person构造器,输出person MyClass结果
10.最后调用MyClass类构造器,输出myclass constructor结果,这样就完成了MyClass类的初始化了。

3. static的访问限制

在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。

4. static的垃圾回收

static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大。非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力。

5. static关键字的误区

  • static关键字不能改变变量和方法的访问权限
  • 静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
  • static不允许用来修饰局部变量。

1.10 抽象类和接口的区别?

抽象类的特点

  • 抽象类不能被实例化,即不能使用new关键字来实例化对象,只能被继承;
  • 包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
  • 抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
  • 抽象类中的抽象方法只有方法体,没有具体实现;
  • 如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类;
  • 抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口的特点

  • 接口可以包含变量、方法;变量被隐式指定为public static final,方法被隐式指定为public abstract(JDK1.8之前);

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

  • JDK1.8中对接口增加了新的特性:

    • 默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;

    • 静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

抽象类和接口的区别

区别 抽象类 接口
成员区别 成员变量 可以变量,也可以常量 只可以常量
构造方法
成员方法 可以抽象,也可以非抽象 只可以抽象 (JDK1.8后提供了默认方法和静态方法)
关系区别 类与类 继承(extends),单继承
类与接口 实现(implements),单实现,多实现
接口与接口 继承(extends),单继承,多继承
设计理念区别 被继承体现的是:”is a”的关系。抽象类中定义的是该继承体系的共性功能 被实现体现的是:”like a”的关系。接口中定义的是该继承体系的扩展功能

1.11 List、Set、Map三者的区别?

List(列表)

List的元素以线性方式存储,可以存放重复对象,List主要有以下两个实现类:

ArrayList: 长度可变的数组,可以对元素进行随机的访问,向ArrayList中插入与删除元素的速度慢。JDK8中ArrayList扩容的实现是通过grow()方法里使用语句newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍扩容)计算容量,然后调用Arrays.copyof()方法进行对原数组进行复制。

LinkedList: 采用链表数据结构,插入和删除速度快,但访问速度慢。

Set(集合)

Set中的对象不按特定(HashCode)的方式排序,并且没有重复对象,Set主要有以下两个实现类:

HashSet:HashSet按照哈希算法来存取集合中的对象,存取速度比较快。当HashSet中的元素个数超过数组大小*loadFactor(默认值为0.75)时,就会进行近似两倍扩容(newCapacity = (oldCapacity << 1) + 1)。

TreeSet:TreeSet实现了SortedSet接口,能够对集合中的对象进行排序。

Map(映射)

Map是一种把键对象和值对象映射的集合,键对象不可重复,它的每一个元素都包含一个键对象和值对象。Map主要有以下实现类:

HashMap:HashMap基于散列表实现,其插入和查询<K,V>的开销是固定的,可以通过构造器设置容量和负载因子来调整容器的性能。

LinkedHashMap:类似于HashMap,但是迭代遍历它时,取得<K,V>的顺序是其插入次序,或者是最近最少使用(LRU)的次序。

TreeMap:TreeMap基于红黑树实现。查看<K,V>时,它们会被排序。TreeMap是唯一的带有subMap()方法的Map,subMap()可以返回一个子树。

1.12 Map的遍历方式

1. 在 for 循环中使用 entrySet (最常见和最常用的)

for (Map.Entry<String, String> entry : map.entrySet()) {
    
    
    String mapKey = entry.getKey();
    String mapValue = entry.getValue();
    System.out.println(mapKey + ":" + mapValue);
}

2. 使用 for循环遍历 key 或者 values,一般适用于只需要 Map 中的 key 或者 value 时使用。性能上比 entrySet 较好。

// 打印键集合 keySet
for (String key : map.keySet()) {
    
    
    System.out.println(key);
}
// 打印值集合 values
for (String value : map.values()) {
    
    
    System.out.println(value);
}

3. 使用迭代器(Iterator)遍历

Iterator<Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    
    
    Entry<String, String> entry = it.next();
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + ":" + value);
}

4. 通过键找值遍历,这种方式的效率比较低

for(String key : map.keySet()){
    
    
    String value = map.get(key);
    System.out.println(key+":"+value);
}

1.13 HashMap和HashTable的区别?

HashMap HashTable
父类 AbstractMap Dictionary
线程安全性 线程不安全 线程安全
效率 效率高 效率低
对null的支持 key可以为null,且只能有一个;可以有多个value为null key和value都不能为null
初始容量和扩容 默认大小11,扩容变为2n+1 默认大小16,扩容变为2n
计算hash值的方式 (h = key.hashCode()) ^ (h >>> 16) key.hashCode()

1.14 HashMap为何线程不安全?

  • put时key相同导致其中⼀个线程的value被覆盖;
  • 多个线程同时扩容,造成数据丢失;
  • 多线程扩容时导致Node链表形成环形结构造成死循环

1.15 HashMap的机制

1. 存储结构

在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突

在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是 O(n)。链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 才会转红黑树。
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

2. 不用红黑树,用二叉查找树可以么?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),数据量大时遍历查找会非常慢。

3. 默认加载因子是多少?为什么是 0.75,不是其他值?

table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳键值对的最大值。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多

默认的loadFactor是0.75,0.75是对空间和时间效率的一个平衡选择,较高的值会降低空间开销,但提高查找成本,一般不要修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。
  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

4. HashMap 中 key 的存储索引是怎么计算的?

  • 首先根据key的值计算出hashcode的值:h = key.hashCode()
  • 然后根据hashcode计算出hash值,通过hashCode()的高16位异或低16位实现:h ^ (h >>> 16)
  • 最后通过对length取模: hash & (length-1) 计算得到存储的位置。

5. HashMap 的put方法流程?

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
  2. 如果数组是空的,则调用 resize 进行初始化;
  3. 如果没有哈希冲突直接放在对应的数组下标里;
  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;
  5. 如果冲突后,且 key 不存在,且该节点是红黑树,就将这个节点挂在树上;
  6. 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树并插入键值对;链表小于8,插入键值对。

6. HashMap 的扩容方式?
Hashmap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 Hashmap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。

JDK1.7中,使用头插法,需要重新计算hash值
JDK1.8中,使用尾插法,不需要重新计算hash值,扩容后元素的位置在原来的位置,或者原来的位置 +oldCap (原来哈希表的长度)上。

7. HashMap 链表是怎么转红黑树的?

①使用treeifyBin()方法转红黑树判断。先根据数组长度判断是进行扩容还是转红黑树操作。

  • 先根据hash计算出当前链表所在table数组中的位置,然后将其数据结构从单向链表Node转为双向链表TreeNode;
  • 如果双向链表TreeNode的头节点hd不为null,则调用treeify()方法对TreeNode双向链表进行树型化操作;
final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
    int n, index; Node<K,V> e;
    // 数组长度小于64时,就先进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 如果新增的node 要插入的数组位置已经有node存在了,取消插入操作
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
        // 步骤一:遍历链表中每个节点,将Node转化为TreeNode
        // hd指向头节点,tl指向尾节点
        TreeNode<K,V> hd = null, tl = null;
        do {
    
    
            // 将链表Node转换为红黑树TreeNode结构
            TreeNode<K,V> p = replacementTreeNode(e, null);
            // 以hd为头结点,将每个TreeNode用prev和next连接成新的TreeNode链表
            if (tl == null)
                hd = p;
            else {
    
    
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 步骤二:如果头结点hd不为null,则对TreeNode双向链表进行树型化操作
        if ((tab[index] = hd) != null)
            // 执行链表转红黑树的操作
            hd.treeify(tab);
    }
}

treeify()方法真正将链表转成红黑树;大体上分为三步:

  • 1、遍历TreeNode双向链表,确定待插入节点x在其父节点的左边还是右边,然后将其插入节点到红黑树中;
  • 2、插入节点之后树结构发生变化,需要通过变色和旋转操作维护红黑树的平衡;
  • 3、因为调整了红黑树,root节点可能发生了变化,所以需要把最新的root节点放到双向链表的头部,并插⼊到table数组中。
final void treeify(Node<K,V>[] tab) {
    
    
    TreeNode<K,V> root = null;
    // 最开始的x表示TreeNode双向链表的头结点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
    
    
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        // 构建树的根节点
        if (root == null) {
    
    
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
    
    
            // 第一部分
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // p 表示parent节点
            for (TreeNode<K,V> p = root;;) {
    
    
                // dir表示x节点在parent节点的左侧还是右侧
                int dir, ph; // ph表示parent节点的hash值
                K pk = p.key; // pk表示parent节点的key值
                // x节点在parent节点的左侧
                if ((ph = p.hash) > h)
                    dir = -1;
                // x节点在parent节点的右侧
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                // 第二部分
                TreeNode<K,V> xp = p; // xp表示x的父节点
                // 如果p节点的左节点/右节点不为空,则令p = p.left/p.right,继续循环
                // 直到p.left/p.right为空
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
    
    
                    // 令待插入节点x的父节点为xp, 即p
                    x.parent = xp;
                    // 根据dir判断插入到xp的左子树(<0)还是右子树(>0)
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 往红黑树中插入节点后,进行树的平衡操作
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 将root节点插入到table数组中
    moveRootToFront(tab, root);
}

③在往红黑树中插入一个节点之后,会调用 balanceInsertion()方法进行树的平衡操作

对红黑树进行平衡操作时,主要分两部分:直接返回、变色旋转后返回;

第一部分:直接返回根节点
1、如果待插入节点x没有parent节点,直接把x的节点变为黑色,并作为根节点返回;
2、如果x的父节点为黑色、或者没有祖父节点,则直接返回根节点root;

第二部分:根据x节点在父节点xp的左侧还是右侧、xp节点再其父节点xpp的左侧还是右侧进行旋转、变色

  1. 如果x的父节点xp是祖父节点xpp的左子节点:
    0)若x的祖父节点xpp的右子节点xppr存在并且是红色,直接交换祖父节点和其子节点的颜色、并返回。
    1)若x是其父节点xp的右子节点,交换x和xp的位置,然后对x进行左旋;
    2)接着设置父节点xp为黑色、祖父节点xpp为红色,以xpp节点为轴右旋。
  2. x的父节点xp是祖父节点的右子节点:
    这里和上面的类似,只是左旋变右旋、右旋变左旋;
    0)若x的祖父节点xpp的左子节点xppl并在并且是红色,直接交换祖父节点和其子节点的颜色、并返回。
    1)若x是其父节点xp的左子节点,交换x和xp的位置,然后对x进行右旋;
    2)接着设置父节点xp为黑色、祖父节点xpp为红色,以xpp节点为轴左旋。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    
    
    // 将待插入节点x的节点颜色置为红色
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    
    
        // 1、如果x没有父节点,自己变为黑色节点、作为根节点返回
        if ((xp = x.parent) == null) {
    
    
            x.red = false;
            return x;
        }
        // 2、如果x的父节点为黑色或者没有祖父节点,则直接返回root
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 3、如果x的父节点xp是祖父节点的左子节点
        if (xp == (xppl = xpp.left)) {
    
    
            // 1)仅变色:
            //    如果xp和xppr都是红色节点,则把xppr、xp置为黑色、zpp置为红色。
            if ((xppr = xpp.right) != null && xppr.red) {
    
    
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // 2)旋转 + 变色:
            else {
    
    
                // (1)如果x 是其父节点xp的右子节点。令当前节点为其父节点,然后对当前节点x进行左旋
                if (x == xp.right) {
    
    
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // (2)如果x的父节点xp不为空
                if (xp != null) {
    
    
                    // 令xp的颜色为黑色
                    xp.red = false;
                    // 如果x的祖父节点xpp不为空
                    if (xpp != null) {
    
    
                        // 令xpp的颜色为红色
                        xpp.red = true;
                        // 对祖父节点xpp进行右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 4、x的父节点xp是祖父节点的右子节点
        else {
    
    
            // 逻辑和3、类似,只是单纯的左变右、右变左
            if (xppl != null && xppl.red) {
    
    
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
    
    
                if (x == xp.left) {
    
    
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
    
    
                    xp.red = false;
                    if (xpp != null) {
    
    
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

HashMap 链表转红黑树的源码解析,参考:https://blog.csdn.net/Saintmm/article/details/121582015

1.16 Java中的异常

Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常(RuntimeException),错误(Error)。

1、运行时异常 RuntimeException及其子类都被称为运行时异常。

  • 除数为零时产生的ArithmeticException异常
  • 数组越界时产生的IndexOutOfBoundsException异常
  • util包集合类并发修改产生的ConcurrentModificationException异常
  • NullPointerException(空指针异常)
  • ClassCastException(类转换异常)

2、被检查异常 Exception类及其子类中除了"运行时异常"之外的其它子类都属于被检查异常。
Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。

  • 克隆时没有实现Cloneable接口,就会抛出CloneNotSupportedException异常
  • 读取文件时文件不存在引发的FileNotFoundException异常
  • JDBC 与数据源交互时产生的SQLException异常
  • 在使用流、文件和目录访问信息时引发的IOException异常

3、错误 Error类及其子类。
是程序无法处理的错误,表示代码运行时 JVM出现的问题。

  • Java虚拟机运行错误(VirtualMachineError
  • 当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError
  • 类定义错误(NoClassDefFoundError

2. JVM篇

2.1 JVM内存模型

线程独占:方法栈,本地方法栈,程序计数器
线程共享:堆,方法区

1. 方法栈
线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法出口等信息。调用方法时执行入栈,方法返回式执行出栈。
2. 本地方法栈
与方法栈类似。执行Java方法是使用栈,执行Native方法时使用本地方法栈。
3. 程序计数器
保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行Native方法时,程序计数器为空。
4. 堆
被线程共享,目的是存放对象的实例,当堆没有可用空间时,会抛出OOM异常。根据对象的存活周期不同,JVM把对象进行分代管理。由垃圾回收器进行垃圾的回收管理。
5. 方法区
用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据。JDK1.7的永久代和JDK1.8的元空间都是方法区的一种实现。

2.2 类加载过程

在这里插入图片描述
类加载过程分为加载、连接、初始化。其中连接包括验证、准备、解析
加载:通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象。
验证:确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全。
准备:进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null)。不包含final修饰的静态变量,因为final变量在编译时分配。
解析:将常量池中的符号引用替换为直接引用的过程。
初始化:主要完成静态块执行以及静态变量的赋值。先初始化父类,再初始化当前类。只有对类主动使用时才会初始化。

加载机制-双亲委派模式
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器。父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载。

  • 避免类的重复加载
  • 避免Java的核心API被篡改

2.3 如何判断对象可以被回收?

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

2.4 jdk1.8默认使用的垃圾回收器

使用java -XX:+PrintCommandLineFlags -version查看一下

-XX:InitialHeapSize=132500864 //初始堆大小
-XX:MaxHeapSize=2120013824    //最大堆大小
-XX:+PrintCommandLineFlags    //程序运行前打印出用户手动设置或者JVM自动设置的XX选项,因为我们执行时间加上了这个选项,所以这里会打印出来
-XX:+UseCompressedClassPointers // 默认开启类指针压缩
-XX:+UseCompressedOops  // 默认开启对象指针压缩
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC // 默认使用Parallel垃圾收集器
java version "1.8.0_221" // jdk版本
Java(TM) SE Runtime Environment (build 1.8.0_221-b11) // jre
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode) // Hotspot虚拟机,Server模式,混合编译

JDK1.8中默认使用ParallelGC垃圾收集器,包括Parallel Scavenge(新生代)Parallel Old(老年代) 收集器组合。

2.5 GC算法

  • 标记 - 清除算法
  • 复制算法
  • 标记 - 整理算法
  • 分代收集算法

1. 基础:标记 - 清除算法

基本思想:

  • 先标记出所有需要回收的对象;
  • 标记完后,统一回收所有被标记对象。

不足:

  • 效率问题:标记和清理两个过程的效率都不高。
  • 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。

2. 解决效率问题:复制算法

新生代(Young)中使用的GC就是使用的复制算法。

-XX:MaxTenuringThreshold //设置对象在新生代中存活的代数,默认是15

基本思想:

  • 将可用内存分为大小相等的两块,每次只使用其中一块;
  • 当一块内存用完时,将存活的对象复制到另一块内存上去,将这一块内存全部清理掉。

不足:

  • 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。

3. 解决空间碎片问题:标记 - 整理算法

基本思想:

  • 先标记出所有需要回收的对象;
  • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。

不足:

  • 存在效率问题,适合老年代

4. 进化:分代收集算法

  • 新生代: GC 过后只有少量对象存活 —— 复制算法。
  • 老年代: GC 过后对象存活率高 —— 标记 - 整理算法。

参考文章:https://www.toobug.cn/post/4990.html

2.6 JDK监控命令有哪些?

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件,可使用jvisualvm工具加载分析此文件。
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

2.7 JVM性能调优的方法?

设定堆内存大小

-Xmx:堆内存最大限制。

设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代

-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

设定垃圾回收器

  • 新生代 -XX:+UseParNewGC
  • 老年代-XX:+UseConcMarkSweepGC

3. 多线程&并发篇

3.1 线程的几种状态?

线程通常都有五种状态, 创 建 、 就 绪 、 运 ⾏ 、 阻 塞 和 死 亡 \color{red}{创建、就绪、运⾏、阻塞和死亡}

在这里插入图片描述

  • 创建(New) 状态。在⽣成线程对象,并没有调⽤该对象的start⽅法,这是线程处于创建状态。
  • 就绪(Runnable) 状态。当调⽤了线程对象的start⽅法之后,该线程就进⼊了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运⾏之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 运⾏(Running) 状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进⼊了运⾏状态,开始运⾏run函数当中的代码。
  • 阻塞(Blocked) 状态。线程正在运⾏的时候,被暂停,通常是为了等待某个时间的发⽣(⽐如说某项资源就绪)之后再继续运⾏。sleep,suspend,wait等⽅法都可以导致线程阻塞。
  • 死亡(Dead) 状态。如果⼀个线程的run⽅法执⾏结束或者调⽤stop⽅法后,该线程就会死亡。对于已经死亡的线程,⽆法再使⽤start⽅法令其进⼊就绪。

3.2 Java中实现多线程有几种方法?

  1. 继承Thread类,重写run⽅法。
  2. 实现Runnable接⼝,重写run⽅法,并将其实例对象作为Thread构造函数的target。
  3. 实现Callable接⼝通过FutureTask包装器来创建Thread线程。
  4. 通过线程池创建线程。

3.3 Executors线程池框架?

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建⼀个可缓存线程池,如果线程池⻓度超过处理需要,可灵活回收空闲线程,若⽆可回收,则新建线程。
  • newFixedThreadPool创建⼀个定⻓线程池,可控制线程最⼤并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建⼀个定时程池,⽀持定时及周期性任务执⾏。
  • newSingleThreadExecutor 创建⼀个单线程化的线程池,它⽤唯⼀的⼯作线程来执⾏任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执⾏。

3.4 线程池的工作流程?

在这里插入图片描述

  • 当线程数⼩于corePoolSize时,创建线程执⾏任务。
  • 当线程数⼤于等于corePoolSize并且workQueue没有满时,放⼊workQueue中
  • 线程数⼤于等于corePoolSize并且当workQueue满时,新任务新建线程运⾏,线程总数要⼩于maximumPoolSize
  • 当线程总数等于maximumPoolSize并且workQueue满了的时候执⾏handler的rejectedExecution。也就是拒绝策略。

3.5 线程池execute()和submit()的区别?

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的 get() 方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

3.6 有哪些线程安全的集合?是如何实现的?

  • vector:方法上加了synchronized;
  • hashtable:方法上加了synchronized;
  • CopyOnWriteArrayList:通过对底层数组的最新副本实现可变操作。属性只有一个volatile修饰的数组,还有一个ReentrantLock锁;
  • ConcurrentHashMap:Node + CAS + Synchronized来保证并发安全进行实现;

3.7 有哪些类型的锁?有什么特点?

  • 可重⼊锁。指同⼀个线程可以多次获取同⼀把锁,可一定程度避免死锁。例如ReentrantLock和synchronized。
  • 可中断锁。指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,⽽ ReentrantLock是可中断锁。
  • 公平锁/非公平锁。公平锁是指多个线程同时尝试获取同⼀把锁时,获取锁的顺序按照线程达到的顺序,⽽⾮公平锁则允许线程“插队”。非公平锁的优点在于吞吐量比公平锁大。synchronized是⾮公平锁,⽽ReentrantLock的默认实现是⾮公平锁,但是也可以设置为公平锁。
  • 独享锁/共享锁。独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。Synchronized和ReentrantLock是独享锁。ReadWriteLock其读锁是共享锁,其写锁是独享锁。独享锁与共享锁也是通过AQS来实现的。
  • 乐观锁/悲观锁。悲观锁就是利用各种锁进行写操作。乐观锁是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
  • 自旋锁。指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

3.8 CAS和synchronized的区别?应用场景?

  • CAS(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 ——需要读写的内存值 (V)、进行比较的值(A)和拟写入的新值(B)。 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
  • synchronized。Synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

CAS适用于多读场景,冲突较少的情况synchronized适用于多写场景,冲突较多的情况

3.9 Lock接⼝和synchronized的区别及优势?

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 可以尝试获得锁,线程不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入,不可中断,非公平 可重入,可中断,可公平/非公平
性能 少量同步 大量同步

LOCK的优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在⽆法获取锁的时候⽴即返回或者等待⼀段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

3.10 synchronized 关键字的用法?实现原理?

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
  • 修饰静态方法: 也就是给当前类加锁,进入同步代码前要获得当前类的锁。
  • 修饰代码块: 指定加锁对象,进入同步代码块前要获得指定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能。

实现原理 JVM层面

1. synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

2. synchronized 方法使用的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

3.11 双重校验锁的单例模式?(DCL单例)

public class Singleton {
    
    
   private volatile static Singleton uniqueInstance;
   private Singleton() {
    
    
   }
   public static Singleton getUniqueInstance() {
    
    
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
       if (uniqueInstance == null) {
    
    
           //类对象加锁
           synchronized (Singleton.class) {
    
    
               if (uniqueInstance == null) {
    
    
                   uniqueInstance = new Singleton();
               }
           }
       }
       return uniqueInstance;
   }
}

注意:uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在多线程环境下会导致一个线程获得还没有初始化的实例。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

3.12 ConcurrentHashMap的实现原理?

JDK1.7的实现

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组多个HashEntry组成。
Segment数组的意义就是将一个大的table分割成多个小的table来进行分段加锁,每一个Segment元素存储的是HashEntry数组+链表。

1. put操作
对于ConcurrentHashMap的数据插入,进行两次Hash去定位数据的存储位置。Segment继承了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁。

2. get操作
ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表。

3. size操作
在并发操作时,计算size的时候,其他线程还在并发的插入数据,可能会导致计算出来的size和实际的size有相差,JDK1.7版本用两种方案解决此问题:

  • 使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。
  • 如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size。

JDK1.8的实现

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。

即:在并发处理中使用的是乐观锁CAS,当有冲突的时候才进行并发处理synchronized

1. Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。是一个链表,但是只允许对数据进行查找,不允许进行修改

  • volatile:val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序。
  • final:final修饰setValue()方法,不允许更新value。

2. TreeNode
TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8且table长度大于64时会转换成红黑树的结构,链表转换为红黑树的最终目的,是为了解决在map中元素过多,hash冲突较大,而导致的读写效率降低的问题

  • hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换。
  • hashMap的红黑树不一定小于6的时候才会转换为链表,remove减少元素时,通过红黑树根节点及其子节点是否为空来判断;在resize扩容后树节点个数若<=6,将树转链表。

3. TreeBin
TreeBin是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制

4. put操作
对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述:

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
  5. 如果该链表的数量大于阈值8,就执行转换黑红树的操作,break再一次进入循环
  6. 添加成功就调用addCount()方法统计size,并且检查是否需要扩容

5. get操作

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

3.13 volatile关键字的作用?实现原理?使用场景?

  • 使⽤volatile关键字修饰的变量,保证了其在多线程之间的可⻅性,即每次读取到volatile变量,⼀定是最新的数据。
  • 使⽤volatile会禁⽌JVM对指令重排序,当然这也⼀定程度上降低了代码执⾏效率。

注意:volatile不能保证原子性,要是说能保证,也只是对单个volatile变量的读/写具有原子性,但是对于类似volatile++这样的复合操作就无能为力了

实现原理:lock前缀指令

lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置

  2. 使得本CPU的Cache写入内存

  3. 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。

使用场景:状态量标记和双检锁(DCL)的单例模式

4. Spring篇

4.1 Spring的IOC和AOP机制

IOC(控制反转)控制反转也叫依赖注入。利用了工厂模式将对象交给容器管理,你只需要在spring配置文件中配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。

AOP(面向切面编程)使用代理模式实现。(Aspect-Oriented Programming)AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。AOP将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。
实现AOP的技术,主要分为两大类:一是采用动态代理技术利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式引入特定的语法创建“切面”,从而使得编译器可以在编译期间织入有关“切面”的代码

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

JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
②如果代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的

4.2 Spring中Autowired和Resource注解的区别?

异同 @Autowired @Resource
共同点 两者都可以写在字段或者setter方法上。如果写在字段上,那么就不需要再写setter方法。
区别 注解来源不同 Spring提供的注解 由J2EE提供
注入方式不同 按照类型(byType)注入,若想使用按照名称(byName)来装配,可以结合@Qualifier注解 默认按照名称(byName)注入,可以使用name和type属性指定注入方式
装配可选 提供了required属性(默认值为true)以避免注入为空抛出异常,设置@Autowired为false 没有提供可选择装配的特性,一旦无法装配则会抛出异常
构造函数注入 可以写在构造函数上 不可以写在构造函数上

4.3 依赖注入的方式有几种?优缺点?

一、构造器注入
将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。

优点:

  • 对象初始化完成后便可获得可使用的对象。

缺点:

  • 当需要注入的对象很多时,构造器参数列表将会很长;
  • 不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。

二、setter方法注入
IoC Service Provider通过调用成员变量提供的setter方法将被依赖对象注入给依赖类。

优点:

  • 灵活。可以选择性地注入需要的对象。

缺点:

  • 依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。

三、接口注入
依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。

优点

  • 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。

缺点:

  • 侵入行太强,不建议使用。

4.4 Spring的常用的注解有哪些?

@Component、@Controller、@Service、@Repository
@Bean:声明并注入实例对象
@Scope:设置Bean的作用域
@Import:导入其他组件到容器中
@Autowired:依赖注入
@Value:属性注入
@PostConstruct:初始化方法,注解由java提供
@PreDestory:销毁方法,注解由java提供
@Configuration:声明当前类为配置类
@ComponentScan:用于对Component进行扫描
@Aspect:声明一个切面
@After:在方法执行之后执行(方法上)
@Before:在方法执行之前执行(方法上)
@Around:在方法执行之前与之后执行(方法上)
@PointCut:声明切点
@Enable***:这些注解主要是用来开启对xxx的支持

4.5 Spring MVC的工作原理

在这里插入图片描述

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的Handler处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter经过适配调用具体的Controller处理器(也叫后端控制器)。
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体View
  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  11. DispatcherServlet响应用户

4.6 Spring MVC常用的注解有哪些?

@Controller:用于控制层注解
@Service:用于对业务逻辑层进行注解
@Repository:对Dao实现类进行注解
@Component:在类定义之前添加@Component注解,他会被spring容器识别,并转为bean
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径
@RequestParam:用于获取传入参数的值
@PathViriable:用于定义路径参数值
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象
@ResponseBody:注解实现将conreoller方法返回值转化为json对象响应给用户
@CookieValue:用于获取请求的Cookie值
@ModelAttribute:用于把参数保存到model中
@SessionAttributes:使得model中的数据存储一份到session域中

4.7 Spring框架中都用到了哪些设计模式?

  • 工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
  • 单例模式:Bean默认为单例模式
  • 策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
  • 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
  • 适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
  • 观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
  • 桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如项目需要连接多个数据库

4.8 Spring的@ControllerAdvice注解使用

@ControllerAdvice / @RestControllerAdvice 有三方面的功能
1. 全局异常处理

  • 使用@ExceptionHandler 注解用来指明处理异常的类型
  • 可以定义多个方法,不同的方法处理不同的异常

2. 全局数据绑定

  • 使用@ModelAttribute注解标记该方法的返回数据是一个全局数据,name属性用来指定数据的key
  • 在项目的任何Controller中都可以使用Model对象直接获取该数据
  • model.asMap()将数据转成map
  • 通过指定的key来获取数据,未指定key则默认名称为map

3. 全局数据预处理

比如给实体类的对象属性增加前缀

5. SpringBoot篇

5.1 Spring Boot 优缺点

Spring Boot用来简化spring应用开发,约定大于配置,去繁从简

优点

  • 独立运行Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
  • 简化配置spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
  • 自动配置无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
  • 应用监控Spring Boot Actuator提供一系列端点可以监控服务及应用,做健康检测。

缺点

  • 版本迭代速度很快,一些模块改动很大。
  • 由于不用自己做配置,报错时很难定位。

5.2 Spring Boot 的核心注解

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

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

5.3 Spring Boot 的核心配置文件

Spring Boot 的核心配置文件是 application配置文件 和 bootstrap 配置文件。

  • application 配置文件主要用于 Spring Boot 项目的自动化配置。
  • bootstrap 配置文件有以下几个应用场景:

1、使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
2、一些固定的不能被覆盖的属性;
3、一些加密/解密的场景;

yml与properties配置文件格式的区别

区别 .yml .properties
语法不同 配置以“:”进行分割,缩进使用两个空格,不能使用 TAB键 配置以“.”进行分割
数据格式不同 通过“: ”赋值,且Key的冒号后面一定要加一个空格 通过“=”赋值
数据类型不同 支持键值对数据,支持数组格式 "- "表示数组,支持对象格式 只支持键值对数据
配置加载顺序 加载有先后顺序,有序 不保证加载顺序,无序
配置加载方式 无法使用@PropertySource注解加载自定义yml文件 @PropertySource

profile配置,指定配置文件

  • application.properties
  • application-dev.propertie
spring.profiles.active=dev  

日志配置

推荐使用:Slf4j + logbak

  • 控制台输出:logging.level+包名
logging.level.com.example.mapper=debug
  • 日志文件输出
logging.file.name=D:\logs\demo.log
logging.file.path=D:\logs

5.4 如何在Spring Boot启动的时候运行一些特定的代码?

  • 实现ApplicationRunner接口
  • 实现CommandLineRunner接口
  • 实现ServletContextListener接口

5.5 如何理解 Spring Boot 中的 Starters?

Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入spring-boot-starter-data-jpa 启动器依赖就能使用了。

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

spring-boot-starter-parent主要提供了如下默认配置:

  1. java版本默认使用1.8
  2. 编码格式默认使用utf-8
  3. 提供统一的maven依赖版本管理
  4. 默认的资源过滤与插件管理

5.7 Spring Boot 自动配置原理是什么?

注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnXxx条件注解 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否满足这个条件去自动配置。

6. SpringCloud篇

6.1 Spring Cloud 的核心组件有哪些?

服务注册与发现 - Netflix Eureka

由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。

Eureka详细介绍可以参考文章:SpringCloud 服务注册与发现-Eureka

客服端负载均衡 - Netflix Ribbon

Ribbon,主要提供客户端的软件负载均衡算法。Ribbon客户端组件提供一系列完善的配置选项,比如连接超时、重试、重试算法等。Ribbon内置可插拔、可定制的负载均衡组件。

熔断器 - Netflix Hystrix

断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决。如果问题似乎已经得到纠正,应用程序可以尝试调用操作。

Hystrix详细介绍可以参考文章:SpringCloud 熔断器-Hystrix

服务网关 - Spring Cloud Gateway

Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul。而为了提升网关的性能,Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的通信框架Netty。 旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

声明式服务调用 - Spring Cloud OpenFeign

OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,对 Ribbon 进行了集成,利用 Ribbon 维护了可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持。

分布式配置中心 - Spring Cloud Config

Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的实现分布式统一配置管理。分为Config Server和Config Client两部 分。Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用Config Server的接口来读取配置文件。

Spring Cloud Config是静态的,得配合Spring Cloud Bus实现动态的配置更新。

消息总线 - Spring Cloud Bus

Spring Cloud Bus 使用轻量级的消息代理来连接微服务架构中的各个服务,可以将其用于广播状态更改(例如配置中心配置更改)或其他管理指令

Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。

目前 Spring Cloud Bus 支持两种消息代理:RabbitMQKafka

6.2 Spring Cloud的优缺点

优点:

  • 耦合度比较低。不会影响其他模块的开发。
  • 配置比较简单,基本用注解就能实现,不用使用过多的配置文件。
  • 微服务跨平台的,可以用任何一种语言开发。
  • 每个微服务可以有自己的独立的数据库也有用公共的数据库。
  • 只需要关注自己的后端代码即可,然后暴露接口,通过组件进行服务通信。

缺点:

  • 部署比较麻烦。
  • 针对数据的管理比麻烦,因为微服务可以每个微服务使用一个数据库。
  • 统集成测试比较麻烦。
  • 能的监控比较麻烦。【最好开发一个大屏监控系统】

6.3 微服务之间是如何独立通讯的?

  • 同步调用。也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。

    • RestTemplate
    • OpenFegin

    优点: 简单,常见,因为没有中间件代理,系统更简单
    缺点:

    • 只支持请求/响应的模式;
    • 降低了可用性,因为客户端和服务端在请求过程中必须都是可用的
  • 异步调用

    • Spring Cloud Bus
    • MQ消息中间件

    优点:

    • 把客户端和服务端解耦,更松耦合
    • 提高可用性,因为消息中间件缓存了消息,直到消费者可以消费
    • 支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应

    缺点: 消息中间件有额外的复杂

6.4 说说 RPC 的实现原理

RPC,全称 Remote Procedure Call(远程过程调用),即调用远程计算机上的服务。大致分4个步骤:

  1. 建立通信。首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。
  2. 服务寻址。需要Registry来注册服务的地址。
  3. 网络传输。需要有编解码的模块。因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。
  4. 服务调用。服务器端暴露要开放的服务接口;客户端调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。

PRC架构组件

在这里插入图片描述

  • 客户端(Client): 服务调用方(服务消费者)

  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端

  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理

  • 服务端(Server):服务的真正提供者

RPC具体调用过程

1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;
2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;
3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
4、服务端存根(server stub)收到消息后进行解码(反序列化操作);
5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;
6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);
7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
8、客户端存根(client stub)接收到消息
9、客户端存根(client stub)对收到的消息进行解码(反序列化);
10、服务消费方(client客户端)得到最终结果;

RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。

7. MyBatis篇

7.1 #{}和${}的区别是什么?

  • #{}是预编译处理,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
  • $ {}是字符串替换,就是把$ {}替换成变量的值。

使用#{}可以有效的防止SQL注入,提高系统安全性。

7.2 当实体类中的属性名和表中的字段名不一样 ,怎么办 ?

  1. 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
  2. 使用resultMap标签来映射字段名和实体类属性名的对应关系。

7.3 mapper接口的工作原理是什么?接口方法能重载吗?

Mapper 接口的工作原理是JDK动态代理Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,根据类的全限定名+方法名,唯一定位到一个MapperStatement并调用执行器执行所代表的sql,然后将sql执行结果返回

Mapper接口里的方法,是不能重载的,因为是使用 全限定名+方法名 的保存和寻找策略

7.4 Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。
可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

7.5 Mybatis的一级、二级缓存

  • 一级缓存: 基于 PerpetualCacheHashMap 本地缓存,其存储作用域为Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
  • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
  • 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。

8. Mysql篇

8.1 数据库的三范式是什么?

  • 第一范式:列不可再分
  • 第二范式:行可以唯一区分,主键约束
  • 第三范式:表的非主属性不能依赖与其他表的非主属性,外键约束

且三大范式是一级一级依赖的,第二范式建立在第一范式上,第三范式建立第一第二范式上。

8.2 mysql引擎 InnoDB与MyISAM的区别

  1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
  3. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  5. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;

8.3 数据库分页

1、mysql:limit关键字

两个参数分别表示查询初始位置(从0开始)和查询长度,一个参数表示查询长度

2、oracle:rownum隐藏列

  • 查询必须要指定rownum字段;
  • 直接使用rownum时只能使用< <=,不能使用> >= ;
  • 要使用> >=,必须结合子查询,并且必须对rownum字段重命名

3、SQLServer:top

8.4 数据库的事务

多条sql语句,要么全部成功,要么全部失败。

1. 事务的特性

原子性(Atomic):组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始状态。
一致性(Consistency):事务操作成功后,数据库所处的状态和它的业务规则是一致的。即数据不会被破坏。如A转账100元给B,不管操作是否成功,A和B的账户总额是不变的。
隔离性(Isolation):在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对彼此产生干扰
持久性(Durabiliy):一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。

2. MySQL执行事务的语法和流程

InnoDB 存储引擎事务主要通过 UNDO日志REDO日志实现,MyISAM 存储引擎不支持事务。

  • UNDO 日志:复制事务执行前的数据,用于在事务发生异常时回滚数据。
  • REDO 日志:记录在事务执行中,每条对数据进行更新的操作,当事务提交时,该内容将被刷新到磁盘。

开始事务
BEGIN;或START TRANSACTION;这个语句显式地标记一个事务的起始点。

提交事务
COMMIT; 表示提交事务,将事务中所有对数据库的更新都写到磁盘上的物理数据库中,事务正常结束。一旦执行了该命令,将不能回滚事务。

回滚(撤销)事务
ROLLBACK; 表示撤销事务,即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,回滚到事务开始时的状态。这条语句也标志着事务的结束。

3. 事务之间相互影响的种类

  • 脏读:一个事务读取了另一个事务未提交的数据。
  • 不可重复读:就是在一个事务范围内,两次相同的查询会返回两个不同的数据,是因为在此间隔内有其他事务对数据进行了修改。
  • 幻读:指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
  • 丢失更新:两个事务同时读取同一条记录,A先修改记录,B也修改记录(B是不知道A修改过),B提交数据后覆盖了A的修改结果。

4. 事务隔离级别
数据库的事务隔离级别(TRANSACTION ISOLATION LEVEL)是为了尽可能的避免上述事务之间的影响而产生的隔离级别。

隔离级别 脏读 丢失更新 不可重复读 幻读 并发模型 更新冲突检测
未提交读: Read Uncommited 悲观
已提交读: Read commited 悲观
可重复读: Repeatable Read 悲观
可串行读: Serializable 悲观

事务隔离是通过锁来实现的,通过阻塞来隔离上述影响,级别越高,加的锁越多,效率越低下

  • 未提交读:在读取数据时不会加任何锁,也不会进行检测,可能会读到没有提交的数据。
  • 已提交读:只读取提交的数据等待其他事务释放排他锁,读数据的共享锁在读操作完成后会立即释放。这个隔离级别是大多数数据库默认的隔离级别。
  • 可重复读:像已提交读一样,但共享锁会保持到事务结束才会释放。MySQL数据库默认的隔离级别。
  • 可串行读:类似于可重复读,但锁不仅会锁定所查询的数据,也会锁定所查询的范围,这样就阻止了新数据插入所查询的范围。

8.5 mysql 索引

索引是对数据库表中一个或多个列的值进行排序的结构,建立索引有助于快速获取信息。

1. mysql 有4种索引类型

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

    ALTER TABLE table_name ADD PRIMARY KEY (column_name)
    
  • 唯一索引(UNIQUE)数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

    -- 创建唯一索引
    ALTER TABLE table_name ADD UNIQUE (column_name); 
    -- 创建唯一组合索引
    ALTER TABLE table_name ADD UNIQUE (column1,column2); 
    
  • 普通索引(INDEX)

    -- 使用 CREATE INDEX 语句创建索引
    CREATE INDEX indexName ON table_name (column_name)
    -- 修改表结构(添加索引)方式
    ALTER TABLE table_name ADD INDEX indexName (column_name)
    -- 创建组合索引
    ALTER TABLE table_name ADD INDEX indexName (column1,column2,column3);
    
  • 全文索引(FULLTEXT)

    -- 创建全文索引
    ALTER TABLE table_name ADD FULLTEXT (column_name);
    

索引一经创建不能修改,如果要修改索引,只能删除重建。

-- 删除索引
DROP INDEX indexName ON table_name;

2. 索引优缺点

  • 索引加快数据库的检索速度
  • 唯一索引可以确保每一行数据的唯一性
  • 通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能
  • 索引需要占物理和数据空间
  • 索引降低了插入、删除、修改等维护任务的速度

3. 索引设计的原则

  • 适合索引的列是出现在where子句中的列,或者连接子句中指定的列;
  • 基数较小的列,索引效果较差,没有必要在此列建立索引;
  • 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间;
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

8.6 索引优化、SQL优化

1. 查看索引的使用情况

通过SHOW STATUS LIKE 'Handler_read%';查看索引的使用情况:
在这里插入图片描述
Handler_read_key:如果索引正在工作,Handler_read_key的值将很高。
Handler_read_rnd_next:数据文件中读取下一行的请求数,如果正在进行大量的表扫描,值将较高,则说明索引利用不理想。

2. 索引优化规则

  1. 如果MySQL估计使用索引比全表扫描还慢,则不使用索引。
  2. 前导模糊查询不能命中索引,可优化为使用非前导模糊查询。
  3. 数据类型出现隐式转换的时候不会命中索引,特别是当列类型是字符串,一定要将字符常量值用引号引起来。
  4. 复合索引的情况下,查询条件不包含索引列最左边部分(不满足最左原则),不会命中符合索引。注意,最左原则并不是说是查询条件的顺序,而是查询条件中是否包含索引最左列字段。
  5. union、in、or都能够命中索引,建议使用in。查询的CPU消耗:or>in>union。
  6. 用or分割开的条件,如果or前的条件中列有索引,而后面的列中没有索引,那么涉及到的索引都不会被用到。
  7. 负向条件查询不能使用索引,可以优化为in查询。负向条件有:!=、<>、not in、not exists、not like等。
  8. 范围条件查询可以命中索引。范围条件有:<、<=、>、>=、between等。
  9. 数据库执行计算不会命中索引。
  10. 建立索引的列,不允许为null。即使IS NULL可以命中索引。

3. explain 分析SQL的执行计划

通过 explain 命令获取 select 语句的执行计划,通过 explain 我们可以知道以下信息:表的读取顺序,数据读取操作的类型,哪些索引可以使用,哪些索引实际使用了,表之间的引用,每张表有多少行被优化器查询等信息。
在这里插入图片描述
需要重点关注type、rows、filtered、Extra

type由上至下,效率越来越高

  • ALL 全表扫描
  • index 索引全扫描
  • range 索引范围扫描,常用于<,<=,>=,between,in等操作
  • ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中
  • eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询
  • const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询
  • null MySQL不访问任何表或索引,直接返回结果

Extra

  • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。
  • Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化
  • Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。
  • Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。

4. SQL优化规则

  1. 查询语句中不要使用select *
  2. 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
  3. 减少使用IN或者NOT IN,使用exists,not exists或者关联查询语句替代
  4. or 的查询尽量用 union或者union all 代替(在确认没有重复数据或者不用剔除重复数据时,union all会更好)
  5. 应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
  6. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。

5. 确定问题并采用相应的措施

  • 优化索引
  • 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤
  • 改用其他实现方式:ES、数仓等
  • 数据碎片处理

8.7 大表如何优化?

  1. 限定数据查询的范围。禁止不带任何限制数据范围条件的查询语句。
  2. 读/写分离。经典的数据库拆分方案,主库负责写,从库负责读;
  3. 垂直分区。垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
    优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
    缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
  4. 水平分区。水平拆分是指数据表行的拆分,保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分最好分库 。
    优点: 能够支持非常大的数据量存储。
    缺点: 分片事务难以解决 ,跨节点Join性能较差,逻辑复杂。

补充一下数据库分片的两种常见方案:

  • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

8.8 drop、delete与truncate的区别

SQL中的drop、delete、truncate都表示删除,但是三者有一些差别

  • delete和truncate只删除表的数据不删除表的结构;
  • delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效;
  • truncate,drop是ddl,操作立即生效,不能回滚;
  • 速度,一般来说: drop> truncate >delete

8.9 内联接、左外联接、右外联接

  • 内联接(Inner Join):匹配2张表中相关联的记录。
  • 左外联接(Left Outer Join):除了匹配2张表中相关联的记录外,还会匹配左表中剩余的记录,右表中未匹配到的字段用NULL表示。
  • 右外联接(Right Outer Join):除了匹配2张表中相关联的记录外,还会匹配右表中剩余的记录,左表中未匹配到的字段用NULL表示。
  • 在判定左表和右表时,要根据表名出现在Outer Join的左右位置关系。

9. Redis篇

9.1 Redis高性能的原因?

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
  • 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,不存在加锁释放锁操作;
  • 使用多路 I/O 复用模型,非阻塞 IO;
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

9.2 Redis数据类型?使用场景?

数据类型 可以存储的值 操作 应用场景
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增或者自减操作 做简单的键值对缓存
LIST 列表 从两端压入或者弹出元素;对单个或者多个元素进行修剪,只保留一个范围内的元素 存储一些列表型的数据结构
SET 无序集合 添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素 交集、并集、差集的操作
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对;检查某个键是否存在 结构化的数据,比如一个对象
ZSET 有序集合 添加、获取、删除元素;根据分值范围或者成员来获取元素;计算一个键的排名 去重但可以排序,如获取排名前几名的用户

应用场景

  • 计数器
  • 缓存,将热点数据放到内存中
  • 会话缓存,可以使用 Redis 来统一存储多台应用服务器的会话信息
  • 分布式锁实现,SETNX 命令;RedLock 分布式锁
  • Set集合交集、并集、差集的操作
  • ZSet 可以实现排行榜等功能

9.3 Redis持久化

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

RDB:(Redis DataBase)快照
按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点
1、只有一个文件 dump.rdb,方便持久化。
2、容灾性好,一个文件可以保存到安全的磁盘。
3、性能最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能。
4、相对于数据集大时,比 AOF 的启动效率更高。
缺点
1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候

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 启动效率低。

RDB和 AOF 机制对比

  • AOF文件比RDB更新频率高,如果两个都配了优先使用AOF还原数据。
  • AOF比RDB更安全也更大
  • RDB性能比AOF好

9.4 Redis key的过期时间和过期键的删除策略

  • EXPIRE:用于设置key的过期时间(seconds)。
  • PERSIST:用于删除给定 key 的过期时间,使得 key 永不过期。

过期键的删除策略

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。

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

9.5 Redis 缓存异常及解决方式

缓存雪崩
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2、一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
3、给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
1、接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2、从缓存取不到数据,在数据库中也没有取到,也将key放入缓存中,值设置为null,缓存有效时间可以设置短点。需要定期的清理空值的key。避免内存被恶意占满。

缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
1、设置热点数据永远不过期。
2、加互斥锁。

缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
1、一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
2、警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
3、错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
4、严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
缓存降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

缓存更新
缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。
缓存更新的设计模式有四种
1、Cache aside:查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;
2、Read through:在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;
3、Write through:在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;
4、Write behind caching:俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;
Cache aside
为了避免在并发场景下,多个请求同时更新同一个缓存导致脏数据,因此不能直接更新缓存而是令缓存失效
1、先更新数据库后失效缓存:并发场景下,推荐使用延迟失效(写请求完成后给缓存设置1s过期时间),在读请求缓存数据时若redis内已有该数据(其他写请求还未结束)则不更新。当redis内没有该数据的时候(其他写请求已令该缓存失效),读请求才会更新redis内的数据。这里的读请求缓存数据可以加上失效时间,以防第二步操作异常导致的不一致情况。
2、先失效缓存后更新数据库:并发场景下,推荐使用延迟失效(写请求开始前给缓存设置1s过期时间),在写请求失效缓存时设置一个1s延迟时间,然后再去更新数据库的数据,此时其他读请求仍然可以读到缓存内的数据,当数据库端更新完成后,缓存内的数据已失效,之后的读请求会将数据库端最新的数据加载至缓存内保证缓存和数据库端数据一致性;在这种方案下,第二步操作异常不会引起数据不一致,例如设置了缓存1s后失效,然后在更新数据库时报错,即使缓存失效,之后的读请求仍然会把更新前的数据重新加载到缓存内。
推荐使用先失效缓存,后更新数据库,配合延迟失效来更新缓存的模式

9.6 Redis 事务

1. Redis 事务概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

2. Redis事务的三个阶段

  • 事务开始 MULTI
  • 命令入队
  • 事务执行 EXEC

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

3. Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
Redis 不支持回滚,Redis 在事务失败时不进行回滚,而是继续执行余下的命令
如果在一个事务中的命令出现错误,那么所有的命令都不会执行
如果在一个事务中出现运行错误,那么正确的命令会被执行

  • WATCH 命令,是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令,用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC命令,用于执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
  • DISCARD命令,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出
  • UNWATCH命令,可以取消对所有key的监控

4. Redis事务特性

  • 具有ACID中的一致性隔离性
  • 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性
  • Redis事务不保证原子性,且没有回滚,但Redis中单条命令是原子性执行的。

9.6 Redis 集群

Redis 集群方案有主从复制(master-slave)哨兵模式(sentinel)集群(Cluster) 三种方式。

主从复制(master-slave)
一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。

1、主从复制的工作原理
在这里插入图片描述

  1. 当slave启动和master建立MS关系后,会向master发送SYNC命令和master全量同步(初次连接)
  2. master接收到命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
  3. 当快照完成后,master会将快照文件和所有缓存的写命令发送给slave
  4. slave接收到后,会先写入本地磁盘,然后再从本地磁盘加载到内存中
  5. 之后,master每当接收到写命令时就会将命令发送给slave,从而保证数据的一致

2. 主从复制优点

  • 主从复制主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。
  • redis 采用异步方式复制数据到 slave 节点,从 redis2.8 开始,slave 会周期性地确认自己每次复制的数据量;
  • 一个 master node 是可以配置多个 slave node 的,slave node 也可以连接其他的 slave node;
  • slave node 做复制的时候,不会阻塞 master node 的正常工作;
  • slave node 做复制的时候,也不会阻塞对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;

3. 主从复制缺点

  • slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大;
  • 不具备自动容错和恢复功能,master或slave的宕机都会导致部分读写请求失败,需要人工介入;
  • master宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
  • 如果多个 slave 断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要 slave 启动,就会发送sync 请求和主机全量同步,可能会导致 master IO 剧增从而宕机;
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;

Sentinel(哨兵)模式
哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立运行的进程。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个 Redis 实例。

1. 哨兵模式的工作原理

在这里插入图片描述

  • 每个Sentinel进程以每秒钟一次的频率向整个集群中的 Master,Slave以及其他Sentinel发送一个 PING 命令。
  • 如果一个实例服务距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds选项所指定的值, 则这个实例会被 Sentinel进程标记为主观下线(SDOWN)
  • 如果一个 Master 被标记为主观下线(SDOWN),则正在监视这个 Master 的所有 Sentinel要以每秒一次的频率确认 Master 的确进入了主观下线状态
  • 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)
  • 在一般情况下, 每个 Sentinel进程会以每 10 秒一次的频率向集群中的所有 Master、Slave发送INFO命令。
  • 当 Master 被 Sentinel进程标记为客观下线(ODOWN)时,Sentinel进程向下线的 Master的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  • 若没有足够数量的 Sentinel进程同意 Master下线, Master 的客观下线状态就会被移除。若 Master 重新向 Sentinel进程发送 PING 命令返回有效回复,Master的主观下线状态就会被移除。

2. 哨兵的作用

哨兵(sentinel)是 redis 集群机构中非常重要的一个组件,哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动将 slave 切换成 master 。
  • 配置中心:如果故障转移发生了,通过发布订阅模式通知其他的slave node 和 client 客户端新的 master 地址,修改配置文件,让它们切换主机;

3. 哨兵模式的优点

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高(可以看作自动版的主从复制)。

4. 哨兵模式的缺点

  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Cluster 集群模式(Redis官方)

Redis Cluster是一种服务器 Sharding(分片) 技术,3.0版本开始正式提供。

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,每台 Redis 节点上存储不同的内容。

1. Cluster 集群的数据分片
Redis Cluster 集群没有使用一致性 hash,而是引入了哈希槽(hash slot)的概念。Redis 集群有16384 个(2^14)哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分hash槽

这种结构很容易添加或者删除节点。从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

2. Cluster 集群的优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel的监控和自动Failover(故障转移)能力
  • 所有的 redis 节点彼此互联(PING-PONG机制),内部使用二进制协议(gossip 协议,用于节点间进行高效的数据交换)优化传输速度和带宽。
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 高性能,客户端直连redis服务,免去了proxy代理的损耗

3. Cluster 集群的缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

9.7 Redis分布式锁

1. SETNX命令

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系可以使用SETNX命令实现分布式锁。SETNX 是『SET if Not Exists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

SETNX流程及事项如下

  • 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功;
  • 为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间;
  • 释放锁,使用DEL命令将锁数据删除;

2. RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

10. RabbitMQ 篇

10.1 MQ优缺点?

优点:

  • 异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间;
  • 应用解耦:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败;
  • 限流削峰:避免流量过大导致应用系统挂掉的情况;

缺点:

  • 系统可用性降低:系统引入,MQ崩溃,整套系统崩溃。
  • 系统复杂度提高:会产生其他问题。如重复消费、消息丢失、消息传递的顺序性等问题。
  • 数据一致性问题

10.2 ActiveMQ、RabbitMQ、RocketMQ、Kafka 对比

特性 ActiveMQ RabbitMQ RocketMQ Kafka
单机吞吐量 万级,比 RocketMQ、Kafka 低一个数量级 同 ActiveMQ 10 万级,支撑高吞吐 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic 数量对吞吐量的影响 topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的topic topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性 ms 级 微秒级,这是 RabbitMQ 的一大特点,延迟最低 ms 级 延迟在 ms 级以内
可用性 高,基于主从架构实现高可用 同 ActiveMQ 非常高,分布式架构 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 有较低的概率丢失数据 基本不丢 经过参数优化配置,可以做到 0 丢失 同 RocketMQ
功能支持 MQ 领域的功能极其完备 基于 erlang 开发,并发能力很强,性能极好,延时很低 MQ 功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

10.3 RabbitMQ 有哪些重要的角色?

  • 生产者:消息的创建者,负责创建和推送数据到消息服务器;
  • 消费者:消息的接收方,用于处理数据和确认消息;
  • 代理:就是 RabbitMQ 本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。

10.4 RabbitMQ 有哪些重要的组件?

  • ConnectionFactory(连接工厂):应用程序与Rabbit之间建立连接的管理器,程序代码中使用。
  • Channel(信道):消息推送使用的通道。
  • Exchange(交换器):用于接受、分配消息。
  • Queue(队列):用于存储生产者的消息。
  • RoutingKey(路由键):用于把生成者的数据分配到交换器上。
  • BindingKey(绑定键):用于把交换器的消息绑定到队列上。

10.5 RabbitMQ 的消息是怎么发送的?

首先客户端必须连接到 RabbitMQ 服务器才能发布和消费消息,客户端和 rabbit server 之间会创建一个 tcp 连接,一旦 tcp 打开并通过了认证(认证就是你发送给 rabbit 服务器的用户名和密码),你的客户端和 RabbitMQ 就创建了一条 amqp 信道(channel),信道是创建在“真实” tcp 上的虚拟连接,amqp 命令都是通过信道发送出去的,每个信道都会有一个唯一的 id,不论是发布消息,订阅队列都是通过这个信道完成的。

10.6 RabbitMQ 怎么保证消息的稳定性(避免消息丢失)?

1. 生产者发出后保证到达了MQ

RabbitMQ引入了事务机制发送方确认机制(publisher confirm),由于事务机制过于耗费性能所以一般不用。发送方确认机制就是消息发送到MQ那端之后,MQ会回一个确认收到的消息给我们。

2. MQ收到消息保证分发到了消息对应的Exchange
消息找不到对应的Exchange。找不到对应的Queue。这两种情况都可以用RabbitMQ提供的mandatory参数来解决,它会设置消息投递失败的策略,有两种策略:自动删除或返回到客户端。

3. Exchange分发消息入队之后保证消息的持久性
消息持久化,以便MQ重新启动之后消息还能重新恢复过来。消息的持久化要做,还要做队列的持久化Exchange的持久化。创建Exchange和队列时只要设置好持久化,发送的消息默认就是持久化消息。如果出现服务器宕机或者磁盘损坏则上面的手段统统无效,必须引入镜像队列,做异地多活来抵御这种不可抗因素。

4. 消费者收到消息之后保证消息的正确消费
消费者的消息确认

10.7 RabbitMQ 怎么保证消息的顺序性?

RabbitMQ 保证消息的顺序性
RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,同一个 queue 的消息是一定会保证有序的

Kafka 保证消息的顺序性
对于 Kafka 来说,一个 topic 下同一个 partition 中的消息肯定是有序的,导致最终乱序是由于消费者端需要使用多线程并发处理消息来提高吞吐量。可以在线程处理前增加个内存队列,每个线程只负责处理其中一个内存队列的消息

RocketMQ 保证消息的顺序性
对于 RocketMQ 来说,每个 Topic 可以指定多个 MessageQueue,当我们写入消息的时候,会把消息均匀地分发到不同的 MessageQueue 中。要解决 RocketMQ 的乱序问题,我们只需要想办法让同一个Topic 进入到同一个 MessageQueue 中就可以了。因为同一个 MessageQueue 内的消息是一定有序的,一个 MessageQueue 中的消息只能交给一个 Consumer 来进行处理,所以 Consumer 消费的时候就一定会是有序的。

10.8 RabbitMQ 怎么保证消息的幂等性(避免重复消费)?

消息的幂等性:就是即使多次收到了消息,也不会重复消费

  • 生产者不重复发送消息给MQ

mq内部可以为每条消息生成一个全局唯一的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

  • 消费者不重复消费

即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。
1、从MQ拿到数据存到数据库,可以根据数据创建唯一约束。
2、拿到的数据是直接放到redis的set中

11. Elasticsearch基础篇

11.1 ES 中的基本概念

  • index 索引:索引类似于mysql中的数据库,是存数据的地方,包含一堆有相似结构的文档数据。
  • document 文档:类似于mysql中的一行,不同之处在于 ES 中的每个文档可以有不同的字段。文档是es中的最小数据单元,可以认为一个文档就是一条记录
  • Field 字段:Field是Elasticsearch的最小单位,一个document里面有多个field
  • shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。
  • replica 副本:任何一个服务器随时可能故障或宕机,此时 shard 可能会丢失,因此可以为每个 shard 创建多个 replica 副本。多个replica还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认5个),replica shard(随时修改数量,默认1个),默认每个索引10个 shard,5个primary shard,5个replica shard,最小的高可用配置,是2台服务器。

11.2 ES中的倒排索引是什么?

传统的检索方式是通过文章,逐个遍历找到对应关键词的位置。
倒排索引是通过分词策略,形成了词和文章的映射关系表,也称倒排表,这种词典 + 映射表即为倒排索引。 其中词典中存储词元,倒排表中存储该词元在哪些文中出现的位置。时间复杂度O(1)

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

  • 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
  • 查询速度快。O(len(str)) 的查询时间复杂度。

11.3 ES集群部署

ES集群部署可以参考 elasticsearch环境集群部署,此文介绍比较详细。

11.4 ES中的索引、文档的相关操作

ES基本操作可以参考 elasticsearch入门基本操作,此文介绍比较详细。

11.5 text 和 keyword类型的区别?

两个的区别主要分词的区别:keyword 类型是不会分词的,直接根据字符串内容建立倒排索引,keyword类型的字段只能通过精确值搜索到;text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引

11.6 query 和 filter 的区别?

  • query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;
  • filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。

12. Linux篇

12.1 绝对路径、当前路径、根目录、主目录

  • 绝对路径: 如/etc/init.d
  • 当前目录和上层目录: ./ ../
  • 主目录: ~/
  • 切换目录: cd
  • 切换至主目录:cd ~cd $HOME
  • 切换至上次所在目录:cd -
  • 查看当前路径:pwd

12.2 进程相关命令

  • 查看进程:ps aux

a:显示当前终端下的所有进程信息,包括其他用户的进程。
u:使用以用户为主的格式输出进程信息。
x:显示当前用户在所有终端下的进程。

  • 查看进程:ps -elf

-e:显示系统内的所有进程信息。
-l:使用长(long)格式显示进程信息。
-f:使用完整的(full)格式显示进程信息。

ps命令可以配合管道命令grep 一起使用 ps -elf | grep java

  • 查看被进程打开文件的信息:lsof ,此命令需要安装
  • 列出在指定端口上打开的文件:lsof -i:端口号

结束进程

  • 结束某一个进程:kill -9 PID
  • 结束指定用户的所有进程:kill -9 'lsof -t -u tt'

lsof -u tt 是列出tt用户所有打开的文件,加上 -t 选项之后表示结果只列出PID列

12.3 目录/文件相关命令

Linux常见的处理目录的命令:

  • ls(list files): 列出目录及文件名
  • cd(change directory):切换目录
  • pwd(print work directory):显示目前的目录
  • mkdir(make directory):创建一个新的目录
  • rmdir(remove directory):删除一个空的目录
  • cp(copy file): 复制文件或目录
  • rm(remove): 删除文件或目录
  • mv(move file): 移动文件与目录,或修改文件与目录的名称

ls (list files)命令:用于列出目前工作目录所含文件及子目录。

ls [-alrtAFR] [name...]

-a 显示所有文件及目录 (. 开头的隐藏文件也会列出)
-l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出 可简写为ll
-r 将文件以相反次序显示(原定依英文字母次序)
-t 将文件依建立时间之先后次序列出
-A 同 -a ,但不列出 “.” (目前目录) 及 “…” (父目录)
-F 在列出的文件名称后加一符号;例如可执行档则加 “*”, 目录则加 “/”
-R 若目录下有文件,则以下之文件亦皆依序列出

mkdir(make directory):创建一个新的目录

mkdir [-mp] 目录名称

-m :配置文件的权限
-p :递归创建多级目录

rmdir:删除空的目录

rmdir [-p] 目录名称

-p :从该目录起,递归删除多级空目录

cp:复制文件或目录

cp [options] source1 source2 .... destination

-a:相当于 -pdr 的意思,至于 pdr 请参考下列说明;(常用)
-d:若来源档为连结档的属性(link file),则复制连结档属性而非文件本身;
-f:为强制(force)的意思,若目标文件已经存在且无法开启,则移除后再尝试一次;
-i:若目标档(destination)已经存在时,在覆盖时会先询问动作的进行(常用)
-l:进行硬式连结(hard link)的连结档创建,而非复制文件本身;
-p:连同文件的属性一起复制过去,而非使用默认属性(备份常用);
-r:递归持续复制,用于目录的复制行为;(常用)
-s:复制成为符号连结档 (symbolic link),亦即『捷径』文件;
-u:若 destination 比 source 旧才升级 destination !

scp:用于 Linux 之间复制文件和目录

scp [可选参数] [[user@]host1]file_source [[user@]host1]file_target

rm:删除文件或目录

rm [-fir] 文件或目录

-f :强制删除,忽略不存在的文件,不会出现警告信息;
-i :互动模式,在删除前会询问使用者是否动作
-r :递归删除

mv:移动文件与目录,或修改名称

mv [-fiu] source destination

-u :若目标文件已经存在,且 source 比较新,才会执行 (update)

Linux系统中使用以下命令来查看文件的内容:

  • cat 由第一行开始显示文件内容
  • tac 从最后一行开始显示,可以看出 tac 是 cat 的倒着写!
  • nl 显示的时候,顺道输出行号!
  • more 一页一页的显示文件内容
  • less 与 more 类似,但是比 more 更好的是,他可以往前翻页!
  • head 只看头几行
  • tail 只看尾几行

cat:由第一行开始显示文件内容

cat [options] 文件

-A :相当于 -vET 的整合选项,可列出一些特殊字符而不是空白而已;
-b :列出行号,仅针对非空白行做行号显示,空白行不标行号!
-E :将结尾的断行字节 $ 显示出来;
-n :列出行号,连同空白行也会有行号,与 -b 的选项不同;
-T :将 [tab] 按键以 ^I 显示出来;
-v :列出一些看不出来的特殊字符

more:一页一页的显示文件内容

more 文件

  • 空格键 (space):代表向下翻一页;
  • Enter :代表向下翻『一行』;
  • /字串 :代表在这个显示的内容当中,向下搜寻『字串』这个关键字;
  • :f :立刻显示出档名以及目前显示的行数;
  • q :代表立刻离开 more ,不再显示该文件内容。
  • b :代表往回翻页,不过这动作只对文件有用,对管线无用。

less:一页一页的显示文件内容,可以向上翻页和搜索

less 文件

  • 空格键 :向下翻动一页;
  • [pagedown]键:向下翻动一页;
  • [pageup]键 :向上翻动一页;
  • /字串 :向下搜寻『字串』的功能;
  • ?字串 :向上搜寻『字串』的功能;
  • n :重复前一个搜寻 (与 / 或 ? 有关)
  • N :反向的重复前一个搜寻 (与 / 或 ? 有关)
  • q :离开 less 这个程序;

head:查看文件前面几行

head [-n number] 文件

-n :后面接数字,代表显示几行

tail:查看文件后面几行,一般用于查看日志信息

tail [-fn number] 文件

-f :循环读取
-n :后面接数字,代表显示几行

12.4 搜索文件命令

linux查找文件的命令:

  • find命令,可以查找任何想要的文件;
  • locate命令,查不到最新变动过的文件;
  • whereis命令,只搜索二进制文件
  • which命令,只搜索二进制文件
  • grep命令

find [指定目录] [指定条件] [指定动作]

1. 根据名称查找,可以使用*,?通配符

  • -name 区分大小写
  • -iname 不区分大小写
find /etc -name init

2. 根据文件大小查找 -size

find / -size +204800

这条命令的功能是,在根目录下查找大于 100MB 的文件。因为它的单位是数据块,而一个数据块是 0.5KB,所以 100MB 是 204800 个数据块,所以要写 204800。
若把「+」换成「-」,就是查找小于 100MB 的文件;若换成「=」,就是查找等于 100MB 的文件。

3. 根据所有者查找 -user
4. 根据所有组查找 -group

5. 根据时间查找

find /etc -cmin -5

这条命令的功能是,在 /etc 下查找 5 分钟内被修改过属性的文件和目录。

  • -amin 访问时间(a - access)
  • -cmin 文件属性(c - change)
  • -mmin 文件内容 (m - modify)

6. 根据文件类型查找 -type

find /etc -type f

这条命令的功能是查找 etc 目录下的所有文件。

  • -type f,文件
  • -type d,目录
  • -type l,软链接

7. 连接符 -a、-o

若查找的条件有多个,可通过连接符将不同的选项连接起来。其中,-a表示 and,即通过「-a」连接的多个条件要同时满足,-o表示 or,通过「-o」连接的多个条件只满足其中一个即可。

locate 文件

find 命令是通过遍历磁盘来查找文件,搜索速度相对较慢,而 locate 命令是在 Linux 系统内的一个文件数据库中查找你所需要的文件,查找速度比 find 快很多。

# 查找init文件,区分大小写。若想忽略大小写 加 -i
locate init

locate 的缺点

  • 创建一个文件后立马使用locate搜索这个文件会搜不到,因为这个文件还没被更新到文件数据库里去。可以使用updatedb手动更新一下数据库
  • locate 不查找 tmp 目录下的文件

which 命令

which 是查找命令文件的命令,可以查找命令所在位置。

which ls

whereis 命令

whereis 也是查找命令文件的命令,可以查找命令所在位置,和其帮助文档所在位置。

whereis ls

grep 字串 [目录/文件]

grep是查找文件内容

grep multiuser /etc/inittab

忽略大小写 -i

grep -i multiuser /etc/inittab

排除指定字符串 -v

如果想看配置文件的内容,但是不想看注释,就可以在搜索文件内容时排除「#」所在的行。但是有的注释并不是单独一行,而是写在配置语句的后面,这样的话单纯地排除「#」所在的行就会把配置语句也排除掉,造成误伤。也就是说我们只能排除掉以「#」开头的行。-v ^#

grep -v ^# /etc/inittab

12.5 网络相关命令

  • ping:查看网络是否连通
  • netstat:检验本机各端口的网络连接情况
  • ifconfig:查看 ip 地址
  • hostname:显示主机名字
  • ssh:登录到其他系统

12.6 压缩/解压缩命令

1、.tar

解包:tar -xvf FileName.tar
打包:tar -cvf FileName.tar DirName
(注:tar是打包,不是压缩)

2、.gz

解压1:gunzip FileName.gz
解压2:gzip -d FileName.gz
压缩:gzip FileName

3、.tar.gz 和 .tgz

解压:tar -zxvf FileName.tar.gz
压缩:tar -zcvf FileName.tar.gz DirName

4、.zip

解压:unzip FileName.zip
压缩:zip FileName.zip DirName

5、.rar

解压:rar -x FileName.rar
压缩:rar -a FileName.rar DirName

12.7 权限相关命令

  • chgrp:修改文件和目录的所属组
chgrp [-R] 所属组 文件或目录
  • chown:修改文件和目录的所有者和所属组
chown [-R] 所有者 文件或目录
chown [-R] 所有者:所属组 文件或目录
  • chmod:修改文件或目录的权限
chmod [-R] 权限值 文件名

12.8 环境变量相关命令

  • 设置环境变量:export
  • 删除环境变量:unset
  • 设置只读变量:readonly,不能用unset删除
  • 查看所有环境变量 env
  • 查看某个环境变量,如 home: env $HOMEecho $HOME

Linux变量可分为两类:

  • 永久的:需要修改配置文件,变量永久生效。
  • 临时的:使用export命令声明即可,变量在关闭shell时失效。

设置变量的三种方法

  • /etc/profile文件中添加变量【对系统所有用户生效(永久的)】
vi /etc/profile
export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib

注:修改文件后要想马上生效还要运行source /etc/profile不然只能在下次重进此用户时生效。

  • 在用户目录下的.bash_profile文件中增加变量【对单一用户生效(永久的)】
vi /home/zhangsan/.bash.profile
export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib

注:修改文件后要想马上生效还要运行source /home/zhangsan/.bash_profile不然只能在下次重进此用户时生效。

  • 直接运行export命令定义变量【只对当前shell(BASH)有效(临时的)】

在shell的命令行下直接使用export 变量名=变量值定义变量,该变量只在当前的shell(BASH)或其子shell(BASH)下是有效的,shell关闭了,变量也就失效了,再打开新shell时就没有这个变量,需要使用的话还需要重新定义。

12.9 系统服务相关命令

  • chkconfig 命令用于检查,设置系统的各种服务

chkconfig [param][系统服务] 或 chkconfig [系统服务][on/off/reset]

--add 增加所指定的系统服务,让 chkconfig 指令得以管理它,并同时在系统启动的叙述文件内增加相关数据。
--del  删除所指定的系统服务,不再由 chkconfig 指令管理,并同时在系统启动的叙述文件内删除相关数据。
--list 列出chkconfig 所知道的所有命令。

  • service 命令的作用是去 /etc/init.d 目录下寻找相应的服务,可以启动、停止、重启系统服务,还可以显示所有系统服务的当前状态

service 系统服务 status/start/stop/restart

  • systemctl管理系统服务,启动、停止、重启、禁用、查看系统服务,该命令集成了命令 service、chkconfig、setup、init 的大部分功能于一身。

systemctl status/start/stop/restart 系统服务.service

12.10 软件安装与卸载命令

yum 查找、安装、删除某一个、一组甚至全部软件包

yum [options] [command] [package ...]

options:可选,选项包括 -y(当安装过程提示选择全部为 “yes”),-q(不显示安装的过程)等等。
command:要进行的操作。
package:安装的包名。

  • 列出所有可更新的软件清单命令:yum check-update
  • 更新所有软件命令:yum update
  • 仅安装指定的软件命令:yum -y install <package_name>
  • 仅更新指定的软件命令:yum update <package_name>
  • 列出所有可安裝的软件清单命令:yum list
  • 删除软件包命令:yum remove <package_name>
  • 查找软件包命令:yum search <keyword>

rpm 查找、安装、删除某一个、一组甚至全部软件包

rpm [options] [package ...]

-a  查询所有套件。
-e  删除指定的套件。
-h   套件安装时列出标记。
-i   显示套件的相关信息。
-l  显示套件的文件列表。
-p  查询指定的RPM套件档。
-q  使用询问模式,当遇到任何问题时,rpm指令会先询问用户。
-v  显示指令执行过程。
--nodeps  不验证套件档的相互关联性。

  • rpm -qa | grep <package_name> 查看软件安装版本信息
  • rpm -qi <package_name> 查看软件安装详细信息
  • rpm -ql <package_name> 查看软件安装目录
  • rpm -ivh <package_name> 安装软件
  • rpm -evh <package_name> 卸载软件

12.11 建立软链接(快捷方式)命令

  • 软链接: ln -s slink source
  • 硬链接: ln link source

12.12 其他常用命令

  • history:查看用过的命令列表
  • df -h:检查文件系统的磁盘空间占用情况。
  • su:切换用户
  • sudo:以系统管理者的身份执行指令

猜你喜欢

转载自blog.csdn.net/weixin_45698637/article/details/123882459