1 Java基础

1.Java 基础 知识

1.1面向对象的特征(了解)

面向对象的特征:封装、继承、多态、抽象。
封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。
继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
多态:指允许不同的对象对同一消息做出相应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
抽象表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。在Java中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。

1.2 Java的基本数据类型有哪些(了解)

Java面试宝典汇总202203 - 图1

1.3JDK JRE JVM 的区别 (必会)

Java面试宝典汇总202203 - 图2
JDK(Java Development Kit)是整个 Java 的核心,是java开发工具包,包括了 Java 运行环境 JRE、Java 工具和 Java 基础类库。
JRE(Java Runtime Environment)是运行 JAVA 程序所必须的环境的集合,包含java虚拟机和java程序的一些核心类库。
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是整个 java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。

1.4重载和重写的区别(必会)

重载: 发生在同一个类中,方法名必须相同,参数类型不同.个数不同.顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名.参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

1.5 Java中==和equals的区别(必会)

== 的作用:
  基本类型:比较的就是值是否相同
  引用类型:比较的就是地址值是否相同
equals 的作用:  引用类型:默认情况下,比较的是地址值。
特:String、Integer、Date这些类库中equals被重写,比较的是内容而不是地址!
面试题:请解释字符串比较之中 “ == ” 和 equals() 的区别?
答: ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较; equals():比较的是两个字符串的内容,属于内容比较。

1.6 String、StringBuffer、StringBuilder三者之间的区别(必会)

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)

String 中的String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[] ,String对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
小结:
(1)如果要操作少量的数据用 String;
(2)多线程操作字符串缓冲区下操作大量数据用 StringBuffer;
(3)单线程操作字符串缓冲区下操作大量数据用 StringBuilder。

1.7接口和抽象类的区别是什么?(必会)

实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符

1.8 string常用的方法有哪些?(了解)

indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。

1.9什么是单例模式?有几种?(必会)

单例模式:某个类的实例在 多线程环境下只会被创建一次出来。
单例模式有饿汉式单例模式、懒汉式单例模式和双检锁单例模式三种。
饿汉式:线程安全,一开始就初始化。
Java面试宝典汇总202203 - 图3
懒汉式:非线程安全,延迟初始化。
Java面试宝典汇总202203 - 图4
双检锁:线程安全,延迟初始化。
Java面试宝典汇总202203 - 图5

1.10反射(了解)

在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息 以及动态调用对象方法的功能成为 Java 语言的反射机制。
获取 Class 对象的 3 种方法 :
调用某个对象的 getClass()方法
Person p=new Person();
Class clazz=p.getClass();
调用某个类的 class 属性来获取该类对应的 Class 对象
Class clazz=Person.class;
使用 Class 类中的 forName()静态方法(最安全/性能最好)
Class clazz=Class.forName(“类的全路径”); (最常用)

1.11 jdk1.8的新特性(高薪常问)

1 Lambda 表达式
Lambda 允许把函数作为一个方法的参数。
Java面试宝典汇总202203 - 图6
2 方法引用
方法引用允许直接引用已有 Java 类或对象的方法或构造方法。
Java面试宝典汇总202203 - 图7
上例中我们将 System.out::println 方法作为静态方法来引用。
3 函数式接口
有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为 Lambda 表达式。通常函数式接口上会添加@FunctionalInterface 注解。
4 接口允许定义默认方法和静态方法
从 JDK8 开始,允许接口中存在一个或多个默认非抽象方法和静态方法。
5 Stream API
新添加的 Stream API(java.util.stream)把真正的函数式编程风格引入到 Java 中。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。
Java面试宝典汇总202203 - 图8
6 日期/时间类改进
之前的 JDK 自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如 commons-lang 包等。不过 JDK8 出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等。
这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。
7 Optional 类
Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。
Java面试宝典汇总202203 - 图9
Java面试宝典汇总202203 - 图10
8 Java8 Base64 实现
Java 8 内置了 Base64 编码的编码器和解码器。

1.12 Java的异常(必会)

Java面试宝典汇总202203 - 图11
Throwable是所有Java程序中错误处理的父类,有两种资类:Error和Exception。
Error:表示由JVM所侦测到的无法预期的错误,由于这是属于JVM层次的严重错误,导致JVM无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。
Exception:表示可恢复的例外,这是可捕捉到的。
1.运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
2.非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
常见的RunTime异常几种如下
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常

1.13 BIO、NIO、AIO 有什么区别?(高薪常问)

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

1.14 Threadloal的原理(高薪常问)

ThreadLocal:为共享变量在每个线程中创建一个副本,每个线程都可以访问自己内部的副本变量。通过threadlocal保证线程的安全性。
其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本。
ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的。如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。
使用完ThreadLocal之后,记得调用remove方法。 在不使用线程池的前提下,即使不调用remove方法,线程的”变量副本”也会被gc回收,即不会造成内存泄漏的情况。

1.16 同步锁、死锁、乐观锁、悲观锁 (高薪常问)

同步锁:
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
死锁:
何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_conditio机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

1.17说一下 synchronized 底层实现原理?(高薪常问)

一、syn锁住的是对象,对象里边的组成如下:
Java的对象布局
必须是要求对象的大小必须是8的整数倍
1.首先是有一个object header
2.填充数据 :当对象大小不足8的倍数的时候,他会把当前对象填充成8的倍数,假如他现在本身就是8的byte的倍数,此时填充数据就不用了
3.成员变量 :就是咱们成员变量

二、锁升级
2.1 偏向锁
线程要尝试去加锁,他会去判断当前这个mark-word里边是否包含线程id,如果没有线程id的话,他会去利用cas把自己的线程id写入到mark-word里边去第二次这个线程再次过来的时候,他会去判断当前这个mark-word里边是否包含线程id,如果有了的话,他就会把他自己的线程id和对象头里边的线程id进行对比,如果发现是一样的此时就标识获得到了锁
如果你在没加锁的情况打印对象头:他默认就是 无锁可偏向,如果你没加锁的情况计算了hashCode码,无锁不可偏向,
如果此时你加锁无法成为偏向锁,直接膨胀成一把轻量级锁

2.2 轻量级锁
1、 升级成轻量级锁三个条件
a、你现在已经是无锁不可偏向,此时加锁那么他就直接是一把轻量级锁
b、没有关闭延迟偏向锁打开,他会自动成为轻量级锁
c、如果程序出现交替执行,他也会成为一把轻量级锁
2、原理
a、首先方法压栈,此时这个方法栈帧就压栈,栈帧种就创建两个和锁有关系的空间 displace hrd ,owner
b、他会将锁里边的mark-word里边信息拷贝到hrd种
c、他会用栈帧owner 指针去指向我们的对象头
d、对象头中轻量级指针会指向当前创建出来的这个栈帧
e、锁会把当前的状态修改成00
f、如果说完成了以上4件事情,那么此时才表示加锁成功,这把锁就是属于当前线程的

2.3 重量级锁
1、原理
a、Java如果发现要创建一把重量级锁,我们Java就会为我们创建一个C++的ObjectMonitor,会让对象头中monitor指向我们这个ObjectMonitor对象如果你进入到锁内部时,这个ObjectMonitor他会 发起汇编指定 monitorenter,当你出syn代码代码块的时候,他会发出monitorexit指令如果你在执行syn过程中出现了异常,其实上他还是会执行monitorexit这样一个指令
b、对象头里边monitor指针会指向 ObjectMonitor 对象,当多个线程来加锁的时候他,他们就会执行monitorenter 指令,进入到 ObjectMonitor进入到entrylist中等待抢锁,他们会利用cas 来进行抢锁,如果抢锁成功,ObjectMonitor,他会把他内部的owner的指针去指向咱们抢锁成功 线程,然后会让计数器+1,如果此时是抢锁的线程是持有锁的线程,那么此时count就会再+1 ,释放锁的时候,把count进行—,直到count== 0的时候就把owner 置为null,如果你对线程调用wait方法,此时这些被wait的线程他就会进入到waitSet中,只有当你去调用notifyall方法的时候他才会从新开始这样一套流程
c、如果是同步方法,那么他执行的 指令 acc_synchronized ,但是这哥们是隐式调用

1.18 synchronized 和 volatile 的区别是什么?(高薪常问)

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

1.19synchronized 和 Lock 有什么区别? (高薪常问)

首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

1.20 手写冒泡排序?(必会)

Java面试宝典汇总202203 - 图12

2.集合(必会)

2.1常见的数据结构(了解)

常用的数据结构有:数组,栈,队列,链表,树,散列,堆,图等
Java面试宝典汇总202203 - 图13
数组是最常用的数据结构,数组的特点是长度固定,数组的大小固定后就无法扩容了 ,数组只能存储一种类型的数据 ,添加,删除的操作慢,因为要移动其他的元素。
是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读取出来。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结节(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。
是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事物,例如家谱、单位的组织架构等等。有二叉树、平衡树、红黑树、B树、B+树。
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
是计算机学科中一类特殊的数据结构的统称,堆通常可以被看作是一棵完全二叉树的数组对象。
的定义:图是由一组顶点和一组能够将两个顶点相连的边组成的

2.2集合和数组的区别(了解)

区别:数组长度固定 集合长度可变
数组中存储的是同一种数据类型的元素,可以存储基本数据类型,也可以存储引用数据类型;
集合存储的都是对象,而且对象的数据类型可以不一致。在开发当中一般当对象较多的时候,使用集合来存储对象。

2.3List 和 Map、Set 的区别(必会)

List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
List中存储的数据是有顺序的,并且值允许重复;
Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;
Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。

2.4 List 和 Map、Set 的实现类(必会)

Java面试宝典汇总202203 - 图14
(1)Connection接口:
List 有序,可重复
ArrayList
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
Vector
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全,效率低, 已给舍弃了
LinkedList
优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高Set 无序,唯一
HashSet
底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?
依赖两个方法:hashCode()和equals()
LinkedHashSet
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一
TreeSet
底层数据结构是红黑树。(唯一,有序)
1. 如何保证元素排序的呢?
自然排序
比较器排序
2.如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定
(2)Map接口有四个实现类:
HashMap
基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键, 线程不安全。
HashTable
线程安全,低效,不支持 null 值和 null 键;
LinkedHashMap
线程不安全,是 HashMap 的一个子类,保存了记录的插入顺序;
TreeMap
能够把它保存的记录根据键排序,默认是键值的升序排序,线程不安全。

2.5Hashmap的底层原理(高薪常问)

HashMap在JDK1.8之前的实现方式 数组+链表,
但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表或者数值+红黑树实现,主要的目的是提高查找效率

Java面试宝典汇总202203 - 图15
Java面试宝典汇总202203 - 图16

  1. Jdk8数组+链表或者数组+红黑树实现,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,当红黑树节点 小于 等于6 时又会退化为链表。
  2. 当new HashMap():底层没有创建数组,首次调用put()方法示时,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[],用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.

默认的负载因子大小为0.75,数组大小为16。也就是说,默认情况下,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍。

  1. 在我们Java中任何对象都有hashcode,hash算法就是通过hashcode与自己进行向右位移16的异或运算。这样做是为了计算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机

map.put(k,v)实现原理
(1)首先将k,v封装到Node对象当中(节点)。
(2)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(3)下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
map.get(k)实现原理
(1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

  1. Hash冲突
    不同的对象算出来的数组下标是相同的这样就会产生hash冲突,当单线链表达到一定长度后效率会非常低。
  2. 在链表长度大于8的时候,将链表就会变成红黑树,提高查询的效率。

2.6 Hashmap和hashtable ConcurrentHashMap区别(高薪常问)

区别对比一(HashMap 和 HashTable 区别):
1、HashMap 是非线程安全的,HashTable 是线程安全的。
2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。
3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。
4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ①是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用HashTable。
区别对比二(HashTable 和 ConcurrentHashMap 区别):
HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是JDK1.7使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

3.多线程(必会)

3.1什么是线程?线程和进程的区别?(了解)

线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间 内存共享,这使多线程编程可以拥有更好的性能和用户体验。

3.2创建线程有几种方式(必会)

1.继承Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类
2.实现Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3..实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4.使用线程池创建(使用java.util.concurrent.Executor接口)
Java面试宝典汇总202203 - 图17
Java面试宝典汇总202203 - 图18

3.3Runnable和Callable的区别?(必会)

主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息

3.4如何启动一个新线程、调用start和run方法的区别?(必会)

Java面试宝典汇总202203 - 图19
线程对象调用run方法不开启线程。仅是对象调用方法。
线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行
调用start方法可以启动线程,并且使得线程进入就绪状态,而run方法只是thread的一个普通方法,还是在主线程中执行。

3.5线程有哪几种状态以及各种状态之间的转换?(必会)

  1. 第一是new->新建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
    2. 第二是Runnable->就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
    3. 第三是Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码
    4. 第四是阻塞状态。阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (1)等待 – 通过调用线程的wait() 方法,让线程等待某工作的完成。
    (2)超时等待 – 通过调用线程的sleep() 或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    (3)同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    5. 第五是dead->死亡状态: 线程执行完了或者因异常退出了run()方法,该线程结束生命周期.

Java面试宝典汇总202203 - 图20

3.6线程相关的基本方法?(必会)

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等
1.线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
2.线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态.
3.线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
4.线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
5.Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸.
6.线程唤醒(notify)
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

3.7 wait()和sleep()的区别?(必会)

1. 来自不同的类
wait():来自Object类;
sleep():来自Thread类;
2.关于锁的释放:
wait():在等待的过程中会释放锁;
sleep():在等待的过程中不会释放锁
3.使用的范围:
wait():必须在同步代码块中使用;
sleep():可以在任何地方使用;
4.是否需要捕获异常
wait():不需要捕获异常;
sleep():需要捕获异常;

4. 线程池

4.1为什么需要线程池(了解)

在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:
1、使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗
2、由于没有线程创建和销毁时的消耗,可以提高系统响应速度
3、通过线程池可以对线程进行合理管理,根据系统的承受能力调整可运行线程数量的大小等

4.2线程池的分类(高薪常问)

Java面试宝典汇总202203 - 图21

  1. newCachedThreadPool:创建一个可进行缓存重复利用的线程池
  2. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,线程池中的线程处于一定的量,可以很好的控制线程的并发量
  3. newSingleThreadExecutor:创建一个使用单个 worker 线程的Executor ,以无界队列方式来运行该线程。线程池中最多执行一个线程,之后提交的线程将会排在队列中以此执行
  4. newSingleThreadScheduledExecutor:创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行
  5. newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行
  6. newWorkStealingPool:创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的CPU个数

    4.3核心参数(高薪常问)

    corePoolSize:核心线程池的大小
    maximumPoolSize:线程池能创建线程的最大个数
    keepAliveTime:空闲线程存活时间
    unit:时间单位,为keepAliveTime指定时间单位
    workQueue:阻塞队列,用于保存任务的阻塞队列
    threadFactory:创建线程的工程类
    handler:饱和策略(拒绝策略)

    4.4线程池的原理(高薪常问)

    Java面试宝典汇总202203 - 图22
    线程池的工作过程如下:
    当一个任务提交至线程池之后, 1. 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线程来执行任务。否则进入
    2. 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入3.
    3. 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。如果线程池满了,则交给饱和策略来处理任务。

    4.5拒绝策略(了解)

    ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出RejectedExecutionException异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选择重试或者放弃提交等策略。
    ThreadPoolExecutor.DiscardPolicy: 也是丢弃任务,但是不抛出异常,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
    ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险。
    ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它。

    4.6线程池的关闭(了解)

    关闭线程池,可以通过shutdown和shutdownNow两个方法
    原理:遍历线程池中的所有线程,然后依次中断
    1、shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
    2、shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

    5.Jvm-参考就业面试题

    虚拟机,一种能够运行java字节码的虚拟机。

6.1 JDK1.8 JVM运行时内存(高薪)
Java面试宝典汇总202203 - 图23
程序计数器:
线程私有的(每个线程都有一个自己的程序计数器), 是一个指针. 代码运行, 执行命令. 而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了.记录代码执行的位置
Java虚拟机栈:
线程私有的(每个线程都有一个自己的Java虚拟机栈). 一个方法运行, 就会给这个方法创建一个栈帧, 栈帧入栈执行代码, 执行完毕之后出栈(弹栈)存引用变量,基本数据类型
本地方法栈:
线程私有的(每个线程都有一个自己的本地方法栈), 和Java虚拟机栈类似, Java虚拟机栈加载的是普通方法,本地方法加载的是native修饰的方法.
native:在java中有用native修饰的,表示这个方法不是java原生的.
堆:
线程共享的(所有的线程共享一份). 存放对象的,new的对象都存储在这个区域.还有就是常量池.
元空间: 存储.class 信息, 类的信息,方法的定义,静态变量等.而常量池放到堆里存储
JDK1.8和JDK1.7的jvm内存最大的区别是, 在1.8中方法区是由元空间(元数据区)来实现的, 常量池.
1.8不存在方法区,将方法区的实现给去掉了.而是在本地内存中,新加入元数据区(元空间).

5.1 JDK1.8堆内存结构(高薪常问)

Java面试宝典汇总202203 - 图24
Young 年轻区(代): Eden+S0+S1, S0和S1大小相等, 新创建的对象都在年轻代
Tenured 年老区: 经过年轻代多次垃圾回收存活下来的对象存在年老代中.
Jdk1.7和Jdk1.8的区别在于, 1.8将永久代中的对象放到了元数据区, 不存永久代这一区域了.

5.2 Gc垃圾回收(高薪常问)

JVM的垃圾回收动作可以大致分为两大步,首先是「如何发现垃圾」,然后是「如何回收垃圾」。说明一点, 线程私有的不存在垃圾回收, 只有线程共享的才会存在垃圾回收, 所以堆中存在垃圾回收.
5.3.1 如何发现垃圾
Java语言规范并没有明确的说明JVM使用哪种垃圾回收算法,但是常见的用于「发现垃圾」的算法有两种,引用计数算法和根搜索算法。

  1. 引用计数算法

该算法很古老(了解即可)。核心思想是,堆中的对象每被引用一次,则计数器加1,每减少一个引用就减1,当对象的引用计数器为0时可以被当作垃圾收集。
优点:快。
缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为0。

  1. 根搜索算法(也叫可达性分析)

根搜索算法是把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾。
Java中可作为GC Root的对象有
  1.虚拟机栈中引用的对象
  2.本地方法栈引用的对象
  2.方法区中静态属性引用的对象
  3.方法区中常量引用的对象
5.3.2 如何回收垃圾
Java中用于「回收垃圾」的常见算法有4种:

  1. 标记-清除算法(mark and sweep)

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
缺点:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片。

  1. 标记-整理算法

是在标记-清除算法基础上做了改进,标记阶段是相同的,但标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
优点:内存被整理后不会产生大量不连续内存碎片。

  1. 复制算法(copying)

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。
缺点:可使用的内存只有原来一半。

  1. 分代收集算法(generation)

当前主流JVM都采用分代收集(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为年轻代、年老代、永久代,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
Java面试宝典汇总202203 - 图25

  1. 年轻代(Young Generation)

1.所有新生成的对象首先都是放在年轻代的。
2.新生代内存按照8:1:1的比例分为一个eden区和两个Survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

  1. 年老代(Old Generation)

1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概是2倍),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率比较高。

  1. 持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,从JDK8以后已经废弃, 将存放静态文件,如Java类、方法等这些存储到了元数据区.

5.3 JVM调优参数(了解)

这里只给出一些常见的性能调优的参数及其代表的含义。(大家记住5.6个就行, 并不需要都记住。)
Java面试宝典汇总202203 - 图26
图:JVM参数配置参考
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。注意:此值一般设置成和-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss256k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
Java面试宝典汇总202203 - 图27
图:JVM参数参考
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4。(该值默认为2)
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4。

2 Web

1.网络通讯部分

1.1 TCP 与 UDP 区别? (了解)

Java面试宝典汇总202203 - 图28
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。
UDP是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中的传输层协议,它是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
TCP和UDP都是来自于传输层的协议。传输层位于应用层和网络层之间,负责位于不同主机中进程之间的通信。
TCP 与 UDP 区别
Java面试宝典汇总202203 - 图29
1.TCP基于连接UDP无连接
2.TCP要求系统资源较多,UDP较少
3.TCP保证数据正确性,UDP可能丢包
4.TCP保证数据顺序,UDP不保证

1.2什么是HTTP协议?

客户端和服务器端之间数据传输的格式规范,格式简称为“超文本传输协议”。
是一个基于请求与响应模式的、无状态的、应用层的协议,基于 TCP 的连接方式

1.3 TCP的三次握手

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。
Java面试宝典汇总202203 - 图30
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是 双方确认自己与对方的发送与接收是正常的。
SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。
第一次握手:客户端给服务器发送一个SYN。客户端发送网络包,服务端收到了。服务器得出结论:客户端的发送能力,服务端的接收能力正常。
第二次握手:服务端收到SYN报文之后,会应答一个SYN+ACK报文。服务端发包,客户端收到了。客户端得出结论:服务端的接收和发送能力,客户端的接收和发送能力正常。但是此时服务端不能确认客户端的接收能力是否正常。
第三次握手;客户端收到SYN+ACK报文之后,回应一个ACK报文。客户端发包,服务端收到了。服务器得出结论:客户端的接收和发送能力,自己的接收发送能力都正常。
通过三次握手,双方都确认对方的接收以及发送能力正常。

1.4 HTTP中重定向和请求转发的区别?

实现:
转发:用request的getRequestDispatcher()方法得到ReuqestDispatcher对象,调用forward()方法
request.getRequestDispatcher(“other.jsp”).forward(request, response);
重定向:调用response的sendRedirect()方法
response.sendRedirect(“other.jsp”);
1> 重定向2次请求,请求转发1次请求
2> 重定向地址栏会变,请求转发地址栏不变
3> 重定向是浏览器跳转,请求转发是服务器跳转
4> 重定向可以跳转到任意网址,请求转发只能跳转当前项目

1.5 Get和Post的区别?

1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。
2. Get传送的数据量较小,一般传输数据大小不超过2k-4k(根据浏览器不同,限制不一样,但相差不大这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制。
3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。
4. Get执行效率却比Post方法好。Get是form提交的默认方法。

2.cookie和session的区别?(必会)

1.存储位置不同
cookie的数据信息存放在客户端浏览器上。
session的数据信息存放在服务器上。
2.存储容量不同
单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。
对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制。
3.存储方式不同
cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
4.隐私策略不同
cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。
session存储在服务器上,不存在敏感信息泄漏的风险。
5. 有效期上不同
开发可以通过设置cookie的属性,达到使cookie长期有效的效果。
session依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的效果。
6.服务器压力不同
cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。
session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。

3.Jsp和Servlet(了解)

1.Jsp和Servlet的区别?

相同点:
jsp经编译后就变成了servlet,jsp本质就是servlet,jvm只能识别java的类,不能识别jsp代码,web容器将jsp的代码编译成jvm能够识别的java类。其实就是当你通过 http 请求一个 JSP 页面是,首先 Tomcat 会调用 service()方法将JSP编译成为 Servlet,然后执行 Servlet。

不同点:
JSP侧重视图,Sevlet主要用于控制逻辑。
Servlet中没有内置对象 。
JSP中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。

2.Servlet的生命周期

// 1. servlet对象创建时,调用此方法
public void init(ServletConfig servletConfig);

// 2. 用户访问servlet时,调用此方法
public void service(ServletRequest servletRequest, ServletResponse servletResponse);

// 3. servlet对象销毁时,调用此方法
public void destroy();

3.JSP九大内置对象

out对象:用于向客户端、浏览器输出数据。
request对象:封装了来自客户端、浏览器的各种信息。
response对象:封装了服务器的响应信息。
exception对象:封装了jsp程序执行过程中发生的异常和错误信息。
config对象:封装了应用程序的配置信息。
page对象:指向了当前jsp程序本身。
session对象:用来保存会话信息。也就是说,可以实现在同一用户的不同请求之间共享数
application对象:代表了当前应用程序的上下文。可以在不同的用户之间共享信息。
pageContext对象:提供了对jsp页面所有对象以及命名空间的访问。

4.Ajax的介绍(必会)

Ajax 即”Asynchronous JavaScript And XML”(异步 JavaScript 和 XML),是指一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更新部分网页的技术。
$.ajax({
选项
})
常见的选项有:
type:请求方式,常见的值有”get”,”post”等,默认值:get
url:请求的路径,”/ajax/hello”
data:请求的参数,参数的常见写法有 键值对或者json
方式1: name=tom&pwd=123
方式2: {“name”:”tom”,”pwd”:”123”}
success:请求成功后的回调函数 function(返回值的参数名){}
contentType:用来设置请求参数的mime类型,默认值:表单的enctype默认值 name=tom&pwd=123
error:ajax请求时内部发生错误时执行的回调函数 function(){}
dataType:指定返回值的类型 常见值:text json
async:是否异步 默认值true
Ajax应用程序的优势在于:
1. 通过异步模式,提升了用户体验
2. 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用
3. Ajax引擎在客户端运行,承担了一部分本来由服务器承担的工作,从而减少了大用户量下的服务器负载。

3 数据库

1.连接查询(必会)

1.左连接
(左外连接)以左表为基准进行查询,左表数据会全部显示出来,右表 如果和左表匹配 的数据则显示相应字段的数据,如果不匹配,则显示为 NULL;
2.右连接
(右外连接)以右表为基准进行查询,右表数据会全部显示出来,右表 如果和左表匹配的数据则显示相应字段的数据,如果不匹配,则显示为 NULL;

2.聚合函数(必会)

1.聚合函数
SQL中提供的聚合函数可以用来统计、求和、求最值等等。
2.分类
COUNT:统计行数量
SUM:获取单个列的合计值
AVG:计算某个列的平均值
MAX:计算列的最大值
MIN:计算列的最小值

3.SQL关键字(必会)

1.分页
MySQL的分页关键词limit
SELECT FROM student3 LIMIT 2,6; 查询学生表中数据,从第三条开始显示,显示6条
2.分组
MySQL的分组关键字:group by
SELECT sex, count(
) FROM student3 GROUP BY sex;
3. 去重
去重关键字:distinct
select DISTINCT NAME FROM student3;

4. SQL Select 语句完整的执行顺序: (必会)

查询中用到的关键词主要包含如下展示,并且他们的顺序依次为form…join…on…where…group by…avg()/sum()…having..select…distinct…
order by…limit…
from: 需要从哪个数据表检索数据
where: 过滤表中数据的条件
group by: 如何将上面过滤出的数据分组算结果
order by : 按照什么样的顺序来查看返回的数据

5. 数据库三范式(必会)

第一范式:1NF 原子性,列或者字段不能再分,要求属性具有原子性,不可再分解;
第二范式:2NF 唯一性,一张表只说一件事,是对记录的惟一性约束,要求记录有惟一标识,
第三范式:3NF 直接性,数据不能存在传递关系,即每个属性都跟主键有直接关系,而不是间接关系。

6. 存储引擎 (高薪常问)

1.MyISAM存储引擎

主要特点:
MySQL5.5版本之前的默认存储引擎
支持表级锁(表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁);
不支持事务,外键。
适用场景:对事务的完整性没有要求,或以select、insert为主的应用基本都可以选用MYISAM。在Web、数据仓库中应用广泛。
特点:
1、不支持事务、外键
2、每个myisam在磁盘上存储为3个文件,文件名和表名相同,扩展名分别是
.frm ———-存储表定义
.MYD ————MYData,存储数据
.MYI ————MYIndex,存储索引

2.InnoDB存储引擎

主要特点:
MySQL5.5版本之后的默认存储引擎;
支持事务;
支持行级锁(行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁);
支持聚集索引方式存储数据。

7.数据库事务(必会)

1.事务特性

原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。
一致性:事务的执行使得数据库从一种正确状态转换成另一种正确状态
隔离性:在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,
持久性:事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。

2.隔离级别

(1)读未提交(read Uncommited):
在该隔离级别,所有的事务都可以读取到别的事务中未提交的数据,会产生脏读问题,在项目中基本不怎么用, 安全性太差;
(2) 读已提交(read commited):
这是大多数数据库默认的隔离级别,但是不是MySQL的默认隔离级别;这个隔离级别满足了简单的隔离要求:一个事务只能看见已经提交事务所做的改变,所以会避免脏读问题;由于一个事务可以看到别的事务已经提交的数据,于是随之而来产生了不可重复读和虚读等问题(下面详细介绍这种问题,结合问题来理解隔离级别的含义);
(3 ) 可重复读(Repeatable read):
这是MySQL的默认隔离级别,它确保了一个事务中多个实例在并发读取数据的时候会读取到一样的数据;不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

(4) 可串行化(serializable):
事物的最高级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争,一般为了提升程序的吞吐量不会采用这个;

8.索引

4 索引的概念和优点(了解)

概念:
索引存储在内存中,为服务器存储引擎为了快速找到记录的一种数据结构。索引的主要作用是加快数据查找速度,提高数据库的性能。
优点:

  1. 创建唯一性索引,保证数据库表中每一行数据的唯一性
  2. 大大加快数据的检索速度,这也是创建索引的最主要的原因
  3. 加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  4. 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。

    5 索引的分类(必会)

    1.普通索引:最基本的索引,它没有任何限制。
    2.唯一索引:与普通索引类似,不同的就是索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
    3.主键索引:它是一种特殊的唯一索引,用于唯一标识数据表中的某一条记录,不允许有空值,一般primary key 来约束。
    4.联合索引(又叫复合索引):多个字段上建立的索引,能够加速复合查询条件的检索。
    5.全文索引:老版本 MySQL 自带的全文索引只能用于数据库引擎为 MyISAM 的数据表,新版本 MySQL 5.6 的 InnoDB 支持全文索引。默认 MySQL 不支持中文全文检索,可以通过扩展 MySQL,添加中文全文检索或为中文内容表提供一个对应的英文索引表的方式来支持中文。

6 索引的底层实现原理(高薪常问)

  1. 索引结构

索引是在Mysql的存储引擎(InnoDB,MyISAM)层中实现的, 而不是在服务层实现的. 所以每种存储引擎的索引都不一定完全相同, 也不是所有的存储引擎都支持所有的索引类型的, Mysql目前提供了以下4种索引:
B+Tree 索引: 最常见的索引类型, 大部分索引都支持B+树索引.
Hash 索引: 只有Memory引擎支持, 使用场景简单.

  1. Tree索引(空间索引): 空间索引是MyISAM引擎的一个特殊索引类型, 主要地理空间数据, 使用也很少.
  2. Full-text(全文索引): 全文索引也是MyISAM的一个特殊索引类型, 主要用于全文索引, InnoDB从Mysql5.6版本开始支持全文索引.
    Java面试宝典汇总202203 - 图31
  3. BTree结构

B+Tree是在BTree基础上进行演变的, 所以我们先来看看BTree, BTree又叫多路平衡搜索树, 一颗m叉BTree特性如下:

  1. 树中每个节点最多包含m个孩子.
  2. 除根节点与叶子节点外, 每个节点至少有[ceil(m/2)] 个孩子(ceil函数指向上取整).
  3. 若根节点不是叶子节点, 则至少有两个孩子.
  4. 每个非叶子节点由n个Key和n+1个指针组成, 其中 [ceil(m/2) -1 ] <= n <= m-1.

以5叉BTree为例, key的数量: 公式推导 [ceil(m/2) -1 ] <= n <= m-1.
所以 2 <= n <= 4, 中间节点分裂为父节点,两边节点分裂为子节点.
Java面试宝典汇总202203 - 图32
3.B+Tree 结构
B+Tree为BTree的变种, B+Tree与BTree的区别:
1.B+Tree的叶子节点保存所有的key信息, 依key大小顺序排列.
2.B+Tree叶子节点元素维护了一个单项链表.
所有的非叶子节点都可以看作是key的索引部分.
Java面试宝典汇总202203 - 图33
由于B+Tree只有叶子节点保存key信息, 查询任何key都要从root走的叶子. 所以B+Tree查询效率更稳定.
Mysql中的B+Tree
MySql索引数据结构对经典的B+Tree进行了优化, 在原B+Tree的基础上, 增加了一个指向相邻叶子节点的链表指针, 就形成了带有顺序指针的B+Tree, 提高区间访问的性能.
MySql中的B+Tree索引结构示意图:
Java面试宝典汇总202203 - 图34

7 如何避免索引失效(高薪常问)

  1. 范围查询, 右边的列不能使用索引, 否则右边的索引也会失效.

索引生效案例
select from tb_seller where name = “小米科技” and status = “1” and address = “北京市”;
select
from tb_seller where name = “小米科技” and status >= “1” and address = “北京市”;
索引失效案例
select * from tb_seller where name = “小米科技” and status > “1” and address = “北京市”;
address索引失效, 因为status是大于号, 范围查询.

  1. 不要在索引上使用运算, 否则索引也会失效.

比如在索引上使用切割函数, 就会使索引失效.
select * from tb_seller where substring(name, 3, 2) = “科技”;

  1. 字符串不加引号, 造成索引失效.

如果索引列是字符串类型的整数, 条件查询的时候不加引号会造成索引失效. Mysql内置的优化会有隐式转换.
索引失效案例
select * from tb_seller where name = “小米科技” and status = 1

  1. 尽量使用覆盖索引, 避免select *, 这样能提高查询效率.

如果索引列完全包含查询列, 那么查询的时候把要查的列写出来, 不使用select *
select sellerid, name, status from tb_seller where name = “小米科技” and staus = “1” and address = “西安市”;

  1. or关键字连接

用or分割开的条件, 如果or前面的列有索引, or后面的列没有索引, 那么查询的时候前后索引都会失效
如果一定要用or查询, 可以考虑下or连接的条件列都加索引, 这样就不会失效了.
索引失效案例:
select * from tb_seller where name = “小米科技” or createTiem = “2018-01-01 00:00:00”;
Java面试宝典汇总202203 - 图35

9.数据库锁(高薪常问)

1.行锁和表锁

1.主要是针对锁粒度划分的,一般分为:行锁、表锁、库锁
行锁:访问数据库的时候,锁定整个行数据,防止并发错误。
表锁:访问数据库的时候,锁定整个表数据,防止并发错误。
2.行锁 和 表锁 的区别:
表锁:开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
行锁:开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高

2.悲观锁和乐观锁

(1)悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
(2)乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

10.MySql优化(高薪常问)

Java面试宝典汇总202203 - 图36

  1. 定位执行效率慢的sql语句.(了解)
  • 命令:show status like ‘Com__‘,通过这条命令, 我们可以知道当前数据库是以查询为主还是更新为主. 如果是查询为主, 就重点查询; 如果增删改多就重点优化写入操作.
  • explain + sql语句查询sql执行过程, 通过执行计划,我们能得到哪些信息:

A:哪些步骤花费的成本比较高
B:哪些步骤产生的数据量多,数据量的多少用线条的粗细表示,很直观
C:这条sql语句是否走索引

  • show profile分析SQL,可以查看所有sql语句的执行效率(所用时间). 前提是这个命令需要被打开, 严格的说也就是打开这个命令后执行的所有sql语句, 它都能记录下执行时间, 并展示出来. 可以通过这个命令分析哪些sql语句执行效率低. 耗时长, 就更有针对性的优化这条sql.
  • 慢查询日志(常用的工具)

慢查询日志记录了所有执行时间超过参数 long_query_time的sql语句的日志, long_query_time默认为10秒(可以通过配置文件设置), 日志保存在 /var/lib/mysql/目录下, 有个slow_query.log文件,

2) 优化索引(高薪)

2.1 索引设计原则
索引的设计需要遵循一些已有的原则, 这样便于提升索引的使用效率, 更高效的使用索引.

  • 对查询频次较高, 且数据量比较大的表, 建立索引.
  • 索引字段的选择, 最佳候选列应当从where子句的条件中提取, 如果where子句中的组合比较多, 那么应当挑选最常用, 过滤效果最好的列的组合.
  • 使用唯一索引, 区分度越高, 使用索引的效率越高.
  • 索引并非越多越好, 如果该表赠,删,改操作较多, 慎重选择建立索引, 过多索引会降低表维护效率.
  • 使用短索引, 提高索引访问时的I/O效率, 因此也相应提升了Mysql查询效率.
  • 如果where后有多个条件经常被用到, 建议建立符合 索引, 复合索引需要遵循最左前缀法则, N个列组合而成的复合索引, 相当于创建了N个索引.

复合索引命名规则 index表名列名1列名2列明3
比如:create index idx_seller_name_sta_addr on tb_seller(name, status, address)
2.2 避免索引失效

  • 如果在查询的时候, 使用了复合索引, 要遵循最左前缀法则, 也就是查询从索引的最左列开始, 并且不能跳过索引中的列.
  • 尽量不要在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  • 应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。
  • 不做列运算where age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数.计算表达式等, 都会是索引失效.
  • 查询 like,如果是 ‘%aaa’ 也会造成索引失效.
  • 应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描

3) Sql语句调优(高薪)

  • 根据业务场景建立复合索引只查询业务需要的字段,如果这些字段被索引覆盖,将极大的提高查询效率.
  • 多表连接的字段上需要建立索引,这样可以极大提高表连接的效率.
  • where条件字段上需要建立索引, 但Where条件上不要使用运算函数,以免索引失效.
  • 排序字段上, 因为排序效率低, 添加索引能提高查询效率.
  • 优化insert语句: 批量列插入数据要比单个列插入数据效率高.
  • 优化order by语句: 在使用order by语句时, 不要使用select *, select 后面要查有索引的列, 如果一条sql语句中对多个列进行排序, 在业务允许情况下, 尽量同时用升序或同时用降序.
  • 优化group by语句: 在我们对某一个字段进行分组的时候, Mysql默认就进行了排序, 但是排序并不是我们业务所需的, 额外的排序会降低效率. 所以在用的时候可以禁止排序, 使用order by null禁用.

select age, count(*) from emp group by age order by null

  • 尽量避免子查询, 可以将子查询优化为join多表连接查询.

4) 合理的数据库设计(了解)
根据数据库三范式来进行表结构的设计。设计表结构时,就需要考虑如何设计才能更有效的查询, 遵循数据库三范式:

  1. 1. **第一范式**:数据表中每个字段都必须是不可拆分的最小单元,也就是确保每一列的原子性;
  2. 1. **第二范式**:满足一范式后,表中每一列必须有唯一性,都必须依赖于主键;
  3. 1. **第三范式**:满足二范式后,表中的每一列只与主键直接相关而不是间接相关(外键也是直接相关),字段没有冗余。

注意:没有最好的设计,只有最合适的设计,所以不要过分注重理论。三范式可以作为一个基本依据,不要生搬硬套。有时候可以根据场景合理地反规范化:
A:保留冗余字段。当两个或多个表在查询中经常需要连接时,可以在其中一个表上增加若干冗余的字段,以 避免表之间的连接过于频繁,一般在冗余列的数据不经常变动的情况下使用。
B:增加派生列。派生列是由表中的其它多个列的计算所得,增加派生列可以减少统计运算,在数据汇总时可以大大缩短运算时间, 前提是这个列经常被用到, 这也就是反第三范式。
C:分割表。
数据表拆分:主要就是垂直拆分和水平拆分。
水平切分:将记录散列到不同的表中,各表的结构完全相同,每次从分表中查询, 提高效率。
垂直切分:将表中大字段单独拆分到另外一张表, 形成一对一的关系。
D: 字段设计

  1. 表的字段尽可能用NOT NULL
  2. 字段长度固定的表查询会更快
  3. 把数据库的大表按时间或一些标志分成小表

    11.深入了解Mysql原理

    1)mysql的底层架构
    了解底层架构是什么样子?select name from user where id =1
    1、SQL接口:sql的入口,当线程监听到 sql发送过来之后,mysql其实根本不知道这是啥意思,他会把sql交给sql的入口,sql接口
    2、SQL解析器:相当于知道这条sql到底想干嘛这条sql 想从user表中 去拿id =1 的 name的这个字段 ,但是问题在于怎么样去把这个事给做好呢?
    3、SQL优化器:选择一个最优解两种做法:
    1. 先提取出来所有user表的数据,然后再去过滤出来id=1 的这条记录,然后再去拿name字段的值 2. 先提取出来所有user表的数据,去拿到所有的name字段的值,然后再去找这么多字段里边id = 1的这个字段通过咱们sql优化器,他就选取一个最优的获得数据方式去拿到这个数据
    4、你要知道咱们这个数据库其实说白了,他还是要在去操作磁盘数据,或者是内存数据,那么现在就有问题,你已经知道做什么事情,怎么做这件事情,但是呢,你只是站在sql的角度上明白,比如说这个数据到底在哪儿呢?这些问题其实有待解决,我们就可以使用 存储引擎
    Java面试宝典汇总202203 - 图37
    2)mysql的undolog和redolog还有binglog分别是什么?
    只要我操作了数据,我们mysql就会将数据从磁盘上边读取到innodb 引擎中buffer pool,以后如果要读取相同数据,此时其实只需要到Buffer pool里边去读取undolog redolog binglog 日志update user set name =b where id = 1比如:当你这把id =1 name =a 这条记录加载到内存,你把这条记录改变了b,假设你现在想要事务的回滚—>b 回滚成a的样子,mysql其实当把这个数据 id =1 name =a 加载到内存来之后 ,现在就会把这个原来的值记录到undolog日志中但是假如说:你现在呢事务已经提交了,但是此时宕机了,那么此时buffer pool 里边数据不就丢了吗?如果丢了的话,那你怎么能说你的事务没有问题呢?所以在完成前三步之后,就会基于 redolog的落盘策略去保证 就是说事务提交之后,然后当前修改过的数据必须要落盘binglog日志文件,他其实也是用于恢复数据时候但是他和redolog不一样的点: 三个点
    1.binglog日志他是基于mysql级别的,但是redolog其实他是基于innodb引擎,也就是说,如果不是innodb引擎那么他其实就没有redolog日志
    2.binglog 他可以用户删除库删除表之后的数据恢复,还可以用于 主从之间的 数据同步
    3.binglog 他是没有大小限制的,这个redolog日志大小是有限的, 如果超过限定的量,他这个时候,就会出现覆盖,而我们binglog日志其实他是一个文件接着一个文件的写,此时这个数据其实就不会覆盖咱们这里边1,2,3,4 属于非事务操作咱们 5,6 其实上他们已经属于事务操作了
    Java面试宝典汇总202203 - 图38
    8 sql优化(重要)
    一、第一定位慢sql
    两种可能
    1、执行完了的
    通过慢查询日志 :在慢查询日志中可以规定超过多长时间可以作为慢sql利用mysqldumpslow来分析慢查询日志
    2、没有执行完的
    并show profile查看哪些sql执行时间比较慢,并kill进行分析
    二、通过以上两种方式定位出来后
    利用explain先查看索引是否失效possible_keys :判断你可能使用到了索引key:具体用到了哪个索引
    看possible_keys 是否有值
    1、没有值:大概率是没有创建索引,为其创建索引
    2、possible_keys:有值 但是key没有值索引失效了
    失效的原因:(随便记几个常见的)
    0.没有使用到最左匹配原则(重点)
    1.like:%号在前索引会失效
    2.用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。 3. in 走索引, not in 索引失效
    4. varchar字段不带’’ 也会失效
    5.在索引列上进行运算操作
    6.范围查询右边的列,不能使用索引

三、如果有创建索引,但性能依然低
1、尽量对于要查询的字段使用复合索引,并且利用覆盖索引,减少回表的查询
2、然后是复合索引,mysql5.6以后对复合索引还做了进一步的优化,索引下推,可以通过索引包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数(索引下推,需要学习一下)原来他查询辅助列索引时,会忽略掉一列,然后,再拿到结果后,再去向主键列索引去发起多次回表,筛选另一列条件,而现在,一次就从辅助列中拿到数据了
3、考虑索引的过滤性,对于一个大表,不止要创建索引,还要考虑索引过滤性,过滤性好,执行速度才会快,如果过滤性因为业务确实不高,但业务查询频繁,也要作为联合索引的组成部分,因为如果没有索引,此时你的sql就必须将这些查询条件放在最后,当利用联合索引查出其他数据后,还需要将这些数据再次加载到内存去过滤掉 这些非索引数据
4、调大buffer pool
5、尽量最大限度的使用到最左匹配原则,让所有字段都匹配上,如果有用户发起的sql不满足sql匹配原则,
比如:sex字段,用户在没有查询的时候,我们可以在sql 处凭借 sex in( 0,1 ) 使不满足最左匹配原则的数据,匹配上完整索引
6、对于辅助索引的查询,会进行回表查询,并且磁盘是进行随机io读写,这时候可以开启MRR,mrr可以在回表的时候,mrr可以将随机io读变成了顺序io读,提高效率。 MRR,全称「Multi-Range Read Optimization」。 简单说:MRR 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。

四、框架

1. Mybatis框架

1.1谈一谈你对Mybatis框架的理解(了解)

MyBatis 是一款优秀的持久层框架,一个半 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 几乎避免了所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

1.2在mybatis中,${} 和 #{} 的区别是什么?(必会)

{} 是占位符,预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。

1.3 MyBatis编程步骤是什么样的?(了解)

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

1.4在mybatis中,resultType和ResultMap的区别是什么?(必会)

如果数据库结果集中的列名和要封装实体的属性名完全一致的话用 resultType 属性
如果数据库结果集中的列名和要封装实体的属性名有不一致的情况用 resultMap 属性,通过resultMap手动建立对象关系映射,resultMap要配置一下表和类的一一对应关系,所以说就算你的字段名和你的实体类的属性名不一样也没关系,都会给你映射出来。

1.5在Mybatis中你知道的动态SQL的标签有哪些?作用分别是什么?(必会)

  1. if是为了判断传入的值是否符合某种规则,比如是否不为空.
    2. where标签可以用来做动态拼接查询条件,当和if标签配合的时候,不用显示的声明类型where 1 = 1这种无用的条件
    3. foreach标签可以把传入的集合对象进行遍历,然后把每一项的内容作为参数传到sql语句中.
    4. include可以把大量的重复代码整理起来,当使用的时候直接include即可,减少重复代码的编写;
    5. 适用于更新中,当匹配某个条件后,才会对该字段进行跟新操作

    1.6谈一下你对mybatis缓存机制的理解?(了解)

    Mybatis有两级缓存,一级缓存是SqlSession级别的,默认开启,无法关闭;二级缓存是Mapper级别的,二级缓存默认是没有开启的,但是可以手动开启。
    1. 一级缓存:基础PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,Session中的所有Cache(缓存)就将清空
    2. 二级缓存其存储作用域为Mapper(Namespace),使用二级缓存属性类需要实现Serializable序列化接口
    3. 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了C(增加)/U(更新)/D(删除)操作后,默认该作用域下所有select中的缓存将被clear.
    需要在setting全局参数中配置开启二级缓存,如下conf.xml配置:
    Java面试宝典汇总202203 - 图39
    当我们的配置文件配置了cacheEnabled=true时,就会开启二级缓存,二级缓存是mapper级别的,如果你配置了二级缓存,那么查询数据的顺序应该为:二级缓存→一级缓存→数据库。

    2. Spring框架

    2.1 Spring的两大核心是什么?谈一谈你对IOC的理解? 谈一谈你对DI的理解? 谈一谈你对AOP的理解?(必会)

  2. Spring的两大核心是:IOC(控制反转)和AOP(面向切面编程) DI(依赖注入)
    2. IOC的意思是控制反转,是指创建对象的控制权的转移,以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到Spring容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。最直观的表达就是,IOC让对象的创建不用去new了,可以由spring根据我们提供的配置文件自动生产,我们需要对象的时候,直接从Spring容器中获取即可.
    Spring的配置文件中配置了类的字节码位置及信息, 容器生成的时候加载配置文件识别字节码信息, 通过反射创建类的对象.
    Spring的IOC有三种注入方式 :构造器注入, setter方法注入, 根据注解注入。
    3. DI的意思是依赖注入,和控制反转是同一个概念的不同角度的描述,即应用程序在运行时依赖Io c容器来动态注入对象需要的外部资源。
    4. AOP,一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect). SpringAOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
    5. Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理:
    (1)JDK 动态代理只提供接口代理,不支持类代理,核心 InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起,Proxy 利用 InvocationHandler 动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
    (2) 如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。

    2.2 Spring的生命周期?(高薪常问)

  3. 实例化一个Bean,也就是我们通常说的new
    2. 按照Spring上下文对实例化的Bean进行配置,也就是IOC注入
    3. 如果这个Bean实现dao了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID
    4. 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean)
    5. 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,该方式同样可以实现步骤4,但比4更好,以为ApplicationContext是BeanFactory的子接口,有更多的实现方法
    6. 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用After方法,也可用于内存或缓存技术
    7. 如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法
    8. 如果这个Bean关联了BeanPostProcessor接口,将会调用postAfterInitialization(Object obj, String s)方法
    注意:以上工作完成以后就可以用这个Bean了,那这个Bean是一个single的,所以一般情况下我们调用同一个ID的Bean会是在内容地址相同的实例
    9. 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy方法
    10. 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法

    2.3 Spring支持bean的作用域有几种? 每种作用域是什么样的?(必会)

    Spring支持如下5种作用域:
    (1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。
    (2)prototype:为每一个bean请求创建一个实例。
    (3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
    (4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
    (5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。

    2.4 BeanFactory和ApplicationContext有什么区别(了解)

    BeanFactory:
    Spring最顶层的接口,实现了Spring容器的最基础的一些功能, 调用起来比较麻烦, 一般面向Spring自身使用
    BeanFactory在启动的时候不会去实例化Bean,从容器中拿Bean的时候才会去实例化
    ApplicationContext:
    是BeanFactory的子接口,扩展了其功能, 一般面向程序员身使用
    ApplicationContext在启动的时候就把所有的Bean全部实例化了

    2.5 Spring框架中都用到了哪些设计模式?(必会)

  4. 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例
    2. 单例模式:Bean默认为单例模式
    3. 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
    4. 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate
    5. 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被自动更新,如Spring中listener的实现—ApplicationListener

    2.6 Spring事务的实现方式和实现原理(必会)

    Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

spring事务实现主要有两种方法
1、编程式,beginTransaction()、commit()、rollback()等事务管理相关的方法
2、声明式,利用注解Transactional 或者aop配置

2.6 你知道的Spring的通知类型有哪些,分别在什么时候执行?(了解)

Spring的通知类型有四种,分别为:
前置通知[]before]:在切点运行之前执行
后置通知[after-returning]:在切点正常结束之后执行
异常通知[after-throwing]:在切点发生异常的时候执行
最终通知[after]:在切点的最终执行
Spring还有一种特殊的通知,叫做环绕通知
环绕通知运行程序员以编码的方式自己定义通知的位置, 用于解决其他通知时序问题

2.7 Spring的对象默认是单例的还是多例的? 单例bean存不存在线程安全问题呢?(必会)

  1. 在spring中的对象默认是单例的,但是也可以配置为多例。
    2. 单例bean对象对应的类存在可变的成员变量并且其中存在改变这个变量的线程时,多线程操作该bean对象时会出现线程安全问题。
    原因是:多线程操作如果改变成员变量,其他线程无法访问该bean对象,造成数据混乱。
    解决办法:在bean对象中避免定义可变成员变量;
    在bean对象中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中。

    2.8 @Resource和@Autowired依赖注入的区别是什么? @Qualifier使用场景是什么?(了解)

    @Resource
    只能放在属性上,表示先按照属性名匹配IOC容器中对象id给属性注入值若没有成功,会继续根据当前属性的类型匹配IOC容器中同类型对象来注入值
    若指定了name属性@Resource(name = “对象id”),则只能按照对象id注入值。
    @Autowird
    放在属性上:表示先按照类型给属性注入值如果IOC容器中存在多个与属性同类型的对象,则会按照属性名注入值
    也可以配合@Qualifier(“IOC容器中对象id”)注解直接按照名称注入值。
    放在方法上:表示自动执行当前方法,如果方法有参数,会自动从IOC容器中寻找同类型的对象给参数传值
    也可以在参数上添加@Qualifier(“IOC容器中对象id”)注解按照名称寻找对象给参数传值。
    @Qualifier使用场景:
    @Qualifier(“IOC容器中对象id”)可以配合@Autowird一起使用, 表示根据指定的id在Spring容器中匹配对象

    2.8 Spring的常用注解(必会)

  2. @Component(任何层) @Controller @Service @Repository(dao): 用于实例化对象
    2. @Scope : 设置Spring对象的作用域
    3. @PostConstruct @PreDestroy : 用于设置Spring创建对象在对象创建之后和销毁之前要执行的方法
    4. @Value: 简单属性的依赖注入
    5. @Autowired: 对象属性的依赖注入
    6. @Qualifier: 要和@Autowired联合使用,代表在按照类型匹配的基础上,再按照名称匹配。
    7. @Resource 按照属性名称依赖注入
    8. @ComponentScan: 组件扫描
    9. @Bean: 表在方法上,用于将方法的返回值对象放入容器
    10. @PropertySource: 用于引入其它的properties配置文件
    11. @Import: 在一个配置类中导入其它配置类的内容
    12. @Configuration: 被此注解标注的类,会被Spring认为是配置类。Spring在启动的时候会自动扫描并加载所有配置类,然后将配置 类中bean放入容器
    13. @Transactional 此注解可以标在类上,也可以表在方法上,表示当前类中的方法具有事务管理功能。

    2.9 Spring的事务传播行为(高薪常问)

    spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
    备注(方便记忆): propagation传播
    require必须的/suppor支持/mandatory 强制托管/requires-new 需要新建/ not -supported不支持/never从不/nested嵌套的
    ① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
    ② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
    ③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
    ④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
    ⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    ⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
    ⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

    2.10 Spring中的隔离级别 (高薪常问)

    ISOLATION隔离的意思
    ① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
    ② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据。
    ③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新。解决脏读问题
    ④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新。行锁
    ⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。表锁

2.11 spring中的bean是否是线程安全的?

不是,Spring框架中的单例bean不是线程安全的 , spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。

实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全 的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了, 最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。

2.12 Spring中什么时候 @Transactional会失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是不会失效的。

同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效

2.13 什么是循环引用, 如何解决循环引用?

循环引用的bean之间会构成一个环,如下图所示,A、B、C之间构成了一个环形。
Java面试宝典汇总202203 - 图40
要想打破这个环,那么这个环中至少需要有一个bean可以在自身的依赖还没有得到满足前,就能够被创建出来(最起码要被实例化出来,可以先不注入其需要的依赖)

这种bean只能是通过属性注入依赖的类,因为它们可以先使用默认构造器创建出实例,然后再通过setter方法注入依赖。而通过构造器注入依赖的类,在它的依赖没有被满足前,无法被实例化

3.SpringMVC框架

3.1谈一下你对SpringMVC框架的理解(了解)

SpringMVC是一个是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
在我看来,SpringMVC就是将我们原来开发在servlet中的代码拆分了,一部分由SpringMVC完成,一部分由我们自己完成

3.2 SpringMVC主要组件(必会)

前端控制器 DispatcherServlet:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。
处理器映射器 HandlerMapping:根据请求的URL来查找Handler
处理器适配器 HandlerAdapter:负责执行Handler
处理器 Handler:处理业务逻辑的Java类
视图解析器 ViewResolver:进行视图的解析,根据视图逻辑名将ModelAndView解析成真正的视图(view)
视图View:View是一个接口, 它的实现类支持不同的视图类型,如jsp,freemarker,pdf等等

3.3谈一下SpringMVC的执行流程以及各个组件的作用(必会)

Java面试宝典汇总202203 - 图41
1. 用户发送请求到前端控制器(DispatcherServlet)
2. 前端控制器(DispatcherServlet)收到请求调用处理器映射器(HandlerMapping),去查找处理器(Handler)
3. 处理器映射器(HandlerMapping)找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4. 前端控制器(DispatcherServlet)调用处理器映射器(HandlerMapping)
5. 处理器适配器(HandlerAdapter)去调用自定义的处理器类(Controller,也叫后端控制器)。
6.自定义的处理器类(Controller,也叫后端控制器)将得到的参数进行处理并返回结果给处理器映射器(HandlerMapping)
7. 处理器适配器(HandlerAdapter)将得到的结果返回给前端控制器(DispatcherServlet)
8. DispatcherServlet(前端控制器)将ModelAndView传给视图解析器(ViewReslover)
9. 视图解析器(ViewReslover)将得到的参数从逻辑视图转换为物理视图并返回给前端控制器(DispatcherServlet)
10. 前端控制器(DispatcherServlet)调用物理视图进行渲染并返回
11. 前端控制器(DispatcherServlet)将渲染后的结果返回

3.4说一下SpringMVC支持的转发和重定向的写法(必会)

1)转发:
forward方式:在返回值前面加”forward:”,比如”””forward:user.do?name=method4”

2) 重定向:
redirect方式:在返回值前面加redirect:, 比如”redirect:http://www.baidu.com

3.5 SpringMVC的常用注解(必会)

1.@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
2.@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
3.@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
4.@PathVariable 用户从url路径上获取指定参数,标注在参数前 @PathVariabel(“要获取的参数名”)。
5.@RequestParam: 标注在方法参数之前,用于对传入的参数做一些限制,支持三个属性:
- value:默认属性,用于指定前端传入的参数名称
- required:用于指定此参数是否必传
- defaultValue:当参数为非必传参数且前端没有传入参数时,指定一个默认值

  1. @ControllerAdvice 标注在一个类上,表示该类是一个全局异常处理的类。
    7. @ExceptionHandler(Exception.class) 标注在异常处理类中的方法上,表示该方法可以处理的异常类型。

    3.6谈一下SpringMVC统一异常处理的思想和实现方式(必会)

    使用SpringMVC之后,代码的调用者是SpringMVC框架,也就是说最终的异常会抛到框架中,然后由框架指定异常处理类进行统一处理
    方式一: 创建一个自定义异常处理器(实现HandlerExceptionResolver接口),并实现里面的异常处理方法,然后将这个类交给Spring容器管理
    方式二: 在类上加注解(@ControllerAdvice)表明这是一个全局异常处理类
    在方法上加注解(@ExceptionHandler),在ExceptionHandler中有一个value属性,可以指定可以处理的异常类型

    3.7在SpringMVC中, 如果想通过转发将数据传递到前台,有几种写法?(必会)

    方式一:直接使用request域进行数据的传递
    request.setAttirbuate(“name”, value);
    方式二:使用Model进行传值,底层会将数据放入request域进行数据的传递
    model.addAttribuate(“name”, value);
    方式三:使用ModelMap进行传值,底层会将数据放入request域进行数据的传递
    modelmap.put(“name”,value);
    方式四:借用ModelAndView在其中设置数据和视图
    mv.addObject(“name”,value);
    mv.setView(“success”);
    return mv;

    3.8在SpringMVC中拦截器的使用步骤是什么样的?(必会)

    1 定义拦截器类:
    SpringMVC为我们提供了拦截器规范的接口,创建一个类实现HandlerInterceptor,重写接口中的抽象方法;
    preHandle方法:在调用处理器之前调用该方法,如果该方法返回true则请求继续向下进行,否则请求不会继续向下进行,处理器也不会调用
    postHandle方法:在调用完处理器后调用该方法
    afterCompletion方法:在前端控制器渲染页面完成之后调用此方法
    2 注册拦截器:
    在SpringMVC核心配置文件中注册自定义的拦截器






    3.9 在SpringMVC中文件上传的使用步骤是什么样的? 前台三要素是什么?(必会)

    文件上传步骤:
    1.加入文件上传需要的commons-fileupload包
    2.配置文件上传解析器,springmvc的配置文件的文件上传解析器的id属性必须为multipartResolver
    3.后端对应的接收文件的方法参数类型必须为MultipartFile,参数名称必须与前端的name属性保持一致
    文件上传前端三要素:
    1.form表单的提交方式必须为post
    2.enctype属性需要修改为:multipart/form-data
    3.必须有一个type属性为file的input标签,其中需要有一个name属性;如果需要上传多个文件需要添加multiple属性

    3.10 SpringMVC 中如何解决GET|POST请求中文乱码问题?(了解)

    (1)解决post请求乱码问题:在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8;

    CharacterEncodingFilter
    org.springframework.web.filter.CharacterEncodingFilter

    encoding
    utf-8



    CharacterEncodingFilter
    /*

  2. get请求中文参数出现乱码解决方法有两个:

①修改tomcat配置文件添加编码与工程编码一致,如下:

②另外一种方法对参数进行重新编码:
String userName= new String(request.getParamter(“userName”).getBytes(“ISO8859-1”),”utf-8”)
ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。

3.11 Springmvc中的拦截器和过滤器的区别是什么?

1.过滤器:
  依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据.
比如:在过滤器中修改字符编码;在过滤器中修改 HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等

  关于过滤器的一些用法可以参考下列文章:
    继承HttpServletRequestWrapper以实现在Filter中修改HttpServletRequest的参数:https://www.zifangsky.cn/677.html

  在SpringMVC中使用过滤器(Filter)过滤容易引发XSS的危险字符:https://www.zifangsky.cn/683.html

2.拦截器:
  依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用.
因此可以使用spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

关于拦截器的一些用法可以参考下列文章:
    SpringMVC中使用拦截器(interceptor)拦截CSRF攻击(修):https://www.zifangsky.cn/671.html

    SpringMVC中使用Interceptor+cookie实现在一定天数之内自动登录:https://www.zifangsky.cn/700.html

3.执行顺序
  过滤器的运行是依赖于servlet容器的,跟springmvc等框架并没有关系。并且多个过滤器的执行顺序跟web.xml文件中定义的先后关系有关。
  拦截器的执行顺序跟在SpringMVC的配置文件中定义的先后顺序有关。

9 Dubbo

4.1 什么是dubbo(必会)

工作在soa面向服务分布式框架中的服务管理中间件。Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。关于注册中心、协议支持、服务监控等内容。
Dubbo使用的是缺省协议, 采用长连接和nio异步通信, 适合小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
反之, dubbo缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

4.2 Dubbo的实现原理(必会)

Java面试宝典汇总202203 - 图42

4.3 节点角色说明(必会)

Provider: 暴露服务的服务提供方。
Consumer: 调用远程服务的服务消费方。
Registry: 服务注册与发现的注册中心。
Monitor: 统计服务的调用次调和调用时间的监控中心。
Container: 服务运行容器。

4.4 调用关系说明(必会)

  1. 服务容器负责启动,加载,运行服务提供者。
    2. 服务提供者在启动时,向注册中心注册自己提供的服务。
    3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
    4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
    5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
    6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

    4.5 在实际开发的场景中应该如何选择RPC框架(了解)

    SpringCloud : Spring全家桶,用起来很舒服,只有你想不到,没有它做不到。可惜因为发布的比较晚,国内还没出现比较成功的案例,大部分都是试水,不过毕竟有Spring作背景,还是比较看好。
    Dubbox:相对于Dubbo支持了REST,估计是很多公司选择Dubbox的一个重要原因之一,但如果使用Dubbo的RPC调用方式,服务间仍然会存在API强依赖,各有利弊,懂的取舍吧。
    Thrift: 如果你比较高冷,完全可以基于Thrift自己搞一套抽象的自定义框架吧。
    Hessian:如果是初创公司或系统数量还没有超过5个,推荐选择这个,毕竟在开发速度.运维成本.上手难度等都是比较轻量.简单的,即使在以后迁移至SOA,也是无缝迁移。
    rpcx/gRPC:在服务没有出现严重性能的问题下,或技术栈没有变更的情况下,可能一直不会引入,即使引入也只是小部分模块优化使用。

    4.6 Dubbo的核心架构

    Java面试宝典汇总202203 - 图43
    dubbo的核心原理(在讲完dubbo的大致内容) 实实在在能够帮你找到工作的东西
    首先有一层接口,接口完事之后config,config去读取配置文件,拿到注册列表信息,需要针对当前这个接口生成一个代理问题,完事之后,其实现在dubbo就已经开始工作了,dubbo这个时候会从消费者方拉取下来的服务列表,然后把服务列表交给cluster这一层(他现在相当于就知道我能够调用哪些服务器了)但是,他得去进行负载均衡,这个负载均衡是依赖于cluster这一层 bloadBalance来选择
    但是现在咱们如果想要通信还得选择一个对应的协议来完成 —>就是dubbo的通信协议
    然后这个 其实我们的请求就按照我们选择的协议进行组织数据了
    你得先封装成一个对象,这个对象request对象,现在就可以传输了,但是由于涉及到网络传输,所以这个时候需要序列化,而且dubbo的底层采用的netty/mina这两个框架在进行传输数据的,所以说,你要先序列化,然后再把序列化好的数据交个 netty/mina然后再由他们两兄弟去把这个请求发出去。
    消费者方法就依赖于传输层 当然也是netty/mina拿到了序列化过后的数据,那么他就需要层层去解析。首先反序列化,然后通过exchange拿到他封装request对象,然后再去通过protocal拿到他的协议,然后走到现在终于知道消息者方到底想要干嘛了,然后这个时候其实可以去帮他实现了他想完成的事情,但是作为程序员而言,你是不是只有一个接口,明显dubbo过来的服务他没法调用,所以dubbo又对生产者方的接口生成了一个代理对象,然后再通过这个代理类对象去完成我们的调用。

    10 Zookeeper

    1. Zookeeper是什么(了解)

    Zookeeper 是一个分布式协调服务的开源框架, 主要用来解决分布式集群中应用系统的一致性问题, 例如怎样避免同时操作同一数据造成脏读的问题.
    ZooKeeper 本质上是一个分布式的小文件存储系统. 提供基于类似于文件系统的目录树方式的数据存储, 并且可以对树中的节点进行有效管理. 从而用来维护和监控你存储的数据的状态变化. 通过监控这些数据状态的变化,从而可以达到基于数据的集群管理.
    在大数据生态系统里,很多组件的命名都是某种动物,比如hadoop就是大象, hive就是蜜蜂, 而Zookeeper就是动物管理员.

    2. Zookeeper的数据模型(必会)

  • ZK本质上是一个分布式的小文件存储系统.
  • ZK表现为一个分层的文件系统目录树结构, 既能存储数据, 而且还能像目录一样有子节点. 每个节点可以存最多1M左右的数据.
  • 每个节点称做一个Znode, 每个Znode都可以通过其路径唯一标识.
  • 而且客户端还能给节点添加watch, 也就是监听器, 可以监听节点的变化, 这个功能常在实际开发中作为监听服务器集群机器上下线操作.

2.1 节点结构
Java面试宝典汇总202203 - 图44
图中的每个节点称为一个 Znode。 每个 Znode 由 3 部分组成:
① stat:此为状态信息, 描述该 Znode 的版本, 权限等信息
② data:与该 Znode 关联的数据
③ children:该 Znode 下的子节点
2.2 节点类型
Znode 有2大类4小类, 两大类分别为永久节点和临时节点.

  • 永久节点(Persistent): 客户端和服务器端断开连接后,创建的节点不会消失, 只有在客户端执行删除操作的时候, 他们才能被删除.
  • 临时节点(Ephemeral): 客户端和服务器端断开连接后,创建的节点会被删除.

Znode 还有一个序列化的特性, 这个序列号对于此节点的父节点来说是唯一的, 这样便会记录每个子节点创建的先后顺序. 它的格式为“%10d”(10 位数字, 没有数值的数位用 0 补充, 例如“0000000001”),因此节点可以分为4小类:

  1. - 永久节点(Persistent)
  2. - 永久_序列化节点(Persistent_Sequential)
  3. - 临时节点(Ephemeral)
  4. - 临时_序列化节点(Ephemeral_Sequential)

3. Zookeeper的watch监听机制(高薪常问)

  • 在ZooKeeper中还支持一种watch(监听)机制, 它允许对ZooKeeper注册监听, 当监听的对象发生指定的事件的时候, ZooKeeper就会返回一个通知.
  • Watcher 分为以下三个过程:客户端向ZK服务端注册 Watcher、服务端事件发生触发 Watcher、客户端回调 Watcher 得到触发事件情况.
    触发事件种类很多,如:节点创建,节点删除,节点改变,子节点改变等。
  • Watcher是一次性的. 一旦被触发将会失效. 如果需要反复进行监听就需要反复进行注册.

    3.1 监听器原理

    Java面试宝典汇总202203 - 图45

  • 首先要有一个main()线程

  • 在main线程中创建Zookeeper客户端, 这时就会创建两个线程, 一个复制网络连接通信(connect), 一个负责监听(listener).
  • 通过connect线程将注册的监听事件发送给zk, 常见的监听有

监听节点数据的变化get path [watch]
监听节点状态的变化 stat path [watch]
监听子节点增减的变化 ls path [watch]

  • 将注册的监听事件添加到zk的注册的监听器列表中
  • 监听到有数据或路径变化, 就会将这个消息发送给listener线程.
  • listener线程内部调用了process()方法.此方法是程序员自定义的方法, 里面可以写明监听到事件后做如何的通知操作.

    3.2 监听器实际应用

    监听器+ZK临时节点能够很好的监听服务器的上线和下线.
    Java面试宝典汇总202203 - 图46

  • 第一步: 先想zk集群注册一个监听器, 监听某一个节点路径

  • 第二步: 主要服务器启动, 就去zk上指定路径下创建一个临时节点.
  • 第三步: 监听器监听servers下面的子节点有没有变化, 一旦有变化, 不管新增(机器上线)还是减少(机器下线)都会马上给对应的人发送通知.

    4. Zookeeper的应用场景(高薪常问)

    ZK提供的服务包括: 统一命名服务, 统一配置管理, 统一集群管理, 集群选主, 服务动态上下线, 分布式锁等.

    4.1 统一命名服务

    统一命名服务使用的是ZK的node节点全局唯一的这个特点.
    在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。例如:IP不容易记住,而域名容易记住。创建一个节点后, 节点的路径就是全局唯一的, 可以作为全局名称使用.
    Java面试宝典汇总202203 - 图47

    4.2 统一配置管理

    统一配置管理, 使用的是Zookeeper的watch机制

  • 需求: 分布式环境下, 要求所有节点的配置信息是一致的, 比如Kafka集群. 对配置文件修改后, 希望能够快速同步到各个节点上.

  • 方案: 可以把所有的配置都放在一个配置中心, 然后各个服务分别去监听配置中心, 一旦发现里面的内容发生变化, 立即获取变化的内容, 然后更新本地配置即可.
  • 实现: 配置管理可交由Zookeeper实现
  • 可将配置信息写入Zookeeper上的一个Znode.
  • 各个客户端服务器监听这个Znode.
  • 一旦Znode中的数据被修改, Zookeeper将通知各个客户端服务器.

Java面试宝典汇总202203 - 图48

4.3 统一集群管理

统一集群管理使用的是Zookeeper的watch机制

  • 需求: 分布式环境中, 实时掌握每个节点的状态是必要的, 可以根据节点实时状态做出一些调整.
  • 方案: Zookeeper可以实现实时监控节点状态变化
  • 可将节点信息写入Zookeeper上的一个Znode.
  • 监听这个Znode可获取它的实时状态变化.

Java面试宝典汇总202203 - 图49

4.4 集群选主

集群选主使用的是zookeeper的临时节点.

  • 需求: 在集群中, 很多情况下是要区分主从节点的, 一般情况下主节点负责数据写入, 从节点负责数据读取, 那么问题来了, 怎么确定哪一个节点是主节点的, 当一个主节点宕机的时候, 其他从节点怎么再来选出一个主节点呢?
  • 实现:使用Zookeeper的临时节点可以轻松实现这一需求, 我们把上面描述的这个过程称为集群选主的过程, 首先所有的节点都认为是从节点, 都有机会称为主节点, 然后开始选主, 步骤比较简单
    1. - 所有参与选主的主机都去Zookeeper上创建**同一个临时节点**,那么最终一定只有一个客户端请求能够 创建成功。
    2. - 成功创建节点的客户端所在的机器就成为了Master,其他没有成功创建该节点的客户端,成为从节点
    3. - 所有的从节点都会在主节点上注册一个子节点变更的Watcher,用于监控当前主节点是否存活,一旦 发现当前的主节点挂了,那么其他客户端将会重新进行选主。

Java面试宝典汇总202203 - 图50

4.5 分布式锁

分布式锁使用的是Zookeeper的临时有序节点

  • 需求: 在分布式系统中, 很容出现多台主机操作同一资源的情况, 比如两台主机同时往一个文件中追加写入文本, 如果不去做任何的控制, 很有可能出现一个写入操作被另一个写入操作覆盖掉的状况.
  • 方案: 此时我们可以来一把锁, 哪个主机获取到了这把锁, 就执行写入, 另一台主机等待; 直到写入操作执行完毕,另一台主机再去获得锁,然后写入.
    这把锁就称为分布式锁, 也就是说:分布式锁是控制分布式系统之间同步访问共享资源的一种方式
  • 实现: 使用Zookeeper的临时有序节点可以轻松实现这一需求.
  1. 所有需要执行操作的主机都去Zookeeper上创建一个临时有序节点.
  2. 然后获取到Zookeeper上创建出来的这些节点进行一个从小到大的排序.
  3. 判断自己创建的节点是不是最小的, 如果是, 自己就获取到了锁; 如果不是, 则对最小的节点注册一个监听.
  4. 如果自己获取到了锁, 就去执行相应的操作. 当执行完毕之后, 连接断开, 节点消失, 锁就被释放了.
  5. 如果自己没有获取到锁, 就等待, 一直监听节点是否消失,锁被释放后, 再重新执行抢夺锁的操作.

Java面试宝典汇总202203 - 图51

5. Zookeeper集群高级

5.1 ZK集群介绍

Zookeeper在一个系统中一般会充当一个很重要的角色, 所以一定要保证它的高可用, 这就需要部署Zookeeper的集群. Zookeeper 有三种运行模式: 单机模式, 集群模式和伪集群模式.

  • 单机模式: 使用一台主机不是一个Zookeeper来对外提供服务, 有单点故障问题, 仅适合于开发、测试环境.
  • 集群模式: 使用多台服务器, 每台上部署一个Zookeeper一起对外提供服务, 适合于生产环境.
  • 伪集群模式: 在服务器不够多的情况下, 也可以考虑在一台服务器上部署多个Zookeeper来对外提供服务.

    5.2 数据一致性处理

    ZK是一个分布式协调开源框架, 用于分布式系统中保证数据一致性问题, 那么ZK是如何保证数据一致性的呢?

    5.2.1 集群角色

    Leader: 负责投票的发起和决议, 更新系统状态, 是事务请求(写请求) 的唯一处理者, 一个ZooKeeper同一时刻只会有一个Leader.
    对于create创建/setData修改/delete删除等有写操作的请求, 则需要统一转发给leader 处理, leader 需要决定编号和执行操作, 这个过程称为一个事务.
    Follower: 接收客户端请求, 参与选主投票. 处理客户端非事务(读操作)请求,转发事务请求(写请求)给 Leader;
    Observer: 针对访问量比较大的 zookeeper 集群, 为了增加并发的读请求. 还可新增观察者角色.
    作用: 可以接受客户端请求, 把请求转发给leader, 不参与投票, 只同步leader的状态.
    Java面试宝典汇总202203 - 图52

    5.2.2 Zookeeper的特性

    1)Zookeeper: 一个领导者(Leader), 多个跟随者(Follower)组成的集群.
    2)集群中只要有半数以上节点存活, Zookeeper集群就能正常服务.
    3)全局数据一致: 每个Server保存一份相同的数据副本, Client无论连接到哪个Server, 数据都是一致的.
    4)更新请求顺序性: 从同一个客户端发起的事务请求,最终会严格按照顺序被应用到zookeeper中.
    5)数据更新原子性: 一次数据更新要么成功, 要么失败。
    6)实时性,在一定时间范围内,Client能读到最新数据。

    5.2.3 ZAB协议
  • Zookeeper采用ZAB(Zookeeper Atomic Broadcast)协议来保证 分布式数据一致性 。

  • ZAB并不是一种通用的分布式一致性算法,而是一种专为Zookeeper设计的崩溃可恢复的原子消息广播算法。
  • ZAB协议包括两种基本模式: 崩溃恢复 模式和 消息广播 模式:
  • 消息广播模式主要用来进行事务请求的处理
  • 崩溃恢复模式主要用来在集群启动过程,或者Leader服务器崩溃退出后进行新的Leader服务器的选举以及数据同步.

    5.2.4 ZK集群写数据流程

    Java面试宝典汇总202203 - 图53

  • Client向Zookeeper的Server1上写数据, 发送一个写请求.

  • 如果Server1不是Leader, 那么Server1会把接受的请求进一步转发给Leader, 因为每个Zookeeper的Server里面有一个是Leader. 这个Leader会将写请求广播给各个Server, 比如Server1和Server2, 各个Server会将该写请求加入待写队列, 并向Leader发送成功信息(ack反馈机制).
  • 当Leader收到半数以上Server的成功信息, 说明该写操作可以执行. Leader会向各个Server发送事务提交信息, 各个Server收到信息后会落实队列里面的写请求, 此时写成功.
  • Server1会进一步通知Client数据写成功了, 这是就认为整个写操纵成功.

    5.3 ZK集群选举机制
  • Zookeeper服务器有四个状态:
    looking: 寻找leader状态, 当服务器处于该状态时, 它会认为当前集群中没有leader, 因此需要进入leader选举状态.
    leading: 领导者状态, 表明当前服务器角色是leader.
    following: 跟随者状态, 表明当前服务器角色是follower.
    observing:观察者状态, 表明当前服务器角色是observer。

  • 半数机制: 集群中半数以上机器存活, 集群可用, 所以Zookeeper适合安装奇数台服务器. 集群启动时, 如果当前机器票数超过了总票数一半则为Leader, Leader产生后, 投过票的机器就不能再投票了.
  • Zookeeper虽然在配置文件中没有指定主从节点. 但是, Zookeeper工作时, 是有一个节点Leader, 其他则为Follower, Leader是通过内部的选举机制临时产生的.
    配置文件中会指定每台ZK的myid, 而且不能重复, 通常用1,2,3…区分每台ZK的myid.
    5.3.1 集群启动器的选举机制
    在集群初始化阶段, 当有一台服务器server1启动时, 其单独无法进行和完成leader选举, 当第二台服务器server2启动时, 此时两台机器可以相互通信, 每台机器都试图找到leader, 于是进入leader选举过程.
    Java面试宝典汇总202203 - 图54
  1. 服务器 1 启动, 服务器 1 状态保持为looking.
  2. 服务器 2 启动, 发起一次选举. 服务器 1 投票给比自己ID号大的服务器2. 服务器2投票给自己.
    投票结果: 服务器 1 票数 0 票, 服务器 2 票数 2 票, 没有半数以上结果, 选举无法完成, 服务器 1, 2 状态保持looking.
  3. 服务器 3 启动, 发起一次选举. 此时服务器 1 和 2 都会更改选票为服务器 3, 服务器3投票给自己.
    投票结果: 服务器 1 为 0 票, 服务器 2 为 0 票, 服务器 3 为 3 票. 此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader. 服务器 1,2 更改状态为follower,服务器 3 更改状态为leader;
  4. 服务器 4 启动, 发起一次选举. 此时服务器 1,2,3 已经不是looking状态, 不会更改选票信息, 服务器4投票给自己.
    投票结果:服务器 3 为 3 票,服务器 4 为 1 票。此时服务器 4服从多数,更改选票信息为服务器 3,并更改状态为 following;
  5. 服务器 5 启动,同 4 一样当小弟.
    5.3.2 服务器运行时期的Leader选举
    在zk运行期间, leader与非leader服务器各司其职, 即便当有非leader服务器宕机或者新加入, 此时也不会影响leader. 但是一旦leader服务器宕机了, 那么整个集群将会暂停对外服务, 进入新一轮leader选 举, 其过程和启动时期的Leader选举过程基本一致.
    假设正在运行的有server1,server2,server3三台服务器,当前leader是server2,若某一时刻leader挂了, 此时便开始leader选举. 选举过程如下:
  • 变更转态, server1和server3变更为looking状态.
  • 开始投票, 每台服务器投票给比自己myid大的机器, 没有比自己大的就投给自己.
  • 这样server3有2票, server1有1票, server3的票数超过了集群一半, 当选leader, server1变更状态follower.

    6. 为什么zookeeper集群的数目,一般为奇数个?(高薪常问)

    1.容错
    由于在增删改操作中需要半数以上服务器通过,来分析以下情况。
    2台服务器,至少2台正常运行才行(2的半数为1,半数以上最少为2),正常运行1台服务器都不允许挂掉
    3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最少为2),正常运行可以允许1台服务器挂掉
    4台服务器,至少3台正常运行才行(4的半数为2,半数以上最少为3),正常运行可以允许1台服务器挂掉
    5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最少为3),正常运行可以允许2台服务器挂掉
    6台服务器,至少3台正常运行才行(6的半数为3,半数以上最少为4),正常运行可以允许2台服务器挂掉
    通过以上可以发现,3台服务器和4台服务器都最多允许1台服务器挂掉,5台服务器和6台服务器都最多允许2台服务器挂掉
    但是明显4台服务器成本高于3台服务器成本,6台服务器成本高于5服务器成本。这是由于半数以上投票通过决定的。
    2.防脑裂
    一个zookeeper集群中,可以有多个follower.observer服务器,但是必需只能有一个leader服务器。
    如果leader服务器挂掉了,剩下的服务器集群会通过半数以上投票选出一个新的leader服务器。
    集群互不通讯情况:
    一个集群3台服务器,全部运行正常,但是其中1台裂开了,和另外2台无法通讯。3台机器里面2台正常运行过半票可以选出一个leader。
    一个集群4台服务器,全部运行正常,但是其中2台裂开了,和另外2台无法通讯。4台机器里面2台正常工作没有过半票以上达到3,无法选出leader正常运行。
    一个集群5台服务器,全部运行正常,但是其中2台裂开了,和另外3台无法通讯。5台机器里面3台正常运行过半票可以选出一个leader。
    一个集群6台服务器,全部运行正常,但是其中3台裂开了,和另外3台无法通讯。6台机器里面3台正常工作没有过半票以上达到4,无法选出leader正常运行。
    通过以上分析可以看出,为什么zookeeper集群数量总是单出现,主要原因还是在于第2点,防脑裂,对于第1点,无非是正本控制,但是不影响集群正常运行。但是出现第2种脑裂的情况,zookeeper集群就无法正常运行了。

7. zookeeper 如何进行数据同步

Java面试宝典汇总202203 - 图55
1、客户端只能处理读请求,如果遇见写请求,那么只能将其转发给leader,如右上角转发给leader消息02

2、当leader 收到转发消息或者是客户端写数据时,zk 会将数据写入到磁盘文件中,但并不会提交数据,在写入磁盘日志文件成功后,会按照顺序将向follower 发起2pc 提交中的提议请求

3、follower 收到消息后,写入到自己的日志文件中,并且回复ack,若zk的leader 收到ack 超过半数,则leader将磁盘日志中的数据写入到zk的leader 中,再向其他人发起commit,follower 提交日志文件中的数据

8.zookeeper的leader选举

1、当leader挂掉之后,zk就会发起leader选举
2、每个机器在进行leader 选举的过程中 都会处于looking 状态,开始投票-> 投递给自己
3、服务器之间会进行两两对比,统计谁的zxid(事务id)大, 如果zxid 相同
4、以serverId作为选举的标准,如果两两对比后,弱者要更改他的选票
5、半数机制5.leader选举的时机
1、 leader 挂掉的时候 2、集群启动 3、超过半数的follower 不再跟随leader 的时候。

6.SpringBoot

6.1 SpringBoot是什么(了解)

是Spring的子项目,主要简化Spring开发难度,去掉了繁重配置,提供各种启动器,可以让程序员很快上手,节省开发时间.

6.2 SpringBoot的优点(必会)

SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。
版本锁定:解决是maven依赖版本容易冲突的问题,集合了常用的并且测试过的所有版本
使用了Starter(启动器)管理依赖并能对版本进行集中控制,如下的父工程带有版本号, 就是对版本进行了集中控制.


org.springframework.boot
spring-boot-starter-parent
2.0.0.RELEASE

起步依赖 :解决了完成某一个功能要整合的jar包过多的问题,集合了常用的jar包
自动配置:解决了整合框架或者技术的配置文件过多,集合了所有的约定的默认配置
内置Tomcat:通过内置的tomcat,无需再用其他外置的Tomcat就直接可以运行javaEE程序
总之:人们把Spring Boot 称为搭建程序的脚手架。其最主要作用就是帮我们快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让我们关注于业务而非配置。

6.3 运行SpringBoot项目的方式(必会)

  • 可以打包
  • 可以使用Maven插件直接运行.
  • 直接运行main方法.

    6.4 SpringBoot的启动器starter(必会)

    (1)什么是starter?
    starter启动器,可以通过启动器集成其他的技术,比如说: web, mybatis, redis等等.可以提供对应技术的开发和运行环境.
    比如: pom中引入spring-boot-starter-web, 就可以进行web开发.
    (2)starter执行原理?

  • SpringBoot在启动时候会去扫描jar包中的一个名为spring.factories.

  • 根据文件中的配置,去加载自动配置类. 配置文件格式是key=value, value中配置了很多需要Spring加载的类.
  • Spring会去加载这些自动配置类, Spring读取后,就会创建这些类的对象,放到Spring容器中.后期就会从Spring容器中获取这些类对象.

(3)SpringBoot中常用的启动器

  • spring-boot-starter-web, 提供web技术支持
  • spring-boot-starter-test
  • spring-boot-starter-jdbc
  • spring-boot-starter-jpa
  • spring-boot-starter-redis…等等

    6.5 SpringBoot运行原理剖析(必会)

    (一) SpringApplication类作用及run()方法作用

  • SpringApplication这个类整合了其他框架的启动类, 只要运行这一个类,所有的整合就都完成了.

  • 调用run函数, 将当前启动类的字节码传入(主要目的是传入@SpringBootApplication这个注解), 以及main函数的args参数.
  • 通过获取当前启动类的核心信息, 创建IOC容器.

(二) 当前启动类@SpringBootApplication详细剖析
run函数传入的当前启动类字节码, 最重要的是传入了@SpringBootApplication, 点开该注解源码, 会发现有多个注解组成,接下来会详细解释每个注解的含义.
点开这个注解源码, 发现有4类注解.
Java面试宝典汇总202203 - 图56
(1) 第一类: JDK原生注解4个
@Target(ElementType.TYPE) //当前注解的使用范围
@Retention(RetentionPolicy.RUNTIME) //生命周期
@Documented //声明在生成doc文档时是否带着注解
@Inherited //声明是否子类会显示父类的注解

(2)第二类: @SpringBootConfiguration
点开该注解源码, 会发现本质是@Configuration,定义该类是个配置类功能等同于xml配置文件.
Java面试宝典汇总202203 - 图57

提到@Configuration就要提到他的搭档@Bean, 使用这两个注解就可以创建一个简单的Spring配置类, 可以用来替代相应的xml配置文件.可以理解为创建了IOC容器了.
(3)第三类: @ComponentScan, 包扫描功能.
这个注解对应Spring的XML配置中的@ComponentScan,其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义, 最终将这些bean定义加载到IoC容器中.
也可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围, 如果不指定, 则默认扫描@ComponentScan所在类的package及子包进行扫描。注:所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages, 这样能扫描root package及子包下的所有类.
(4)第四类: @EnableAutoConfiguration
点开源码会发现,本质是@import, 自动导入功能
Java面试宝典汇总202203 - 图58
1. @EnableAutoConfiguration也是借助@Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器.
@EnableAutoConfiguration会根据类路径中的jar依赖为项目进行自动配置, 如:添加了spring-boot-starter-web依赖, 会自动添加Tomcat和SpringMVC的依赖, SpringBoot会对Tomcat和SpringMVC进行自动配置.
2. 那么SpringBoot是如何完成自动配置的呢?
A. SpringBoot自动配置的注解是 @EnableAutoConfiguration.B. 我们用的时候是在启动类上加@SpringBootApplication,这个注解是复合注解,内部包含 @EnableAutoConfigurationC. @EnableAutoConfiguration内部有一个@Import, 这个注解才是完成自动配置的关键.D. @Import导入一个类(AutoConfigurationImportSelector),这个类内部提供了一个方法(selectImports). 这个方法会扫描导入的所有jar包下的spring.factories文件. 解析文件中自动配置类key=value, 将列表中的类创建,并放到Spring容器中.
Java面试宝典汇总202203 - 图59
6.5.3 总结
总之一个@SpringBootApplication注解就搞定了所有事, 它封装了核心的@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan这三个类,大大节省了程序员配置时间,这就是SpringBoot的核心设计思想.

6.6 SpringBoot热部署(了解)

导入spring-boot-devtools这个jar包: 就可以完成热部署了.

6.7 SpringBoot中的配置文件(必会)

(1)有哪些配置文件?
bootstrap: yml/application
application: yml/application
(2)上面两种配置文件有什么区别?

  1. bootstrap由父ApplicationContext加载, 比application配置文件优先被加载.
  2. bootstarp里的属性不能被覆盖.
  3. application: springboot项目中的自动化配置.
  4. bootstrap:
    使用spring cloud config配置中心时, 需要加载连接配置中心的配置属性的, 就 可以使用bootstrap来完成.
    加载不能被覆盖的属性.
    加载一些加密/解密的数据.

(3)读取配置文件的方式?

  • 读取默认配置文件

需要注入Environment类, 使用environment.getProperty(peorperties中的key), 这样就能获得key对应的value值
@value(${key.value}) 直接读取

  • 读取自定义配置文件
  • 自定义配置文件后缀必须是.propeties
    • 编写和自定义配置文件对应的java类, 类上放3个注解
  • @ConfigurationProperties(“前缀”)
  • @PropertySource(“指定配置文件”)
  • @Component包扫描
  • 读取的时候就跟读取默认配置文件一样.

    6.8 SpringBoot支持哪些日志框架(了解)

    Java Utils logging
    Log4j2
    Lockback
    如果你使用了启动器,那么springboo默认将Lockback作为日志框架.

    6.9 SpringBoot常用注解(必会)

  • @SpringBootApplication:它封装了核心的@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan这三个类,大大节省了程序员配置时间,这就是SpringBoot的核心设计思想.

  • @EnableScheduling是通过@Import将Spring调度框架相关的bean定义都加载到IoC容器
  • @MapperScan:spring-boot支持mybatis组件的一个注解,通过此注解指定mybatis接口类的路径,即可完成对mybatis接口的扫描
  • @RestController 是@Controller 和@ResponseBody的结合,一个类被加上@RestController注解,数据接口中就不再需要添加@ResponseBody,更加简洁。
  • @RequestMapping,我们都需要明确请求的路径.
  • @GetMappping,@PostMapping, @PutMapping, @DeleteMapping 结合@RequestMapping使用, 是Rest风格的, 指定更明确的子路径.
  • @PathVariable:路径变量注解,用{}来定义url部分的变量名.
  • @Service这个注解用来标记业务层的组件,我们会将业务逻辑处理的类都会加上这个注解交给spring容器。事务的切面也会配置在这一层。当让 这个注解不是一定要用。有个泛指组件的注解,当我们不能确定具体作用的时候 可以用泛指组件的注解托付给spring容器
  • @Component和spring的注解功能一样, 注入到IOC容器中.
  • @ControllerAdvice 和 @ExceptionHandler 配合完成统一异常拦截处理.

备注: 面试的时候记住6.7个即可~

7. SpringCloud

7.1 SOA和微服务的区别?(必会)

谈到SOA和微服务的区别, 那咱们先谈谈架构的演变

  1. 集中式架构

项目功能简单, 一个项目只需一个应用, 将所有功能部署在一起, 这样的架构好处是减少了部署节点和成本.
Java面试宝典汇总202203 - 图60
缺点: 代码耦合,开发维护困难, 无法水平扩展, 单点容错率低,并发能力差

  1. 垂直拆分架构

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:
Java面试宝典汇总202203 - 图61

优点:

  • 系统拆分实现了流量分担,解决了并发问题
  • 可以针对不同模块进行优化, 方便水平扩展,负载均衡,容错率提高

缺点:

  • 系统间相互独立,会有很多重复开发工作,影响开发效率
  1. 分布式服务

当垂直应用越来越多, 随着项目业务功能越来越复杂, 并非垂直应用这一条线进行数据调用, 应用和应用之间也会互相调用, 也就是完成某一个功能,需要多个应用互相调用, 这就是将功能拆完来完成的分布式架构.
Java面试宝典汇总202203 - 图62
优点: 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点: 系统间耦合度变高,调用关系错综复杂,难以维护.

  1. 服务治理架构SOA

SOA :面向服务的架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键, 而最初的服务治理基石是Dubbo服务治理
Java面试宝典汇总202203 - 图63
以前分布式服务的问题?

  • 服务越来越多,需要管理每个服务的地址, 调用关系错综复杂,难以理清依赖关系
  • 服务过多,服务状态难以管理,无法根据服务情况动态管理

SOA服务治理架构的优点:

  • 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
  • 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
  • 动态监控服务状态监控报告,人为控制服务状态

SOA服务治理架构的缺点:

  • 服务间依然会有依赖关系,一旦某个环节出错会影响较大(容错机制)
  • 服务关系复杂,运维、测试部署困难,不符合开发-运维一站式维护的思想.
  1. 微服务

前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实缺有一些差别:
微服务的特点:

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责.
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
  • 自治:自治是说服务间互相独立,互不干扰
  • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
  • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
  • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
  • 数据库分离:每个服务都使用自己的数据源
  • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护. 基于docker容器式开发.

Java面试宝典汇总202203 - 图64
目前微服务微服务架构主流的是SpringBoot+Dubbo和SpringBoot+SpringCloud的架构模式.
综上, 无论是SOA还是微服务, 都需要进行服务调度, 目前主流的服务调度室RPC和HTTP两种协议, 而Dubbo基于RPC的远程调度机构, SpringCloud是基于Rest风格(基于http协议实现的)的Spring全家桶微服务服务治理框架. 说到这里也可以继续说下Dubbo和SpringCloud的区别.

7.2 SpringCloud是什么?(了解)

SpringCloud是一系列框架的集合,集成SpringBoot,提供很多优秀服务:服务发现和注册,统一配置中心, 负载均衡,网关, 熔断器等的一个微服务治理框架.

7.3 SpringCloud的优势?(了解)

  • 因为SpringCloud源于Spring,所以它的质量,稳定性,持续性都是可以保证的。
  • SpringCloiud天然支持SpringBoot框架,就可以提高开发效率,能够实现需求。
  • SpringCloud更新很快,后期支持很给力。
  • SpringCloud可以用来开发微服务。

    7.4 SpringCloud有哪些核心组件?(必会)

  • Eureka: 注册中心, 服务注册和发现

  • Ribbon: 负载均衡, 实现服务调用的负载均衡
  • Hystrix: 熔断器
  • Feign: 远程调用
  • Zuul: 网关
  • Spring Cloud Config: 配置中心

(1)Eureka
提供服务注册和发现, 是注册中心. 有两个组件: Eureka服务端和Eureka客户端

  • Eureka服务端: 作为服务的注册中心, 用来提供服务注册, 支持集群部署.
  • Eureka客户端: 是一个java客户端, 将服务注册到服务端, 同事将服务端的信息缓存到本地, 客户端和服务端定时交互.
  1. 原理

Java面试宝典汇总202203 - 图65

  • Eureka-Server:就是服务注册中心(可以是一个集群),对外暴露自己的地址。
  • 提供者:启动后向Eureka注册自己信息(地址,服务名称等),并且定期进行服务续约
  • 消费者:服务调用方,会定期去Eureka拉取服务列表,然后使用负载均衡算法选出一个服务进行调用。
  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
  1. 服务下线、失效剔除和自我保护
  • 服务的注册和发现都是可控制的,可以关闭也可以开启。默认都是开启
  • 注册后需要心跳,心跳周期默认30秒一次,超过90秒没发心跳认为宕机
  • 服务拉取默认30秒拉取一次.
  • Eureka每个60秒会剔除标记为宕机的服务
  • Eureka会有自我保护,当心跳失败比例超过阈值(默认85%),那么开启自我保护,不再剔除服务。
  • Eureka高可用就是多台Eureka互相注册在对方上.

(2)Ribbon

  • Ribbon是Netflix发布的云中服务开源项目. 给客户端提供负载均衡, 也就是说Ribbon是作用在消费者方的.
  • 简单来说, 它是一个客户端负责均衡器, 它会自动通过某种算法去分配你要连接的机器.
  • SpringCloud认为Ribbon这种功能很好, 就对它进行了封装, 从而完成负载均衡.
  • Ribbon默认负责均衡策略是轮询策略.

(3)Hystrix熔断器

  • 有时候可能是网络问题, 一些其它问题, 导致代码无法正常运行, 这是服务就挂了, 崩溃了. 熔断器就是为了解决无法正常访问服务的时, 提供的一种解决方案.
  • 解决因为一个服务崩溃而引起的一系列问题, 使问题只局限于这个服务中,不会影响其他服务.
  • Hystrix提供了两种功能, 一种是服务降级, 一种是服务熔断.
  1. 服务降级原理
    • Hystrix为每个服务分配了小的线程池, 当用户发请求过来, 会通过线程池创建线程来执行任务, 当创建的线程池已满或者请求超时(这里和多线程线程池不一样,不存在任务队列), 则启动服务降级功能.
    • 降级指的请求故障时, 不会阻塞, 会返回一个友好提示(可以自定义, 例如网站维护中请稍后重试), 也就是说不会影响其他服务的运行.
  2. 服务熔断原理

Java面试宝典汇总202203 - 图66
状态机有3个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。
  • Open:打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。

(4)Feign: 远程调用组件

  • 后台系统中, 微服务和微服务之间的调用可以通过Feign组件来完成.
  • Feign组件集成了Ribbon负载均衡策略(默认开启的, 使用轮询机制), Hystrix熔断器(默认关闭的, 需要通过配置文件进行设置开启)
  • 被调用的微服务需要提供一个接口, 加上@@FeignClient(“url”)注解
  • 调用方需要在启动类上加上@EnableFeignClients, 开启Feign组件功能.

(5)Zuul: 路由/网关

  • 对于项目后台的微服务系统, 每一个微服务都不会直接暴露给用户来调用的, 但是如果用户知道了某一个服务的ip:端口号:url:访问参数, 就能直接访问你. 如果再是恶意访问,恶意攻击, 就会击垮后台微服务系统.因此, 需要一个看大门的大boss, 来保护我们的后台系统.
  • Zuul类似Nginx, 反向代理功能. 用户访问服务, 首先会到Zuul中心, Zuul去Eureka中拉取服务, 获取服务列表, 获取具体的地址, 再通过具体的地址去访问目标微服务.
  • Zuul提供了过滤和路由两个功能, 过滤可以分配权限, 允许谁可以访问谁不可以访问, 路由则是去Eureka拉取服务提访问地址.
  • Zuul网关也继承了Ribbon负载均衡和Hystix熔断机制.

(6)Spring Cloud Config

  • 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring Cloud Config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中.
  • 那我们如果想在不重启微服务的情况下更新配置如何来实现呢? 我们使用SpringCloudBus来实现配置的自动更新.
    • 创建Config Server微服务, 用于存放配置文件, 默认使用git存储此项目.
    • 创建Config Client微服务, 用于拉取git上的配置文件, 项目中集成SpringCloudBus对应的消息中间件jar包.
  • 更新配置文件的流程

    • 手动向Mq队列中发送消息,http://127.0.0.1:12000/actuator/bus-refresh(固定地址)
    • Config Client中的MQ监听到消息, 去git服务器上加载新的配置文件, 并向mq中生产一条配置文件变更的消息.
    • 其他被集中管理的微服务也集成了mq,监听到消息, 向Config Client中重新获取最新的配置文件.

      7.5 SpringBoot和SpringCloud的关系(必会)

    • SpringBoot是为了解决Spring配置文件冗余问题, 简化开发的框架.

    • SpringCloud是为了解决微服务之间的协调和配置问题, 还有服务之间的通信, 熔断, 负载均衡远程调度任务框架.
    • SpringCloud需要依赖SpringBoot搭建微服务, SpringBoot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,SpringCloud很大的一部分是基于SpringBoot来实现。
    • SpringBoot不需要依赖SpringCloud就可以独立开发. SpringBoot也可以集成Dubbo进行开发.

      7.6 SpringCloud和Dubbo的区别(高薪常问)

  • SpringCloud和Dubbo都是主流的微服务架构.

  • SpringCloud是Apache下的Spring体系下的微服务解决方案.
  • Dubbo是阿里系统中分布式微服务治理框架.
  • 技术方面对比
  • SpringCloud功能远远超过Dubbo, Dubbo只实现了服务治理(注册和发现). 但是SpringCloud提供了很多功能, 有21个子项目
  • Dubbo可以使用Zookeeper作为注册中心, 实现服务的注册和发现, SpringCloud不仅可以使用Eureka作为注册中心, 也可以使用Zookeeper作为注册中心.
  • Dubbo没有实现网关功能, 只能通过第三方技术去整合. 但是SpringCloud有zuul路由网关, 对请求进行负载均衡和分发. 提供熔断器, 而且和git能完美集成.
  • 性能方面对比
  • 由于Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC。
  • 而SpringCloud是基于Http协议+Rest接口调用远程过程的,相对来说,Http请求会有更大的报文,占的带宽也会更多。
  • 使用Dubbo时, 需要给每个实体类实现序列化接口, 将实体类转化为二进制进行RPC通信调用.而使用SpringCloud时, 实体类就不需要进行序列化.

刚才有提到注册中心不一样,那么Eureka和Zookeeper有什么区别? 我们继续往下说~

7.7 Eureka和Zookeeper的区别(高薪常问)

在谈这个问题前我们先看下CAP原则: C(Consistency)-数据一致性; A(Availability)-服务可用性; P(Partition tolerance)-服务对网络分区故障的容错性, 这三个特性在任何分布式系统中不能同时满足, 最多同时满足两个, 而且P(分区容错性)是一定要满足的.

  • Eureka满足AP(服务可用性和容错性), Zookeeper满足CP(数据一致性和容错性)
  • Zookeeper满足CP, 数据一致性, 服务的容错性. 数据在各个服务间同步完成后才返回用户结果, 而且如果服务出现网络波动导致监听不到服务心跳, 会立即从服务列表中剔除, 服务不可用.
  • Eureka满足AP, 可用性, 容错性. 当因网络故障时, Eureka的自我保护机制不会立即剔除服务, 虽然用户获取到的服务不一定是可用的, 但至少能够获取到服务列表. 用户访问服务列表时还可以利用重试机制, 找到正确的服务. 更服务分布式服务的高可用需求.
  • Eureka集群各节点平等, Zookeeper集群有主从之分.
  1. 如果Zk集群中有服务宕机,会重新进行选举机制,选择出主节点, 因此可能会导致整个集群因为选主而阻塞, 服务不可用.
  2. Eureka集群中有服务宕机,因为是平等的各个服务器,所以其他服务器不受影响.
  • Eureka的服务发现者会主动拉取服务, ZK服务发现者是监听机制
  1. Eureka中获取服务列表后会缓存起来, 每隔30秒重新拉取服务列表.
  2. Zk则是监听节点信息变化, 当服务节点信息变化时, 客户端立即就得到通知.

    7.8 互联网系统设计的时候常用的理论有哪些?

    1. CAP理论
    2. BASE理论

      7.9 CAP理论是什么?为什么互联网系统中不能同时满足?常见的工具如redis和zookeeper符合什么理论?

      (1)CAP理论是什么
  3. 一致性(Consistency) (所有节点在同一时间具有相同的数据)

  4. 可用性(Availability) (保证每个请求不管成功或者失败都有响应)
  5. 分隔容忍(Partition tolerance) (系统中任意节点挂掉货数据丢失不会影响整个系统的运行)

(2)为什么互联网系统中不能同时满足
因为再互联网项目中我们都会做集群,不同的节点部署到不同的地区,某一个地区的节点出现了问题并不会影响其他地区的节点所以分区容错性一定满足,就是因为这个特性导致一致性和可用性相互矛盾,因为某一些节点挂掉那么如果我们的系统可能那么就会有大量的数据写入进来但是因为节点挂了无法完成数据同步所以一致性无法满足,如果我们的系统需要保证一致性那么系统就一定不能接入新的数据请求否则数据就无法完成同步一致性也得不到保证所以只能让系统不可用。
(3)redis和zookeeper符合什么理论

  1. Redis 满足的是AP理论,所以使用redis就可能出现不一致的情况
  2. Zookeeper 满足的是CP理论,如果数据出现不一致他会停止对外服务等待数据同步以后在恢复服务功能

(4)如果给你一个XXX场景你会选择CP还是AP

  1. 牵涉到敏感数据比如钱的一定使用CP
  2. 其他大部分的业务都使用AP保证服务可用

    7.10 什么是BASE理论?

    1. Basically Available(基本可用):分布式系统在出现不可预知故障的时候,允许损失部分可用性,比如阿里云的服务器做到99.99%的可用性
    2. Soft state(软状态):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据听不的过程存在延时。该状态书面含义不好再面试中进行表达,如果遇到可用使用实际的业务场景来描述,比如:我们转账的时候会出现一个转账中的状态该状态就是一个软状态。
    3. Eventually consistent(最终一致性): 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。最典型的案例也是转账,当A同学向B同学转账500的时候A同学的钱已经扣除但是B同学的钱并没有时时增加,而是在2小时或者24小时内到账,在此期间数据其实是不平衡的,但是过了一段时间以后数据最终平衡了。

10.1 什么是接口的幂等性,你知道哪些方案可以实现接口的幂等性

(1)接口的幂等性: 同一个接口,多次发出同一个请求,必须保证操作只执行一次,比如下单以后增加积分,对于同一个用户的同一个订单无论我们调用多少次增加积分的接口都应该保证积分只增加一次。
(2)接口幂等性的解决方案-不同的场景会选择不同的方案

  1. 1. 基于数据库来实现-先select然后再insert,同样是用户增加积分的案例,在增加积分之前先查询当前的订单是否增加过积分了,如果没有增加则增加积分如果增加过了则忽略。
  2. 1. 基于redis来实现-可以把所有增加过积分的订单写入redis然后每次增加积分的时候判断redis中是否有这个记录如果没有则增加积分如果有则忽略该请求
  3. 1. 基于数据库的悲观锁来实现-比如在转账的业务中我们通常都是先查询用户的金额然后在进行更新的操作,在多线程的环境下可能多个线程都是先执行查询操作得到了500元的金额然后执行update更新操作,就可能会导致当前的用户金额变成负数,此时就可以使用悲观锁来解决这个该问题,悲观锁在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待
  4. select * from user id=123 for update;
  5. 1. 基于数据库的乐观锁来实现-主要添加一个version属性来控制更新的操作,对于这个功能大家应该是比较熟悉的就不展开介绍了。
  6. 1. 基于数据库的唯一性索引来实现-增加积分的时候我们可以对订单号这个字段设置为唯一,如果一个订单已经消耗过了则再次插入数据的时候就会报错。
  7. 1. 根据数据库的状态来解决-很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态,我们可以通过状态来判断是否要继续执行某一个业务,比如停车场的支付功能,我们可以通过停车场的支付二维码进行支付,也可以通过出口的人工扫码进行支付,如果我们已经扫码支付了但是到出口的时候砸门未及时打开工作人员就会要求你人工支付,那么你第二次支付的时候就可以判断下支付的状态是否是已支付如果是则不再进行支付操作。
  8. 1. 分布式锁的方式实现-这个也很好理解不在展开

7.12 Eureka、Zookeeper、Nacos、Consul这四个注册中心有什么区别?

在回答这个问题的时候要重点关注他们的实现协议、负载均衡、雪崩保护、实例的注销、跨注册中心同步数据以及对其他框架的支持情况。
Java面试宝典汇总202203 - 图67

五、技术点

11 Redis

1.1 Redis是什么?

Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能非关系型(NoSQL)的(key-value)键值对数据库。可以用作数据库、缓存、消息中间件等。

1.2 Redis 的存储结构有哪些?

String,字符串,是 redis 的最基本的类型,一个 key 对应一个 value。是二进制安全的,最大能存储 512MB。
Hash,散列,是一个键值(key=>value)对集合。string 类型的 field 和value 的映射表,特别适合用于存储对象。每个 hash 可以存储 2^32 -1 键值对(40 多亿)
List,列表,是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列边或者尾部(右边)。最多可存储 2^32- 1 元素(4294967295, 每个列表可存储 40 亿)
Set,集合, 是 string 类型的无序集合,最大的成员数为 2^32-1(4294967295, 每个集合可存储 40 多亿个成员)。
Sorted set,有序集合,和 set 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。zset 的成员是唯一的,但分数(score)却可以重复。

1.3 Redis 的优点?

1 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库。Redis 支持事务 、持久化
2、单线程操作,避免了频繁的上下文切换。
3、采用了非阻塞 I/O 多路复用机制。I/O 多路复用就是只有单个线程,通过跟踪每个 I/O 流的状态,来管理多个 I/O 流。

1.4 为什么要用 Redis

高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
Java面试宝典汇总202203 - 图68
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

Java面试宝典汇总202203 - 图69

1.5 redis的持久化

Redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。
AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。

1.6 Redis 的缺点

1.6.1 缓存和数据库双写一致性问题
一致性的问题很常见,因为加入了缓存之后,请求是先从 redis中查询,如果 redis 中存在数据就不会走数据库了,如果不能保证缓存跟数据库的一致性就会导致请求获取到的数据不是最新的数据。
解决方案:
1、编写删除缓存的接口,在更新数据库的同时,调用删除缓存
的接口删除缓存中的数据。这么做会有耦合高以及调用接口失败的情况。
2、消息队列:ActiveMQ,消息通知。
1.6.2缓存的并发竞争问题
并发竞争,指的是同时有多个子系统去 set 同一个 key 值。
解决方案:
1、最简单的方式就是准备一个分布式锁,大家去抢锁,抢到
锁就做 set 操作即可
1.6.3缓存雪崩问题
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波
请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:
1、给缓存的失效时间,加上一个随机值,避免集体失效。
2、使用互斥锁,但是该方案吞吐量明显下降了。
3、搭建 redis 集群。
1.6.4缓存击穿问题
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
1、利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,
再去请求数据库。没得到锁,则休眠一段时间重试
2、采用异步更新策略,无论 key 是否取到值,都直接返回,
value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程
去读数据库,更新缓存。
1.6.5 缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
1.接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2.从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

1.7 Redis 集群

1.7.1主从复制

Java面试宝典汇总202203 - 图70
主从复制原理
从服务器连接主服务器,发送 SYNC 命令。主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成RDB 文件并使用缓冲区记录此后执行的所有写命令。主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令。从服务器收到快照文件后丢弃所有旧数据,载入收到的快照。主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令。
从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令(从服务器初始化完成)。主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)。
优点
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。为了分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由 Master 来完成Slave同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在Master-Slave 同步期间,客户端仍然可以提交查询或修改请求。Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据。
缺点
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP才能恢复。主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP后还会引入数据不一致的问题,降低了系统的可用性。Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

1.7.2 哨兵模式

Java面试宝典汇总202203 - 图71
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。为此,Redis2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。哨兵的作用就是监控 Redis 系统的运行状况,它的功能包括以下两个。
1、监控主服务器和从服务器是否正常运行。
2、主服务器出现故障时自动将从服务器转换为主服务器。

哨兵的工作方式
每个 Sentinel (哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从务器以及其他 Sentinel(哨兵)进程发送一个 PING 命令。如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被Sentinel(哨兵)进程标记为主观下线(SDOWN)。如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN),则 Master 主服务器会被标记为客观下线(ODOWN)。
在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一169 / 196次的频率向集群中的所有Master主服务器,Slave从服务器发送 INFO命令。当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel (哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。
优点
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。主从可以自动切换,系统更健壮,可用性更高。
缺点
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

1.7.3 Redis-Cluster 集群

Java面试宝典汇总202203 - 图72
redis 的哨兵模式基本已经可以实现高可用,读写分离,但是在这种模式下每台 redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0 上加入了 cluster 模式,实现的 redis 的分布式存储,也就是说每台 redis 节点上存储不同的内容。Redis-Cluster 采用无中心结构,它的特点如下:
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。节点的fail是通过集群中超过半数的节点检测失效时才生效。客户端与 redis 节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

工作方式
在 redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 key 到达的时候,redis会根据 crc16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。为了保证高可用,redis-cluster 集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点A1 都宕机了,那么该集群就无法再提供服务了。

1.8 Redis的分布式锁

方案一:
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
1>安全特性:互斥访问,即永远只有一个 client 能拿到锁
2>避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
3>容错性:只要大部分 Redis 节点存活就可以正常提供服务
Redis实现分布式锁
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
Java面试宝典汇总202203 - 图73
使用SETNX完成同步锁的流程及事项如下:
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间
释放锁,使用DEL命令将锁数据删除
方案二:
是利用setNx方法和setEx
这个方法,就是向redis 中去写一个字符串,如果redis 中本身不存在这个数据,setNx 就能写入成功,并且返回1,如果client拿到返回值结果是1,就表示他获得到了锁,执行业务逻辑,执行完毕后,删除锁,如果没有写入成功,redis 就会返回 0,如果拿到0 就表示没有获得到锁,他就需要自旋去进行抢锁
以上方式有很多的问题
1、不能防止死锁
2、有可能自己删除掉了别人的锁
3、存在一个续期逻辑

问题1 的解决:给redis 设置key的时候,添加了一个原子性的 加锁和过期时间

问题2 的产生:当设置了过期时间后,如果a 执行业务时候出现了卡顿,把过期时间卡过去了,b线程就会进入到锁的内部,a会接着往下执行,也会走道删除锁逻辑,此时a就把b线程的锁进行删除
问题2 的解决一:我们可以给对应线程生成一个random的值,由于每个线程在执行逻辑时候,都有一个栈帧,栈帧都有局部变量表,局部表里边就会有属于线程自己的 变量,在删除锁逻辑前,我们可以去判断此时redis 中的这个锁和局部变量表的数据是否一致,如果一致则表示当前这把锁,是自己的就删除,否则此时的锁就是别人的锁,就不删
问题2 的解决二、由于你需要比拿值,比值,删值,此时无论是哪一步卡住,都有可能导致 线程删除掉了别人的锁,于是我们就需要去实现 lua 表达式,去保证拿值,比值,删值 的原子性 (redis的官网也是这么说的)

问题3的产生:无论是问题1的解决还是问题2的解决也是,其实还是让其他线程进入到锁的内部了,所以此时我们就可以让锁进行一个续期逻辑我们就使用了redission,他就是redis 提供的对于分布式支持的lock锁,他的底层其实就是看门狗原理
1、redission 会进行自动续期,并且会默认给锁30 s时间,每隔1/3 30秒 就会自动续期,续期成看门狗时间
2、redission 的锁有一个是不用传递过期时间的,一个是需要传递过期时间
如果你传递了过期时间,此时redis 不会去进行续约,如果时间到了之后,如果说有别的线程删除了其他的锁,会报错
3、 如果你没有传递时间,此时redis 的看门狗机制就会生效,在源码就传入了一个-1
reddsion 会根据当前锁失效时间是否是-1 ,来判断到底是走的哪个api,如果是传入了时间,只进行抢锁,如果是没有传入时间,则使用的是看门狗时间(ConnectionManager()、getCfg()、getLockWatchdogTimeout) ,如果没有抢锁成功,自旋 , 除了抢锁以外,他还会利用异步编排的 oncomplete 在抢锁完毕后,进行锁续期逻辑
底层会有一个 timeTask的内部类,在此逻辑中进行续约,每次续约又续约成看门狗的默认时间,在1/3时间时候,又会执行续约逻辑

12 RabbitMQ

2.1消息中间件的区别?

Java面试宝典汇总202203 - 图74

2.2 为什么要使用MQ?

因为项目比较大,做了分布式系统,所有远程服务调用请求都是同步执行经常出问题,所以引入了mq

Java面试宝典汇总202203 - 图75

2.3 RabbitMQ是什么?

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
PS:也可能直接问什么是消息队列?消息队列就是一个使用队列来通信的组件

2.4 RabbitMQ特点?【重要】

可靠性: RabbitMQ使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。
灵活的路由 : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个 交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。
扩展性: 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。
高可用性 : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。
多种协议: RabbitMQ除了原生支持AMQP协议,还支持STOMP, MQTT等多种消息 中间件协议。
多语言客户端 :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。
管理界面 : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。
插件机制 : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。

2.5 AMQP是什么?AMPQ与JMS有什么区别知道吗?

AMQP 一个提供统一消息服务的应用层标准高级消息队列的链接协议,RabbitMQ是主要根据AMQP协议进行数据通信和传输的。有点类似于HTTP协议。
JMS是定义了统一的接口(API),来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式。 JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。 JMS规定了两种消息模式;而AMQP的消息模式更加丰富。

2.6 AMQP协议3层?

Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。

2.7 AMQP模型的几大组件?

  • 交换器 (Exchange):消息代理服务器中用于把消息路由到队列的组件。
  • 队列 (Queue):用来存储消息的数据结构,位于硬盘或内存中。
  • 绑定 (Binding):一套规则,告知交换器消息应该将消息投递给哪个队列。

    2.8说说生产者Producer和消费者Consumer?

    生产者

  • 消息生产者,就是投递消息的一方。

  • 消息一般包含两个部分:消息体(payload)和标签(Label)。

消费者

  • 消费消息,也就是接收消息的一方。
  • 消费者连接到RabbitMQ服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。

    2.9为什么需要消息队列?【重要】

    从本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断的演进。
    从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100 个在线用户已经很了不得,到现在坐拥10亿日活的微信。此时,我们需要有一个「工具」来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。因此,消息队列就应运而生了。
    它常用来实现:异步处理、服务解耦、流量控制(削峰)。

    2.10说说Broker服务节点、Queue队列、Exchange交换器?

  • Broker可以看做RabbitMQ的服务节点。一般请下一个Broker可以看做一个RabbitMQ服务器。

  • Queue:RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
  • Exchange:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。

    2.11消息队列有什么优缺点【重要】

    优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。缺点有以下几个:

  • 系统可用性降低 系统引入的外部依赖越多,越容易挂掉。万一 MQ 挂了,MQ 一挂,整套系统崩 溃,你不就完了?

  • 系统复杂度提高 硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?
  • 怎么保证消息传递的顺序性?问题一大堆。
  • 一致性问题 A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致 了。
  • 如何解决:

1、系统可用性降低:集群模式保证高可用。 镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
2、系统复杂度提高 加入Rabbit确实会增加系统复杂度,但MQ的解耦、提速、削峰这些方面的收益超过管理MQ的成本,所以该用还得用。
3、一致性问题 RabbitMQ分布式消息是最终一致性的,即使可能因为消息失败而导致前后消息不一致,但分布式系统是在不同服务器上的,不能像简单的本地回滚一样,所以它通过发送延迟消息和定时消息来定事进行消息补偿,最证最终消息是一致性的,即是一个完整的事务。 RabbitMQ本身是有事务的功能的,但是分布式的事务处理效率太低,且发生问题的可能性不高,所以多是选择放弃强一致性,而采用最终一致性。
4、消息顺序问题 在MQ中将有顺序要求的AB两个消息分别用两个队列与两个消费端手工ack接收,并且在消息上需要有对应的同组编号信息,以及发送次数,如果B执行的前提是已经消费了A,那需要在消费端判断A消息是否已经正确接收(也就是查询成功的消息库)。 如果A已经消费成功,则消费B,如果A消费失败,或者A还没有消费,则B消息也直接返回为消息失败,并且不重回队列。并且让消息提供方重新发送AB消息,如果连续三次发送消息仍然消费失败,则AB两个消息第四次处理时就扔入死信队列中,等待人工处理。

2.12 如何保证消息的可靠性?【重要】

  1. 消息补偿。(延迟消息+定时扫描)<br /> 由生产者发送消息给MQMQ会将消息给到消费者,消息者收到消息后,会返回一个消息确认,这个“消息确认”会被MQ放到“回调检查服务”中,“回调检查服务”会将收到的消息给到定时检查的MDB 但在这个过程中,如果出现异常或者网络波动,就会导致消息到不了回调检查服务,所以为了保证能够消息可靠性,会由生产者延迟一段时间后,再发送一个相同的消息给MQ,这个消息会直接被MQ发送到回调检查服务。 回调检查服务会将延迟发送的消息和MDB中的消息对比,如果MDB中没有该消息,就会调用生产者,让生产者重新发送消息。 但在以上过程,还可能出现“延迟发送消息”也出问题,为了更深层保证消息的可靠性,还需要一个定时检查服务,每隔一段固定时间,定时检查服务会将MDB里的消息和DB中的消息进行匹配(检查某个时间段的表,而不是全表扫描),如果有MDB缺失的消息,就会调用生产者重新发送消息。<br />总结就是:<br />**生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。**<br />**RabbitMQ自身:持久化、集群、普通模式、镜像模式。**<br />**RabbitMQ到消费者:basicAck机制、死信队列、消息补偿机制。**

2.13 什么是RoutingKey路由键?

生产者将消息发送给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。

2.14 Binding绑定?

通过绑定将交换器和队列关联起来,一般会指定一个BindingKey,这样RabbitMq就知道如何正确路由消息到队列了。

2.15 交换器4种类型?

主要有以下4种。

  • fanout:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
  • direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中。
  • topic:

匹配规则:
RoutingKey为一个 点号'.': 分隔的字符串。比如:java.xiaoka.show
BindingKey和RoutingKey一样也是点号“.“分隔的字符串。
BindingKey可使用 和 # 用于做模糊匹配,匹配一个单词,#匹配多个或者0个

  • headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配。性能差,基本用不到。

    2.16生产者消息运转?

    1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
    2.Producer声明一个交换器并设置好相关属性。
    3.Producer声明一个队列并设置好相关属性。
    4.Producer通过路由键将交换器和队列绑定起来。
    5.Producer发送消息到Broker,其中包含路由键、交换器等信息。
    6.相应的交换器根据接收到的路由键查找匹配的队列。
    7.如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
    8.关闭信道。
    9.管理连接。

    2.17消费者接收消息过程?

    1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
    2.向Broker请求消费响应的队列中消息,可能会设置响应的回调函数。
    3.等待Broker回应并投递相应队列中的消息,接收消息。
    4.消费者确认收到的消息,ack。
    5.RabbitMq从队列中删除已经确定的消息。
    6.关闭信道。
    7.关闭连接。

    2.18交换器无法根据自身类型和路由键找到符合条件队列时,有哪些处理?

    mandatory :true 返回消息给生产者。
    mandatory: false 直接丢弃。

    2.19什么是TTL?什么是死信队列,消息成为死信有哪几种情况?什么是延迟队列?【重要】

    TTL:全称 Time To Live(存活时间/过期时间) 1、如果给消息设置过期时间,即使到了过期时间,消息也不会立马被清除,只有等消息到了队列的头上,才会被判断是否过期清除。 2、如果给整个队列设置过期时间,即每一个进入队列的消息,都会各自被设置为了相同的过期时间。而非整个队列定时隔一段时间清除。 3、如果单独消息和整个队列两则都设置了过期时间,以时间短的为准。
    死信队列:英文缩写:DLX 。Dead Letter Exchange(死信交换机) 消息成为死信的三种情况:
    队列消息长度到达限制;
    消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
    原队列存在消息过期设置,消息到达超时时间未被消费;
    延迟队列:即消息进入队列后不会立即被消费,只有到到达指定时间后,才会被消费。 RabbitMQ中并未提供延迟队列功能,采用【TTL】+【死信队列】 组合实现延迟队列的效果。 场景: 1、下单后,30分钟未支付,取消订单,回滚库存。 2、新用户注册成功7天后,发送短信问候。

    2.20导致的死信的几种原因?

  • 消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。

  • 消息TTL过期。
  • 队列满了,无法再添加。

    2.21延迟队列?

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

    2.22优先级队列?

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

  • 可以通过x-max-priority参数来实现。
  • 当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。

    2.23 事务机制?【重要】

    RabbitMQ 客户端中与事务机制相关的方法有三个:
    channel.txSelect 用于将当前的信道设置成事务模式。
    channel . txCommit 用于提交事务 。
    channel . txRollback 用于事务回滚,如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,通过txRollback来回滚。

    2.24 发送确认机制?

    生产者把信道设置为confirm确认模式,设置后,所有再改信道发布的消息都会被指定一个唯一的ID,一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这样生产者就知道消息到达对应的目的地了。

    2.25 消息传输保证层级?

    At most once:最多一次。消息可能会丢失,但不会重复传输。
    At least once:最少一次。消息绝不会丢失,但可能会重复传输。
    Exactly once: 恰好一次,每条消息肯定仅传输一次。

    2.26 集群中的节点类型?

    内存节点:ram,将变更写入内存。
    磁盘节点:disc,磁盘写入操作。
    RabbitMQ要求最少有一个磁盘节点。

    2.27 队列结构?

    通常由以下两部分组成?
    rabbit_amqqueue_process:负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的 confirm 和消费端的 ack) 等。
    backing_queue:是消息存储的具体形式和引擎,并向 rabbit amqqueue process提供相关的接口以供调用。

    2.28 RabbitMQ中消息可能有的几种状态?【重要】

    alpha: 消息内容(包括消息体、属性和 headers) 和消息索引都存储在内存中 。
    beta: 消息内容保存在磁盘中,消息索引保存在内存中。
    gamma: 消息内容保存在磁盘中,消息索引在磁盘和内存中都有 。
    delta: 消息内容和索引都在磁盘中 。

    2.29在何种场景下使用了消息中间件?

  • 接口之间耦合比较严重

  • 面对大流量并发时,容易被冲垮
  • 存在性能问题

    2.30生产者如何将消息可靠投递到MQ?

    1.Client发送消息给MQ
    2.MQ将消息持久化后,发送Ack消息给Client,此处有可能因为网络问题导致Ack消息无法发送到Client,那么Client在等待超时后,会重传消息;
    3.Client收到Ack消息后,认为消息已经投递成功。

    2.31 MQ如何将消息可靠投递到消费者?

    1.MQ将消息push给Client(或Client来pull消息)
    2.Client得到消息并做完业务逻辑
    3.Client发送Ack消息给MQ,通知MQ删除该消息,此处有可能因为网络问题导致Ack失败,那么Client会重复消息,这里就引出消费幂等的问题;
    4.MQ将已消费的消息删除

    2.32 如何保证RabbitMQ消息队列的高可用?

    RabbitMQ 有三种模式:单机模式,普通集群模式,镜像集群模式。
    单机模式:就是demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式
    普通集群模式:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。
    镜像集群模式:这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据(元数据指RabbitMQ的配置数据)还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。

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

    RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。 我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据(发送的数据库和确认接收的数据库匹配),写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。 假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

    2.34 RabbitMQ如何保证消息的不重复消费呢?【重要】

    一条数据重复出现两次,数据库里就只有一条数据,这就是保证了系统的幂等性。 保障了消息的幂等性,同一条消息被重复消费也就不影响了,因为不影响最终执行结果。 方法一:采用乐观锁机制保证消息幂等性。在数据库中会增加一个版本字段,执行时也会匹配版本,如果版本不一致,SQL语句的匹配就不成立,就不会执行。
    方法二:你拿到这个消息做数据库的insert操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
    方法三:你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
    方法四:如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将以K-V形式写入redis.那消费者开始消费前,先去redis中查询有没有消费记录即可,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

    2.35 如何防止消息丢失?【重要】

    1、在生产者丢失——confirm确认模式 1)使用RabbitMQ事务机制,但它是同步的,且很耗性能。 2)开启confirm确认模式,确认消息是否从“生产者”发送到“交换机”,成功回传ack消息,失败可以重试或抛异常。且confirm模式是异步回调接口通知MQ是否接收到消息。一般都采用这种方式。
    2、在MQ中丢失——持久化 开启RabbitMQ持久化,防止RabbitMQ自己弄丢数据。除非极小概率还没来得及持久化,MQ就先挂了,即使这样,也只会丢失极少的数据量。 所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。 但持久化的过程也是很耗性能的。
    3、在消费者丢失——ack机制 用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
    以上三种方式,只是防止丢失,但具体补消息,还需要靠消息的补偿机制(也就是消息的可靠性保证)。

    13 MongoDb

    3.1 MongoDB是什么?

    mongodb是属于文档型的非关系型数据库,是开源、高性能、高可用、可扩展的
    数据逻辑层次关系:文档=>集合=>数据库
    在关系型数据库中每一行的数据对应mongodb里是一个文档。mongodb的文档是以BSON(binary json)格式存储的,其格式就是json格式。
    Java面试宝典汇总202203 - 图76
    1>集合
    集合是一组文档(即上面的 users 集合)。集合相当于关系数据库中的表,但集合中的文档长度可不同(集合中的文档中的键值对个数可不同)、集合中文档的key可不同。向集合中插入第一个文档时,集合会被自动创建。
    2>文档
    文档是一组键值对,用{ }表示,字段之间用逗号分隔。相当于关系数据库中的一行(一条记录)。示例:一个文档
1
2
3
4
5
6
{
id: “1”,
name: “张三”,
age: 28,
email: “zhangs@gmail.com”
}

这样写是为了方便看字段,也可以写在一起{id:”1”,name:”张三”,age:28,email:”zhangs@gmail.com”},一样的。
说明:
文档中的键值对是有序的
一个文档中不能有重复的key(对应关系数据库中的一条记录)
以”_”开头的key是保留的,有特殊含义。
3>字段
即一个键值对,key必须是String类型,value可以是任意类型。

3.2 MongoDB和关系型数据库mysql区别

上图中,左边的是 MySQL 数据库中 users 表,右边是 MongoDB 中 users 集合。虽然表现形式不同,但是数据内容还是一样的。其中:

test:表示数据库
users:表示集合,类似MySQL中的表
{id:”1”,name:”张三”,age:28,email:”zhangs@gmail.com”}:表示一个文档,类似于MySQL中的记录
id、name、age和email:表示字段
Java面试宝典汇总202203 - 图77

3.3 MongoDB有3个数据库

一个MongoDB中可以建立多个数据库,这些数据库是相互独立的,有自己的集合和权限。不同的数据库使用不同的文件存储(不存储在一个文件中)。

MongoDB默认有3个数据库:
admin: 从权限的角度来看,这是”root”数据库。将一个用户添加到这个数据库,这个用户会自动继承所有数据库的权限。一些特定的服务器端命令也只能在这个数据库中运行,比如列出所有的数据库或者关闭服务器。
local: 这个数据库永远不会被复制,里面的数据都是本地的(不会复制到其他MongoDB服务器上),可以用来存储限于本地单台服务器的任意集合
config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。

3.4Mongo中的数据类型

  1. null
    2. false和true
    3. 数值
    4. UTF-8字符串
    5. 日期 new Date()
    6. 正则表达式
    7. 数组
    8. 嵌套文档
    9. 对象ID ObjectId()
    10. 二进制数据
    11. 代码

3.5 MongoDB适用业务场景

网站数据:MongoDB 非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性
缓存:由于性能很高,MongoDB 也适合作为信息基础设施的缓存层。在系统重启之后,由 MongoDB 搭建的持久化缓存层可以避免下层的数据源过载
大尺寸,低价值的数据:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储
高伸缩性的场景:MongoDB 非常适合由数十或数百台服务器组成的数据库。MongoDB的路线图中已经包含对 MapReduce 引擎的内置支持
用于对象及 JSON 数据的存储:MongoDB的 BSON 数据格式非常适合文档化格式的存储及查询.

14 Nginx

3.1 Nginx是什么?

Nginx 是一个高性能的 HTTP 和反向代理服务器,及电子邮件代理服务器,同时也是一个非常高效的反向代理、负载平衡。

3.2 Nginx的作用?

1.反向代理,将多台服务器代理成一台服务器。
2.负载均衡,将多个请求均匀的分配到多台服务器上,减轻每台服务器的压力,提高 服务的吞吐量。
3.动静分离,nginx 可以用作静态文件的缓存服务器,提高访问速度

3.3 Nginx的优势?

  1. 可以高并发连接(5 万并发,实际也能支持 2~4 万并发)。
  2. 内存消耗少。
  3. 成本低廉。
  4. 配置文件非常简单。
  5. 支持 Rewrite 重写。
  6. 内置的健康检查功能。
  7. 节省带宽。
  8. 稳定性高。
  9. 支持热部署。

    3.4 什么是反向代理?

    反向代理是指以代理服务器来接受 internet 上的连接请求,然后将请求,发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
    反向代理总结就一句话:代理端代理的是服务端.

    3.5 什么是正向代理?

    一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
    正向代理总结就一句话:代理端代理的是客户端。

    3.6 什么是负载均衡?

    负载均衡即是代理服务器将接收的请求均衡的分发到各服务器中,负
    载均衡主要解决网络拥塞问题,提高服务器响应速度,服务就近提供,达到更好的访问质量,减少后台服务器大并发压力。

    3.7 Nginx是如何处理一个请求的?

    首先,nginx 在启动时,会解析配置文件,得到需要监听的端口与 ip地址,然后在 nginx 的 master 进程里面先初始化好这个监控的 socket,再进行listen,然后再 fork 出多个子进程出来, 子进程会竞争 accept 新的连接。
    此时,客户端就可以向 nginx 发起连接了。当客户端与 nginx 进行三次握手,与 nginx
    建立好一个连接后,此时,某一个子进程会 accept 成功,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体,接着,根据事件调用相应的事件处理模块,如 http 模块与客户端进行数据的交换。
    最后,nginx 或客户端来主动关掉连接,到此,一个连接就寿终正寝了。

    3.8 为什么Nginx性能这么高?

    得益于它的事件处理机制:异步非阻塞事件处理机制:运用了 epoll模型,提供了一个队列,排队解决。

    15 JWT

    JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
    在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
    每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
    4.1组成
    一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
    头部(Header)
    头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
    {“typ”:”JWT”,”alg”:”HS256”}
    在头部指明了签名算法是HS256算法。 我们进行BASE64编码(http://base64.xpcha.com/),编码后的字符串如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
    载荷(playload)
    载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
    1. 标准中注册的声明(建议但不强制使用)
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token
    2 .公共的声明
    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
    3. 私有的声明
    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
    这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim, JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。定义一个payload:
    {“sub”:”1234567890”,”name”:”John Doe”,”admin”:true}
    然后将其进行base64加密,得到Jwt的第二部分。
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRta W4iOnRydWV9
    签证(signature)
    jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
    header (base64后的)
    payload (base64后的)
    secret
    这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    注意
    secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
    4.2 使用场景
    1. 一次性验证
    比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其它可能的账户……这种场景就和 jwt 的特性非常贴近,jwt 的 payload中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。
    2. restful api 的无状态认证
    使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改……spring security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2 或 jwt 来做 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单。
    3.使用 jwt 做单点登录+会话管理(不推荐)
    4.3面试问题:
  1. JWT token 泄露了怎么办?(常问)

使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 并且使用 cookie 而不是 LocalStorage 存储 jwt,这样可以防止 XSS 攻击和 CSRF攻击。

  1. Secret如何设计?

jwt 唯一存储在服务端的只有一个 secret,个人认为这个 secret 应该设计成和用户相关的属性,而不是一个所有用户公用的统一值。这样可以有效的避免一些注销和修改密码时遇到的窘境。

  1. 注销和修改密码?

传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。分析下痛点:注销变得复杂的原因在于 jwt 的无状态。提供几个方案,视具体的业务来决定能不能接受:
仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧可以访问系统。
清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不变,但是由于 secret 不存在或改变,则无法完成校验。这也是为什么将 secret 设计成和用户相关的原因。
借助第三方存储自己管理 jwt 的状态,可以以 jwt 为 key,实现去 Redis 一类的缓存中间件中去校验存在性。方案设计并不难,但是引入 Redis 之后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差不多了。
修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret。

  1. 如何解决续签问题

传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不由己了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签。
解决方案
1. 每次请求刷新 jwt。
jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,那就让它变好了,每次请求都返回一个新的 jwt 给客户端。只是这种方案太暴力了,会带来的性能问题。
2.只要快要过期的时候刷新 jwt
此方案是基于上个方案的改造版,只在前一个jwt的最后几分钟返回给客户端一个新的 jwt。这样做,触发刷新 jwt 基本就要看运气了,如果用户恰巧在最后几分钟访问了服务器,触发了刷新,万事大吉。如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导致未刷新 jwt,无疑会令用户抓狂。
3. 完善 refreshToken
借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新 jwt。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。
4. 使用 Redis 记录独立的过期时间
在 Redis 中单独为每个 jwt 设置了过期时间,每次访问时刷新 jwt 的过期时间,若 jwt 不存在与 Redis 中则认为过期。

  1. 如何防止令牌被盗用?

令牌:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJObzAwMDEiLCJpYXQiOjE1NjkxNTg4MDgsInN1YiI6IuS4u-mimCIsImlzcyI6Ind3dy5pdGhlaW1hLmNvbSIsImV4cCI6MTU2OTE1ODgyMywiYWRkcmVzcyI6IuS4reWbvSIsIm1vbmV5IjoxMDAsImFnZSI6MjV9.lkaOahBKcQ-c8sBPp1Op-siL2k6RiwcEiR17JsZDw98
如果令牌被盗,只要该令牌不过期,任何服务都可以使用该令牌,有可能引起不安全操作。我们可以在每次生成令牌的时候,将用户的客户端信息获取,同时获取用户的IP信息,然后将IP和客户端信息以MD5的方式进行加密,放到令牌中作为载荷的一部分,用户每次访问微服务的时候,要先经过微服务网关,此时我们也获取用户客户端信息,同时获取用户的IP,然后将IP和客户端信息拼接到一起再进行MD5加密,如果MD5值和载荷不一致,说明用户的IP发生了变化或者终端发生了变化,有被盗的嫌疑,此时不让访问即可。这种解决方案比较有效。
当然,还有一些别的方法也能减少令牌被盗用的概率,例如设置令牌超时时间不要太长。

六、探花交友

一、项目介绍

探花交友是一个陌生人在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台提供大数据分析,通过后台推荐系统帮我们快速匹配自己的“意中人”。项目主要分为交友,圈子,消息,视频,我的五大块。项目的亮点是使用MongoDB 技术实现海量数据的存储;使用FastDFS实现图片的上传和下载;使用百度AI技术实现非人脸无法上传。项目采用前后端分离的方式开发,采用的是YApi在线文档进行数据交互管理。

1.1 开发技术

前端:
- flutter + android + 环信SDK + redux + shared_preferences + connectivity + iconfont + webview + sqflite
后端:
- Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo
- MongoDB geo 实现地理位置查询
- MongoDB 实现海量数据的存储
- Redis 数据的缓存
- Spark + MLlib 实现智能推荐
- 第三方服务 环信即时通讯
- 第三方服务 阿里云 OSS 、 短信服务

1.2 技术架构

Java面试宝典汇总202203 - 图78

1.3 开发方式

探花交友项目采用前后端分离的方式开发,就是前端由前端团队负责开发,后端负责接口的开发,这种开发方式有2点好处:
扬长避短,每个团队做自己擅长的事情
前后端可以并行开发,事先约定好接口地址以及各种参数、响应数据结构等。
Java面试宝典汇总202203 - 图79
我们在项目中使用YApi 在线接口文档来进行项目开发。YApi是一个开源的接口定义、管理、提供mock数据的管理平台。具体操作如下:
Java面试宝典汇总202203 - 图80
接口定义:
Java面试宝典汇总202203 - 图81

二、功能介绍

探花交友平台,涵盖了主流常用的一些功能,如:交友、聊天、动态等。具体功能如列表所述:

15.1 功能列表

功能 说明 备注
注册、登录 用户无需单独注册,直接通过手机号登录即可 首次登录成功后需要完善个人信息
交友 主要功能有:测灵魂、桃花传音、搜附近、探花等 通过不同的趣味模式让我们找到自己更熟悉的人
圈子 类似微信朋友圈,用户可以发动态、查看好友动态等
消息 通知类消息 + 即时通讯消息
小视频 类似抖音,用户可以发小视频,评论等 显示小视频列表需要进行推荐算法计算后进行展现。
我的 我的动态、关注数、粉丝数、通用设置等

2.2 注册登录

用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
Java面试宝典汇总202203 - 图82Java面试宝典汇总202203 - 图83Java面试宝典汇总202203 - 图84Java面试宝典汇总202203 - 图85

2.2.1 技术要点:

阿里大于 + Redis + JWT + OSS云存储 + 百度AI

2.2.2 涉及的表:
tb_user(用户表)
tb_info(用户详细信息表)

2.2.3 实现分析:

实现用户登录我们要处理四个接口来进行实现,分别是:
发送手机验证码(阿里大于)
- 阿里云平台短信发送
校验用户登录(Redis)
- 后台需要验证手机号与验证码是否正确
- 校验成功之后,需要按照JWT规范进行返回响应
首次登录完善个人信息(百度AI + OSS)
校验token是否有效(Redis + JWT)
- 校验存储到Redis中的token是否有效

2.2.3.1 发送手机验证码流程:

Java面试宝典汇总202203 - 图86
用户在客户端输入手机号,点击发送验证码,向后台tanhua-server进行请求,调用后台阿里云短信平台进行短信发送。tanhua-server服务会会携带用户给发过来的两个参数(用户的手机号和后台生成的随机6位数)通过调用阿里大于第三方平台进行接口的响应。如果阿里云短信平台在接受响应成功后,会把验证码发送给客户,并把这个验证码保存到Redis中,并给这个验证码设置有效期为5分钟。

2.2.3.2校验用户登录流程:

Java面试宝典汇总202203 - 图87
当用户在手机上接受到验证码之后,开始输入验证码校验。当用户输入完6位验证码之后,会自动触发后台tanhua-server服务进行校验功能。对用户传输过来的手机号和验证码进行验证。如果校验通过会根据客户的手机号生成一个token,给客户返回一份,方便用户在App上访问其他微服务的时候进行身份的识别。同时在通过RocketMQ响应给JWT的鉴权中心通知其他微服务。在用户通过校验之后,我们会做3个工作:

  1. 我们会根据用户的手机号给用户创建一个账户并存入mysql的用户表中。
  2. 我们会调用RocketMQtemplate发送一条消息给消息中间件,用来给后台做log日志的统计,便于后台管理系统展示平台用户日活量,注册量等等。
  3. 把创建的新用户id注册到环信通讯中,来实现即时通信的功能。
    2.2.3.3用户登录
    Java面试宝典汇总202203 - 图88
    考虑到我们项目采用的是前后端分离架构,我们采用JWT生成token的方式来给用户进行身份识别,当我们登录成功后我们会根据用户的id和手机号生成一个token返回给用户并保存到Redis中。这样用户在访问受保护的资源的时候,我们会在后台进行校验客户端发过来的token,如果校验通过则进行放行。
    在这块我们考虑到三个问题:
    1.是token续签;2. token的安全性; 3. 如何统一处理token;
    在解决token续签的问题上,我们这里采取的是在请求头获取token时判断token是否存在,如果不存在就创建token并保存到Redis中,如果存在我们就重新从Redis中获取token并进行续签(Duration.ofHours(1))。在安全问题上,我们这里采用加密加盐(SignatureAlgorithm.HS256,secret)的方式来解决。为了实现token统一校验,我们这里采用的是springmvc拦截机制+Threadlocal局部线程的方式来解决。这里我们自定义了一个TokenInterceptor拦截器,实现preHandle() 方法,这样的话就可以在用户请求进入controller层之前进行拦截,通过token获取了用户对象,并将用户对象存储到Threadlocal中,这样就实现了token统一校验的功能。具体的实现过程如下图:
    Java面试宝典汇总202203 - 图89
    2.2.3.4首次登录完善个人信息
    用户在完成登录操作之后,如果是第一次登录,用户会跳到完善个人信息页面,这块我们需要输入用户昵称,生日,所在地,设置头像。其中,头像数据需要做图片上传,这里采用阿里云的OSS服务作为我们的图片存储服务器。并且对头像要做人脸识别,非人脸照片不得上传,这里我们调用的是百度AI的人脸识别接口进行校验。我们需要在本地创建一个ossTemplate模板和aipFaceTemplate模板,把百度AI和阿里OSS给我们提供的SDK进行修改。我会在yml中创建以上两个模板所需要的参数。

    2.3 通用功能的实现

    Java面试宝典汇总202203 - 图90
    2.3.1 涉及的表:
    | tb_settings(通用设置) | | —- | | tb_question(陌生人问题表) | | tb_black_list(黑名单) |

2.3.2 实现分析:

通用设置,包含探花交友APP基本的软件设置功能。主要功能有设置陌生人问题,通用设置,黑名单等。设置陌生人问题这块主要为了在添加你的时候需要回答问题,根据用户回答的问题,进行确认关系。我们这块主要做问题的添加和问题查看。这块就是根据用户的id查询用户的问题,问题查看就是如果没有问题那么就设置问题,如果有就update重新更新一下。通用设置这块我们主要做的接口是给用户推送喜欢通知,推送评论通知,推送平台公告通知,这块我们根据用户的id去通用设置表里边保存或者更新用户通知情况。黑名单这块我们做的是黑名单列表的查询和黑名单的移除。主要查询用户表和黑名单表,如果黑名单移除,那么直接在黑名单里边根据用户的id删除。这块主要做了黑名单列表分页的问题,因为随着我们往下拉取,后台会根据用户的id动态展示黑名单列表。这块就使用到mybatisPlus的分页Ipage。通过创建Page对象,传入当前页,和每页条数,调用findPage(page,XXX条件)来实现;

2.4 今日佳人

在用户登录成功后,就会进入首页,首页中有今日佳人、推荐好友、探花、搜附近等功能。今日佳人,是根据用户的行为数据统计出来匹配值最高的人。缘分值的计算是由用户的行为进行打分,如:点击、点赞、评论、学历、婚姻状态等信息组合而成的。

Java面试宝典汇总202203 - 图91

2.4.1 技术要点:

MongoDB

2.4.2 涉及的表:

recommend_user(推荐用户表)

2.4.3 实现流程:

Java面试宝典汇总202203 - 图92

2.4.4 实现分析:

当我们进入首页之后,上边会有一个滑动区域,有今日佳人,探花,公告等模块。我在这块负责今日佳人模块实现,主要根据用户的id查找对应的推荐佳人表,根据缘分值进行排序,获取第一个进行展示。缘分值是我们通过用户平时浏览平台动态和浏览小视频,对浏览的内容进行点赞、评论的情况以及用户本身的学历、婚姻状态等信息进行计算的。我们会把计算的最终值存储到MongoDB中方便用户进行快速查询。当用户对圈子,视频的动态执行操作时,如:发布、浏览、点赞、喜欢等,平台就会通过RocketMQ给推荐系统发送消息。推荐系统接收消息,并且处理消息数据,处理之后将结果数据写入到MongoDB中,Spark系统拉取数据,然后进行推荐计算,计算之后的结果数据写入到Redis中,为每个用户都进行个性化推荐,如果有用户没有数据的,查询MongoDB中的默认数据。我们这块动态计分规则是:浏览 +1,点赞 +5,喜欢 +8,评论 + 10,文字长度:50以内1分,50~100之间2分,100以上3分。

2.5 公告

主要用于接收平台发布的一些消息和福利,比如:新增功能通知,活动通知,版本迭代通知。
Java面试宝典汇总202203 - 图93Java面试宝典汇总202203 - 图94

2.5.1 涉及的表:

Tb_nnouncement(公告表)

2.5.2 实现分析:

这块我们主要做两个事情,一个是公告查询分页,一个是公告对象转换VO。分页的话我们这里使用的是MybatisPlus的分页功能,公告对象转化VO主要是为了解耦和不做代码污染,我们通过创建一个list的方式把公告对象遍历到我们创建的AnnouncementVO对象中,在这里我们要做一个时间的校验,如果创建时间不存在我们需要给他生成个时间在进行加入。

2.6 圈子

类似微信朋友圈,我们可以发布图片,文字;可以对圈子里边的内容进行点赞,评论,喜欢操作。和微信朋友圈不一样的功能是微信只能查看好友的圈子,但是我们这个平台圈子推荐的不仅仅是好友还有系统智能推荐用户点赞喜欢的好友。
Java面试宝典汇总202203 - 图95Java面试宝典汇总202203 - 图96

2.6.1 技术要点:

MongoDB + Redis + MongoDB geo + rocketMQ + 华为云内容审核 + Spark + Mllib

2.6.2 涉及的表:
quanzi_publish(发布表)
quanzialbum{userId}(相册表)
quanzitime_line{userId}(时间线表)
quanzi_comment(评论表)
tanhua_users(好友表)
tb_user(用户表)

2.6.3 实现分析:

2.6.3.1 通过页面分析,剖析如下:

1、推荐频道为根据问卷及喜好推荐相似用户动态(RocketMQ+Spark + MLlib)
2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态
3、图片最多不超过6张或发布一个小视频(FastDFS)
4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整。(MongoDB Geo)
5、动态下方显示距离为发布动态地与本地距离
6、显示用户浏览量
7、显示点赞数、评论数 转发数

Java面试宝典汇总202203 - 图97

2.6.3.2 发布流程:

Java面试宝典汇总202203 - 图98
- 用户发布动态,首先将动态内容写入到发布表。
- 然后,将发布的指向写入到自己的相册表中。
- 最后,将发布的指向写入到好友的时间线中。

2.6.3.3 查看流程:

Java面试宝典汇总202203 - 图99
- 用户查看动态,如果查看自己的动态,直接查询相册表即可
- 如果查看好友动态,查询时间线表即可
- 如果查看推荐动态,查看推荐表即可
由此可见,查看动态的成本较低,可以快速的查询到动态数据。

2.6.3.4 实现话术:

我负责项目中的圈子功能的实现,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。我们这里采用spark + mllib实现智能推荐,这样的话就会根据我们自身的条件,喜好进行匹配推送用户感兴趣的动态。针对用户上传的动态我们采用rocketMQ发送消息调用华为云内容审核进行自动处理,考虑到需要处理海量的动态信息数据,我们这里选用了MongoDB+Redis的形式来进行数据的存储,其中MongoDB负责存储,Redis负责缓存。
首先是查询好友动态接口的实现,用户点击好友动态之后,服务器会从ThreadLocal中获取用户id,根据用户id从该用户对应的好友动态时间线表中查询出所有好友动态id以及对应的好友id,再根据动态id从总动态表查询动态详细信息,根据动态发布的时间倒序排序。再根据好友id从用户详情表中查询好友用户详细信息,再将这些信息构造成vo对象返回给前端进行分页展示。
接下来说一下用户发布动态接口的实现,用户在客户端写好文本,上传好图片,确定好位置点击发布按钮之后,请求会携带一系列参数,文本内容,图片文件,地理位置的经纬度等,后端代码先创建对象,设置相应的属性,保存到MongoDB动态表中,此时动态的状态属性默认设置为0,表示待审核。然后通过rocketMQ向消息中间件发送一条消息,包含动态id,消息消费者接收到消息之后会调用华为云的内容审核服务,对文字信息和图片信息进行审核,审核通过后将动态的状态属性改为1,表示已通过,不然就改为2,表示驳回。然后前端在展示动态的时候只会查询状态为已通过的动态。动态发布成功之后也会像消息中间件发送消息,消息内容包括动态id(objectId类型)、用户id、操作类型、Long类型的pid。消息消费者会以此生成一条该用户对该动态的一个评分记录保存到MongoDB的动态评分表中,作为大数据推荐的依据。
然后说一下用户对动态的一系列操作的接口的实现。我们是将用户的点赞、评论、喜欢操作记录到MongoDB的一个comment表中,用type字段来区分具体的操作类型,比如1代表点赞,2代表评论,3代表喜欢。用户进行操作之后服务器就会生成相应操作类型的comment记录保存到MongoDB的comment表中。保存成功后向消息中间件发送消息,消息内容包括动态id(objectId类型)、用户id、操作类型、Long类型的pid。消息消费者会以此生成一条该用户对该动态的一个评分记录保存到MongoDB的动态评分表中,作为大数据推荐的依据。
最后是推荐频道的动态展示接口的实现,服务器使用Spark + Mllib技术,根据用户的动态评分表,基于用户userCF算法进行推荐,将推荐的结果以pid组成的字符串形式存入Redis。用户进入推荐频道之后,服务器去查找Redis有没有推荐结果,如果没有的话,就从MongoDB中查询默认的推荐动态数据展示,如果有推荐结果,就获取字符串,转换成pid的数组,再根据这些pid从MongoDB的总动态表中查找对应动态信息,构造vo对象的分页结果,返回给前端展示给用户。

2.7 小视频

用户可以上传小视频,也可以查看小视频列表,并且可以对推荐的视频进行点赞操作。通过页面分析我们需要获取视频展示出来的视频封页,好友的头像,昵称,点赞数量。
Java面试宝典汇总202203 - 图100

2.7.1 技术要点:

FastDFS + CDN

2.7.2 涉及的表:
video(小视频表)
quanzi_comment(评论表)
quanzi_publish(发布表)
tb_user(用户表)

2.7.3 实现分析:

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。对于存储而言,小视频的存储量以及容量都是非常巨大的,所以我们选择自己搭建分布式存储系统 FastDFS进行存储。对于推荐算法,我们将采用多种权重的计算方式进行计算,对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速。发布视频的流程是:客户通过点击发布视频,会向后台传递3个数据,一个是视频封面图片,一个是视频,一个是文字。我们在后台会把视频封面图片保存到阿里的OSS图片存储服务器上会给我们返回一个图片的存储链接,把视频上传到本地部署的FastDFS上边会给我们返回一个视频的存储链接,之后我们这边构造一个Video对象,把需要的参数(文字,图片URL,视频URL,用户ID)通过调用VideoAPI进行保存到MongoDB中,之后构造返回值给用户进行显示发布成功。接下来用户会跳转到小视频列表页面。分页查询视频列表是根据后台推荐系统进行智能推荐的,这个根据用户浏览视频的时候点赞,喜欢,评论的情况进行分数积累,把对应的分数值存储到推荐表中,当用户下拉刷新的时候,会从MongoDB中查询出推荐的视频。

2.8 探花

左划喜欢,右划不喜欢,每天限量不超过50个,开通会员可增加限额。双方互相喜欢则配对成功,可以互加好友。
通过页面分析:我们需要展示用户的信息,包括:头像,昵称,年龄,是否单身,学历等。
实现:数据来源推荐系统计算后的结果。

Java面试宝典汇总202203 - 图101

2.8.1 技术要点:

MongoDB + OSS + spark + 环信

2.8.2 涉及的表:
recommend_quanzi(推荐表)
tanhua_users(好友表)
tb_user(用户表)

2.8.3 实现分析:

当用户的通过点击探花进入时,会根据用户的id去MongoDB中查询10条推荐的好友信息,推荐依据是来源于推荐系统通过spak + Mallib 根据用户平时喜欢,点赞,评论进行生成。当用户选择喜欢的时候我们会把用户和喜欢好友的id写入到环信通讯中,再重新存入到MongoDB中,方便用户做选择。当用户选择不喜欢时候,我们要清除当前数据,不予以推荐。我们这块实现主要做了三个接口,分别是展示卡片列表接口,右滑喜欢接口,左滑不喜欢接口。展示列表我们根据获取的用户id去推荐表中查询默认的推荐好友。如果选择不喜欢会根据当前用户的id和推荐人的id调用接口将该用户的从推荐表中删除,如果选择喜欢,会根据当前用户的id和推荐人的id通过调用之前的一个喜欢接口生成一个实例保存到mongoDB数据库中,并且判断所喜欢的用户是否为自己的粉丝,如果是的话则将两人添加到好友关系表中,并且将环信中的两人绑定为好友关系,这样在好友列表中可以查询到对方并且可以进行聊天了。

2.9 测试灵魂

测灵魂是我们项目的一大亮点。平台会提供对应的初级灵魂,中级灵魂,高级灵魂对应的答卷。通过用户答题,我们会在后台进行用户行为数据分析,给用户发送一份信息报告,同时也会推荐和用户本人灵魂属性相符的人。我们这块会为每个灵魂等级设置10个问题,测试题为顺序回答,回答出初级题,我们才可以点击中级灵魂测试。

Java面试宝典汇总202203 - 图102Java面试宝典汇总202203 - 图103Java面试宝典汇总202203 - 图104

2.9.1 技术要点:

MySQL + MongoDB

2.9.2 涉及的表:

tb_questionnaire(调查问卷)
tb_test_question(问卷问题)
tb_option(问题选项)
tb_conclusion(性格信息) Mysql中的表
testSoul_Questionnaire (调查问卷) MongoDB中的表
testsoul_user_score(用户得分)
testsoul_report

2.9.3 实现分析:

灵魂测试是通过用户答题的方式来进行分类,寻找同一类型或者内心深处的朋友。我们这块主要采用的技术有:MongoDB +Sprak+Mllib 当用户点击灵魂测试,我们会去后台加载用户的灵魂信息。如果是新用户我们会停留在初级灵魂题,后边的中级灵魂,高级灵魂则需解锁上一级才能进行答题。如果是老用户我们会加载用户之前测试灵魂的结果进行展示。如果之前他测评通过的是高级灵魂,那么就可以查看初级,中级,高级灵魂的测评。每一级的测评灵魂题都会有10道题,测评结束之后会根据用户的回答生成一个测试报告,通过点击我们可以查看属于自己的唯一一份灵魂报告。生成的报告主要根据用户的属性外向,判断,抽象,理性组成。我们可以把自己的报告进行分享或者选择重做。当进行报告查看时候我们还可以查看系统给我推荐的测试结果相同的好友。通过点击好友头像,我们可以通过回答陌生人问题来进行确认好友,如果对方回复并通过就可以进行正常的交友啦。

2.10 桃花传音

Java面试宝典汇总202203 - 图105Java面试宝典汇总202203 - 图106Java面试宝典汇总202203 - 图107Java面试宝典汇总202203 - 图108

2.10.1 技术要点:

MongoDB + FastDFS + Quartz

2.10.2 涉及的表:
user_sound(用户语音)
user_soundTime(语音剩余次数)
tb_user(用户表)

2.10.3 实现分析:

语音匹配类似漂流瓶类似,可以发送匿名语音,可以收听匿名语音。收听完匿名语音可以选择喜欢或不喜欢。如果双方互相喜欢那么可以配对成功互相关注,一个人每天只能接收8次,自己接收过语音,其他人不能再使用。如果双方都点击喜欢,就可以添加为好友。这块我们采用的是MongoDB做数据存储,使用Dubbo做分布式服务的调用,使用FastDFS存储语音。用户可以发布一段语音文件,上传到fastDFS中,并且将返回的地址值,与当前用户的一些信息封装成实例保存到mongoDB中,其他用户会进行接收操作时,随机在数据库中获取一条语音信息,然后进行选择打招呼,和扔掉,打招呼的话就可以进行调用环信的发送一条信息,如果对方回复的话则成为好友,同时该语音文件会在数据库中删除,如果扔掉的话则不会再数据库中消失,继续等待被获取。

七、黑马头条

一、项目介绍
1 项目概述
黑马头条是类型与今日头条的新闻类型软件,用户可以查看他人发布的新闻头条,对其进行关注点赞之类的,也可以自己申请成为发布人,发布的内容,可以由平台管理员在后台管理系统对发布的内容进行审核、查看、也能对已经发布的进行下架等权限操作。
2 技术架构
黑马头条项目采用的是分布式微服务+大数据技术架构实现的,其中使用的技术有SpringCloud、springboot、nacos、freemarker、oss、redis、Kafka、git和一些第三方接口,比如阿里云的文章和图片审核。
二、业务介绍
1 登录及网关校验
登录:
用户点击登录,后台系统会根据登录的用户进行判断,如果用户是登录的操作,会判断其登录的数据是否有效,如果数据不一致不让其登录,只有账号和密码对比一致才放行,生成一个token返回,后续用户的所有操作都需要携带token,没有就不让访问。
网关校验:
在网关中配置全局过滤器,对除登录以外的操作都进行拦截,校验是否在请求中存在token,校验token是否有效或过期,如果没有问题,才会路由到具体微服务进行访问数据。
2 app端用户实名认证
普通的app端用户想要成为自媒体人,具备发布文章的权限,需要进行实名认证,用户需要上传身份证等身份识别信息,系统会根据app端用户上传的资料进行审核,审核包括自动审核和人工审核
自动审核:
调用第三方的接口(公安系统,百度ai有提供该接口,审核一次费用为6毛)对用户上传的身份信息进行鉴别。
人工审核:
如果用户上传的身份信息比较模糊,或者第三方接口不能确定其身份,可以转入人工审核。人工审核是由官方的管理人员进行肉眼鉴别。
不管是自动审核或人工审核,一旦通过审核后,会在自媒体端开通一个账号,用户登录自媒体端可以进行文章的发布。
3 自媒体素材管理
自媒体人发布文章的时候,有可能需要在文章中插入图片,所以在后台系统可以让用户统一进行图片的管理,我们的处理是将上传的图片保存到minio中,本地mysql只存储图片的路径。
我们的功能是:可以上传图片,删除图片,也可以对图片进行筛选收藏,在用户写文章选择的时候可以更方便的查找。
4 自媒体文章发布
自媒体人登录到自媒体系统后,可以编写自己的文章进行发布,比如,设置文章的标题,内容(文本内容和图片),文章分类,发布时间,封面图片设置等等。
我们的系统,主要是针对于一些新闻咨询类,或是小长文,目前只提供了在自媒体端进行文章的发布,并没有在移动端提供发布文章的入口,主要是考虑,手机端发布文章不太方便,后期有可能会继续添加该功能。
5 自媒体文章审核
自媒体人发布文章以后,最终是供app端的用户进行检索查看,在app端展示之前,需要对文章进行一个审核。主要审核的是文章文本内容或图片是否存在违规。审核主要包含两部分,第一是自动审核,第二是人工审核。
自动审核:
我们针对于文章的文本内容审核的时候,主要有两种方式,第一是优先查看文章是否包含了自管理的敏感词内容,第二个是调用了阿里的自然语言处理的接口进行审核。如果审核失败,则发布不成功。
我们针对于文章的图片审核的时候,主要是依赖于阿里云的第三方接口进行审核,违规,则文章发布不成功。
人工审核:
当调用阿里云的接口,审核的结果为不确定的时候,会自动转到人工审核,由后台的管理人员进行肉眼查看鉴别文章是否违规。
6 自媒体文章定时发布
在自媒体人发布文章的时候,是可以指定文章的发布时间的,作者可以选择当下时间,也可以选择一个未来的时间进行发布文章。我们的处理思路是,采用的redis实现的延迟队列来完成的。
如果用户选择的当下时间,我们把它存储到redis的list中进行排队审核(左进右出,或右进左出)
当用户选择的是未来时间,我们使用的是redis中的zset进行存储,主要是因为zset可以按照分值进行排序,我们设置的分值就是文章发布时间的毫秒值,如果到了发布时间,则会自动导入(定时任务实现,每分钟同步数据)到list进行消费数据。
7 app端文章详情展示
自媒体人编写的文章通常字数都比较多,有可能是是几千字,当这些大文本数据加载的时候,如果是直接查询数据库进行加载,效率是比较低的,所以我当时设计的时候,就采用了静态模板技术,当文章发布审核通过以后,我们就会针对文章内容生成对应的html静态文件,并且直接上传了minio中进行访问,当用户查看详情的时候,可以直接从minio中访问文章的静态详情页面,无须再次查询数据库
这样做(生成静态html文件),后期也方便使用cdn进行网络缓存优化,加快文章详情页面的响应速度。
8 app端文章搜索
文章搜索功能
app端用户可以在搜索框中输入关键字进行文章的检索,后台使用的技术是全文检索技术,elasticsearch来实现的。我们针对于用户输入的关键字进行分词之后到文章的标题和内容中进行检索,并且在返回数据的时候,设置了标题字段为高亮展示。
es数据索引同步
当自媒体人发布的文章审核成功后,我们会把该文章同步到索引库里面,供用户进行搜索,这块主要使用的MQ来完成的,增加了数据的伸缩扩展性。
当文章被下架或删除的时候,我们也会使用MQ通知es索引库进行数据的同步。
9 热数据处理
app端用户访问的首页主要展示是近期的热点数据,热点数据的评判标准为:文章的:点赞量,收藏量,评论量,阅读量
我们在实现筛选热文章数据的时候,采用了两种方式:第一个是定时计算文章的热度,第二个是实时计算文章的热度。
定时计算:
我们使用定时任务,每天凌晨1点去检索前5天的所有文章进行分值的计算(点赞量,收藏量,评论量,阅读量),并且会根据不同的行为进行加权处理,比如:阅读的权重是没有点赞的权重高的。最终计算完文章的分值以后,会进行排序,把热度较高的数据存入redis缓存中。当用户去访问app端首页的时候,首先去查看是redis中的数据,并且是每天计算后的热点数据。
实时计算:
实时计算是当天文章的热度,我们使用的kafkaStream来完成实时计算。当天用户行为的权重会更高,我们会实时的根据用户的行为,更新文章的分值,把相对较高的文章数据展示在app端首页的最前面。同样也是按照分值进行排序。
三、相关技术
1 nacos
nacos是springCloud Alibaba中的一个组件
Nacos 致力于服务发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,可以实现动态服务发现、服务配置管理、服务及流量管理。 可以更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构的服务基础设施。
2 nacos与eureka的区别
①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
②临时实例心跳不正常会被剔除,非临时实例则不会被剔除
③Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
④Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
3 minio
minio可以云存储海量的图片、视频、文档,minio有着数据保护、高性能、可扩容、SDK支持、有操作页面、功能简单、丰富的api、文件变化主动通知等特点。
它的读写速度比之前用的fastdfs要快很多。都属于分布式文件存储系统。
4 freemarker
freemarker是一款模板引擎技术,可以用来生成一些静态文件,比如:html文件,java文件,也可以做为springmvc的视图。
我们在项目中就是文章详情里面使用了freemarker来生成html静态文件。直接访问静态文件效率会更高。
技术对比:
1.Jsp 为 Servlet 专用,不能单独进行使用。
2.Thymeleaf 为新技术,功能较为强大,但是执行的效率比较低。
3.Velocity从2010年更新完 2.0 版本后
5 分布式主键
我们在项目中的文章表中,使用了雪花算法来使用主键,主要是因为文章的数据,随着业务量的增大,我们做了水平分库分表的操作,这样使用了雪花算法以后,也不会导致主键的重复。
雪花算法是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
6 敏感词过滤dfa算法
dfa算法敏感词过滤分两个阶段过滤:
第一:
一次性的把所有的敏感词存储到多个map中,会把每个字存入map的key,一次敏感词就是一个嵌套的map.里面必须包含一个属性为:idEnd;根据isEnd来判断是否是敏感词的结尾词,如果是1就等于是结尾词,如果是0就是非结尾词。
第二:
就是让文章的内容到初始化后的敏感词库中去找是否存在敏感词,就是通过一个字一个字的遍历来寻找是否存在一个连续的词组,如果找到就是包含了敏感词。
7 分布式锁setnx
我们项目中主要采用的redis的setnx的特性来做的敏感词。
这种加锁的思路是,如果 key 不存在则为 key 设置 value,如果 key 已存在则 SETNX 命令不做任何操作
1.客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2.客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3.客户端A执行代码完成,删除锁
4.客户端B在等待一段时间后再去请求设置key的值,设置成功
5.客户端B执行代码完成,删除锁
8 kafka
8.1 介绍一些Kafka的一些概念

  • producer:发布消息的对象称之为主题生产者(Kafka topic producer)
  • topic:Kafka将消息分门别类,每一类的消息称之为一个主题(Topic)
  • consumer:订阅消息并处理发布的消息的对象称之为主题消费者(consumers)
  • broker:已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker)。 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。
  • 分区Partition

主题可以被分为若干个分区(partition),同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性。topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个文件进行存储。partition中的数据是有序的,partition之间的数据是没有顺序的。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。

  • 副本Replica

Kafka 中消息的备份又叫做 副本(Replica),副本的数量是可以配置的,Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica);所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从ISR列表(保持同步的副本列表)中删除,重新创建一个Follower。

  • Zookeeper

kafka对与zookeeper是强依赖的,是以zookeeper作为基础的,即使不做集群,也需要zk的支持。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行重平衡。

  • 消费者群组Consumer Group

生产者与消费者的关系就如同餐厅中的厨师和顾客之间的关系一样,一个厨师对应多个顾客,也就是一个生产者对应多个消费者,消费者群组(Consumer Group)指的就是由一个或多个消费者组成的群体。

  • 偏移量Consumer Offset

偏移量(Consumer Offset)是一种元数据,它是一个不断递增的整数值,用来记录消费者发生重平衡时的位置,以便用来恢复数据。

  • 重平衡Rebalance

消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。
8.2 你知道 Kafka 是如何做到消息的有序性?
生产者在写消息的时候,可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
也可以在发送消息的时候,可以指定同一个分区,同一个分区下的数据是有序的。
8.3 Kafka如何保证高可用
集群:

  • Kafka 的服务器端由被称为 Broker 的服务进程构成,即一个 Kafka 集群由多个 Broker 组成
  • 这样如果集群中某一台机器宕机,其他机器上的 Broker 也依然能够对外提供服务。这其实就是 Kafka 提供高可用的手段之一

数据副本复制机制:
ISR(in-sync replica)需要同步复制保存的follower
如果leader失效后,需要选出新的leader,选举的原则如下:
第一:选举时优先从ISR中选定,因为这个列表中follower的数据是与leader同步的
第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取
极端情况,就是所有副本都失效了,这时有两种方案
第一:等待ISR中的一个活过来,选为Leader,数据可靠,但活过来的时间不确定
第二:选择第一个活过来的Replication,不一定是ISR中的,选为leader,以最快速度恢复可用性,但数据不一定完整
8.4 Kafka 分区的目的?
分区对于 Kafka 集群的好处是:实现负载均衡。分区对于消费者来说,可以提高并发度,提高效率,增加消息的伸缩性
9 分布式事物seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案;
Seata事务管理中有者三个重要角色:TC-事务协调者,TM-事务管理者,RM-资源管理者;
Seata提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入;
TCC模式:最终一致的分阶段事务模式,有业务侵入;
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式;
SAGA模式:长事务模式,有业务侵入。
AT模式原理
阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据到更新前

10 分布式任务调度框架xxl-job
大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台
分布式任务调度,可以解决以下问题:

  • 做集群任务的重复执行问题
  • cron表达式定义可以统一在平台进行管理控制
  • 定时任务失败了,可以进行日志查看,并且可以发警告邮件
  • 如果任务量过大,可以平均的分到多个分片中去同时执行任务(业务量取模的方式 业务量%总分片数)

11 kafkaStream流式计算
kafkaStream是一个kafka的一个特性,我们在项目中使用了实时的热点文章计算,用kafkaStream来作为实时计算的技术主要是因为
①:可以非常方便的集成到java的项目中,只需要导入对应的类库即可使用
②:我们的业务量目前并不是很大,使用kafkaStream完全可以支撑我们项目的运行,如果后期业务量上来以后,我们也考虑了使用像flink等大数据技术来实现。
kafkaStream中提供了像时间窗口和聚合的操作。通过kafka发来的消息,进行时间窗口进行聚合,最终计算处理完消息后把消息发送出去进行下一步的处理。

八、其他面试题

1.nacos与eureka的区别

参考回答:
① Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
② 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
③ Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
④ Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;

  • Eureka采用AP方式
  • naocs默认是AP模式,可以采用CP模式

    2 项目中是如何解决高并发的问题的?

    参考回答:

  • 前端处理

    • 采用前后端分离的模式,前端项目单独部署到服务器上面
    • 前端加入CDN 加速服务
    • 前端引入Nginx,如果不够,加入集群Nginx
  • 后端处理

    • 服务单一原则,如app端目前涉及到了5个微服务,如果某一个压力较大,可根据实际情况增加单个服务集群数量
    • 尽量使用缓存技术来做
    • 限流
    • 降级
    • 熔断

      3 项目中做了哪些优化或者哪块使用了缓存?

      参考回答:
  • 在进行开发文章自动审核的时候使用到了缓存,用到了缓存的定时任务;

  • 在开发app端对文章进行点赞、关注、不喜欢、与文章收藏中也使用到了缓存
  • 热点文章存储在redis中,首页加载首先访问缓存*
  • 文章详情-大文本内容静态化方案,不用直接查询,有利于cdn
  • 文章内容存储,大文本压缩存储

    4 你们项目组的成员有哪些?

    参考回答:
    10人团队 4个后端 (运维1个 测试2个 2个前端 产品经理 1人) 项目经理 1人
    核心思想为:不易说团队太大,通常研发团队不超过10人,都是以组为单位

    5 你们开发业务的流程是什么?

    参考回答:
    1,产品经理说需求—》后端+前端+测试 2,定接口 3,后端的工作:设计(技术选型,数据库表)+编码 前端 开发页面 4,部署测试环境 5,接口联调 6,测试 —> 提bug 专门的bug管理工具(禅道) 每个人都会开通一个禅道账号 个人修复bug以后,提交为 已解决———》 测试人员回归测试 7,部署正式环境

    6 业务量急增,某个接口被疯狂调用怎么解决?

    参考回答:
    ①:用户登陆后生成token,返回客户端,在网关进行校验
    ②:可以在网关中设置限流操作,通常的限流操作有:令牌桶算法

    7 你们的app用户量有多少?你们项目的的qps是多少、有多少台服务器?

    参考回答:
    我们app端用户量,目前是10万,经过测试,最高的并发集中在晚上7点至9点,其中有查询文章的接口最高有几次达到了接近2000的qps
    目前生产的服务器使用的tomcat9,使用jmeter压测后,处理的并发数极限为400左右,所以文章那个微服务通常都是6、7台服务器做了集群。其他访问量较少的集群数量更低一些。

    8 你们项目的开发周期?

    参考回答:
    我们的项目开发周期大概7个月左右,平时后台有4个人一块开发,中间偶尔也会有人员变动,每个人负责不同的模块进行开发。

    9 讲一下当业务出现异常的时候,你是怎么处理的?

    参考回答:
    1,先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题,解决问题
    2,如果问题较为复杂,情况就可能会有多种,可能是代码的问题,也有可能是数据的问题

    10 你的需求是用什么方式分析的、用了什么工具?

    参考回答:
    1 产品经理先开会沟通,讲明白需求(最好拿到产出物料,如原型、需求文档)
    2 拿到任务以后,通过原型和需求抽象实体,主要用到的工具是PowerDesigner、也可以是word文档(不专业)
    3 创建实体之间的表,确定是否存在新的技术,最后实现

    11 怎么样从目前跑的业务中查找出哪些是慢sql?

    参考回答:
    我们项目是微服务开发,做了微服务的监控,使用的是skywalking,当有接口比较慢,是可以通过图形界面看到的,针对于较慢的接口是有排名的,然后再具体分析是否是sql问题,或者是其他问题

    12.你们系统的并发有多少

    一般的系统都是没有太大的并发的,所以不要张口就来几千几万的并发,像国内的一些二线电商比如苏宁瓜子二手车他们的一些核心接口的并发也才3000+,所以我们做的初创型的系统不要说太多,如果你对高并发的解决方案不太熟悉那么你可以回答几十(50-99),如果对并发的解决方案比较熟悉的话可以回答几百(500+)

    13.看你们线上使用了es,那么你们的数据量大概有多少?用了多少台服务器进行存储

    es节点的分片推荐配置方案:
    每个分片推荐大小是10-30G
    分片的数量 = 节点数量*1-3倍

14.你们的项目做了多久,你在里面主要做什么,是否有网址可以访问一下

  1. 一般的项目的开发周期都是控制在4个月以内,因为超过4个月失败的概率会急剧增加
  2. 主要的职责就看你定位如何,简历编写如何了,初级开发可以做一些增删查改的业务,中级开发可以做一些异常单管理系统、单点登录、购物车、秒杀等。
  3. 告诉面试官就是因为疫情公司垮掉了才出来找工作的,所以项目不能访问或者系统属于内部系统无法提供外网地址或者自己在写简历的时候就从码云上找一些符合当前预期并且提供了外网访问地址的项目到自己的简历中

15.在项目的开发过程中你是否遇到过特别坑的问题或者特别难忘的问题

这个问题考察的点是想看下你是否真的做过开发,如果你的简历中写了2-3年的工作经验那么你肯定是遇到过一些事情的,如果你一个都没遇到过那么可能你的简历就存在水分了或者你开发的全部是没有技术含量的功能,同时如果你遇到过看下你遇到的事情深度怎么样然后如何去解决的,从侧面看下你在公司开发的功能水平怎么样从而推测你的个人能力以及在上一家公司的地位水平。
要像解决这个问题大家可以准备一个jvm的或者mysql优化的或者用设计模式去优化复杂系统的场景去回答。