Java 面试总结

7 篇文章 0 订阅
9 篇文章 0 订阅

java 基础

说说你对jvm中类加载过程的理解?

在这里插入图片描述
源码.java —>(javac编辑器)—>字节码.class —> 类加载器(JVM)—>运行时数据区(JVM)—>执行引擎(JVM) —>机器码 —>机器识别处理

类加载过程了包括五个阶段:加载、验证、准备、解析、初始化

  • 加载
  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将二进制字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的java.lang.Class对象,作为这个类在方法区中各种数据的访问入口
  • 验证
  1. 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全。
  2. 验证阶段的四个步骤:
    文件格式检验:这一阶段主要是为了验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理
    元数据检验:是对字节码描述的信息进行语义分析,以保证其描述的信息符合 java 语言规范的要求。
    字节码检验:通过数据流控制流分析,确定程序语义是合法符合逻辑的。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
    符号引用检验:这个阶段的校验是发生在虚拟机将符号引用(间接引用)转化为直接引用的时候,但是这个转化的动作是发生在解析阶段
  • 准备
  1. 该阶段正式为类变量分配内存并设置类变量初始值。这些变量所使用的内存将在方法区中进行分配。此时进行内存分配的仅包括类变量,而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。
  2. 在这里分配的静态类变量是将其值定义为默认值而非声明值。因为在该阶段并未执行任何Java方法,正确的赋值将在初始化阶段执行。
  • 解析
  1. 该阶段虚拟机会将常量池内的符号引用(间接引用)替换为直接引用的过程。
    符号引用:代码中声明的变量名称,与虚拟机中内存的布局无关
    直接引用:可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。与虚拟机中内存的布局有关。
  • 初始化
  1. 这是类加载的最后一步,真正执行类中定义的字节码,也就是.class文件。 初始化阶段是执行类构造器方法的过程,以及真正初始化类变量和其他资源的过程

有的书籍也分为加载、连接【验证、准备、解析】和初始化三个阶段。

java中的集合类有哪些?

在这里插入图片描述

Collection

List

对于实现了RandomAccess 接口的List 是可以实现随机访问(RandomAccess 类型作为判断是否可以随机访问的标识)

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
 if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
 	return Collections.indexedBinarySearch(list, key);
 else
    return Collections.iteratorBinarySearch(list, key);
}

总结:实现了RandomAccess 接口的list,优先选择普通for循环,其次选择foreach,未实现RandomAccess 接口的list,优先选择Iterator遍历,foreach 底层也是使用Iterator 实现,大数据量禁忌使用for遍历

ArrayList
  1. 底层:使用Object [] 数组来存储数据
  2. 特点:数组在内存中是一块连续的内存空间
  3. 优点:由于属于数组可以直接使用下标找到对应数组位置的元素,所以对于随机查询很有优势
  4. 缺点:由于内存空间的连续性,在添加和删除元素操作的时候需要移动部分元素的空间位置,所以效率比较低下。
  5. 内存消耗:由于它们使用的扩容机制决定了用数组来存储数据会浪费数组未实际存储的空元素位置的内存空间
LinkedList
  1. 底层:底层使用链表的方式( transient Node first ) 来存储数据(jdk1.6之前使用循环链表,jdk1.6之后改为双向链表)
  2. 特点:链表中的节点在内存中不要求连续性,是散列分布在内存中。每个节点利用首尾指针进行连接成线性结构
  3. 优点:由于节点是使用首尾指针域连接前驱和后继结点,所以在对链表进行添加/删除元素操作的时候只需要改变目标节点前驱和后继结点的指针域即可
  4. 缺点:由于链表式结构办法使用下标快速随机查找,所以每次查找都要遍历一遍链表才行
  5. 内存消耗:每个结点都会因为使用首尾指针域来连接该节点的前驱/后继结点,造成内存消耗
Vector
  1. 底层:底层都是使用Object [] 数组来存储数据
  2. 初始值:两者在没有设置初始容量的时候,均是默认初始化一个长度为10 的Object 数组
  3. 扩容:ArrayList (int newCapacity = oldCapacity + (oldCapacity >> 1)😉 每次扩容都是原来数组长度的1.5倍。Vector (int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity)😉 每次扩容都是原来数组长度的2倍
  4. 安全性:ArrayList 是线程不安全的,Vector 的操作方法都使用synchronized 同步锁修饰,所以线程安全。

Set

HashSet
  1. 底层:使用 HashMap<E,Object> map 来存储数据(准确来说是使用 HashMap 的 key 来存储数据)
  2. 特点:元素不重复,无顺序
  3. 优点:可以实现集合内元素的自动去重
  4. 缺点:
  5. 内存消耗:
LinkedHashSet
  1. 底层:底层是TreeMap。
  2. 特点:提供一个使用树结构存储Set接口的实现,对象以升序顺序存储,访问和遍历的时间很快。
  3. 优点:有顺序
  4. 缺点:
  5. 内存消耗:
TreeSet
  1. 底层:继承自 HashSet
  2. 特点:以元素插入的顺序来维护集合的链接表,允许以插入的顺序在集合中迭代; 底层是HashMap。
  3. 优点:有顺序
  4. 缺点:
  5. 内存消耗:

Queue

LinkedList

双向队列

PriorityQueue
  1. 底层:使用 Object[] queue 来存储数据
  2. 特点:实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。
  3. 优点:有顺序
  4. 缺点:
  5. 内存消耗:

Map

HashMap

底层实现:
  1. jdk1.8之前:使用数组和链表的方式结合在一次使用也就是链表散列( transient Node<K,V>[] table; ),即数组中每个元素都是一条链表的首节点。
  2. jdk1.8之后:在链表散列的基础上做了增强,即当链表的长度大于8的时候链表就会自动转为红黑树结构存储,数组中的每个元素都为一棵红黑树的根结点
存储过程:一个key/value执行put 操作后,把key计算出hashcode。hashcode % length = 数组上的存储位置(当length为2的幂次方时候,hashcode % length == (length-1) & hashcode)。
// 一般计算hashcode方式:
public int hashCode() {
	int h = hash;
 	if (h == 0 && value.length > 0) {
 		char val[] = value;
		for (int i = 0; i < value.length; i++) {
	 		h = 31 * h + val[i];    // 以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模
	 		// 因为计算机对乘法和除法的计算性能低下,一般使用移位法代替乘/除法计算提升性能。使用31可以得到更好的性能: 31 * i == (i << 5) - i
 		}
 		hash = h;
 	}
 	return h;
 }
 // HashMap 中的扰动函数hash:
 static final int hash(Object key) {
 	int h;
 	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 	// 采用无符号右移16位和异或运算是为了降低哈希碰撞
 }
扩容机制:

HashMap 默认初始化容量是16,加载因子0.75,即16 * 0.75 = 12 当前容量已使用12 个的时候会触发扩容机制,因为取模寻位的公式:hashcode % length == (length-1) & hashcode,公式成立的前提是length为2的幂次方。计算机采用二进制按位与(&)操作相对于做除法取模(%)能够提升性能,所以每次扩容都是原来的两倍。

加载因子(loadFactor):
  1. jdk默认是 static final float DEFAULT_LOAD_FACTOR = 0.75f;
  2. 它是控制数组中数据的疏密程度的,数值范围在 0 - 1 之间,数值越大表示数组越密集。
  3. 思考:为什么jdk 默认是0.75?
    3.1. 太大:很难触发扩容机制,则数组的密集度太高不利于查询数据元素
    3.2. 太小:扩容太过频繁,则数组存放的数据太过分散,内存空间利用率太低
    在这里插入图片描述

HashTable

描述:

HashTable是较为远古的使用Hash算法的容器结构了,现在基本已被淘汰,单线程转为使用HashMap,多线程使用ConcurrentHashMap。

继承体系:

HashTable<K,V>也是一种key-value结构,它继承自Dictionary<K,V>,实现了Map<K,V>和Cloneable以及Serializable接口。

扩容:

HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;

hash映射:

Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;

hashmap和hashtable的区别?

安全性:

HashTable 内部方法基本上都用synchronized 同步锁修饰,所以线程安全。HashMap 没有使用同步锁,所以线程不安全

效率:

HashTable使用了同步锁的数据结构在线程并发的情况下需要线程等待,所以效率低下。HashMap 是为了追求高效率,牺牲安全性。key/value要求:HashMap 允许key 只能有一个null,value 可以有一个或多个。HashTable 中若是添加了一个key = null,就会报NullPointerException

扩容机制:

HashMap 扩容见上。HashTable 在没有指定初始容量的时候,默认容量为11,加载因子0.75。每次扩容都是2 * n + 1(n为原来容量)

底层数据结构:

HashMap 见上。HashTable 使用数组 + 链表的方式,不会自动转化为红黑树的机制

HashMap 和 HashSet 比较:

  1. HashSet 底层是使用HashMap 来存储数据。因为HashSet 是对象型存储,所以每次添加对象都是存放到HashMap的key位置,而value 则是存放一个常量(private static final Object PRESENT = new Object()😉。
  2. 正是因为HashMap 中每个key都是唯一的,所以HashSet 存储的对象都是去重的。
  3. 性能上HashMap 要比HashSet 高效率。

HashTable 和 ConcurrentHashMap 比较:

安全性:

因为HashMap 线程不安全,而线程安全的HashTable 又效率低下,所以ConcurrentHashMap 就是为了解决HashMap 线程不安全性和HashTable 效率低下问题。

锁机制:

HashTable 使用synchronized 修饰操作方法,这种方式是全表锁(在任意时刻同步锁只能被一个线程获取,其他线程等待其释放锁) ConcurrentHashMap 并不是直接使用synchronized 锁住方法

jdk1.8之前:

使用分段锁的方式达到线程安全,把整个数据集拆分成多个segment(每个segment 中存在一个链表节点元素的HashEntry[]),数据结构segment 继承了ReentrantLock 可重用锁来实现并发控制。

jdk1.8之后:

使用synchronized + CAS(乐观锁的一种,采用版本号的方式实现并发控制) 的方式控制线程安全,直接对数组元素/链表首节点/红黑树根结点上锁。这样只要不发生hash冲突,有并发现象,效率得到提高。

CAS 机制:

定义:

CAS(Compare And Swap)比较和交换机制。java平台对这种操作机制做了封装,在Unsafe 类下:如,unsafe.compareAndSwapInt(this, valueOffset, expect, update);这其中有三个重要的参数:valueOffset——内存位置,expect——旧预期值,update——新交换值

思想:

它的思想源自乐观锁的一种,采用版本号的方式实现并发控制。每个线程操作共享数据的时候都维持一个自己的版本号,关键就是获取版本号操作时必须要求原子性,否则没办法保证并发控制。

public final int incrementAndGet() {
 	return unsafe.getAndAddInt(this, valueOffset, 1) + 1;   // 获取当前版本号+1,并返回
}
public final int getAndAddInt(Object var1, long var2, int var4) {
 	int var5;
 	do {
 		var5 = this.getIntVolatile(var1, var2);     // 采用volatile 关键字保证每次获得的旧预期值都是最新的
 	} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));    // 重试机制
	return var5;
}

流程:

CAS操作时,先用valueOffset 读取内存中的版本号,再和自身的旧预期值进行比较。true:把内存位置的值换成新交换值,false:不作任何处理

public final boolean weakCompareAndSet(int expect, int update) {
 	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

重试机制(循环CAS):

由上面的代码可知为了保证原子性java的原子类中使用了一个死循环进行CAS操作。成功了就跳出循环体返回,失败了就重新从内存中读取旧预期值和重新设计更新值直到成功为止。

ABA问题:

CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。

synchronized 底层实现:java 对象头 + monitor 机制

对象在内存中有三部分:

对象头、实例数据、对齐填充

对象头:

ClassMetadataAddress——类型指针(jvm就是通过这个查询该对象属于哪个类型的实例)、MarkWord——标记字段(标记哈希码、锁状态、GC年龄代等)

monitor:

它是有三部分组成owner、EntryList、WaitSet
在这里插入图片描述

owner:

EntryList中竞争胜利者,记录当前锁拥有者

EntryList:

等待队列,存放多线程并发锁竞争中的失败者,等待下一次获得锁

WaitSet:

挂起队列,存放owner 中执行了wait() 方法后进入等待队列

操作机制:

多线程并发竞争锁时,这些线程都会进入到EntryList 等待队列中。只有一个竞争胜利者可以进入owner(获得锁)执行,执行完毕走出owner(释放锁)。EntryList 中剩下的线程就会再次竞争选出以为胜利者进入owner 。当在owner 中的线程执行了wait() 方法该线程就会进入WaitSet 挂起队列。等待被通知(执行notify())后再次进入EntryList。
在这里插入图片描述

JVM 对 synchronized 的处理:

synchronized 修饰代码块:

编译器会把synchronized 翻译成monitorenter(获取锁) 和 monitorexit(释放锁) 两个指令,分别放于代码块的开始和结尾的位置。

synchronized 修饰方法:

编译器会为方法生成一个ACC_SYNCHRONIZED 标志,jvm 根据这个标志来判断是否需要同步。

说说Java的反射的优点与缺点

什么是反射

指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法.这种动态获取信息,以及动态调用对象方法的功能叫java语言的反射机制.

原理

在这里插入图片描述

优点:

在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:

  1. 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
  2. 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

说说反射可以应用在哪些场景?

反编译:

.class–>.java

动态访问:

通过反射机制访问java对象的属性,方法,构造方法等

动态加载:

反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。

Class.forName("com.mysql.jdbc.Driver"); // 动态加载mysql驱动

说说怎么通过反射获取类中的一个静态变量?

public static int key = 10;

public static void main(String[] args) {
    try {
        // 通过反射加载类信息
        Class<?> aClass = Class.forName("cn.chaoyou.interview.Reflect");
        // 根据指定属性名称初始化一个属性对象
        Field field = aClass.getField("key");
        // 从属性对象中获取属性值
        int fieldValue = field.getInt(null);
        System.out.println(fieldValue);
        // 从属性对象中设置属性值
        field.set(null, 5);
        fieldValue = field.getInt(null);
        System.out.println(fieldValue);
    } catch (Exception e){
        e.printStackTrace();
    }
}

说说对象是什么时候被jdk的gc回收的?

手动执行System.gc()方法会触发Full GC(非常不建议)

新生区

  1. 程序空闲的时候会执行 Minor GC
  2. Eden内存空间不够用的时候会触发Minor GC

老年代

  1. 内存空间不足以接收新时代要晋升到老年代对象的时候会触发Full GC
  2. 当对一个大数组对象分配空间的时候,在老年代中找不到一段那么大的连续空间的时候会触发Full GC

方法区

  1. 内存空间不足的时候会触发CMS GC

Java中有哪些常用的线程安全的容器

同步容器类:

  1. HashTable
  2. Vector

并发容器:

  1. ConcurrentHashMap/ConcurrentHashSet(分段,底层哈希实现的同步Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。量级较synchronized低。key和value不能为null)

Sorted容器:

  1. ConcurrentSkipListMap/ConcurrentSkipListSet(底层跳表(SkipList)实现的同步Map(Set)。有序,效率比ConcurrentHashMap稍低。)

List:

  1. CopyOnWriteArrayList/CopyOnWriteArraySet(写时复制集合。写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组。)

Queue:

  1. ConcurrentLinkedQueue(基础链表同步队列。)
  2. LinkedBlockingQueue(阻塞队列,队列容量不足自动阻塞,队列容量为0自动阻塞。)
  3. ArrayBlockingQueue(底层数组实现的有界队列。自动阻塞。根据调用API(add/put/offer)不同,有不同特性。)
  4. SynchronusQueue(同步队列,是一个容量为0的队列。是一个特殊的TransferQueue。必须现有消费线程等待,才能使用的队列。)

说说静态变量在项目中有哪些作用?

类变量

一种是被static修饰的变量;

实例变量

一种是没有被static修饰的变量;

两者区别

  1. 对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
  2. 对于实例变量,每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
  3. 所以一般在需要实现以下两个功能时使用静态变量:
    3.1. 对象之间共享值时
    3.2. 方便访问变量时

数据共享

static int = arraySize = 100;

全局常量

static final String CODE = “ABCD”;

Jdk1.8的接口的default和static方法?

  1. 用于提供一套默认的实现
  2. 实现类对于该方法就不需要强制来实现
  3. 实现类可以选择使用默认的实现,也可以重写自己的实现
  4. 当实现类为接口扩展方法时,只需要提供该方法的默认实现即可
  5. 实现类不会报语法错误:Xxx不是抽象的, 并且未覆盖Yxx中的抽象方法。
  6. static方法使用类名即可调用,default方法需要使用实例对象才能调用

项目是怎么处理一些业务类型的报错信息的?

  1. 自定义一些运行时异常
/**
 * Created by admin on 2018/4/26.
 * 业务异常.
 */
public class BusinessException extends RuntimeException {
  public BusinessException(String message) {
    super(message);
  }

  public BusinessException() {
  }
}

说说你jvm中双亲委派机制的理解?

类加载器

类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的实现代码块称之为“类加载器

启动类加载器

主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

扩展类加载器

主要负责加载jre/lib/ext目录下的一些扩展的jar。

应用程序类加载器

主要负责加载应用程序的主函数类

双亲委派机制

打开“java.lang”包下的ClassLoader类。然后将代码翻到loadClass方法:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
//              -----??-----
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 首先,检查是否已经被当前类加载器加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 存在父类加载器,递归的交由父类加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 直到最上面的Bootstrap类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

其实这段代码已经很好的解释了双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
在这里插入图片描述

工作原理

  1. 当一个类加载器收到了类加载的请求。
  2. 类加载器先判断该类是否已经被当前类加载器完成加载了
    2.1. 如果已经被加载,则停止向双亲委派
    2.2. 如果没有被加载,则把类加载请求委派给双亲(父类加载器)处理
  3. 递归 2 过程,直到到达Bootstrap classLoader之前,都是在检查是否已加载过,并不会选择自己去加载。
  4. 直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了
  5. 如果自己无法加载,会下沉到子加载器去加载,一直到最底层
  6. 如果没有任何加载器能加载,就会抛出ClassNotFoundException。

为什么要设计这种机制

这种设计有个好处是防止一个类被多个类加载器加载而造成在系统中出现多个不同的类,到后面应用程序都不知道到底使用哪个类比较合适。双亲委派机制存在时,不管在哪个类加载器收到类加载请求,都会首先委派父类加载器进行类加载。最终类加载请求会到达BootstrapClassLoader(启动类加载器)执行,由于都是在同一个加载器执行操作,加载之前先判断先前是否已经加载过了,所以基本不会出现一个类被加载出多个不同的类。

说说jdk和jre的区别?

在这里插入图片描述

jdk

java development kit(java 开发工具包),主要包含了:jre、java源码的编译器javac、监控工具jconsole、分析工具jvisualvm

jre

java runable environment(java 运行环境),包含了java虚拟机,java基础类库

在什么情况下会出现内存泄露和内存溢出问题?

内存泄漏(Memory Leak)

就是申请了内存,无法释放已申请的内存空间,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。

内存溢出(Out Of Memory,OOM)

就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。

两者之间的关系

两者是不同的概念,大量的内存泄漏可能会引起内存溢出(内存泄漏积累到堆无法为新执行的程序分配内存时,就会出现内存溢出)

常见的内存溢出案例

加载图片或音频过大,超出申请的内存

  1. 对图片进行压缩处理(不推荐,图片多起来,你再怎么压缩也是要耗很大的内存)
  2. 使用第三方加载图片框架(推荐,开源,省时又省事)Glide ,Picasso ,Fresco等
  3. 减少Bitmap对象的引用,并及时的回收

对象引用没及时回收,导致堆积,超出所申请的内存

  1. 动态回收内存
  2. 对像引用采用软引用(方便内能够对此进行回收)
  3. 对象复用,存在的对象不要重复多次new它,应该循环利用
  4. 注意对象复用的生命周期(static和程序进程一样长)
  5. 单例模式的合理使用,单例模式避免重复创建对象,但也注意他的生命周期和程序进程一样长容易因为持有的对象没有正常回收导致内存泄漏
  6. 监听器不使用时及时注销
  7. 尽量减少抽象对象的使用

程序造成死循环或者循环过多

  1. 避免在循环中创建对象

一次性查询大量数据到内存中

  1. 尽量执行分批查询操作,避免全量查询SQL

避免内存溢出方法

  1. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
  2. 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
  3. 适量为堆分配足够大的内存

常见的内存泄漏情况

Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

静态集合类引起内存泄露

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,它们容器中所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

解决方案:

集合对象用完之后要及时清空集合中元素,并且把集合对象设置为null。

当集合里面对象的属性被修改,再调用remove()方法时不起作用。

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。

监听器

在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器。我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象时忘记去删除这些监听器,从而增加了内存泄漏的机会。

解决方法:

监听器用完要及时关闭

各种连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

public static void main(String[] args) throws SQLException {
    Connection connection = null;
    try {
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "admin", "123456");
        Statement statement = connection.createStatement();
        ResultSet rs = statement.executeQuery("select * from student;");
        while (rs.next()){
            String username = rs.getString("username");
            String password = rs.getString("password");
        }
    } catch (Exception e){
        e.printStackTrace();
    } finally {
        if (null != connection){
            connection.close();
        }
    }
}

单例模式

不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,

其他引发内存泄漏

  1. 流对象使用完之后没有关闭
public static void main(String[] args) throws IOException {
    InputStream input = null;
    try {
        input = new FileInputStream("file absolute path");
    } catch (Exception e){
        e.printStackTrace();
    } finally {
        if (null != input){
            input.close();
        }
    }
}

  1. 线程未终止造成内存泄漏
public static void main(String[] args) throws IOException {
    ExecutorService executorService = null;
    try {
        executorService = Executors.newFixedThreadPool(10);
    } catch (Exception e){
        executorService.execute(new Runnable() {
            @java.lang.Override
            public void run() {
                System.out.println("测试多线程!!");
            }
        });
    } finally {
        if (null == executorService){
            executorService.shutdown();
        }
    }
}

怎么排查内存泄漏问题?

某个业务系统在一段时间突然变慢,我们怀疑是因为出现内存泄露问题导致的,于是踏上排查之路

确定频繁Full GC现象

  1. 使用ps命令找到PID
ps aux | grep 进程名字
  1. 利用“虚拟机统计信息监视工具:jstat”监视虚拟机各种运行状态信息
# 意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比
jstat -gcutil PID 1000

结果如下:S0(survivor0)、S1(survivor1)、E(Eden)、O(老年代)、M(方法区)、CCS(类空间)、YGC(年轻代gc次数)、YGCT(年轻代gc耗时)、FGC(老年代gc次数)、FGCT(老年代gc耗时)、GCT(堆总gc耗时)
在这里插入图片描述

找出导致频繁Full GC的原因

分析方法通常有两种:

  1. 把堆dump下来再用MAT等工具进行分析,但dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程有些折腾,不到万不得已最好别这么干。
  2. 更轻量级的在线分析,使用“Java内存影像工具:jmap”生成堆转储快照(一般称为headdump或dump文件)。
    2.1. 利用jmap初步分析内存映射
# 主要是找出对象的引用出现了未被垃圾回收收集,通知开发人员优化相关代码。
jmap -histo:live PID | head -7

在这里插入图片描述
3. 如果上面一步还无法定位到关键信息,那么需要拿到heap dump,生成离线文件。

jmap -dump:live,format=b,file=~/heap.hprof PID
  1. 拿到heap dump文件,利用 idea 插件visualVM来分析heap profile。
    在这里插入图片描述
    4.1. 监视
    在这里插入图片描述

4.2. 线程
在这里插入图片描述

4.3. 抽样器
4.3.1. CPU
在这里插入图片描述
4.3.2. 内存
在这里插入图片描述

4.4. profiler

怎么解决hashmap中的hash冲突问题?

  1. Hash冲突是在计算key的hash值时候出现了重复,HashMap中计算hash值就是通过hashcode与16异或计算来的。
// HashMap 源码中的扰动函数hash:
static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	// 采用无符号右移16位和异或运算是为了降低哈希碰撞
}

在这里插入图片描述

  1. 如图所示,通过异或运算计算出来的hash比较均匀,不容易出现冲突,但是总有一些例外,一旦出现了冲突现象怎么解决呢?

  2. 在数据结构中,处理hash冲常用的办法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而HashMap处理hash冲突的方法就是链地址法

  3. 链地址法:基本思想是将所有哈希地址冲突的元素构成一条冲突链(单链表),并依次将hash冲突的节点链到冲突链尾结点的后继指针,因而查找、插入和删除主要在冲突链中进行,链地址法适用与经常进行插入和删除的场景。
    在这里插入图片描述

  4. jdk1.8后优化:存储的元素越来越多,冲突链越来越长,当查找一个元素时效率不仅没有提高,反而下降了,于是就把链表换成了一个适合查找的树形结构——红黑树。原来链表的优点是增删操作效率高,现在查找的效率也大大提高了。
    在这里插入图片描述

  5. 注意:只有在链表的长度大于8且数组长度小于64的时候才会将链表转成红黑树。

  6. 为什么使用红黑树?
    7.1. 红黑树是一个自平衡的二叉查找树,在每个节点增加一个存储位表示节点的颜色,红色或者黑色。通过任意一条从根到子叶的路径上各个节点颜色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此红黑树是一种弱平衡二叉树。查询效率非常高。

  7. 为什么非要等到链表长度大于等于8的时候才转变为红黑树,而不是直接变为红黑树?
    8.1. 因为构造红黑树要比构造链表复杂,另外在链表的节点不多的时候,数组+链表+红黑树的结构不一定比数组+链表的结构性能高。
    8.2. HashMap扩容的时候,会造成红黑树不断的进行拆分重组,这是非常耗时的。所以,在链表长度比较长的时候才转变为红黑树,这样才会提高效率。

线程安全

有哪些实现多线程的方式?

继承Thread类

继承Thread类,重写run()方法,创建Thread对象调用start()方法启动线程。

public class ThreadDemo extends Thread {
    @Override
    public void run() {
        int t = 1;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + (t++));
        }
    }

    public static void main(String[] args) {
        ThreadDemo td1 = new ThreadDemo();
        ThreadDemo td2 = new ThreadDemo();
        td1.setName("Thread1");
        td2.setName("Thread2");
        td1.start();
        td2.start();
    }
}

实现Runnable接口

实现Runnable接口,实现run()方法,接口的实现类的实例作为Thread的target传入带参的Thread构造函数,调用start()方法启动线程。

public class RunnableDemo implements Runnable {
    @Override
    public void run() {
        int t = 1;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + (t++));
        }
    }

    public static void main(String[] args) {
        RunnableDemo rd = new RunnableDemo();
        Thread tr1 = new Thread(rd);
        Thread tr2 = new Thread(rd);
        tr1.setName("Thread1");
        tr2.setName("Thread2");
        tr1.start();
        tr2.start();
    }
}

Callable和FutureTask创建线程实现方式

  1. 创建Callable接口的实现类 ,并实现Call方法;
  2. 创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值 ;
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程;
  4. 调用FutureTask对象的get()来获取子线程执行结束的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableFutureTaskDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int t = 1;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + (t++));
        }
        return t;
    }

    public static void main(String[] args) {
        Callable<Integer> cftd1 = new CallableFutureTaskDemo();
        Callable<Integer> cftd2 = new CallableFutureTaskDemo();
        FutureTask<Integer> ft1 = new FutureTask<>(cftd1);
        FutureTask<Integer> ft2 = new FutureTask<>(cftd2);
        Thread t1 = new Thread(ft1);
        Thread t2 = new Thread(ft2);
        t1.setName("Thread1");
        t2.setName("Thread2");
        t1.start();
        t2.start();
        try {
            System.out.println(ft1.get());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

线程池实现方式

newFixedThreadPool

定长线程池,每提交一个任务都会新建一个线程,直到线程池容量限制为止。可控制线程最大并发数,超出的线程会在队列中等待

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo implements Runnable {
    private static int task_num = 2;  //任务数量

    @Override
    public void run() {
        int t = 1;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + (t++));
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i <task_num; i++) {
            ExecutorDemo ed = new ExecutorDemo();
            executorService.execute(ed);
        }
        executorService.shutdown();
    }
}

newCachedThreadPool

缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则灵活新建线程,不会对线程池的容量有任何限制。

public static void main(String[] args) {  
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
    for (int i = 0; i < 10; i++) {  
        final int index = i;  
        try {  
            Thread.sleep(10);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        cachedThreadPool.execute(new Runnable() {  
            public void run() {  
                System.out.println(index);  
            }  
        });  
    }  
}

newScheduledThreadPool

定长线程池,支持定时及周期性任务执行。

public static void main(String[] args) {  
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
    for (int i = 0; i < 10; i++) {  
        scheduledThreadPool.schedule(new Runnable() {  
            public void run() {  
                System.out.println("delay 3 seconds");  
            }  
        }, 3, TimeUnit.SECONDS);  
    }  

}

newSingleThreadExecutor

单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

public static void main(String[] args) {  
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  
    for (int i = 0; i < 10; i++) {  
        final int index = i;  
        singleThreadExecutor.execute(new Runnable() {  
            public void run() {  
                try {  
                    System.out.println(index);  
                    Thread.sleep(2000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
    }  
}

怎么保证线程的安全性?

线程安全的三种体现

原子性

提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic, synchronized)

atomic

基于 Atomic 的数据类型的读写操作都是具有原子性的。例如,AtomicInteger 的实现源码如下:

/**
 * 以原子方式将当前值递增 1。
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

/**
 * 并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等
 * 
 * @param var1 变量的内存地址,V
 * @param var2 旧的预期值,O
 * @param var4 增量,N
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    // 自旋锁
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
synchronized
  1. synchronized是一种同步锁,通过锁实现原子操作。
  2. 定义:在 Java 中最基本的互斥同步手段就是 synchronized 关键字,被 synchronized 关键字修饰的代码块经过编译之后,编译器会在代码块的前后分别加上 monitorrenter 、monitorexit 字节码。这两个字节码都需要一个 reference 类型的参数
  3. monitorrenter:执行这个字节码时,线程会尝试获得对象的锁,如果这个对象没有被锁定或者线程已经拿到了这个对象的锁了,锁计数器加1
  4. monitorexit:执行这个字节码时,线程会主动释放对象的锁,在锁释放成功之后,锁计数器减1
  5. reference:参数来指明要锁定的和解锁的对象(锁计数器)
  6. 注意点:synchronized 同步锁对同一线程来说可重入的,其他线程只能产生阻塞等待

可见性

一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile, final)

synchronized

使用 synchronized 关键字修饰的代码块中的变量,因为 synchronized 在编译的时候会产生 lock 和 unlock 过程,在 unlock 之前必须完成变量值同步回主内存中,其他线程只能在unlock之后才有机会读取变量

volatile
保证不同线程对共享变量操作时的可见性
  1. 定义:是指当一个线程修改了共享变量的值,新值对于其他线程来说是可以立即可知的
  2. 原理:所有的线程在初始化的时候会将使用到的主内存变量拷贝一份副本在线程内的工作内存中,普通变量直接被线程使用了,而 volatile 修饰的变量在被线程使用之前会重新刷一遍主内存中变量值(即执行一次 read + load 操作),并且修改后会立即同步到主内存中(store + write)
禁止指令重排序
  1. 定义:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元处理
  2. 原理:被 volatile 修饰的变量在编译的时候会加上”lock addl $0x0, (%esp)“指令,执行这条指令相当于立了一个“内存屏障(memory barrier)”,在这道内存屏障之后的指令不能重排到屏障之前的位置
适用的场景
  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  2. 变量不需要与其他状态的变量共同参与不变约束
final

使用 final 关键字修饰的变量,因为被 final 修饰的变量无法被修改,所以所有线程拿到的值都是一样的

有序性

  1. 有序性是指,在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
  2. 可以通过volatile、synchronized、lock保证有序性
  3. JMM具有先天的有序性,即不需要通过任何手段就可以得到保证的有序性。这称为happens-before原则
  4. 如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性。虚拟机可以随意地对它们进行重排序
happens-before原则:
  1. 程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行。
  2. 锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
  3. volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
  4. 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
  5. 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
  8. 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

volatile关键的作用?

  1. 保证不同线程对共享变量操作时的可见性
  2. 禁止指令重排序

volatile是怎么保证可见性的?

volatile 修饰的变量在被线程使用之前还会到主内存中刷新一遍变量值(即执行一次 read + load 操作),并且修改后会立即同步到主内存中(store + write)

什么场景下需要考虑线程安全问题?

访问共享变量或资源

典型的场景有访问共享对象的属性,访问 static 静态变量,访问共享的缓存等等。因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题

// 共享资源
private static Integer num = 10;

// 竞争共享资源的方法
public static void computeData() {
    for (int i = 0; i < 100; i++) {
        num ++;
    }
}

public static void main(String[] args) {
    try {
        Runnable runnable = new Runnable() {
            @java.lang.Override
            public void run() {
                computeData();
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(num);
    } catch (Exception e){
        e.printStackTrace();
    }
}

依赖时序的操作

如果我们操作的正确性是依赖时序的,而在多线程的情况下又不能保障执行的顺序和我们预想的一致,这个时候就会发生线程安全问题

// “检查与执行”并非原子性操作
if (map.containsKey(key)) {
    map.remove(obj)
}
  1. 代码中首先检查 map 中有没有 key 对应的元素
  2. 如果有则继续执行 remove 操作。
  3. 此时,这个组合操作就是危险的,因为它是先检查后操作,而执行过程中可能会被打断。
  4. 如果此时有两个线程同时进入 if() 语句,然后它们都检查到存在 key 对应的元素,于是都希望执行下面的 remove 操作。
  5. 随后一个线程率先把 obj 给删除了,而另外一个线程它刚已经检查过存在 key 对应的元素,if 条件成立,所以它也会继续执行删除 obj 的操作。
  6. 但实际上,集合中的 obj 已经被前面的线程删除了,这种情况下就可能导致线程安全问题

说说你对线程池的理解?

线程池

  1. 从字面含义来看,是指管理一组同构工作线程的资源池。
  2. 线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。
  3. 工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

线程池优点

  1. 通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
  2. 当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
  3. 通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态。
  4. 可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。

java线程池

接口体系

在这里插入图片描述

ThreadPoolExecutor

线程池的初始化涉及到一个非常重要的类ThreadPoolExecutor。这个类的构造方法设置了线程池的必要参数。我们从源码入手,看一下ThreadPoolExecutor类的构造参数有哪些必要参数。

/**
 * 使用给定的初始参数创建一个新的 {@code ThreadPoolExecutor}.
 *
 * @param corePoolSize 要保留在池中的线程数,即使它们处于空闲状态,除非设置了 {@code allowCoreThreadTimeOut}
 * @param maximumPoolSize 池中允许的最大线程数
 * @param keepAliveTime 当线程数大于核心时,这是多余的空闲线程在终止前等待新任务的最长时间。
 * @param unit {@code keepAliveTime} 参数的时间单位
 * @param workQueue 用于在执行任务之前保存任务的队列。 该队列将仅保存由 {@code execute} 方法提交的 {@code Runnable} 任务。
 * @param threadFactory 执行程序创建新线程时使用的工厂
 * @param handler 在执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
corePoolSize

线程池中保持空闲线程的基本数量

maximumPoolSize

线程池的最大容量,表示可同时活动线程数量的上限

keepAliveTime

当线程池中空闲线程数量超过了基本数量时,超过部分的线程的存活时间,线程空置时间超过这个值就会被执行回收操作

unit

keepAliveTime的单位。

workQueue

用于保存等待执行任务的队列。ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排列方法有3种:

  1. 无界队列:在任务急剧增加时容易导致资源耗尽;
  2. 有界队列:在队列满之后,需要相应的饱和策略来应对;
  3. 同步移交:在线程池达到最大线程数且无空闲队列时,需要相应的饱和策略来应对;
threadFactory

线程工厂,在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用该方法。

handler

饱和策略。主要有4种:

  1. AbortPolicy:终止,抛出RejectedExecutionException,默认饱和策略;
  2. CallerRunsPolicy:将某些任务回退到调用者,降低新任务的流量;
  3. DiscardPolicy:直接丢弃;
  4. DiscardOldestPolicy:丢弃最早的未被处理的任务。

初始化线程池方式

JDK对线程池提供了有效的支持。我们可以通过Executors类中的静态工厂方法来创建线程池,下图展示了常用的四种静态工厂方法,我们将一一对其进行说明。
在这里插入图片描述

newFixedThreadPool

定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newCachedThreadPool

缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newScheduledThreadPool

定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor

单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

Executor的生命周期

为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口

ExecutorService的生命周期

源码分析:了解线程池的参数之后,我们来看看ThreadPoolExecutor类的核心部分源码。

public class ThreadPoolExecutor extends AbstractExecutorService {
    /**
     * 执行
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        // 当前任务数小于线程池的基本大小
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 线程池状态isRunning并且工作队列可以加入
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 通过addWorker(command, false)新建线程执行任务,若失败则拒绝。
        else if (!addWorker(command, false))
            reject(command);
    }
    
    /**
     * 平缓关闭
     */
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 状态为SHUTDOWN
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    
    /**
     * 暴力关闭
     */
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 状态为STOP
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

	/**
     * 已终止
     */
	protected void finalize() {
        shutdown();
    }
}
运行

ExecutorService在创建之后处于运行状态

关闭

调用shutdown方法进入关闭状态(不再接受新任务,但已接受的任务包括未开始执行的任务会继续执行);调用shutdownNow方法进入关闭状态(尝试取消所有运行中和尚未执行的任务)

已终止

ExecutorService关闭之后,在所有任务完成之后,进入终止状态

MySQL

mysql怎么进行性能优化的?

查询优化

SHOW STATUS

  1. 可以使用SHOW STATUS语句查询一些Mysql数据库的性能参数
  2. SHOW STATUS语句语法如下所示:
-- 查询mysql服务器的连接次数
SHOW STATUS LIKE 'Connections';

-- 查询mysql服务器的慢查询次数。慢查询次数参数可以结合慢查询日志,找出慢查询语句,然后针对慢查询语句进行表结构优化或者查询语句优化。
SHOW STATUS LIKE 'Slow_queries';
  1. 其中,value是要查询的参数值,一些常用的性能参数如下:
    a、Connections,连接mysql服务器的次数。
    b、Uptime,mysql服务器的上线时间。
    c、Slow_queries,慢查询的次数。
    d、Com_select,查询操作的次数。
    e、Com_insert,插入操作的次数。
    f、Com_update,更新操作的次数。
    g、Com_delete,删除操作的次数。

分析查询语句

EXPLAIN
能干什么
  1. 表的读取顺序
  2. 数据读取操作的操作类型
  3. 哪些索引可以使用
  4. 哪些索引被实际使用
  5. 表之间的引用
  6. 每张表有多少张被优化器查询
字段解释
id:表示查询中执行 select 子句或操作表的顺序
select_type:查询类型:simple、primary、subquery、derived、union、union result
table:显示这一行数据关于哪一张表的
type:访问排序类型
possible_keys:显示可能应用在这张表中的索引列表
key:实际使用的索引,如果为 NULL,则没有使用索引
key_len:表示索引中使用到的字节数,可通过该列计算查询中的索引长度,在不损失查询条件精确性的情况下,长度越短越好
ref:显示索引的哪一列被使用了,如果可能的话,是一个常量,哪些列或常量被用于查询索引列上的值
rows:根据表统计信息和索引选用情况,大致估算出找到目标的记录需要读取的行数
Extra:包含一些不合适在其他列中显示,但又十分重要的额外信息

数据库结构优化

表结构优化

  1. 尽量将表字段定义为NOT NULL约束,这时由于在MySQL中含有空值的列很难进行查询优化,NULL值会使索引以及索引的统计信息变得很复杂。
  2. 对于只包含特定类型的字段,可以使用enum、set 等符合数据类型。
  3. 数值型字段的比较比字符串的比较效率高得多,字段类型尽量使用最小、最简单的数据类型。例如P地址可以使用int类型。
  4. 尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED
  5. VARCHAR的长度只分配真正需要的空间
  6. 尽量使用TIMESTAMP而非DATETIME,
  7. 单表不要有太多字段,建议在20以内
  8. 合理的加入冗余字段可以提高查询速度

表拆分

垂直拆分
  1. 垂直拆分按照字段进行拆分,其实就是把组成一行的多个列分开放到不同的表中,这些表具有不同的结构,拆分后的表具有更少的列。例如用户表中的一些字段可能经常访问,可以把这些字段放进一张表里。另外一些不经常使用的信息就可以放进另外一张表里。
  2. 插入的时候使用事务,也可以保证两表的数据一致。缺点也很明显,由于拆分出来的两张表存在一对一的关系,需要使用冗余字段,而且需要join操作,我们在使用的时候可以分别取两次,这样的来说既可以避免join操作,又可以提高效率。
水平拆分
  1. 水平拆分按照行进行拆分,常见的就是分库分表。以用户表为例,可以取用户ID,然后对ID取10的余数,将用户均匀的分配进这 0-9这10个表中。查找的时候也按照这种规则,又快又方便。
  2. 有些表业务关联比较强,那么可以使用按时间划分的。例如每天的数据量很大,需要每天新建一张表。这种业务类型就是需要高速插入,但是对于查询的效率不太关心。表越大,插入数据所需要索引维护的时间也就越长。

分区

  1. 使用分区是大数据处理后的产物。比如系统用户的注册推广等等,会产生海量的日志,当然也可以按照时间水平拆分,建立多张表。但在实际操作中,容易发生忘记切换表导致数据错误。
  2. 分区适用于例如日志记录,查询少。一般用于后台的数据报表分析。对于这些数据汇总需求,需要很多日志表去做数据聚合,我们能够容忍1s到2s的延迟,只要数据准确能够满足需求就可以。
  3. MySQL主要支持4种模式的分区:range分区、list预定义列表分区,hash 分区,key键值分区。

读写分离

  1. 大型网站会有大量的并发访问,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是不堪设想。这时候,我们需要考虑如何减少数据库的联接。
  2. 我们发现一般情况对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,这样分析可以采用数据库集群的方案。其中一个是主库,负责写入数据,我们称为写库;其它都是从库,负责读取数据,我们称为读库。这样可以缓解一台服务器的访问压力

数据库集群

  1. 如果访问量非常大,虽然使用读写分离能够缓解压力,但是一旦写操作一台服务器都不能承受了,这个时候我们就需要考虑使用多台服务器实现写操作。
  2. 例如可以使用MyCat搭建MySql集群,对ID除3的余数,这样可以把数据分别存放到3台不同的服务器上,由MyCat负责维护集群节点的使用。

mysql服务器优化

  1. 配置文件的参数调优
  2. mysql的存储引擎的选择

什么情况下会造成索引失效?

  1. 最佳左前缀法则:如果查询用到的是组合索引,要遵循索引最左前缀法则,是指查询从索引的最左列开始,并且不要跳过中间的索引列
  2. 不在索引列上做任何操作(计算、函数、自动或手动的类型转换),会导致索引失效而转向全表扫描
  3. 查询索引不能使用在范围条件右边的列
  4. 尽量使用覆盖索引(只访问索引的查询,且索引列和查询列一致,顺序无关),减少使用 select *
  5. MySQL 在使用不等于(!= 或者 <>)的时候无法使用索引会导致全表扫描(8.0以后可以使用Using index condition)
  6. is null,is not null 也无法使用索引(8.0以后可以使用Using index condition)
  7. like 以通配符开头(’%abc…’)mysql 索引会失效变成全表扫描
  8. 字符串不加单引号会引起索引失效
  9. 少用 or,用它来连接时会索引失效(只有当两边都为索引列时才生效)

按数据结构区分有多少种索引?

hash索引

当添加一条数据到表中的时候,首先会对主键进行hash,然后将这条数据存在的地址和hash值建立一个映射关系,当我们根据主键查找这条数据的时候,只需要将主键进行hash,得到hash值,最后根据hash值就可以直接定位到这条数据。所以hash算法只需要进行一次磁盘IO,查询速度是非常快的。
在这里插入图片描述

B-树索引

B-树又称为多路平衡查找树,它在平衡二叉树的基础之上,划分出来多个叉。正是因为每个节点有多个子节点,所以有效地降低了树的高度,提升了查找效率。
在这里插入图片描述

B-树的几大特性:

  1. 节点从左到右递增排序
  2. 每个数据节点后面都会紧跟着一个指针,该指针是指向下一级的内存地址。下一级指的是位于当前指针左右两边数值中间的数据记录所存在内存中的地址。
  3. 叶子节点 的指针为空
  4. 所有索引元素是不重复的。
  5. 每个索引节点都存着当前指向的记录数据(或内存地址)

B+树索引

B+树其实是B树的一个变种,它在B树的基础之上做了一些改善,将索引节点所关联的数据记录全部移到叶子节点上了,目的是为了可以存储更多的索引节点,但是却增加了索引节点的冗余,因为叶子节点包含了所有的索引节点。
在这里插入图片描述

B+树具有以下几个特性:

  1. 叶子节点包含所有的索引节点
  2. 非叶子节点不存储数据记录
  3. 叶子节点之间使用指针连接,提高区间访问的便利
  4. 指针所指向的索引节点向右边递增排序

mysql的b+ tree优化了什么?

mysql的b+ tree优化了什么?
在这里插入图片描述
1.增加了一个双向的指针
2.首尾节点也通过指针进行关联起来
主要目的是为了更加友好的支持索引内部的范围查找。如果不加双向链表指针,我们每次查找的时候,都要回到根节点查找,增加了磁盘IO,增加查询时间。

说说你对mysql事务的理解?

一个或一组sql 语句组成一个执行单元(原子性),这个执行单元要么全部执行,要么全部不执行。事务是由单独单元的一个或多个SQL 语句组成,在这个单元中,每个MySQL 语句是互相依赖的,而整个单独单元作为一个不可分割的整体,如果单元中的某条SQL 语句一旦执行失败或产生错误,整个单元将会进行回滚操作,所有收到影响的数据将会返回到事务开启前的一个状态;如果单元中的所有SQL 语句均执行成功,则事务被顺利执行。

事务特性

  1. 原子性:原子性是指事务是一个不可分割的处理单位,事务中的操作要么都发生,要么都不发生
  2. 一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态
  3. 隔离性:是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行各个事务之间互相独立
  4. 持久性:是指一个事物一旦被提交,它对数据库中数据的改变就是永久性的,就接下来的其他操作和数据库故障不应该对其有任何影响

隔离级别

  1. 读未提交(read uncommitted):允许事务读取未被其他事务提交的变更
  2. 读已提交(read committed):只允许事务读取已经被其他事务提交了的变更
  3. 可重复读(repeatable read):确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新(行锁)
  4. 串行化(serializable):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表进行更新、插入和删除操作(表锁)
    在这里插入图片描述

传播属性

  1. PROPAGATION_REQUIRED(需要):如果存在一个事务,则支持当前事务,如果没有事务则开启一个新的事务
  2. PROPAGATION_SUPPORTS(支持):如果存在一个事务,则支持当前事务,如果没有事务则非事务地执行
  3. PROPAGATION_MANDATORY(必要的):如果存在一个事务,则支持当前事务,如果没有一个活动的事务则则抛出异常
  4. PROPAGATION_REQUIRES_NEW(总是开启一个新的):总是开启一个新的事务,如果已经存在一个活动的事务,则把当前事务挂起。
  5. PROPAGATION_NOT_SUPPORTED(不支持):总是非事务的执行,并挂起任何存在的事务。
  6. PROPAGATION_NEVER(局部):总是非事务的执行,如果存在一个活动的事务,则抛出异常
  7. PROPAGATION_NESTED(嵌套):如果存在一个活动的事务,则运行在一个嵌套的事务中,如果没有活动事务,则按则按REQUIRED属性执行

各种数据库产品对事务隔离级别的支持程度

在这里插入图片描述

Mysql中为什么要创建索引?

优点

  1. 可以加快 数据的检索速度,这也是创建索引的最主要的原因。
  2. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  3. 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  4. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点

  1. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  2. 索引需要占用物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
  3. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

Mysql中的索引为什么使用的是B+树?

相比于二叉树

  1. B+树又称为多路平衡查找树,它在平衡二叉树的基础之上,增加叉的数量。正是因为每个节点有多个子节点,所以有效地降低了树的高度,提升了查找效率。

相比于B-树

  1. B+树的磁盘读写代价更低:B+树的非叶节点不保存数据地址,叶子节点才保存数据地址,因此其内部节点相对B树更小。那么磁盘块所能容纳的节点数量也越多,一次性读入内存要查找的节点数量也就越多,相对IO读写次数就降低了。
  2. B+树的查询效率更加稳定:由于B+树非叶结点并不存储数据地址,叶子节点才保存数据地址。所以任何数据的查找必须从根节点到叶子节点的路径。查询每个数据的路径长度相同,导致每一个数据的查询效率相当。

怎么监测数据库查询效率低下?

  1. 慢查询的开启并捕获
  2. explain + 慢 SQL 分析
  3. show profile 查询 SQL 在 MySQL 服务器里面的执行细节和生命周期情况
  4. SQL 数据库服务器的参数调优

什么隔离级别下会产生幻读?

  1. 读未提交
  2. 读已提交
  3. 可重复读

可重复读的实现原理是什么?

使用的的一种叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本并发控制,类似于乐观锁的一种实现方式

实现方式

  1. InnoDB在每行记录后面保存两个隐藏的列来,分别保存了这个行的创建时间和行的删除时间。这里存储的并不是实际的时间值,而是系统版本号,当数据被修改时,版本号加1
  2. 在读取事务开始时,系统会给当前读事务一个版本号,事务会读取版本号<=当前版本号的数据
  3. 此时如果其他写事务修改了这条数据,那么这条数据的版本号就会加1,从而比当前读事务的版本号高,读事务自然而然的就读不到更新后的数据了

mybatis

什么是mybatis

  1. Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC加载驱动创建连接创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。
<!-- mybatis-config.xml 文件 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="dbUser.properties"></properties>
    <!--com.lmei.entity下的所有类定义别名 -->
    <typeAliases>
        <package name="cn.zdxh.lcy.domain" />
    </typeAliases>
    <!-- 设置环境参数集 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql:///travel"></property>
                <property name="username" value="root"></property>
                <property name="password" value="123456"></property>
            </dataSource>
        </environment>
    </environments>
    <!-- Mapper接口地址映射 -->
    <mappers>
        <mapper resource="cn/zdxh/lcy/mapper/registMapper.xml" />
        .....
    </mappers>
</configuration>
  1. 作为一个半ORM框架,MyBatis 可以使用XML注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

称Mybatis是半自动ORM映射工具,是因为在查询关联对象或关联集合对象时,需要手动编写sql来完成。不像Hibernate这种全自动ORM映射工具,Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。

  1. 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql,并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
    3.1. XML
<!--设置domain类和数据库中表的字段一一对应,注意数据库字段和domain类中的字段名称不致,此处一定要!-->
<resultMap id="BaseResultMap" type="cn.zdxh.lcy.domain.Register">
    <!--<id column="regist_id" property="regist_id" jdbcType="INTEGER" />-->
    <result column="regist_username" property="regist_username" jdbcType="VARCHAR" />
    <result column="regist_password" property="regist_password" jdbcType="VARCHAR" />
    <result column="regist_phone" property="regist_phone" jdbcType="VARCHAR" />
    <result column="regist_email" property="regist_email" jdbcType="VARCHAR" />
    <result column="regist_time" property="regist_time" jdbcType="VARCHAR" />
</resultMap>
<!-- 查询单条记录 -->
<select id="selectUserById" parameterType="Integer" resultType="cn.zdxh.lcy.domain.Register" resultMap="BaseResultMap">
    select * from register where regist_id = #{regist_id}
</select>

3.2. 注解

@Results(id = "registerMap", value = {
        @Result(column="regist_id", property="regist_id", jdbcType = JdbcType.BIGINT),
        @Result(column="regist_username", property="regist_username", jdbcType = JdbcType.VARCHAR),
        @Result(column="regist_password", property="regist_password", jdbcType = JdbcType.VARCHAR),
        @Result(column="regist_phone", property="regist_phone", jdbcType = JdbcType.VARCHAR),
        @Result(column="regist_email", property="regist_email", jdbcType = JdbcType.VARCHAR),
        @Result(column="regist_time", property="regist_time", jdbcType = JdbcType.VARCHAR)
})
@ResultMap("registerMap")
@Select("select * from register where regist_id=#{regist_id}")
Register findRegisterById(String regist_id);
  1. 由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。

mybatis的mapper类下的方法可以重载吗?

Answer:

不能

Question:

在投鞭断流时,Mybatis使用package+Mapper+method全限名作为key,去xml内寻找唯一sql来执行的。类似:key=x.y.UserMapper.getUserById,那么,重载方法时将导致矛盾。对于Mapper接口,Mybatis禁止方法重载(overLoad)。

Mybaits的优缺点:

优点

  1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余代码,不需要手动开关连接源。
  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
  4. 能够与Spring很好地集成;
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系维护。

缺点

  1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
  2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

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

  1. ${}是字符串替换,#{}是预处理;
  2. Mybatis在处理 时 , 就 是 把 {}时,就是把 {}直接替换成变量的值。而Mybatis在处理#{}时,会对sql语句进行预处理,将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
  3. 使用#{}可以有效的防止SQL注入,提高系统安全性。

通常一个mapper.xml文件,都会对应一个Dao接口,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

  1. Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象MapperProxy,它会拦截接口方法。根据类的全限定名+方法名定位到一个MapperStatement,并调用执行器执行它的sql,然后将sql执行结果返回。
/**
 * 映射器代理,代理模式
 *
 */
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  /**
   * 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 业务方法,会优先从缓存中获取
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  /**
   * 获取业务方法对象 MapperMethod,会优先从缓存中获取
   */
  private MapperMethod cachedMapperMethod(Method method) {
    // 从缓存中获取
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      // 缓存中没有,则自行初始化对象
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      // 把对象存储到缓存中
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  /**
   * Object中通用的方法
   */
  @UsesJava7
  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  /**
   * Backport of java.lang.reflect.Method#isDefault()
   */
  private boolean isDefaultMethod(Method method) {
    return ((method.getModifiers()
        & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
        && method.getDeclaringClass().isInterface();
  }
}

  1. Mapper接口里的方法,是不能重载的,因为是使用类的全限定名+方法名的保存和寻找策略。
  2. 这里所说的 Mapper 接口是指 mapper.xml 接口和 dao层接口

Dao接口即Mapper接口。接口的全限定名,就是映射文件中的namespace的值;
接口的方法名,就是映射文件中Mapper的Statement的id值;
接口方法内的参数**#{regist_username}**,就是传递给sql的参数。

<!-- 设置 registMapper 接口的业务逻辑,唯一标识 = namespace + id(cn.zdxh.lcy.mapper.registMapper.selectUserById) -->
<mapper namespace="cn.zdxh.lcy.mapper.registMapper">
    <!-- 查询单条记录 -->
    <select id="selectUserById" parameterType="String" resultType="cn.zdxh.lcy.domain.Register">
        select * from register where regist_username = #{regist_username}
    </select>
</mapper>

当调用接口方法时,接口全限定名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个SQL标签,比如 insert、update、select、delete 等标签,都会被解析为一个MapperStatement对象。

/**
 * 解析语句(select|insert|update|delete)
 * 
 * <select
 *   id="selectPerson"
 *   parameterType="int"
 *   parameterMap="deprecated"
 *   resultType="hashmap"
 *   resultMap="personResultMap"
 *   flushCache="false"
 *   useCache="true"
 *   timeout="10000"
 *   fetchSize="256"
 *   statementType="PREPARED"
 *   resultSetType="FORWARD_ONLY">
 *   SELECT * FROM PERSON WHERE ID = #{id}
 * </select>
 */
public void parseStatementNode() {
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  //如果databaseId不匹配,退出
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  //暗示驱动程序每次批量返回的结果行数
  Integer fetchSize = context.getIntAttribute("fetchSize");
  //超时时间
  Integer timeout = context.getIntAttribute("timeout");
  //引用外部 parameterMap,已废弃
  String parameterMap = context.getStringAttribute("parameterMap");
  //参数类型
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);
  //引用外部的 resultMap(高级功能)
  String resultMap = context.getStringAttribute("resultMap");
  //结果类型
  String resultType = context.getStringAttribute("resultType");
  //脚本语言,mybatis3.2的新功能
  String lang = context.getStringAttribute("lang");
  //得到语言驱动
  LanguageDriver langDriver = getLanguageDriver(lang);

  Class<?> resultTypeClass = resolveClass(resultType);
  //结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
  String resultSetType = context.getStringAttribute("resultSetType");
  //语句类型, STATEMENT|PREPARED|CALLABLE 的一种
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  //获取命令类型(select|insert|update|delete)
  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  //是否要缓存select结果
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  //仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
  //这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // Include Fragments before parsing
  //解析之前先解析<include>SQL片段
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // Parse selectKey after includes and remove them.
  //解析之前先解析<selectKey>
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  //解析成SqlSource,一般是DynamicSqlSource
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  String resultSets = context.getStringAttribute("resultSets");
  //(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
  String keyProperty = context.getStringAttribute("keyProperty");
  //(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
  }

  //又去调助手类
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

/**
 * org.apache.ibatis.builder.MapperBuilderAssistant:把 MappedStaement 存储到 Configuration 类的 Map<String, MappedStatement> mappedStatements 容器中
 */
public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {

  if (unresolvedCacheRef) {
    throw new IncompleteElementException("Cache-ref not yet resolved");
  }

  id = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource)
      .fetchSize(fetchSize)
      .timeout(timeout)
      .statementType(statementType)
      .keyGenerator(keyGenerator)
      .keyProperty(keyProperty)
      .keyColumn(keyColumn)
      .databaseId(databaseId)
      .lang(lang)
      .resultOrdered(resultOrdered)
      .resultSets(resultSets)
      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
      .resultSetType(resultSetType)
      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);

  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }

  MappedStatement statement = statementBuilder.build();
  // 调用 configuration 类
  configuration.addMappedStatement(statement);
  return statement;
}

/**
 * org.apache.ibatis.session.Configuration:容器存储元素的方法
 */
public void addMappedStatement(MappedStatement ms) {
  // ms.getId() 为 sql 语句中的 id 变量值(方法名)
  mappedStatements.put(ms.getId(), ms);
}

举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。

Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?

  1. 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;

原因:namespace + id 是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。

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

  1. Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页
  2. 可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
  3. 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

简述Mybatis的插件运行原理,以及如何编写一个插件。

  1. 答:Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
  2. 编写插件:实现Mybatis的Interceptor接口并重写intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,最后在配置文件中配置你编写的插件。
  3. 插件实现:敏感数据进行脱敏处理案例

Mybatis是否支持延迟加载?它的实现原理是什么?

  1. Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false
    1.1. 启动赖加载配置:mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="dbUser.properties"></properties>
    <!--com.lmei.entity下的所有类定义别名 -->
    <typeAliases>
        <package name="cn.zdxh.lcy.domain" />
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
            	.....
            </dataSource>
        </environment>
    </environments>
    <settings>
        <!-- 查询时,关闭关联对象即时加载以提高性能 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
        <setting name="aggressiveLazyLoading" value="false" />
    </settings>
    <mappers>
        ......
    </mappers>
</configuration>

1.2. mapper接口中的业务方法使用赖加载:***Mapper.xml

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "shfq.lazy_load.vo.Blog">
    <select id="selectBlog" parameterType="int" resultMap="blogResultMap">
        SELECT * from blog where id=#{id}
    </select>

    <resultMap id="blogResultMap" type="shfq.lazy_load.vo.Blog">
        <id column="id" property="id"></id>
        <result column="content" property="content"></result>
        <association property="author" column="author_id" select="selectAuthor" fetchType="lazy"/>
    </resultMap>

    <select id="selectAuthor" parameterType="int" resultType="shfq.lazy_load.vo.Author">
        SELECT * from author where id=#{id}
    </select>
</mapper>
  1. 延迟加载的基本原理是,使用CGLIB创建目标对象的代理对象(Enhancer),当调用目标方法时,进入拦截器方法。比如,调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会先单独查询关联B对象的sql,把B查询上来,然后再调用a.setB(b),于是a的对象b属性就有值了,最后完成a.getB().getName()方法的调用。
/**
 * mybatis 源码:使用cglib创建代理对象
 * @param type                            原生的对象类型
 * @param callback                       回调方法,就是实现了MethodInterceptor接口的子类,会有accept方法
 * @param constructorArgTypes    构造函数的参数类型
 * @param constructorArgs            构造函数的参数值
 * @return
 */
private static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  Enhancer enhancer = new Enhancer();
  //回调方法,就是实现了MethodInterceptor接口的子类,会有accept方法
  enhancer.setCallback(callback);
  //要生成代理对象的原生类
  enhancer.setSuperclass(type);
  
  
  try {
    //获取目标类型的 writeReplace 方法,如果没有,异常中代理类设置enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
    type.getDeclaredMethod(WRITE_REPLACE_METHOD);
    // ObjectOutputStream will call writeReplace of objects returned by writeReplace
    log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
  } catch (NoSuchMethodException e) {
    enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
  } catch (SecurityException e) {
    // nothing to do here
  }
  
  
  Object enhanced = null;
  //如果构造函数没有参数,创建代理对象
  if (constructorArgTypes.isEmpty()) {
    enhanced = enhancer.create();
  } else {
    //否则,初始化带有参数的构造函数
    Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
    Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
    //创建带有参数的代理对象
    enhanced = enhancer.create(typesArray, valuesArray);
  }
  return enhanced;
}
  1. 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

Mybatis的缓存机制:

一级缓存

一级缓存 Mybatis的一级缓存是指SQLSession,一级缓存的作用域是SQlSession, Mabits默认开启一级缓存。 在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 当执行SQL时候两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。 每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中。 Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句。Value为查询出来的结果集映射成的java对象。 SqlSession执行insert、update、delete等操作commit后会清空该SQLSession缓存。

SqlSession 接口

对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  // 数据库操作有关的职责都会委托给Executor
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;
}

Executor 接口

  1. SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。
  2. Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行学习每个类的基本作用。
    在这里插入图片描述
public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  // 缓存容器
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;
}

Cache

MyBatis中Cache接口的实现类,如下图所示:
在这里插入图片描述

PerpetualCache

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();
}

二级缓存

二级缓存是mapper级别的,Mybatis默认是没有开启二级缓存的。 第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放代该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。 如果调用相同namespace下的mapepr映射文件中增删改sql,并执行了commit操作,此时要刷新缓存(flushCache=“true”),否则会出现脏读情况。

<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User"  flushCache="true">

探究多表操作对二级缓存的影响

现有这样一个场景,有两个表,部门表dept(deptNo,dname,loc)和 部门数量表deptNum(id,name,num),其中部门表的名称和部门数量表的名称相同,通过名称能够联查两个表可以知道其坐标(loc)和数量(num),现在我要对部门数量表的 num 进行更新,然后我再次关联dept 和 deptNum 进行查询,你认为这个 SQL 语句能够查询到的 num 的数量是多少?来看一下代码探究一下

  1. DeptNum.java
public class DeptNum {

    private int id;
    private String name;
    private int num;

    get and set...
}
  1. DeptVo.java
public class DeptVo {

    private Integer deptNo;
    private String  dname;
    private String  loc;
    private Integer num;

    public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
        this.deptNo = deptNo;
        this.dname = dname;
        this.loc = loc;
        this.num = num;
    }

    public DeptVo(String dname, Integer num) {
        this.dname = dname;
        this.num = num;
    }

    get and set

    @Override
    public String toString() {
        return "DeptVo{" +
                "deptNo=" + deptNo +
                ", dname='" + dname + '\'' +
                ", loc='" + loc + '\'' +
                ", num=" + num +
                '}';
    }
}
  1. DeptDao.java
public interface DeptDao {

    ...

    DeptVo selectByDeptVo(String name);

    DeptVo selectByDeptVoName(String name);

    int updateDeptVoNum(DeptVo deptVo);
}

  1. DeptDao.xml
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
  select d.deptno,d.dname,d.loc,dn.num from dept d,deptNum dn where dn.name = d.dname
  and d.dname = #{name}
</select>

<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
  select * from deptNum where name = #{name}
</select>

<update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo">
  update deptNum set num = #{num} where name = #{dname}
</update>

  1. DeptNum 数据库初始值:
    在这里插入图片描述

  2. 测试类对应如下:

/**
 * 探究多表操作对二级缓存的影响
 */
@Test
public void testOtherMapper(){

  // 第一个mapper 先执行联查操作
  SqlSession sqlSession = factory.openSession();
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  DeptVo deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
  // 第二个mapper 执行更新操作 并提交
  SqlSession sqlSession2 = factory.openSession();
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
  sqlSession2.commit();
  sqlSession2.close();
  // 第一个mapper 再次进行查询,观察查询结果
  deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
}
  1. 测试结果如下:
    在这里插入图片描述

  2. 问题总结

在对DeptNum 表执行了一次更新后,再次进行联查,发现数据库中查询出的还是 num 为 1050 的值,也就是说,实际上 1050 -> 1000 ,最后一次联查实际上查询的是第一次查询结果的缓存,而不是从数据库中查询得到的值,这样就读到了脏数据。

  1. 解决方案

如果是两个mapper命名空间的话,可以使用 <cache-ref> 来把一个命名空间指向另外一个命名空间,从而消除上述的影响,再次执行,就可以查询到正确的数据

对于缓存数据更新机制

当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。

Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

  1. 使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
  2. 使用sql列的别名功能,将列的别名书写为对象属性名。
  3. 有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

Mybatis动态sql有什么用?执行原理?有哪些动态sql?

动态sql

  1. 传统的JDBC的方法,在组合SQL语句的时候需要去拼接,稍微不注意就会少少了一个空格,标点符号,都会导致系统错误。Mybatis的动态SQL就是为了解决这种问题而产生的;Mybatis的动态SQL语句值基于OGNL表达式的,方便在SQL语句中实现某些逻辑;可以使用标签组合成灵活的sql语句,提供开发的效率。
  2. Mybatis的动态SQL标签主要由以下几类:
    2.1. If语句(简单的条件判断)
    2.2. Choose(when/otherwise),相当于java语言中的switch,与jstl中choose类似 Trim(对包含的内容加上prefix,或者suffix)
    2.3. Where(主要是用来简化SQL语句中where条件判断,能智能的处理and/or 不用担心多余的语法导致的错误)
    2.4. Set(主要用于更新时候)
    2.5. Foreach(一般使用在mybatis in语句查询时特别有用)

执行原理

根据表达式的值完成逻辑判断 并动态拼接sql的功能。

9种动态sql标签:

<trim></trim>
<where></where>
<set></set>
<foreach collection=""></foreach>
<if test=""></if>
<choose>
    <when test=""></when>
    <otherwise></otherwise>
</choose>
<bind name="" value=""/>

Xml映射文件中,除了常见的select|insert|updae|delete标签外,还有哪些标签?

resultMap、parameterMap、sql、include、selectKey,加上动态sql的9个标签 trim | where | set | foreach | if | choose | when | otherwise | bind 等,其中 为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。

使用MyBatis的mapper接口调用时有哪些要求?

  1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同;
  2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同;
  3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
  4. Mapper接口的类路径即是Mapper.xml文件中的namespace。

***Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 设置-->
<mapper namespace="cn.zdxh.lcy.dao.RegisteDao">
    <!--设置domain类和数据库中表的字段一一对应,注意数据库字段和domain类中的字段名称不致,此处一定要!-->
    <resultMap id="BaseResultMap" type="cn.zdxh.lcy.domain.Register">
        <!--<id column="regist_id" property="regist_id" jdbcType="INTEGER" />-->
        <result column="regist_username" property="regist_username" jdbcType="VARCHAR" />
        <result column="regist_password" property="regist_password" jdbcType="VARCHAR" />
        <result column="regist_phone" property="regist_phone" jdbcType="VARCHAR" />
        <result column="regist_email" property="regist_email" jdbcType="VARCHAR" />
        <result column="regist_time" property="regist_time" jdbcType="VARCHAR" />
    </resultMap>
    <!-- 查询单条记录 -->
    <select id="selectUserById" parameterType="Integer" resultType="cn.zdxh.lcy.domain.Register" resultMap="BaseResultMap">
        select * from register where regist_id = #{regist_ide}
    </select>

     <!--查询全部用户信息记录 -->
    <!--<select id="selectUserAll">-->
        <!--select * from userMapper-->
    <!--</select>-->

    <!-- 插入单条用户信息 -->
    <insert id="addUser" parameterType="cn.zdxh.lcy.domain.Register">
        insert into register(regist_username, regist_password, regist_phone, regist_email, regist_time, header_url) values(#{regist_username}, #{regist_password}, #{regist_phone}, #{regist_email}, #{regist_time}, #{header_url})
    </insert>
    
    <!-- 删除单条用户信息 -->
    <delete id="deleteUser" parameterType="cn.zdxh.lcy.domain.Register">
        delete from register where regist_username = #{regist_username} and regist_password = #{regist_password}
    </delete>
    
    <!-- 用户修改信息 -->
    <update id="updateUser" parameterType="cn.zdxh.lcy.domain.Register">
        update register set regist_password = #{regist_password}, regist_phone = #{regist_phone}, regist_email = #{regist_email} where regist_id = #{regist_id}
    </update>
</mapper>

Dao层的Mapper接口

package cn.zdxh.lcy.dao;

import cn.zdxh.lcy.domain.Register;

public interface RegisteDao {
    Register selectUserById();
    int addUser(Register register);
    int deleteUser(String regist_username, String regist_password);
    int updateUser(Register register);
}

模糊查询like语句该怎么写?

在Java代码中添加sql通配符

<select id=”selectlike”>
 select * from foo where bar like #{value}
</select>
String wildcardname =%smi%;
List<name> names = mapper.selectlike(wildcardname);

在sql语句中拼接通配符,会引起sql注入

<select id=”selectlike”>
     select * from foo where bar like "%"${value}"%"
</select>
String wildcardname = “smi”;
List<name> names = mapper.selectlike(wildcardname);

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

  1. 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>
   select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
  1. 通过来映射字段名和实体类属性名的一一对应的关系。
<select id="getOrder" parameterType="int" resultMap="orderresultmap">
    select * from orders where order_id=#{id}
</select>

<resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
    <!–用id属性来映射主键字段–>
    <id property=”id” column=”order_id”>

    <!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–>
    <result property = “orderno” column =”order_no”/>
    <result property=”price” column=”order_price” />
</reslutMap>

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

  1. insert 方法总是返回一个int值 ,这个值代表的是插入的行数。
  2. 如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。
<!-- 
     主键自动增长策略:usegeneratedkeys,
     主键字段:id 
-->
<insert id=”insertName” usegeneratedkeys=”true” keyproperty=”id”>
     insert into names (name) values (#{name})
</insert>
  1. java代码实现如下:
Name name = new Name();
name.setName("fred");

int rows = mapper.insertName(name);
// 完成后,id已经被设置到对象中
System.out.println("rows inserted = " + rows);
System.out.println("generated key value = " + name.getid());

在mapper中如何传递多个参数?

1)第一种:
//DAO层的函数
public UserselectUser(String name,String area);
//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。
<select id="selectUser"resultMap="BaseResultMap">
    select *  fromuser_user_t   whereuser_name = #{0} anduser_area=#{1}
</select>2)第二种: 使用 @param 注解:
public interface usermapper {
    User selectuser(@Param("username") String username, @Param("hashedpassword") String hashedpassword);
}
然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):
<select id="selectuser" resulttype="user">
    select id, username, hashedpassword
    from some_table
    where
        username = #{username} and
        hashedpassword = #{hashedpassword}
</select>3)第三种:多个参数封装成map
try{
    // 映射文件的命名空间.SQL片段的ID(StudentID),就可以调用对应的映射文件中的SQL
    // 由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数
    Map<String, Object> map = new HashMap();
    map.put("start", start);
    map.put("end", end);
    return sqlSession.selectList("StudentID.pagination", map);
}catch(Exception e){
    e.printStackTrace();
    sqlSession.rollback();
}
finally{
    MybatisUtil.closeSqlSession();
}

一对一、一对多的关联查询 ?

<mapper namespace="com.lcb.mapping.userMapper">
    <!--association  一对一关联查询 -->
    <select id="getClass" parameterType="int" resultMap="ClassesResultMap">
        select
            *
        from class c, teacher t
        where
            c.teacher_id = t.t_id and
            c.c_id = #{id}
    </select>

    <resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
        <!-- 实体类的字段名和数据表的字段名映射 -->
        <id property="id" column="c_id"/>
        <result property="name" column="c_name"/>
        <association property="teacher" javaType="com.lcb.user.Teacher">
            <id property="id" column="t_id"/>
            <result property="name" column="t_name"/>
        </association>
    </resultMap>


    <!--collection  一对多关联查询 -->
    <select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
        select 
            * 
        from class c, teacher t, student s 
        where 
            c.teacher_id = t.t_id and 
            c.c_id = s.class_id and 
            c.c_id = #{id}
    </select>

    <resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
        <id property="id" column="c_id"/>
        <result property="name" column="c_name"/>
        <association property="teacher" javaType="com.lcb.user.Teacher">
            <id property="id" column="t_id"/>
            <result property="name" column="t_name"/>
        </association>

        <collection property="student" ofType="com.lcb.user.Student">
            <id property="id" column="s_id"/>
            <result property="name" column="s_name"/>
        </collection>
    </resultMap>
</mapper> 

MyBatis实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询

  1. 联合查询:是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成;
  2. 嵌套查询:是先查一个表,根据这个表里面的结果的 外键id,再去另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。

MyBatis实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询

  1. 联合查询:是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成
  2. 嵌套查询:是先查一个表,根据这个表里面的 结果的外键id,再去另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。

Mapper编写有哪几种方式?

  1. 接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。
<!-- 在sqlMapConfig.xml中配置mapper.xml的位置:-->
<mappers>
     <mapper resource="mapper.xml 文件的地址" />
     <mapper resource="mapper.xml 文件的地址" />
</mappers>

<!-- 
	定义mapper接口:
	实现类集成SqlSessionDaoSupport:mapper方法中可以this.getSqlSession()进行数据增删改查。
-->
<insert id="addUser" parameterType="cn.zdxh.lcy.domain.Register">
    insert into register(regist_username, regist_password, regist_phone, regist_email, regist_time, header_url) values(#{regist_username}, #{regist_password}, #{regist_phone}, #{regist_email}, #{regist_time}, #{header_url})
</insert>

<!-- spring 配置:-->
<bean id="对象ID" class="mapper 接口的实现">
    <property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
  1. 使用org.mybatis.spring.mapper.MapperFactoryBean: 此方法需要在SqlMapConfig.xml中配置mapper.xml的位置,还需定义mapper接口。
<!-- 在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不 -->
<mappers>
        <mapper resource="mapper.xml 文件的地址" />
        <mapper resource="mapper.xml 文件的地址" />
</mappers>


<!-- 
	定义mapper接口:
		① mapper.xml中的namespace为mapper接口的地址
		② mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致
		③ Spring中定义:
 -->
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="mapper 接口地址" />
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
  1. 使用mapper扫描器:需要编写mapper.xml文件,需要mapper接口,配置mapper扫描器,使用扫描器从spring容器中获取mapper的实现对象。
<!-- 
	mapper.xml文件编写:
		① mapper.xml中的namespace为mapper接口的地址;
		② mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;
		③ 如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。 
		
	定义mapper接口:
		① 注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录
		
	配置mapper扫描器:
 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="mapper接口包地址" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

<!-- 使用扫描器后从spring容器中获取mapper的实现对象。-->

什么是MyBatis的接口绑定?有哪些实现方式?

接口绑定

在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。

接口绑定有两种实现方式

  1. 一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;
@Select("select * from student where username = #{username}")
Student findStudentByUsername(String username);
  1. 一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。
<resultMap id="studentResultMap" type="cn.zdxh.lcy.domain.Student">
    <result column="username" property="username" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="phone" property="phone" jdbcType="VARCHAR" />
    <result column="email" property="email" jdbcType="VARCHAR" />
</resultMap>
<select id="findStudentByUsername" parameterType="String" resultType="cn.zdxh.lcy.domain.Student" resultMap="studentResultMap">
    select 
        *
    from student
    where 
        username = #{username}
</select>

MyBatis与Hibernate有哪些不同?

  1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。
  2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
  3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。

JDBC编程有哪些不足之处,Mybatis是如何解决这些问题的?

  1. 数据库连接的创建、释放频繁造成系统资源浪费从而影响了性能,如果使用数据库连接池就可以解决这个问题。当然JDBC同样能够使用数据源。
    解决:在SQLMapConfig.xml中配置数据连接池,使用数据库连接池管理数据库连接。
  2. SQL语句在写代码中不容易维护,事件需求中SQL变化的可能性很大,SQL变动需要改变JAVA代码。解决:将SQL语句配置在mapper.xml文件中与java代码分离。
  3. 向SQL语句传递参数麻烦,因为SQL语句的where条件不一定,可能多,也可能少,占位符需要和参数一一对应。解决:Mybatis自动将java对象映射到sql语句。
  4. 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。解决:Mbatis自动将SQL执行结果映射到java对象。

Mybatis编程步骤 ?

public class MyBatisUtil{
	public static void main(String[] args) {
	    // 获取mybatis-config配置文件的资源信息
	    String resource = "mybatis-config.xml";
	    InputStream is = MyBatisUtil.class.getClassLoader().getResourceAsStream(resource);
	    // 创建数据源工厂
	    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
	    // 通过SQLSessionFactory创建SQLSession
	    SqlSession sqlSession = factory.openSession();
	    // 通过SQLSession执行数据库操作
	    UserMapper cim = sqlSession.getMapper(UserMapper.class);
	    // 调用mapper接口中的业务方法
	    int ci = cim.deleteUser("123");
	    // 调用session.commit()提交事物
	    sqlSession.commit();
	    // 调用session.close()关闭会话
	    sqlSession.close();
	}
}

SQLMapConfig.xml中配置有哪些内容?

properties(属性)
settings(配置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)

Mybais 常用注解 ?

@Insert : 插入sql , 和xml insert sql语法完全一样
@Select : 查询sql, 和xml select sql语法完全一样
@Update : 更新sql, 和xml update sql语法完全一样
@Delete : 删除sql, 和xml delete sql语法完全一样
@Param : 入参
@Results :结果集合
@Result : 结果
@ResultMap:结果集名称

SpringMVC

什么是Spring MVC ?简单介绍下你对springMVC的理解?

Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller层次结构分离。将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分。简化开发,减少出错,方便组内开发人员之间的配合。

SpringMVC的流程

  1. 用户发送请求至前端控制器DispatcherServlet;
  2. DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler;
  3. 处理器映射器根据请求url找到具体的处理器Handler,生成处理器对象及处理器拦截器(如果有则生成),一并返回给DispatcherServlet;
  4. DispatcherServlet 调用 HandlerAdapter处理器适配器,请求执行Handler;
  5. HandlerAdapter 经过适配调用 具体处理器进行处理业务逻辑;
  6. Handler执行完成返回ModelAndView;
  7. HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
  8. DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
  9. ViewResolver解析后返回具体View;
  10. DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
  11. DispatcherServlet响应用户。
    在这里插入图片描述

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

Springmvc的优点

  1. 可以支持各种视图技术,而不仅仅局限于JSP;
  2. 与Spring框架集成(如IoC容器、AOP等);
  3. 清晰的角色分配:前端控制器(dispatcherServlet) ,请求到处理器映射(handlerMapping),处理器适配器(HandlerAdapter),视图解析器(ViewResolver)。
  4. 支持各种请求资源的映射策略。

SpringMVC怎么样设定重定向和转发的

  1. 转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4"
// 处理器方法返回 ModelAndView 转发到视图
@RequestMapping("user.do")
public ModelAndView login(){
    ModelAndView view = new ModelAndView();
    // 设置参数
    view.addObject("username", "李四");
    view.addObject("password", "123456");
    // 初始化转发策略
    view.setViewName ("forward:user.do?name=method4");
    return view;
}
  1. 重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com"
// 处理器方法返回 ModelAndView 重定向到百度
@RequestMapping("user.do")
public ModelAndView login(){
    ModelAndView view = new ModelAndView();
    // 设置参数
    view.addObject("username", "李四");
    view.addObject("password", "123456");
    // 初始化转发策略
    view.setViewName("redirect:http://www.baidu.com");
    return view;
}

SpringMVC常用的注解有哪些

  1. @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
  2. @RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象。
  3. @ResponseBody:注解实现将 conreoller 方法返回对象转化为 json 对象响应给客户。
  4. @Controller:主要负责处理前端控制器(DispatcherServlet )发过来的请求,经过业务逻辑层处理之后封装层一个model,并将其返回给view进行展示。
  5. @RestController:@Controller + @ResponseBody的效果,省了很多事,我们使用 @RestController 之后就不需要再使用 @Controller 了。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

   /**
    * The value may indicate a suggestion for a logical component name,
    * to be turned into a Spring bean in case of an autodetected component.
    * @return the suggested component name, if any
    * @since 4.0.1
    */
   String value() default "";

}

  1. @PathVariable:主要用来获取 URL 中的参数,Spring Boot 支持 Restfull 风格的 URL,比如一个 GET 请求携带一个参数 id,我们将 id 作为参数接收,可以使用 @PathVariable 注解。
@RequestMapping("/user.do/{id}")
public Integer login(@PathVariable("id") Integer id){
    System.out.println(id);
    return id;
}
  1. @RequestParam:也是用于获取参数,但是这个注解获取的是request中的参数。
/**
 * http 请求:http://localhost:8080/login/user.do?id=10
 */

// handler 接口
@RequestMapping("user.do")
public Integer login(@RequestParam("id") Integer id){
    System.out.println(id);
    return id;
}

SpingMvc中的控制器的注解一般用哪个?有没有别的注解可以替代?

答:一般用 @Controller 注解,也可以使用 @RestController。@RestController 注解相当于 @ResponseBody + @Controller。表示是表现层,除此之外,一般不用别的注解代替。

springMVC和struts2的区别有哪些?

  1. springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。
  2. springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。
  3. Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。

如何解决POST请求中文乱码问题,GET的又如何处理呢?

  1. 解决post请求乱码问题:在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8;
<!-- 设置字符编码拦截器 -->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>
 
 <!-- 设置字符编码拦截器的拦截路径 -->
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  1. get请求中文参数出现乱码解决方法有两个:
  • 修改tomcat配置文件添加编码与工程编码一致,如下:
<ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
  • 另外一种方法对参数进行重新编码:
// ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。
String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8");

SpringMvc里面拦截器是怎么写的

有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承HandlerInterceptorAdapter适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可:

<!-- 配置SpringMvc的拦截器 -->
<mvc:interceptors>
    <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 -->
    <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean>
 
    <!-- 只针对部分请求拦截 -->
    <mvc:interceptor>
       <mvc:mapping path="/modelMap.do" />
       <bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
    </mvc:interceptor>
</mvc:interceptors>

注解原理

注解本质是一个继承了Annotation的特殊接口,其具体实现类是 JDK 动态代理生成的代理类。我们通过反射获取注解时,返回的也是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法,该方法会从memberValues这个Map中查询出对应的值,而memberValues的来源是Java常量池。

java中的元注解

@Target

作用

用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。

源码定义
package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * 返回可以应用注释类型的元素种类的数组。
     * @return 可以应用注释类型的元素种类的数组
     */
    ElementType[] value();
}

我们可以通过以下的方式来为这个 value 传值:

@Target(value = {ElementType.FIELD})
ElementType作用参数

被这个 @Target 注解修饰的注解将根据ElementType枚举字段规定使用范围。其中,ElementType 是一个枚举类型,有以下一些值:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

@Retention

注解指定了被修饰的注解的生命周期,一种是只能在编译期可见,编译后会被丢弃,一种会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃,最后一种则是永久存在的可见性。

作用

用于指明当前注解的生命周期

源码定义
package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

我们可以通过以下的方式来为这个 value 传值:

@Retention(value = RetentionPolicy.RUNTIME)
RetentionPolicy 作用参数

这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:

  • RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
  • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
  • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

@Documented:注解是否应当被包含在 JavaDoc 文档中

@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。

@Inherited:是否允许子类继承该注解

@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

三大内置注解

除了上述四种元注解外,JDK 还为我们预定义了另外三种注解,它们是:

@Override

作用

它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。所以你看,它就是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。

源码定义
package java.lang.annotation;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Deprecated

依然是一种『标记式注解』,永久存在,可以修饰所有的类型,

作用
  1. 被标记的当前类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。
  2. 当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者。
源码定义
package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@SuppressWarnings

作用
  1. 主要用来压制 java 的警告
  2. 如果我们不希望程序启动时,编译器检查代码中的一些警告信息,就可以使用 @SuppressWarnings 注解并给它的 value 属性传入一个参数值来压制编译器的检查。
源码定义
package java.lang.annotation;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * 编译器要在带注释的元素中抑制的警告集。 允许重复名称。 名称的第二次和连续出现将被忽略。 
     * 出现无法识别的警告名称<i>不是</i>一个错误:编译器必须忽略它们无法识别的任何警告名称。 
     * 但是,如果注释包含无法识别的警告名称,它们可以自由发出警告。
     *
     * <p> 字符串 {@code "unchecked"} 用于抑制未经检查的警告。 
     * 编译器供应商应结合此注释类型记录他们支持的其他警告名称。 
     * 鼓励他们合作以确保相同的名称在多个编译器中工作。 @return 要抑制的警告集
     */
    String[] value();
}

SpringMvc怎么和AJAX相互调用的

通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 :

  1. 加入Jackson.jar
  2. 在配置文件中配置json的映射
  3. 在接受Ajax方法里面可以直接返回Object、List等,但方法前面要加上@ResponseBody注解。

Spring MVC的异常处理

  1. 可以将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可。
  2. Spring提供了很多选择和非常灵活的使用方式,下面是一些使用建议:
  • 如果自定义异常类,考虑加上ResponseStatus注解;
  • 对于没有ResponseStatus注解的异常,可以通过使用ExceptionHandler+ControllerAdvice注解,或者通过配置> - SimpleMappingExceptionResolver,来为整个Web应用提供统一的异常处理。
  • 如果应用中有些异常处理方式,只针对特定的Controller使用,那么在这个Controller中使用ExceptionHandler注解。
  • 不要使用过多的异常处理方式,不然的话,维护起来会很苦恼,因为异常的处理分散在很多不同的地方。

SpringMvc的控制器是不是单例模式?如果是,有什么问题?怎么解决?

答:是单例模式,在多线程访问的时候有线程安全问题

解决方案

不要在控制器里面定义可变状态的变量(成员变量)

如果需要使用这些可变状态

可以使用ThreadLocal机制解决,为每个线程单独生成一份变量副本,独立操作,互不影响。

为什么使用单例模式?

  1. 提高性能(不用每次请求都创建对象)
  2. 不需要多例(不要在控制器中定义成员变量)

如果在拦截请求中,我想拦截get方式提交的方法,怎么配置?

答:可以在@RequestMapping注解里面加上method=RequestMethod.GET。

@RequestMapping(value = "/test", method = RequestMethod.GET)
public ModelAndView test(){
    // nothing to do
    return new ModelAndView();
}

如果在拦截请求中,我想拦截提交参数中包含"type=test"字符串,怎么配置

答:直接在@RequestParam中的params参数设置拦截字符串“type=test”

@RequestMapping(value = "/test", params = "type=test")
public ModelAndView test(){
    // nothing to do
    return new ModelAndView();
}

怎样在方法里面得到Request,或者Session?

答:直接在方法的形参中声明request,SpringMvc就自动把request对象传入。

public void getSessionAction(HttpServletRequest request){
    HttpSession session = request.getSession();
}

如果想在拦截的方法里面得到从前台传入的参数,怎么得到?

答:直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。

如果前端传入多个参数,并且参数都是同个对象的,如何快速得到这个对象?

答:直接在方法中声明这个对象,SpringMvc就自动会把属性赋值到这个对象里面。

讲下SpringMVC和Struts1,Struts2的比较的优势

性能上Struts1>SpringMvc>Struts2 开发速度上SpringMvc和Struts2差不多,比Struts1要高

SpringMvc中函数的返回值是什么?

答:返回值可以有很多类型,有String,ModelAndView。ModelAndView类把视图和数据都合并的一起的,但一般用String比较好。

SpringMvc用什么对象从后台向前台传递数据的?

答:通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到。

怎么样把ModelMap里面的数据放入Session里面?

答:可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。

SpringMVC怎么处理返回值的

SpringMVC根据配置文件中InternalResourceViewResolver的前缀和后缀,用前缀+返回值+后缀组成完整的返回值

SpringMVC是怎么接收一个long[]?

  1. handler接口设计成POST方式接收请求
  2. 使用@RequestParam接收long[]参数
@RequestMapping(value = "/test", method = RequestMethod.POST)
public ModelAndView test(@RequestParam("ids") Long[] ids){
    // nothing to do
    System.out.println(ids);
    return new ModelAndView();
}

Spring

Spring是什么?

Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:

  • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
  • Spring Core:核心类库,所有功能都依赖于该类库,提供IOC和DI服务;
  • Spring AOP:AOP服务;
  • Spring Web:提供了基本的面向Web的综合特性,提供对常见框架如Struts2的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器;
  • Spring MVC:提供面向Web应用的Model-View-Controller,即MVC实现。
  • Spring DAO:对JDBC的抽象封装,简化了数据访问异常的处理,并能统一管理JDBC事务;
  • Spring ORM:对现有的ORM框架的支持;
    在这里插入图片描述

Spring的优点

  1. spring属于低侵入式设计,代码的污染极低;
  2. spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;
  3. spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。
  4. spring对于主流的应用框架提供了集成支持。如,mybatis、springmvc等。

Spring的IoC理解:

什么是IOC

  1. IoC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、初始化、销毁)和对象间的依赖关系。
  2. 最直观的表达就是
    2.1. 以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。
    2.2. IoC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记。当你需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制。控制对象生命周期的不再是引用它的对象,而是Spring容器。由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。

什么是DI:

IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所引用的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。即应用程序在运行时依赖 IoC 容器来动态注入对象所引用的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性。

IoC的实现原理:

Spring 的 IoC 的实现原理就是工厂模式 + 反射机制。在 Spring 容器中,Bean 对象如何注册到 IoC 容器,以及Bean对象的加载、实例化、初始化详细过程可以阅读这篇文章:Spring的Bean加载流程_张维鹏的博客-CSDN博客

Spring的AOP理解:

  1. OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
  2. AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
  3. AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

静态代理

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

动态代理

  1. Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,在特定的切点做了增强处理,并回调原对象的方法。
  2. Spring AOP中的动态代理主要有两种方式:JDK动态代理CGLIB动态代理
  3. 动态代理测试代码:
public interface MyInterface {
    public void database();
    public void log();
}

class MyImplement implements MyInterface {
    @Override
    public void database() {
        System.out.println("测试连接数据库操作");
    }

    @Override
    public void log() {
        System.out.println("测试编写系统运行日志");
    }
}

public class DynamicProxyDemo {
    public void database() {
        System.out.println("测试连接数据库操作");
    }

    public void log() {
        System.out.println("测试编写系统运行日志");
    }
}

JDK动态代理

JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;

InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。

/**
 * Author: chaoyou
 * CSDN:https://blog.csdn.net/qq_41910568
 * Date: 2020/5/9 0009 0:17
 * Content:jdk 动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvocationHandler 来处理
 */
public class JdkProxy implements InvocationHandler {
    /**
     * 声明一个代理的目标对象
     */
    private Object target;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Jdk动态代理,监听开始!");
        // 执行代理方法
        Object invoke = method.invoke(target, args);
        System.out.println("Jdk动态代理,监听结束!");
        return invoke;
    }

    /**
     * 为代理目标类创建一个临时类
     */
    public Object getJdkProxy(Object target) {
        this.target = target;
        /**
         * 为代理接口生成一个匿名实现类,并匿名实现类会把代理接口的实现类的方法全部克隆到匿名类中
         *
         * 由newProxyInstance 方法中的target.getClass().getInterfaces() 参数可知,代理目标对象必须要实现接口
         */
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    public static void main(String[] args){
        JdkProxy jdkProxy = new JdkProxy();
        // MyImplement 类实现了 MyInterface 接口
        MyInterface myInterface = (MyInterface) jdkProxy.getJdkProxy(new MyImplement());
        myInterface.database();
        // DynamicProxyDemo 类型没有实现任何接口
        DynamicProxyDemo dynamicProxyDemo = (DynamicProxyDemo) jdkProxy.getJdkProxy(new DynamicProxyDemo());
        dynamicProxyDemo.log();
    }
}

结果:没有实现接口的DynamicProxyDemo类报错了
在这里插入图片描述

CGLIB动态代理

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

/**
 * Author: chaoyou
 * CSDN:https://blog.csdn.net/qq_41910568
 * Date: 2020/5/9 0009 0:27
 * Content:cglib动态代理是利用asm 开源包,对代理对象类的class 文件加载进来,通过修改其字节码生成子类来处理。
 */
public class CglibProxy implements MethodInterceptor {

    /**
     * 声明一个代理的目标对象
     */
    private Object target;

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib动态代理,监听开始!");
        Object invoke = method.invoke(target, objects);
        System.out.println("Cglib动态代理,监听结束!");
        return invoke;
    }

    public Object getCglibProxy(Object target){
        // 初始化代码目标对象
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        Object object = enhancer.create();
        return object;
    }

    public static void main(String[] args){
        CglibProxy jdkProxy = new CglibProxy();
        // MyImplement 类实现了 MyInterface 接口
        MyInterface myInterface = (MyInterface) jdkProxy.getCglibProxy(new MyImplement());
        myInterface.database();
        // DynamicProxyDemo 类型没有实现任何接口
        DynamicProxyDemo dynamicProxyDemo = (DynamicProxyDemo) jdkProxy.getCglibProxy(new DynamicProxyDemo());
        dynamicProxyDemo.log();
    }
}

结果:要代理的类不需要实现接口的限制
在这里插入图片描述

静态代理和动态代理比较

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

AOP和IOC的比较

IoC让相互协作的组件保持松耦合,而AOP编程允许你把遍布于应用各层的通用功能抽离出来形成可重用的功能组件。

Spring AOP里面的几个名词的概念:

  1. 连接点(Join point):指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。
  2. 切面(Aspect):被抽取出来的公共模块,可以用来横切多个对象。切面可以看成 Pointcut切点 + Advice通知的结合,一个切面可以由多个切点和通知组成。

在Spring AOP中,切面可以在类上使用 @AspectJ 注解来实现。

  1. 切点(Pointcut):切点用于定义 要对哪些Join point进行拦截。

切点分为execution方式和annotation方式。execution方式可以用路径表达式指定对哪些方法拦截,比如指定拦截add*、search*。annotation方式可以指定被哪些注解修饰的代码进行拦截,如下的JoinPoint是自定义注解。

package com.example.mongodb.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * Author: chaoyou
 * Content:设置一个日志处理切面类
 */
@Aspect
@Component
public class LogAdvice {
    // 定义一个切点:所有被 JoinPoint 注解修饰的方法会织入advice
    @Pointcut("@annotation(com.example.mongodb.util.annotation.JoinPoint)")
    private void logAdvicePointcut1() {}

    /**
     * 定义一个切面,拦截 com.example.mongodb.controller 包和子包下的所有方法
     *
     * 表达式解释:* com.example.mongodb.controller..*.*(..)
     *
     *      第一个 * 号的位置:表示返回值类型,* 表示所有类型。
     *      包名:表示需要拦截的包名
     *      两个点: 第一个点表当前包,第二个点表示子包
     *      第二个 * 号的位置:表示类名,* 表示所有类。
     *      第三个 * 号的位置:表示方法名,* 表示所有方法
     *      (..):表示方法有任意个参数(可以是int,String等)。
     */

    @Pointcut("execution(* com.example.mongodb.controller..*.*(..))")
    private void logAdvicePointcut2() {}
}
  1. 通知(Advice):指要在连接点(Join Point)上执行的动作,即增强的逻辑,比如权限校验和、日志记录等。通知有各种类型,包括Around、Before、After、After returning、After throwing。
  2. 目标对象(Target):包含连接点的对象,也称作被通知(Advice)的对象。 由于Spring AOP是通过动态代理实现的,所以这个对象永远是一个代理对象。
  3. 织入(Weaving):通过动态代理,在目标对象(Target)的方法(即连接点Join point)中执行增强逻辑(Advice)的过程。
  4. 引入(Introduction):添加额外的方法或者字段到被通知的类。Spring允许引入新的接口(以及对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。
    在这里插入图片描述

Spring通知(Advice)有哪些类型?

  1. 前置通知(Before Advice):在连接点(Join point)之前执行的通知。
  2. 后置通知(After Advice):当连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  3. 环绕通知(Around Advice):包围一个连接点的通知,这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
  4. 返回后通知(AfterReturning Advice):在连接点正常完成后执行的通知(如果连接点抛出异常,则不执行)
  5. 抛出异常后通知(AfterThrowing advice):在方法抛出异常退出时执行的通知
    在这里插入图片描述
同一个Aspect,不同advice的执行顺序:

1、没有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning

2、有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing
java.lang.RuntimeException: 异常发生

Spring容器的启动流程:

详细内容可以阅读这篇文章:Spring容器的启动流程_张维鹏的博客-CSDN博客

  1. 初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中
    1.1. 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
    1.2. 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成 BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
    1.3. 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象

  2. 将配置类的BeanDefinition注册到容器中

  3. 调用refresh()方法刷新容器
    3.1. prepareRefresh()刷新前的预处理:
    3.2. obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory:
    3.3. prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件:
    3.4. postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置
    3.5. invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器:
    3.6. registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
    3.7. initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
    3.8. initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到:
    3.9. onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
    3.10. registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件:
    3.11. finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象;
    3.12. finishRefresh():发布BeanFactory容器刷新完成事件:

BeanFactory和ApplicationContext有什么区别?

  1. BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。
  2. BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:

ApplicationContext 继承 MessageSource,因此支持国际化。
资源文件访问,如URL和文件(ResourceLoader)。
载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
提供在监听器中注册bean的事件。

  1. BeanFactory 和 ApplicationContext 的加载方式

①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。
③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

  1. BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
  2. BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

Spring Bean的生命周期?

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

但具体来说,Spring Bean的生命周期包含下图的流程:
在这里插入图片描述

实例化Bean:

  1. 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化(饿汉模式)。
  2. 对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean(饱汉模式)。

设置对象属性(依赖注入):

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

处理Aware接口:

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

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

BeanPostProcessor前置处理:

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

InitializingBean:

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

init-method:

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

BeanPostProcessor后置处理:

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

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

DisposableBean:

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

destroy-method:

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

如果对bean详细加载流程的感兴趣的读者,可以阅读这篇文章:Spring的Bean加载流程_张维鹏的博客-CSDN博客

Spring中bean的作用域:

singleton(单例):

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

因为所有对象都是共享这一个bean实例,所以spring中作用域为singleton下的bean不能保证安全性

prototype(原型/多例):

每一个bean请求都会创建一个新的实例。

request(请求):

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

session(会话):

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

global-session(全局):

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

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

Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。

prototype作用域的Bean

每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。

singleton作用域的Bean

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

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

解决有状态bean的安全问题

  1. 对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。
  2. 采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。

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

Spring注入bean的几种方式:

案例代码:

package cn.zdxh.lcy.learn;

public class School {
    private String schoolName;

    public School(String schoolName) {
        this.schoolName = schoolName;
    }

    public void setSchoolName(String schoolName) {
        this.schoolName = schoolName;
    }

    public String getSchoolName() {
        return schoolName;
    }
}

// 需求:student 对象中注入 school 依赖的方式
public class Student {
    private School school;
}

set()方法注入;

java

package cn.zdxh.lcy.learn;

public class Student {
    private School school;

    public void setSchool(School school) {
        this.school = school;
    }
}

xml

<!--配置bean,配置后该类由spring管理--> 
<bean name="school" class="cn.zdxh.lcy.learn.School"></bean>
<bean name="student" class="cn.zdxh.lcy.learn.Student"> 
	<!--(1)依赖注入,配置当前类中相应的属性--> 
	<property name="school" ref="school"></property> 
</bean> 

构造器注入:①通过index设置参数的位置;②通过type设置参数类型;

java

package cn.zdxh.lcy.learn;

public class Student {
    private School school;
    private String username;

    public Student(School school, String username) {
        this.school = school;
        this.username = username;
    }
}

xml

<!-- 配置bean,配置后该类由spring管理 --> 
<bean name="school" class="cn.zdxh.lcy.learn.School"></bean> 
<bean name="student" class="cn.zdxh.lcy.learn.Student">
	<!-- 创建构造器注入,如果主类有带参的构造方法则需添加此配置 --> 
	<constructor-arg index="0" ref="school"></constructor-arg> 
	<constructor-arg index="1" type="java.lang.String" ref="username"></constructor-arg> 
</bean> 

静态工厂注入;

java

package cn.zdxh.lcy.learn;

public class School {
    public static final School initSchool() {
        return new School();
    }
}

public class Student {
    private School school = School.initSchool();
}

xml

<!--配置bean,配置后该类由spring管理--> 
<bean name="student" class="cn.zdxh.lcy.learn.Student" > 
	<!-- 使用静态工厂的方法注入对象,对应下面的配置文件 --> 
	<property name="school" ref="getSchool"></property> 
</bean> 
<!-- 此处获取对象的方式是从工厂类中获取静态方法 --> 
<bean name="getSchool" class="cn.zdxh.lcy.learn.School" factory-method="initSchool"></bean> 

实例工厂

java

package cn.zdxh.lcy.learn;

public class School {
    public School initSchool() {
        return new School();
    }
}

public class Student {
    private School school;
}

xml

<!-- 配置bean,配置后该类由spring管理 --> 
<bean name="student" class="cn.zdxh.lcy.learn.Student"> 
<!-- 使用实例工厂的方法注入对象,对应下面的配置文件 --> 
<property name="school" ref="getSchool"></property> 
</bean> 
 
 
<!-- 此处获取对象的方式是从工厂类中获取实例方法 --> 
<bean name="school" class="cn.zdxh.lcy.learn.School"></bean> 
<bean name="getSchool" factory-bean="school" factory-method="initSchool"></bean> 

详细内容请参考这篇文章:Spring中bean的注入方式

Spring如何解决循环依赖问题

详细内容强烈建议参考这篇文章:Spring如何解决循环依赖问题

循环依赖问题案例分析:

public class ClassA {
	private ClassB classB;
 
	public ClassB getClassB() {
		return classB;
	}
 
	public void setClassB(ClassB classB) {
		this.classB = classB;
	}
}
public class ClassB {
	private ClassA classA;
 
	public ClassA getClassA() {
		return classA;
	}
 
	public void setClassA(ClassA classA) {
		this.classA = classA;
	}
}

通过Spring IoC流程的源码分析循环依赖问题

在这里插入图片描述

循环依赖问题的类型

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

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

在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。其实也很好解释:

  • 构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
  • setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

如何解决循环依赖问题?

Spring解决的单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过两个缓存来解决的,请看下图:
在这里插入图片描述

解决构造函数相互注入造成的循环依赖:

  1. 前面说Spring可以自动解决单例模式下通过setter()方法进行依赖注入产生的循环依赖问题。而对于通过构造方法进行依赖注入时产生的循环依赖问题没办法自动解决,那针对这种情况,我们可以使用@Lazy注解来解决。
  2. 也就是说,对于类A和类B都是通过构造器注入的情况,可以在A或者B的构造函数的形参上加个@Lazy注解实现延迟加载。@Lazy实现原理是,当实例化对象时,如果发现参数或者属性有@Lazy注解修饰,那么就不直接创建所依赖的对象了,而是使用动态代理创建一个代理类。
  3. 比如,类A的创建:A a=new A(B),需要依赖对象B,发现构造函数的形参上有@Lazy注解,那么就不直接创建B了,而是使用动态代理创建了一个代理类B1,此时A跟B就不是相互依赖了,变成了A依赖一个代理类B1,B依赖A。但因为在注入依赖时,类A并没有完全的初始化完,实际上注入的是一个代理对象,只有当他首次被使用的时候才会被完全的初始化。

多例循环依赖

这类循环依赖问题可以通过把bean改成单例的解决。

Spring的自动装配

在spring中,使用autowire来配置自动装载模式,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。

基于xml的自动装配方式:

  • no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
  1. 案例代码
package cn.zdxh.lcy.learn;

public class School {
    protected void schoolName(){
        System.out.println("这里是 Spring 自学体系院校");
    }
}

public class Student {
    private School school;

    public void setSchool(School school) {
        this.school = school;
    }

    public void mySchool(){
        school.schoolName();
    }
}
  1. bean 装配
<beans>
	<!--配置bean,配置后该类由spring管理--> 
	<bean name="school" class="cn.zdxh.lcy.learn.School"></bean>
	<bean name="student" class="cn.zdxh.lcy.learn.Student"> 
		<!-- 依赖注入,配置当前类中相应的属性 --> 
		<property name="school" ref="school" autowire="no"></property> 
	</bean> 
</beans>
  1. 测试用例
public class TestDemo {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("resource/spring-config.xml");
        Student student = (Student)context.getBean("student");
        student.mySchool();
    }
}
  1. 测试结果:
    在这里插入图片描述
  • byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
  1. 案例代码:如上不变
  2. bean 装配
<beans>
	<!--配置bean,配置后该类由spring管理--> 
	<bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="byName"></bean>
	<!-- 
		1、byName模式下,是根据Student类下的school变量名和bean中的id名称关联:
			private School school;
			id="school"
		2、如果两者不一致则会报错
			Exception in thread "main" java.lang.NullPointerException
	-->
	<bean id="school" class="cn.suancioud.lcy.learn.School"></bean> 
</beans>
  1. 测试用例:如上不变
  2. 测试结果:
    在这里插入图片描述
  • byType:通过参数的数据类型进行自动装配。
  1. 案例代码:如上不变
  2. bean 装配
<beans>
	<!--配置bean,配置后该类由spring管理--> 
	<bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="byType"></bean>
	<!-- 
		1、byName模式下,是根据Student的全限定类名和bean中的class名称关联:
			private cn.suancioud.lcy.learn.School school;
			class="cn.suancioud.lcy.learn.School"
		2、如果两者不一致则会报错
			Exception in thread "main" java.lang.NullPointerException
	-->
	<bean id="school111" class="cn.suancioud.lcy.learn.School"></bean> 
</beans>
  1. 测试用例:如上不变
  2. 测试结果:
    在这里插入图片描述
  • constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
  1. 案例代码
package cn.zdxh.lcy.learn;

public class School {
    protected void schoolName(){
        System.out.println("这里是 Spring 自学体系院校");
    }
}

public class Student {
    private School school;

    public Student(School school) {
        this.school = school;
    }

    public void mySchool(){
        school.schoolName();
    }
}
  1. bean 装配
<beans>
	<!--配置bean,配置后该类由spring管理--> 
	<bean id="school" class="cn.suancioud.lcy.learn.School"></bean>
    <bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="constructor"></bean>
</beans>
  1. 测试用例:如上不变
  2. 测试结果:
    在这里插入图片描述

基于注解的自动装配方式:

使用@Autowired、@Resource注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,。在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。

开启注解装配

  1. 修改 spring-config.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="school" class="cn.suancioud.lcy.learn.School">
        <!--(1)依赖注入,配置当前类中相应的属性-->
        <property name="disc" value="这里是 Spring 自学体系院校" />
    </bean>
    <bean id="school1" class="cn.suancioud.lcy.learn.School">
        <!--(1)依赖注入,配置当前类中相应的属性-->
        <property name="disc" value="这里是 mybatis 自学体系院校" />
    </bean>
    <bean id="student" class="cn.suancioud.lcy.learn.Student"></bean>

    <!-- 开启注解装配方式 -->
    <context:annotation-config />
</beans>
  1. 案例代码
public class School {
    private String disc;
    protected void schoolName(){
        System.out.println(disc);
    }

    public void setDisc(String disc) {
        this.disc = disc;
    }
}

注解装配方式

@Autowired

在使用@Autowired时,首先在容器中按类型查询对应的bean:

如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。

应用场景

@Autowired可用于:构造函数、成员变量、Setter方法

  1. 案例代码
public class Student {
    @Autowired
    private School school;

    public void mySchool(){
        school.schoolName();
    }
}
  1. 测试用例:如上不变

  2. 结果展示
    在这里插入图片描述

@Resource

这个注解是java中自带的一个注解。它相当于@Qualifier与@AutoWired两者的结合,beanid可以不与set后面相同,当有多个相同class的bean时在后面加上一个参数(name = “beanid”)就可以装配指定id的bean。

注:@Autowired和@Resource之间的区别:
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。

  1. 案例代码
public class Student {
    @Resource(name = "school")
    private School school;

    public void mySchool(){
        school.schoolName();
    }
}
  1. 测试用例:如上不变

  2. 结果展示
    在这里插入图片描述

@Qualifier

这个注解是为了解决一个类在bean容器中有多个bean的情况,因为用前面两种注解无法精确到哪个bean名称

  1. 案例代码
public class Student {
	// Qualifier + Autowired == Resource,必须搭配一起使用
    @Autowired
    @Qualifier("school1")
    private School school;

    public void mySchool(){
        school.schoolName();
    }
}
  1. 测试用例:如上不变

  2. 结果展示
    在这里插入图片描述

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

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过binlog或者undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。

事务实现方式

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

编程式事务

编程式事务管理使用TransactionTemplate。

声明式事务

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

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

事务传播机制

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

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

PROPAGATION_REQUIRES_NEW(总是新的):无论当前存不存在事务,都创建新事务进行执行。

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

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

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

PROPAGATION_MANDATORY(强制性):如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

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

事务的隔离级别

ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。

ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。

ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。

ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。

ISOLATION_SERIALIZABLE:所有事务逐个依次执行。

实现原理

Spring事务 的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

获取连接 Connection con = DriverManager.getConnection();												①																							 

开启事务con.setAutoCommit(true/false);												②

执行CRUD												③

提交事务/回滚事务 con.commit() / con.rollback();												④

关闭连接 conn.close();												⑤

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。 那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子

配置文件开启注解驱动
在相关的类和方法上通过注解@Transactional标识
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有@Transactional注解的类和方法。
根据@Transaction的相关参数进行相关配置注入, 利用代理的方式在类和方法的前置(开启事务)、后继(提交/回滚事务)。

真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

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

Spring设计模式的详细使用案例可以阅读这篇文章:Spring中所使用的设计模式_张维鹏的博客-CSDN博客_spring使用的设计模式

工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

单例模式:Bean默认为单例模式

策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术

模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate

适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。

桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库

Spring框架中有哪些不同类型的事件?

Spring 提供了以下5种标准的事件:

上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。

上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。

上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。

上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。

请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。

如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。

Spring常用注解及其作用

  1. @Controller

标识一个类是Spring MVC controller处理器,用来创建处理http请求的对象。

  1. @RestController

Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。

  1. @Service

用于标注业务层组件,说白了就是用注解的方式把这个类注入到spring配置中。

  1. @Autowired(spring的注解)

用来装配bean,可以写在字段上,或者方法上。
默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,例如:@Autowired(required=false)

  1. @Resource(不属于spring的注解)

@Resource的作用相当于@Autowired
只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。

  1. @RequestParam

用于将请求参数区数据映射到功能处理方法的参数上。

  1. @PathVariable

将请求参数绑定在url地址后面。

@RequestMapping(value="/happy/{dayid}",method=RequestMethod.GET)
public String findPet(@PathVariable String dayid, Model mode) {
	//使用@PathVariable注解绑定 {dayid} 到String dayid
}
  1. @Cacheable

当标记在一个方法上时表示该方法是支持缓存的,
当标记在一个类上时则表示该类所有的方法都是支持缓存的。

  1. @Repository

用于标注数据访问组件,即DAO组件。

  1. @Component

泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。

  1. @Scope

用来配置 spring bean 的作用域,它标识 bean 的作用域。

  1. @SessionAttributes

默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中。

  1. @Qualifier

当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。

  1. @Configuration

使用@Configuration 来注解类表示类可以被 Spring 的 IoC 容器所使用,作为 bean 定义的资源。

@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}
  1. @RequestMapping

这个注解用于将url映射到整个处理类或者特定的处理请求的方法。

  1. @Transactional

事务的注解,可以添加在方法或者类上。一般注解在业务层。

springboot

什么是SpringBoot?

  1. 用来简化Spring应用的初始搭建以及开发过程,使用特定的方式来进行配置
  2. 创建独立的Spring引用程序main方法运行
  3. 嵌入的tomcat无需部署war文件
  4. 简化maven配置
  5. 自动配置Spring添加对应的功能starter自动化配置

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

SpringBoot有哪些优缺点?

优点

独立运行

Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。

简化配置

spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。

自动配置

Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 springboot-starter-web 启动器就能拥有 web 的功能,无需其他配置。

无代码生成和XML配置

SpringBoot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。

应用监控

SpringBoot 提供一系列端点可以监控服务及应用,做健康检测。

缺点

Spring Boot 虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。

SpringBoot、Spring MVC和Spring有什么区别?

Spring

Spring最重要的特征是依赖注入。所有Spring Modules不是依赖注入就是IOC控制反转。

当我们恰当的使用DI或者是IOC的时候,可以开发松耦合应用。

Spring MVC

Spring MVC提供了一种分离式的方法来开发Web应用。通过运用像DispatcherServelet,MoudlAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。

SpringBoot

Spring和Spring MVC的问题在于需要配置大量的参数。

SpringBoot通过一个自动配置和启动的项来解决这个问题。

什么是Spring Boot Starter?

启动器是一套方便的依赖描述符,它可以放在自己的程序中。可以一站式的获取你所需要的Spring和相关技术,而不需要依赖描述符的通过示例代码搜索和复制粘贴的负载。

例如,如果想使用Spring和JPA访问数据库,只需要项目中包含spring-boot-starter-data-jpa 依赖项,你就可以正产是用。

为什么需要spring-boot-maven-plugin?

spring-boot-maven-plugin提供了一些像jar一样打包或者运行应用程序的命令。

spring-boot:run 运行SpringBoot应用程序;
spring-boot:repackage 重新打包你的jar包或者是war包使其可执行
spring-boot:start和spring-boot:stop管理Spring Boot应用程序的生命周期
spring-boot:build-info生成执行器可以使用的构造信息

什么是YAML?

YAML是一种人类可读的数据序列化语言。它通常用于配置文件。
与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML文件就更加结构化,而且更少混淆。可以看出YAML具有分层配置数据。

SpringBoot自动配置的原理

在Spring程序main方法的主类上,添加@SpringBootApplication或者@EnableAutoConfiguration会自动去maven中读取每个starter中的spring.gfactories文件,该文件里配置了所有需要被加载到Spring容器中的bean

RequestMapping和GetMapping的不同之处在哪里?

  1. RequestMapping具有类属性的,可以进行GET、POST、PUT或者其他的注释中具有的请求方法。
  2. GetMapping是Get请求方法中的一个特例,它只是RequestMapping的一个延伸,目的是为了提高清晰度。

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

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

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

SpringBoot 打成jar和普通的jar有什么区别?

Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过java -jar xxx.jar命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。

Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

运行SpringBoot有几种方式?

  1. 打包用命令或者放到容器中运行
  2. 用Maven或Gradle插件运行
  3. 直接执行main方法运行

开启Spring Boot特性有哪几种方式?

  1. 继承spring-boot-starter-parent项目
  2. 导入spring-boot-dependencies项目依赖

什么是Spring Data?

Spring Data 是 Spring 的一个子项目。用于简化数据库访问,支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。Spring Data 具有如下特点:

SpringData 项目支持 NoSQL 存储:

  1. MongoDB (文档数据库)
  2. Neo4j(图形数据库)
  3. Redis(键/值存储)
  4. Hbase(列族数据库)

SpringData 项目所支持的关系数据存储技术:

  1. JDBC
  2. JPA

Spring Data Jpa 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的,就是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!Spring Data JPA 通过规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。

什么是Swagger?你用Spring Boot实现了吗?

Swagger 广泛用于可视化 API,使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。

前后端分离,如何维护接口文档?

前后端分离开发日益流行,大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档,不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。

如何使用Spring Boot实现异常处理?

Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。通过实现一个ControlerAdvice类,来处理控制类抛出的所有异常。

什么是FreeMarker模板?

FreeMarker 是一个基于 Java 的模板引擎,最初专注于使用 MVC 软件架构进行动态网页生成。使用 Freemarker 的主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码,而设计人员可以处理 html 页面设计。最后使用freemarker 可以将这些结合起来,给出最终的输出页面。

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

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

比较一下Spring Security和Shiro各自的优缺点?

由于Spring Boot官方提供了大量的非常方便的开箱即用的Starter,包括Spring Security的Starter,使得在SpringBoot中使用Spring Security变得更加容易,甚至只需要添加一个一来就可以保护所有接口,所以如果是SpringBoot项目,一般选择Spring Security。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。

Shiro和Spring Security相比,主要有如下特点:

  1. Spring Security是一个重量级的安全管理框架;Shiro则是一个轻量级的安全管理框架;
  2. Spring Security概念复杂,配置繁琐;Shiro概念简单、配置简单;
  3. Spring Security功能强大;Shiro功能简单

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

跨域可以在前端通过JSONP来解决,但是JSONP只可以发送GET请求,无法发送其他类型的请求,在RESTful风格的应用中,就显得非常鸡肋,因此推荐在后端通过(CORS,Cross-origin resource sharing)来解决跨域问题。这种解决方案并非Spring Boot特有的,在传统的SSM框架中,就可以通过CORS来解决跨域问题,只不过之前我们是在XML文件中配置CORS,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。

@Configuration
public class CorsConfig implements WebMvcConfigurer {

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

}

项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}

什么是CSRF攻击?

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

Spring Boot的核心注解是哪些?他主由哪几个注解组成的?

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

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

SpringBoot的核心配置文件有哪几个?他们的区别是什么?

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

  1. application配置文件这个容易理解,主要用于Spring Boot项目的自动化配置。
  2. bootstrap配置文件有以下几个应用场景:

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

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

Spring Boot 可 以 通 过 @PropertySource,@Value,@Environment, @ConfigurationProperties 来绑定变量。

Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?

Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架。

保护SpringBoot应用有哪些方法?

在生产中使用HTTPS
使用Snyk检查你的依赖关系
升级到最新版本
启用CSRF保护
使用内容安全策略防止XSS攻击

SpringBoot 2.X有哪些新特性?与1.X有什么区别?

配置变更
JDK版本升级
第三方类库升级
响应式Spring编程支持
HTTP/2支持
配置属性绑定
更多改进与加强

springboot中@Transaction是怎么实现的?

  1. 在相关的类和方法上通过注解@Transactional标识
  2. Spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有@Transactional注解的类和方法。
  3. 根据@Transaction的相关参数进行相关配置注入, 利用代理的方式在类和方法的前置(开启事务)、后继(提交/回滚事务)。

缓存中间件

redis有哪些数据类型?

说说对redis中缓存穿透、缓存击穿和缓存雪崩的理解?

说说redis中的哨兵模式机制是怎么实现的?

消息队列

什么是消息队列

消息队列的优点

  1. 解耦:将系统按照不同的业务功能拆分出来,消息生产者只管把消息发布到 MQ 中而不用管谁来取,消息消费者只管从 MQ 中取消息而不管是谁发布的。消息生产者和消费者都不知道对方的存在;
  2. 异步:主流程只需要完成业务的核心功能;对于业务非核心功能,将消息放入到消息队列之中进行异步处理,减少请求的等待,提高系统的总体性能;
  3. 削峰/限流:将所有请求都写到消息队列中,消费服务器按照自身能够处理的请求数从队列中拿到请求,防止请求并发过高将系统搞崩溃;

消息队列的缺点

  1. 系统的可用性降低:系统引用的外部依赖越多,越容易挂掉,如果MQ 服务器挂掉,那么可能会导致整套系统崩溃。这时就要考虑如何保证消息队列的高可用了
  2. 系统复杂度提高:加入消息队列之后,需要保证消息没有重复消费、如何处理消息丢失的情况、如何保证消息传递的有序性等问题;
  3. 数据一致性问题:A 系统处理完了直接返回成功了,使用者都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,就会导致数据不一致了

Kafka、ActiveMQ、RabbitMQ、RocketMQ 消息队列的选型

在这里插入图片描述
每种MQ没有绝对的好坏,主要依据使用场景,扬长避短,利用其优势,规避其劣势。

  1. 中小型软件公司,技术实力较为一般,建议选RabbitMQ:一方面,erlang语言天生具备高并发的特性,而且管理界面用起来十分方便。代码是开源的,而且社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。
  • 不考虑 rocketmq 的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
  • 不考虑 kafka 的原因是:中小型软件公司不如互联网公司,数据量没那么大,选消息中间件应首选功能比较完备的,所以kafka排除
  1. 大型软件公司:根据具体使用场景在rocketMq和kafka之间二选一。
    一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司有能力对rocketMQ进行定制化开发。至于kafka,如果是大数据领域的实时计算、日志采集功能,肯定是首选kafka了。

RabbitMQ的构造

RabbitMQ 是 AMQP 协议的一个开源实现,所以其内部实际上也是 AMQP 中的基本概念:
在这里插入图片描述

生产者Publisher:生产消息,就是投递消息的一方。消息一般包含两个部分:消息体(payload)和标签(Label)
消费者Consumer:消费消息,也就是接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
Broker服务节点:表示消息队列服务器实体。一般情况下一个Broker可以看做一个RabbitMQ服务器。
Queue:消息队列,用来存放消息。一个消息可投入一个或多个队列,多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
Exchange:交换器,接受生产者发送的消息,根据路由键将消息路由到绑定的队列上。
Routing Key: 路由关键字,用于指定这个消息的路由规则,需要与交换器类型和绑定键(Binding Key)联合使用才能最终生效。
Binding:绑定,通过绑定将交换器和队列关联起来,一般会指定一个BindingKey,通过BindingKey,交换器就知道将消息路由给哪个队列了。
Connection:网络连接,比如一个TCP连接,用于连接到具体broker
Channel: 信道,AMQP 命令都是在信道中进行的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接,一个TCP连接可以用多个信道。客户端可以建立多个channel,每个channel表示一个会话任务。
Message:消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Virtual host:虚拟主机,用于逻辑隔离,表示一批独立的交换器、消息队列和相关对象。一个Virtual host可以有若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段

Exchange交换器的类型

Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers

  • direct:消息中的路由键(RoutingKey)如果和 Bingding 中的 bindingKey 完全匹配,交换器就将消息发到对应的队列中。是基于完全匹配、单播的模式。
  • fanout:把所有发送到fanout交换器的消息路由到所有绑定该交换器的队列中,fanout 类型转发消息是最快的。
  • topic:通过模式匹配的方式对消息进行路由,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。

匹配规则:

  • RoutingKey 和 BindingKey 为一个 点号 ‘.’ 分隔的字符串。 比如: java.xiaoka.show
  • BindingKey可使用 * 和 # 用于做模糊匹配:匹配一个单词,#匹配多个或者0个单词。如:order.
  • headers:不依赖于路由键进行匹配,是根据发送消息内容中的headers属性进行匹配,除此之外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了

生产者消息的过程

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

消费者接收消息过程

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

如何保证消息不被重复消费

正常情况下,Consumer在消费消息后会对Queue响应一个确认(ack),Queue接收后就知道消息已经被成功消费了,然后就从队列中删除该消息,也就不会将该消息再发送给其他消费者了。不同消息队列发出的确认消息形式不同,RabbitMQ是通过发送一个ACK确认消息。但是因为网络故障,Consumer发出的确认并没有传到Queue,导致Queue不知道该消息已经被消费,然后再次将消息发送给了其他Consumer,从而造成重复消费的情况。

重复消费问题的解决思路是:保证消息的唯一性,即使多次传输,也不让消息的多次消费带来影响,也就是保证消息等幂性;幂等性指一个操作执行任意多次所产生的影响均与一次执行的影响相同。具体解决方案如下:

  1. 改造业务逻辑,使得在重复消费时也不影响最终的结果。例如对SQL语句: update t1 set money = 150 where id = 1 and money = 100; 做了个前置条件判断,即 money = 100 的情况下才会做更新,更通用的是做个 version 即版本号控制,对比消息中的版本号和数据库中的版本号。
  2. 基于数据库的的唯一主键进行约束。消费完消息之后,到数据库中做一个 insert 操作,如果出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  3. 通过记录关键的key,当重复消息过来时,先判断下这个key是否已经被处理过了,如果没处理再进行下一步。
  • 通过数据库:比如处理订单时,记录订单ID,在消费前,去数据库中进行查询该记录是否存在,如果存在则直接返回。
  • 使用全局唯一ID,再配合第三组主键做消费记录。比如,利用redis的set结构处理,生产者发送消息时给消息分配一个全局ID。消费者在每次拿到消息后,并且在消费前,先去redis中查询这个ID是否存在,如果存在则放弃这条消息的消费操作,如果不存在则对这条信息进行后续的消费操作,消费完之后,就将这个ID以k-v的形式存入redis中(过期时间根据具体情况设置)。

如何保证消息不丢失,进行可靠性传输

对于消息的可靠性传输,每种MQ都要从三个角度来分析:生产者丢数据、消息队列丢数据、消费者丢数据。以RabbitMQ为例:

生产者丢数据

RabbitMQ提供事务机制(transaction)和确认机制(confirm)两种模式来确保生产者不丢消息。

事务机制

发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())

该方式的缺点是生产者发送消息会同步阻塞等待发送结果是成功还是失败,导致生产者发送消息的吞吐量降下降。

// 开启事务
channel.txSelect
try {
    // 发送消息
} catch(Exception e){
    // 回滚事务
    channel.txRollback;
    //再次重试发送这条消息
    ....
}      
//提交事务
channel.txCommit;

确认机制

生产环境常用的是confirm模式。生产者将信道 channel 设置成 confirm 模式,一旦 channel 进入 confirm 模式,所有在该信道上发布的消息都将会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个确认给生产者(包含消息的唯一ID),这样生产者就知道消息已经正确到达目的队列了。如果rabbitMQ没能处理该消息,也会发送一个Nack消息给你,这时就可以进行重试操作。

Confirm模式最大的好处在于它是异步的,一旦发布消息,生产者就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者便可以通过回调方法来处理该确认消息。

处理Ack的代码如下所示:

@Bean
public RabbitTemplate rabbitTemplate(){
    Logger logger = LoggerFactory.getLogger(MyAMQPConfig.class);

    // 设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
    rabbitTemplate.setMandatory(true);

    // 消息返回,需要配置 spring.rabbitmq.publisher-returns=true
    rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
        String correlationId = message.getMessageProperties().getCorrelationId();
        logger.debug("消息 : {} 发送失败,应答码 : {},原因 : {},交换机 : {},路由键 : {}", correlationId, replyCode, replyText, exchange, routingKey);
    });

    // 开启消息消费确认机制,需要配置:spring.rabbitmq.publisher-confirms=true
    rabbitTemplate.setConfirmCallback(((correlationData, ack, cause) -> {
        if (ack){
			// logger.debug("消息发送到 exchange 成功,id : {}", correlationData.getId());
        } else {
            logger.debug("消息发送到 exchange 失败, : {}", correlationData.getId());
        }
    }));
    return rabbitTemplate;
}

消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘。持久化配置可以和生产者的 confirm 机制配合使用,在消息持久化磁盘后,再给生产者发送一个Ack信号。这样的话,如果消息持久化磁盘之前,即使 RabbitMQ 挂掉了,生产者也会因为收不到Ack信号而再次重发消息。

持久化设置如下(必须同时设置以下 2 个配置):

  • 创建queue的时候,将queue的持久化标志durable在设置为true,代表是一个持久的队列,这样就可以保证 rabbitmq 持久化 queue 的元数据,但是不会持久化queue里的数据;
  • 发送消息的时候将 deliveryMode 设置为 2,将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

这样设置以后,RabbitMQ 就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。

消费者丢数据

消费者丢失数据一般是因为采用了自动确认消息模式。该模式下,虽然消息还在处理中,但是消费中者会自动发送一个确认,通知 RabbitMQ 已经收到消息了,这时 RabbitMQ 就会立即将消息删除。这种情况下,如果消费者出现异常而未能处理消息,那就会丢失该消息。

解决方案就是采用手动确认消息,设置 autoAck = False,等到消息被真正消费之后,再手动发送一个确认信号,即使中途消息没处理完,但是服务器宕机了,那 RabbitMQ 就收不到发的ack,然后 RabbitMQ 就会将这条消息重新分配给其他的消费者去处理。

但是 RabbitMQ 并没有使用超时机制,RabbitMQ 仅通过与消费者的连接来确认是否需要重新发送消息,也就是说,只要连接不中断,RabbitMQ 会给消费者足够长的时间来处理消息。另外,采用手动确认消息的方式,我们也需要考虑一下几种特殊情况:

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

需要注意的点:

  • 消息可靠性增强了,性能就下降了,因为写磁盘比写 RAM 慢的多,两者的吞吐量可能有 10 倍的差距。所以,是否要对消息进行持久化,需要综合考虑业务场景、性能需要,以及可能遇到的问题。若想达到单RabbitMQ服务器 10W 条/秒以上的消息吞吐量,则要么使用其他的方式来确保消息的可靠传输,要么使用非常快速的存储系统以支持全持久化,例如使用 SSD。或者仅对关键消息作持久化处理,且应该保证关键消息的量不会导致性能瓶颈。
  • 当设置 autoAck = False 时,如果忘记手动 ack,那么将会导致大量任务都处于 Unacked 状态,造成队列堆积,直至消费者断开才会重新回到队列。解决方法是及时 ack,确保异常时 ack 或者拒绝消息。
  • 启用消息拒绝或者发送 nack 后导致死循环的问题:如果在消息处理异常时,直接拒绝消息,消息会重新进入队列。这时候如果消息再次被处理时又被拒绝 。这样就会形成死循环。

如何保证消息的有序性

针对保证消息有序性的问题,解决方法就是保证生产者入队的顺序是有序的,出队后的顺序消费则交给消费者去保证。

方法一

拆分queue,使得一个queue只对应一个消费者。由于MQ一般都能保证内部队列是先进先出的,所以把需要保持先后顺序的一组消息使用某种算法都分配到同一个消息队列中。然后只用一个消费者单线程去消费该队列,这样就能保证消费者是按照顺序进行消费的了。但是消费者的吞吐量会出现瓶颈。如果多个消费者同时消费一个队列,还是可能会出现顺序错乱的情况,这就相当于是多线程消费了
在这里插入图片描述

方法二

对于多线程的消费同一个队列的情况,可以使用重试机制:比如有一个微博业务场景的操作,发微博、写评论、删除微博,这三个异步操作。如果一个消费者先执行了写评论的操作,但是这时微博都还没发,写评论一定是失败的,等一段时间。等另一个消费者,先执行发微博的操作后,再执行,就可以成功。

如何处理消息堆积情况?

场景题:几千万条数据在MQ里积压了七八个小时。

出现该问题的原因

消息堆积往往是生产者的生产速度与消费者的消费速度不匹配导致的。有可能就是消费者消费能力弱,渐渐地消息就积压了,也有可能是因为消息消费失败反复复重试造成的,也有可能是消费端出了问题,导致不消费了或者消费极其慢。比如,消费端每次消费之后要写mysql,结果mysql挂了,消费端hang住了不动了,或者消费者本地依赖的一个东西挂了,导致消费者挂了。

所以如果是 bug 则处理 bug;如果是因为本身消费能力较弱,则优化消费逻辑,比如优化前是一条一条消息消费处理的,那么就可以批量处理进行优化。

临时扩容,快速处理积压的消息

  1. 先修复 consumer 的问题,确保其恢复消费速度,然后将现有的 consumer 都停掉;
  2. 临时创建原先 N 倍数量的 queue ,然后写一个临时分发数据的消费者程序,将该程序部署上去消费队列中积压的数据,消费之后不做任何耗时处理,直接均匀轮询写入临时建立好的 N 倍数量的 queue 中;
  3. 接着,临时征用 N 倍的机器来部署 consumer,每个 consumer 消费一个临时 queue 的数据
  4. 等快速消费完积压数据之后,恢复原先部署架构 ,重新用原先的 consumer 机器消费消息。

这种做法相当于临时将 queue 资源和 consumer 资源扩大 N 倍,以正常 N 倍速度消费。

恢复队列中丢失的数据

如果使用的是 rabbitMQ,并且设置了过期时间,消息在 queue 里积压超过一定的时间会被 rabbitmq 清理掉,导致数据丢失。这种情况下,实际上队列中没有什么消息挤压,而是丢了大量的消息。所以就不能说增加 consumer 消费积压的数据了,这种情况可以采取 “批量重导” 的方案来进行解决。在流量低峰期,写一个程序,手动去查询丢失的那部分数据,然后将消息重新发送到mq里面,把丢失的数据重新补回来。

MQ长时间未处理导致MQ写满的情况如何处理

如果消息积压在MQ里,并且长时间都没处理掉,导致MQ都快写满了,这种情况肯定是临时扩容方案执行太慢,这种时候只好采用 “丢弃+批量重导” 的方式来解决了。首先,临时写个程序,连接到mq里面消费数据,消费一个丢弃一个,快速消费掉积压的消息,降低MQ的压力,然后在流量低峰期时去手动查询重导丢失的这部分数据。

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

RabbitMQ 是基于主从(非分布式)做高可用性的,RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式

单机模式

一般没人生产用单机模式

普通集群模式

就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。我们创建的 queue,只会放在其中一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。消费的时候,如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。

优点

普通集群模式主要用于提高系统的吞吐量,可以通过添加更加的节点来线性的扩展消息队列的吞吐量,就是说让集群中多个节点来服务某个 queue 的读写操作

缺点

无高可用性,queue所在的节点宕机了,其他实例就无法从那个实例拉取数据;RabbitMQ 内部也会产生大量的数据传输。

镜像集群模式

RabbitMQ 真正的高可用模式。镜像集群模式下,队列的元数据和消息会存在于多个实例上,每次写消息到 queue 时,会自动将消息同步到各个实例的 queue ,也就是说每个 RabbitMQ 节点都有这个 queue 的完整镜像,包含 queue 的全部数据。任何一个机器宕机了,其它机器节点还包含了这个 queue 的完整数据,其他 consumer 都可以到其它节点上去消费数据。

配置镜像队列的集群都包含一个主节点master和若干个从节点slave,slave会准确地按照master执行命令的顺序进行动作,故slave与master上维护的状态应该是相同的。如果master由于某种原因失效,那么按照slave加入的时间排序,"资历最老"的slave会被提升为新的master。

除发送消息外的所有动作都只会向master发送,然后再由master将命令执行的结果广播给各个slave。如果消费者与slave建立连接并进行订阅消费,其实质上都是从master上获取消息,只不过看似是从slave上消费而已。比如消费者与slave建立了TCP连接之后执行一个Basic.Get的操作,那么首先是由slave将Basic.Get请求发往master,再由master准备好数据返回给slave,最后由slave投递给消费者。

优点

有效解决了普通集群的高可用性缺点。因为有了选举机制,不会因为master的宕机而造成整个集群的瘫痪。

缺点

  1. 性能开销大,消息需要同步到所有机器上,导致网络带宽压力和消耗很重
  2. 非分布式,没有扩展性。如果 queue 的数据量大到这个机器上的容量无法容纳了,此时该方案就会出现问题了

如何开启镜像集群模式呢?

在RabbitMQ 的管理控制台Admin页面下,新增一个镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
在这里插入图片描述

其他:

  1. 交换器无法根据自身类型和路由键找到符合条件队列时,有哪些处理方式:
    1.1. 设置mandatory = true,代表返回消息给生产者;
    1.2. 设置mandatory = false,代表直接丢弃
  2. 消费者得到消息队列中的数据的方式:pushpull
  3. 消息基于什么传输:由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。所以RabbitMQ 使用信道 channel 的方式来传输数据,信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。
  4. 死信队列DLX:DLX也是一个正常的Exchange,和一般的Exchange没有任何区别。能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列出现死信(dead message,就是没有任何消费者消费)的时候,RabbitMQ就会自动将这条消息重新发布到Exchange上去,进而被路由到另一个队列。可以监听这个队列中的消息作相应的处理。消息变为死信的几种情况:
  • 消息被拒绝(basic.reject/basic.nack)同时 requeue=false(不重回队列)
  • TTL 过期
  • 队列达到最大长度,无法再添加
  1. 延迟队列:存储对应的延迟消息,当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。在 RabbitMQ 中并不存在延迟队列,但我们可以通过设置消息的过期时间和死信队列来实现延迟队列,消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
  2. 优先级队列:优先级高的队列会先被消费,可以通过 x-max-priority 参数来实现。但是当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
  3. RabbitMQ 要求集群中至少有一个磁盘节点,其他节点可以是内存节点,当节点加入或离开集群时,必须要将该变更通知到至少一个磁盘节点。如果只有一个磁盘节点,刚好又是该节点崩溃了,那么集群可以继续路由消息,但不能创建队列、创建交换器、创建绑定、添加用户、更改权限、添加或删除集群节点。也就是说集群中的唯一磁盘节点崩溃的话,集群仍然可以运行,但直到该节点恢复前,无法更改任何东西。

消息队列的应用场景

以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景,详情请看

异步处理

场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式

串行的方式

串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端
在这里插入图片描述

并行方式

并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
在这里插入图片描述
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。

因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?

引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:
在这里插入图片描述
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍

应用解耦

场景说明:

用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图
在这里插入图片描述

传统模式的缺点:

  • 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败
  • 订单系统与库存系统耦合

如何解决以上问题呢?

引入应用消息队列后的方案,如下图:
在这里插入图片描述

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功
  • 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
  • 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

流量削锋

流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛

应用场景

秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。

  • 可以控制活动的人数
  • 可以缓解短时间内高流量压垮应用
    在这里插入图片描述
  • 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面
  • 秒杀业务根据消息队列中的请求信息,再做后续处理

日志处理

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下
在这里插入图片描述

  • 日志采集客户端,负责日志数据采集,定时把日志数据写入到Kafka队列
  • Kafka消息队列,负责日志数据的接收,存储和转发
  • 日志处理应用:订阅并消费kafka队列中的日志数据
    以下是新浪kafka日志处理应用案例:转自(http://cloud.51cto.com/art/201507/484338.htm)
    在这里插入图片描述
  • Kafka:接收用户日志的消息队列
  • Logstash:做日志解析,统一成JSON输出给Elasticsearch
  • Elasticsearch:实时日志分析服务的核心技术,一个schemaless,实时的数据存储服务,通过index组织数据,兼具强大的搜索和统计功能
  • Kibana:基于Elasticsearch的数据可视化组件,超强的数据可视化能力是众多公司选择ELK stack的重要原因

消息通讯

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

点对点通讯:

在这里插入图片描述
客户端A和客户端B使用同一队列,进行消息通讯。

聊天室通讯:

在这里插入图片描述
客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

Hadoop

说说Hadoop中对HDFS的理解?

elasticsearch

说说你的elasticsearch集群的部署方案?

集群由三个节点组成(由于节点数少所以使用公用版ES集群设置,三个节点均可参与选举master、作为数据节点、作为搜索节点)。每个索引拆分为三个分片(shard)、索引副本数是2,即每个索引有6个分片(3主 + 6副),所以每个节点分配到两个分片(一主一副,不能为同一份数据)。这样的集群可以保证在一个节点宕机之后还能正常运行,容灾能力只有65%,这样将大大提高es集群的高可用性。

说说elasticsearch集群中的主从复制是怎么配置的?

把同一个索引的主分片(shard)和副分片(replica)分配到不同的节点上,可以在elasticsearch.yml配置文件中设置参数

  1. cluster.routing.allocation.awareness.attributes: rack_one # 一个副本
  2. cluster.routing.allocation.awareness.attributes: rack_one # 两个副本

说说elasticsearch中集群、节点、分片的理解?

  1. 集群:一个ES集群由一个或多个节点(Node)组成,每个集群都有一个cluster name作为标识。
  2. 节点:一个ES集群由一个或多个节点(Node)组成,集群中的所有节点都有独立的node_name作为标识,这个集群的所有节点的cluster_name都是一样的。
  3. 分片:ES是分布式搜索引擎,每个索引有一个或多个分片,索引的数据被分配到各个分片上,相当于一桶水用了N个杯子装。分片分为主分片和副分片两种,副分片作为主分片的副本。

说说elasticsearch中索引的mapping设置原理?

{
  "order": 0,
  "index_patterns": [
    "classify_two*"     # 索引名称格式( * 代表后面一个或多个字符)
  ],
  "settings": {
    "index": {
      "max_result_window": "100000000",   # 索引的最大检索范围
      "refresh_interval": "1s",   # 刷新时限
      "number_of_shards": "3",    # 分片数
      "number_of_replicas": "1",    # 每个分片的副本数
      "analysis": {   # 分词器配置
        "analyzer": {
          "default": {
            "type": "ik_smart"
          }
        }
      }
    }
  },
  "mappings": {
    "data": {
      "properties": {     # 索引字段设置
        "FIELD": {
          "type": "keyword"
        },
      }
    }
  },
  "aliases": {
    "classify_two": {}    # 索引别名设置
  }
}

说说在项目使用elasticsearch的API有哪些?

  1. BoolQueryBuilder
  2. SearchSourceBuilder
  3. SearchRequest
  4. RestHighLevelClient

说说elasticsearch中match、match_phrase和term查询的区别?

  1. match查询会把条件做分词处理(模糊查询,只要一部分词匹配上就行了,无顺序要求)
  2. match_phrase查询会把条件做分词处理(精确查询,必须所有词匹配上才行,且顺序不能变)
  3. term查询不会进行分词处理,match查询keyword字段和term效果一样的。

Elasticsearch集群怎么应对数据量的增长问题?

解答:索引数据的规划,应在做好前期规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。

动态索引层面

  1. 基于模板+时间+rollover api滚动创建索引,举例:设计阶段定义:blog索引的模板格式为:blog_index_时间戳的形式,每天递增数据。
  2. 这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线2的32次幂-1,索引存储达到了TB+甚至更大。
  3. 一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。

存储层面

  1. 冷热数据分离存储,热数据(比如最近3天或者一周的数据),其余为冷数据。
  2. 对于冷数据不会再写入新数据,可以考虑定期force_merge加shrink压缩操作,节省存储空间和检索效率。

部署层面

  1. 一旦之前没有规划,这里就属于应急策略。
  2. 结合ES自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。

操作系统

说说你对 Linux 的管道知识的理解

设计模式

什么是设计模式

设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 设计模式于己于人于系统都是多赢的,它使得代码编写真正工程化,它是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。总体来说,设计模式分为三大类:

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

设计模式的六大原则

开闭原则 (Open Close Principle)

开闭原则指的是对扩展开放,对修改关闭。在对程序进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,我们就需要使用接口或者抽象类

依赖倒转原则 (Dependence Inversion Principle):

依赖倒置原则是开闭原则的基础,指的是针对接口编程,依赖于抽象而不依赖于具体

里氏替换原则 (Liskov Substitution Principle) :

  1. 里氏替换原则是继承与复用的基石,只有当子类可以替换掉基类,且系统的功能不受影响时,基类才能被复用,而子类也能够在基础类上增加新的行为。所以里氏替换原则指的是任何基类可以出现的地方,子类一定可以出现。
  2. 里氏替换原则是对 “开闭原则” 的补充,实现 “开闭原则” 的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。

接口隔离原则 (Interface Segregation Principle):

使用多个隔离的接口,比使用单个接口要好,降低接口之间的耦合度与依赖,方便升级和维护方便

迪米特原则 (Demeter Principle):

迪米特原则,也叫最少知道原则,指的是一个类应当尽量减少与其他实体进行相互作用,使得系统功能模块相对独立,降低耦合关系。该原则的初衷是降低类的耦合,虽然可以避免与非直接的类通信,但是要通信,就必然会通过一个“中介”来发生关系,过分的使用迪米特原则,会产生大量的中介和传递类,导致系统复杂度变大,所以采用迪米特法则时要反复权衡,既要做到结构清晰,又要高内聚低耦合。

合成复用原则 (Composite Reuse Principle):

尽量使用组合/聚合的方式,而不是使用继承。

Java的23种设计模式

接下来我们详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析

创建型:

工厂方法模式

工厂方法模式分为三种:简单工厂模式、工厂方法模式、静态工厂模式。

简单工厂模式

建立一个工厂类,并定义一个接口对实现了同一接口的产品类进行创建。首先看下关系图:
在这里插入图片描述

工厂方法模式

工厂方法模式是对简单工厂模式的改进,简单工厂的缺陷在于不符合“开闭原则”,每次添加新产品类就需要修改工厂类,不利于系统的扩展维护。而工厂方法将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。UML关系图如下:
在这里插入图片描述

静态工厂模式

静态工厂模式是将工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
在这里插入图片描述

抽象工厂模式:

抽象工厂模式主要用于创建相关对象的家族。当一个产品族(电池族、屏幕族)中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。

但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
在这里插入图片描述

  • 抽象工厂 BaseFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
  • 具体工厂 ScreenFactory:用于生产屏幕产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
  • 抽象产品 BaseProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
  • 具体产品 ScreenProduct:这是一个具体的屏幕产品对象
  • 抽象厂商 BaseVentor:定义一个接口,这个接口包含:生产屏幕方法(productScreen)、生产电池方法(productBattery)

建造者模式:

建造者模式将复杂产品的创建步骤分解在在不同的方法中,使得创建过程更加清晰,从而更精确控制复杂对象的产生过程;通过隔离复杂对象的构建与使用,也就是将产品的创建与产品本身分离开来,使得同样的构建过程可以创建不同的对象;并且每个具体建造者都相互独立,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。UML结构图如下:
在这里插入图片描述

  • 抽象建造者 BaseBuilder:相当于建筑蓝图,声明了创建 Phone 对象的各个部件指定的抽象接口。
  • 具体建造者 XiaomiBuilder:实现BaseBuilder抽象接口,构建和装配各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。
  • 指挥者 PhoneDirector:构建一个使用 Builder 接口的对象。主要有两个作用,一是隔离用户与对象的生产过程,二是负责控制产品对象的生产过程。
  • 产品角色 Phone:被构造的复杂对象。XiaomiBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。

单例模式:

单例模式可以确保系统中某个类只有一个实例,该类自行实例化并向整个系统提供这个实例的公共访问点,除了该公共访问点,不能通过其他途径访问该实例。单例模式的优点在于:

  • 系统中只存在一个共用的实例对象,无需频繁创建和销毁对象,节约了系统资源,提高系统的性能
  • 可以严格控制客户怎么样以及何时访问单例对象。
    单例模式的写法有好几种,主要有三种:懒汉式单例、饿汉式单例、登记式单例。
懒汉式单例

懒汉式,顾名思义就是指只有在使用到的时候才会进行初始化对象。

public class BaseBuilder {

    private BaseBuilder() {}
    
    private volatile static Phone phone = null;
    
	/**
     * 使用双重检查锁保证懒汉式线程安全
     */
    public static Phone getInstance() {
        if (null == phone){
            synchronized (BaseBuilder.class){
                if (null == phone){
                    phone = new Phone();
                }
            }
        }
        return phone;
    }
}
饿汉式单例

饿汉式,是指类在刚初始化或者对象刚初始化的时就跟着一起创建。

public class BaseBuilder {

	private BaseBuilder() {}

    private static final Phone phone = new Phone();

    /**
     * 使用静态变量的方式实现类对象
     */
    public static Phone getInstance() {
        return phone;
    }
}

饿汉式和懒汉式区别:

  • 初始化时机与首次调用:
  1. 饿汉式是在类加载时,就将单例初始化完成,保证获取实例的时候,单例是已经存在的了。所以在第一次调用时速度也会更快,因为其资源已经初始化完成。
  2. 懒汉式会延迟加载,只有在首次调用时才会实例化单例,如果初始化所需要的工作比较多,那么首次访问性能上会有些延迟,不过之后就和饿汉式一样了。
  • 线程安全方面:饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,需要通过额外的机制保证线程安全
登记式单例

类似Spring里面的方法,将类名注册到一个容器中,下次从里面直接获取。

public class BaseBuilder {

	// 存储容器
	private static Map<Strnig, Object> containerMap = new HashMap();

	//保护的默认构造子
	private BaseBuilder() {}

    //静态工厂方法,返还此类惟一的实例
    public static Phone getInstance(String name) {
        if(null == name) {
            name = Phone.class.getName();
        }
        Phone phone = containerMap.get(name);
        if(null == phone) {
            try {
            	Phone phone = (Phone)Class.forName(name).newInstance();
                map.put(name, phone);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return phone;
    }
}

原型模式:

使用 clone 实现对象的拷贝

原型模式也是用于对象的创建,通过将一个对象作为原型,对其进行复制克隆,产生一个与源对象类似的新对象。UML类图如下:
在这里插入图片描述
在 Java 中,原型模式的核心是就是原型类 Prototype,Prototype 类需要具备以下两个条件:

  • 实现 Cloneable 接口:
  • 重写 Object 类中的 clone() 方法,用于返回对象的拷贝;
public abstract class Prototype implements Cloneable {
    /**
     * 对象的克隆方法
     * @return
     * @throws CloneNotSupportedException
     */
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Department extends Prototype {

    private String departCode;
    private String departName;

    @Override
    public Department clone(){
        Department department = null;
        try {
            department = (Department)super.clone();
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return department;
    }
}

public class Employee extends Prototype {
    private String username;
    private Department department;

    @Override
    public Employee clone(){
        Employee employee = null;
        try {
            employee = (Employee)super.clone();
            employee.department = this.department.clone();
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return employee;
    }
}

Object 类中的 clone() 方法默认是浅拷贝,如果想要深拷贝对象,则需要在 clone() 方法中自定义自己的复制逻辑。

  • 浅复制:将一个对象复制后,基本数据类型的变量会重新创建,而引用类型指向的还是原对象所指向的内存地址。
  • 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。
/**
 * 浅复制
 */
 @Override
 public Employee clone(){
     Employee employee = null;
     try {
         employee = (Employee)super.clone();
     }
     catch (CloneNotSupportedException e) {
         e.printStackTrace();
     }
     return employee;
 }

/**
 * 深复制
 */
 @Override
 public Employee clone(){
     Employee employee = null;
     try {
         employee = (Employee)super.clone();
         // 深度 copy 对象属性
         employee.department = this.department.clone();
     }
     catch (CloneNotSupportedException e) {
         e.printStackTrace();
     }
     return employee;
 }

使用原型模式进行创建对象不仅简化对象的创建步骤,还比 new 方式创建对象的性能要好的多,因为 Object 类的 clone() 方法是一个本地方法,直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显;

利用序列化实现对象的拷贝

利用序列化来做深复制,把对象写到流里的过程是序列化(Serilization)过程,而把对象从流中读出来的过程则叫做反序列化(Deserialization)过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。利用这个特性,可以做深拷贝 。并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。

public class CloneUtils {

    /**
     * 使用字节流的方式实现对象的深度拷贝
     *
     * @param <T>
     * @param obj		要克隆的对象
     * @return
     */
    public static <T extends Serializable> T clone(Object obj){
        
        T cloneObj = null;

        ByteArrayOutputStream out = null;

        ByteArrayInputStream ios = null;

        try {
            // 写入字节流,将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
            out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配内存,写入原始对象,生成新对象
            ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out){
                    out.close();
                }
                if (null != ios){
                    ios.close();
                }
            } catch (IOException e){
                e.printStackTrace();
            }
        }
        return cloneObj;
    }
}

使用该工具类的对象必须要实现Serializable接口,否则是没有办法实现克隆的。


public class Person implements Serializable{
    private static final long serialVersionUID = 2631590509760908280L;
 
    ..................
    //去除clone()方法
 
}

结构型

上面我们介绍了5种创建型模式,下面我们就开始介绍下7种结构型模式:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。其中对象的适配器模式是各种模式的起源,如下图:
在这里插入图片描述

适配器模式

适配器模式主要用于将一个类或者接口转化成客户端希望的格式,使得原本不兼容的类可以在一起工作,将目标类适配者类解耦;同时也符合“开闭原则”,可以在不修改原代码的基础上增加新的适配器类;将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性,但是缺点在于更换适配器的实现过程比较复杂。

所以,适配器模式比较适合以下场景:

  • 系统需要使用现有的类,而这些类的接口不符合系统的接口。
  • 使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。

下面有个非常形象的例子很好地说明了什么是适配器模式:
在这里插入图片描述
适配器模式的主要实现有三种:类的适配器模式、对象的适配器模式、接口的适配器模式。三者的使用场景如下:

  • 类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
  • 对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper>- 类的方法中,调用实例的方法就行。
  • 接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。
类的适配器模式

在这里插入图片描述

  • 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
  • 需要适配者(Adaptee):需要适配的类或适配者类或接口。
  • 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
代码实现
/**
 * Content:原有功能,需要适配的类或适配者
 */
public interface Adaptee {
    String screen = "屏幕";
    String productScreen();
}

/**
 * Content:目标功能,客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
 */
public interface Target {
    String battery = "电池";
    String productBattery();
}

/**
 * Content:适配器,兼容 Adaptee 和 Target 两者功能
 */
public interface Adapter extends Target, Adaptee {

}

/**
 * Content:适配器的基础抽象类
 */
public abstract class BaseProduct implements Adapter {

    protected Phone phone;

    @Override
    public String productScreen() {
        return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
    }

    @Override
    public String productBattery() {
        return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
    }
}

/**
 * Content:适配器的具体实现类(小米厂商)
 */
public class XiaomiVendor extends BaseProduct {
    public XiaomiVendor(Phone phone) {
        this.phone = phone;
    }
}
对象的适配器模式

在这里插入图片描述

代码实现
/**
 * Content:需要适配的类或适配者类
 */
public class Adaptee {

    private final String screen = "屏幕";

    private Phone phone;

    public String getScreen() {
        return screen;
    }

    public Phone getPhone() {
        return phone;
    }

    public void setPhone(Phone phone) {
        this.phone = phone;
    }

    public String productScreen(){
        return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
    }
}

/**
 * Content:客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
 */
public interface Target {
    String battery = "电池";
    String productBattery();
}

/**
 * Content:通过包装一个需要适配的对象,把原接口转换成目标接口
 */
public abstract class Adapter implements Target {

    private Adaptee adaptee;

    protected Phone phone;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    public String productScreen() {
        return adaptee.productScreen();
    }
}

/**
 * Content:适配器的基础抽象类
 */
public abstract class BaseProduct extends Adapter {

    public BaseProduct(Adaptee adaptee) {
        super(adaptee);
    }

    @Override
    public String productBattery() {
        return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
    }
}

/**
 * Content:适配器的具体实现类
 */
public class XiaomiVendor extends BaseProduct {
    public XiaomiVendor(Phone phone, Adaptee adaptee) {
        super(adaptee);
        adaptee.setPhone(phone);
        this.phone = phone;
    }
}
接口的适配器模式

在这里插入图片描述

代码实现
public interface Product {
    String screen = "屏幕";
    String battery = "电池";

    String productScreen();

    String productBattery();
}

public abstract class ProductWrapper implements Product {
    protected Phone phone;

    public String productScreen(){return null;};

    public String productBattery(){return null;};
}

public class ScreenProduct extends ProductWrapper {

    public ScreenProduct(Phone phone) {
        this.phone = phone;
    }

    @Override
    public String productScreen(){
        return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
    }
}

public class BatteryProduct extends ProductWrapper {

    public BatteryProduct(Phone phone) {
        this.phone = phone;
    }

    @Override
    public String productBattery(){
        return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
    }
}

/**
 * Content:适配器的具体实现类
 */
public class XiaomiVendor {
    private ProductWrapper wrapper;

    private Phone phone;

    public String getScreenProduct() {
        wrapper = new ScreenProduct(this.phone);
        return wrapper.productScreen();
    }

    public String getBatteryProduct() {
        wrapper = new BatteryProduct(this.phone);
        return wrapper.productBattery();
    }

    public XiaomiVendor(Phone phone) {
        this.phone = phone;
    }
}

装饰器模式

装饰器模式可以动态给对象添加一些额外的职责从而实现功能的拓展,在运行时选择不同的装饰器,从而实现不同的行为;比使用继承更加灵活,通过对不同的装饰类进行排列组合,创造出很多不同行为,得到功能更为强大的对象;符合“开闭原则”,被装饰类与装饰类独立变化,用户可以根据需要增加新的装饰类和被装饰类,在使用时再对其进行组合,原有代码无须改变。装饰器模式的UML结构图如下:

代理模式

桥接模式

外观模式

组合模式

享元模式

说说你对设计模式中动态代理的理解

计算机网络

TCP/IP 与 OSI 都是为了使网络中的两台计算机能够互相连接并实现通信与回应,但他们最大的不同在于,OSI 是一个理论上的网络通信模型,而 TCP/IP 则是实际上的网络通信标准。
在这里插入图片描述

OSI七层模型

物理层

实现计算机节点之间比特流的透明传输,规定传输媒体接口的标准,屏蔽掉具体传输介质和物理设备的差异,使数据链路层不必关心网络的具体传输介质,按照物理层规定的标准传输数据就行。

数据链路层

通过差错控制流量控制等方法,使有差错的物理线路变为无差错的数据链路

数据链路层的几个基本方法:数据封装成桢透明传输差错控制流量控制

  • 封装成桢:把网络层数据报加头和尾,封装成帧,帧头中包括源MAC地址目的MAC地址
  • 透明传输:零比特填充、转义字符。
  • 差错控制:接收者检测错误,如果发现差错,丢弃该帧,差错控制方法有 CRC 循环冗余码
  • 流量控制:控制发送的传输速度,使得接收方来得及接收。传输层TCP也有流量控制功能,但TCP是端到端的流量控制,链路层是点到点(比如一个路由器到下一个路由器)。

网络层

实现网络地址与物理地址的转换,并通过路由选择算法为分组通过通信子网选择最适当的路径。

网络层最重要的一个功能就是:路由选择。路由一般包括路由表路由算法两个方面。每个路由器都必须建立和维护自身的路由表。

  • 静态维护,也就是人工设置,适用于小型网络。
  • 动态维护,是在运行过程中根据网络情况自动地动态维护路由表。

传输层

提供源端与目的端之间提供可靠的透明数据传输,传输层协议为不同主机上运行的进程提供逻辑通信。

  • 网络层协议负责的是提供主机间的逻辑通信;
  • 传输层协议负责的是提供进程间的逻辑通信。

会话层

是用户应用程序和网络之间的接口,负责在网络中的两节点之间建立、维持、终止通信。

表示层

处理用户数据的表示问题,如数据的编码、格式转换、加密和解密、压缩和解压缩。

应用层

为用户的应用进程提供网络通信服务,完成和实现用户请求的各种服务。

TCP/IP模型

TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。TCP/IP协议族按照层次由上到下,层层包装。
在这里插入图片描述
上图表示了TCP/IP协议中每个层的作用,而TCP/IP协议通信的过程其实就对应着数据入栈与出栈的过程。入栈的过程,数据发送方每层不断地封装首部与尾部,添加一些传输的信息,确保能传输到目的地。出栈的过程,数据接收方每层不断地拆除首部与尾部,得到最终传输的数据。
在这里插入图片描述

网络层

实现网络地址与物理地址的转换,并通过路由选择算法为分组通过通信子网选择最适当的路径。

IP地址与物理地址:

物理地址是数据链路层和物理层使用的MAC地址,IP地址是网络层和以上各层使用的地址,是一种逻辑地址,其中ARP协议将IP地址转换成物理地址。

ARP地址解析协议的工作原理:

ARP 是根据 IP 地址获取 MAC 地址的一种协议,核心原理就是广播发送ARP请求,单播发送ARP响应。

  • 每个主机都在自己的ARP缓冲区中建立一个ARP列表,以表示 IP 地址和 MAC 地址之间的对应关系。
  • 源主机要发送数据时,先检查ARP列表中是否有该 IP 地址对应的 MAC 地址,如果有,则直接发送数据;如果没有,就向本网段的所有主机广播ARP数据包,用于查询目的主机的MAC地址,该数据包包括的内容有:源主机IP地址源主机MAC地址目的主机的IP
  • 当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
  • 源主机收到 ARP 响应包后,将目的主机的 IP 和 MAC 地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
    在这里插入图片描述

RARP逆地址解析协议:

RARP是逆地址解析协议,作用是完成硬件地址到IP地址的映射,主要用于无盘工作站,因为给无盘工作站配置的IP地址不能保存。工作流程:在网络中配置一台RARP服务器,里面保存着 MAC 地址和 IP 地址的映射关系,当无盘工作站启动后,就封装一个RARP数据包,里面有其MAC地址,然后广播到网络上去,当服务器收到请求包后,就查找对应的MAC地址的IP地址装入响应报文中发回给请求者。因为需要广播请求报文,因此RARP只能用于具有广播能力的网络。
在这里插入图片描述

DHCP协议:

动态主机配置协议,对 IP地址进行集中管理和分配,提升地址的使用率,通过DHCP协议,可以使客户机自动获得服务器分配的lP地址和子网掩码
在这里插入图片描述

ICMP协议:

因特网控制报文协议,用于在IP主机、路由器之间传递控制消息(控制消息是指网络通不通、主机是否可达、路由器是否可用等网络本身的消息),确认 IP 包是否成功到达目标地址。因为 IP 协议并不是一个可靠的协议,它不保证数据被送达,当传送IP数据包发生错误,比如主机不可达、路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机,给主机一个处理错误的机会。

ICMP报文有两种:差错报告报文和询问报文。以下是4种常见的ICMP差错报告报文
在这里插入图片描述

交换机与路由器的区别:

  • 工作所处的OSI层次不一样,交换机工作在OSI第二层数据链路层,路由器工作在OSI第三层网络层;
  • 寻址方式不同:交换机根据MAC地址寻址,路由器根据IP地址寻址;
  • 转发速不同:交换机的转发速度快,路由器转发速度相对较慢。

路由选择协议:

内部网关协议IGP
  • RIP(Routing Information Protocol):是一种动态路由选择协议,基于距离矢量算法,使用“跳数”来衡量到达目标地址的路由距离,并且只与自己相邻的路由器交换信息,范围限制在15跳之内。
  • OSPF:开放最短路径优先协议,使用Dijskra算法计算出到达每一网络的最短路径,并在检测到链路的情况发生变化时(如链路失效),就执行该算法快速收敛到新的无环路拓扑
外部网关协议

BGP:边界网关协议,BGP 是力求寻找一条能够到达目的网络 且 较好的路由,而并非要寻找一条最佳路由。BGP采用路径向量路由选择协议

传输层

传输层主要提供不同主机上进程间 逻辑通信 + 可靠传输 或者 不可靠传输的功能。

TCP 和 UDP

传输控制协议TCP 和 用户数据报协议UDP的区别?
  1. TCP是面向字节流的,基本传输单位是TCP报文段
  2. UDP是面向报文的,基本传输单位是是用户数据报
  • 面向字节流:应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。
  • 面向报文:面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送。因此,应用程序必须选择合适大小的报文。
  1. TCP 注重安全可靠性,连接双方在进行通信前,需进行三次握手建立连接。UDP 是无连接的,使用最大努力交付,即不保证可靠交付。
  2. UDP 不需要连接等待,所以数据传输快;因为TCP 有拥塞控制,所以传输效率相对较低。
  3. TCP首部开销是20个字节;UDP的首部开销是8个字节,这也是减少网络传输开销的一方面。
  4. TCP有拥塞控制和流量控制,而UDP没有拥塞控制和流量控制。
  5. TCP支持点对点通信,提供全双工通信,不提供广播或多播服务;UDP支持一对一、一对多、多对一、多对多的通信模式。
TCP 和 UDP 的适用场景:
  1. 当对网络通讯质量要求不高时,并且要求网络通讯速度能尽量的快,这时就可以使用UDP。比如即使通信: 语音、 视频 、直播等
  2. 当对网络通讯质量有要求时,要求整个数据准确无误可靠的传递给对方,这时就适用使用 TCP 协议,一般用于文件传输、发送和接收邮件等场景。比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议都是使用 TCP 协议

TCP对应的协议:

  • FTP:文件传输协议,使用21端口
  • Telnet:远程终端接入,使用23端口,用户可以以自己的身份远程连接到计算机上,可提供基于DOS模式下的通信服务。
  • SMTP:邮件传送协议,用于发送邮件,使用25端口
  • POP3:邮件传送协议,P用于接收邮件。使用110端口
  • HTTP:万维网超文本传输协议,是从Web服务器传输超文本到本地浏览器的传送协议

UDP对应的协议:

  • DNS:域名解析服务,将域名地址转换为IP地址,使用53号端口;
  • SNMP:网络管理协议,用来管理网络设备,使用161号端口;
  • TFTP:简单文件传输协议,提供不复杂、开销不大的文件传输服务,使用 69 端口;
  • NFS:远程文件服务器
  • RIP:路由信息协议
  • DHCP:动态主机配置协议
  • IGMP:网际组管理协议
TCP的首部字段:

在这里插入图片描述

  1. 源端口和目的端口:分别占16位,指发送方应用程序的端口和目的方应用程序的端口号,通过 IP 地址 + 端口号就可以确定一个进程地址
  2. 序号(Sequense Number,SN):在一个TCP连接中传送的字节流中的每一个字节都按顺序编号,该字段表示本报文段所发送数据的第一个字节的序号。(初始序号称为 Init Sequense Number, ISN)

例如,一报文段的序号是 101,共有 100 字节的数据。这就表明:本报文段的数据的第一个字节的序号是 101,最后一个字节的序号是 200。显然,下一个报文段的数据序号应当从 201 开始,即下一个报文段的序号字段值应为 201。

  1. 确认号 ack:期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则表明:到序号 N-1 为止的所有数据都已正确收到。
  2. 头部长度:指出 TCP报文段的数据起始处 距离 TCP报文段的起始处有多远。这个字段实际上是指出TCP报文段的首部长度。
  3. 保留位:占6位,应置为 0,保留为今后使用。
  4. 6个控制位:用于说明该报文段的性质:
  • 紧急位URG:当 URG = 1 时,表明此报文段中有紧急数据,是高优先级的数据,应尽快发送,不用在缓存中排队。
  • 确认ACK:仅当 ACK = 1 时确认号字段才有效,当 ACK = 0 时确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
  • 推送PSH:接收方收到 PSH = 1 的报文段时,就直接发送给应用进程,而不用等到整个缓冲区都填满了后再向上传送。
  • 复位RST:当 RST = 1 时,表明 TCP 连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。
  • 同步SYN:SYN = 1 表示这是一个连接请求或连接接受报文。当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN = 1 且 ACK = 1。
  • 终止FIN:用来释放一个连接。当 FIN = 1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
  1. 窗口大小:16位,用于控制发送端的滑动窗口大小
  2. 校检和:16位,校验数据段是否未被修改
  3. 紧急指针:16位。

TCP连接的建立与断开

建立连接的三次握手

在这里插入图片描述

  1. 第一次握手:客户端向服务端发送一个 SYN 报文(SYN = 1),并指明客户端初始化序列号 ISN,即seq = x,表示本报文所发送的第一个字节的序号。此时客户端处于 SYN_Sent 状态,等待服务端确认。

三次握手的一个重要功能是客户端和服务端交换 ISN,以便让对方知道接下来接收数据时如何按序列号组装数据。
ISN 是动态生成的,并非固定,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号。

  1. 第二次握手:服务端收到数据包后,由 SYN = 1 知道客户端请求建立连接,那么就会对这个TCP 连接分配缓存和变量(缓存指的是一个字节流队列),接着返回一个确认报文:设置 SYN = 1,ACK = 1,同时指定自己的初始化序列号 ISN,即图中的 seq = y,并把客户端的 ISN + 1 作为确认号 ack 的值,表示已经收到了客户端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1,此时服务端进入SYN_REVD状态。
  2. 第三次握手:客户端收到确认后,检查ACK是否为1,ack是否为 x +1,如果正确,则给服务端发送一个 ACK 报文:设置 ACK = 1,把服务端的 ISN + 1 作为 ack 的值,表示已经收到了服务端发来的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 y + 1,并指明此时客户端的序列号 seq = x + 1,此时客户端和服务器端都进入 ESTABLISHED 状态。完成三次握手,随后Client与Server之间可以开始传输数据了。

此时 SYN 控制位变为 0,表示这不是建立连接的请求了,要正式发数据了。

为什么不能用两次握手进行建立连接?
  1. 三次握手目的是确认双方的接收与发送能力是否正常,同步连接双方的初始化序列号 ISN,为后面的可靠性传输做准备。而两次握手只有服务端对客户端的起始序列号做了确认,但客户端却没有对服务端的初始序列号做确认,不能保证传输的可靠性。
  2. 三次握手可以防止已失效的连接请求报文段突然又传送到了服务端,导致服务器错误地建立连接,浪费服务端的连接资源。

客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达Server。本来这是一个早已失效的报文段,但Server收到此失效的连接请求报文段后:

  • 假设不采用“三次握手”,那么只要Sever发出确认,新的连接就建立了。但由于现在Client并没有发出建立连接的请求,因此不会理睬Server的确认,也不会向Server发送数据。而Server却以为新的连接已经建立,并一直等待Client发来数据,这样,Server的很多资源就白白浪费掉了
  • 而采用“三次握手”协议,只要Server收不到来自Client的确认,就知道Client并没有要求建立请求,就不会建立连接了。
断开连接的四次挥手:

在这里插入图片描述

  1. 第一次挥手:客户端发送一个 FIN 报文,设置 FIN = 1 并指定序列号 seq = u(u 是之前传送过来的最后一个字节的序号 + 1),主动关闭 TCP 连接,此时客户端进入FIN_WAIT_1状态;
  2. 第二次挥手:服务端收到 FIN 报文后,由FIN=1 知道客户端请求关闭连接,则返回确认报文:设置ACK = 1,ack = u + 1,seq = v(v 的值取决于服务器发送给客户端之前的一个包确认号是多少)
  • 服务端进入CLOSE_WAIT状态,此时TCP连接处于半关闭状态,即客户端不能向服务端发送报文,只能接收,但服务端仍然可以向客户端发送数据。
  • 客户端收到服务端的确认后,进入 FIN_WAIT2 状态,等待服务端发出的连接释放报文段。
  1. 第三次挥手:当服务端没有要向客户端发送的数据时,就向客户端发送一个 FIN 报文,设置 FIN = 1 并指定序列号 seq = w(w 的值取决于服务器发送给客户端之前的一个包确认号是多少),用于关闭服务端到客户端的数据传送。此时服务器处于 LAST_ACK 状态。
  2. 第四次挥手:客户端收到 FIN 报文后,发送给服务端一个 ACK 报文作为应答:设置 ACK=1 和 ack = w +1。发送之后,客户端处于 TIME_WAIT状态,如果服务端接收到这个数据包,则进入CLOSED状态,完成四次挥手。
为什么需要 TIME_WAIT 状态:

TIME_WAIT 状态持续 2MSL(最大报文存活时间),约4分钟才转换成CLOSE状态。由于TIME_WAIT 的时间会非常长,因此服务端应尽量减少主动关闭连接,TIME_WAIT 的主要作用有:

  1. 重发丢失的 ACK 报文,保证连接可靠的关闭:

由于网络等原因,无法保证最后一次挥手的 ACK 报文一定能传送给对方,如果 ACK 丢失,对方会超时重传 FIN,主动关闭端会再次响应ACK过去;如果没有 TIME_WAIT 状态,直接关闭,对方重传的FIN报文则被响应一个RST报文,此RST会被动关闭端被解析成错误。同时,服务器就因为接收不到客户端的信息而无法正常关闭。

  1. 保证本次连接的重复数据段从网络中消失:

如果存在两个连接,第一个连接正常关闭,第二个相同的连接紧接着建立;如果第一个连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达,则会干扰第二连接,等待 2MSL 可以让上次连接的报文数据消逝在网络中

为什么需要四次挥手:

TCP 是全双工模式,并且支持半关闭特性,提供了连接的一端在结束发送后还能接收来自另一端数据的能力。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。

通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。

什么是SYN洪泛:

SYN 洪泛是指利用 TCP 需要三次握手的特性,攻击者伪造 SYN 报文向服务器发起连接,服务器在收到报文后用 ACK 应答,但之后攻击者不再对该响应进行应答,造成一个半连接。假设攻击者发送大量这样的报文,那么被攻击主机就会造成大量的半连接,耗尽其资源,导致正常的 SYN 请求因为队列满而被丢弃,使得正常用户无法访问。

半连接队列:服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,完成三次握手后建立起的连接就会放在全连接队列中。

三次握手过程中是否可以携带数据:

第三次握手时是可以携带数据的,但第一二次握手时不可以携带数据。

  • 假如第一次握手可以携带数据的话,那么会放大 SYN 洪泛。如果有人要恶意攻击服务器,每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发送 SYN 报文的话,就会让服务器开辟大量的缓存来接收这些报文,内存会更快容易耗尽,从而拒绝服务。
  • 第三次握手时客户端已经处于 ESTABLISHED 状态,对于客户端来说,它已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的,所以也就可以携带数据了。
TCP的粘包和拆包:
什么是拆包粘包:

拆包粘包数据链路层网络层以及传输层都可能存在。而在传输层中,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。TCP是个“”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

什么情况先会发生拆包粘包:
  1. 应用程序 write 写入的数据字节大于套接口发送缓冲区大小,将大数据拆分成多份小数据发送到网络上,将会发生拆包现象。
  2. 应用程序 write 写入的数据字节小于套接字缓冲区大小,网卡将聚集多次写入的数据发送到网络上,这将会发生粘包现象。
  3. 进行MSS大小的TCP分段:程序需要发送的数据大小和TCP报文段能发送MSS(Maximum Segment Size,最大报文长度)是不一样的。大于MSS时,就需要把程序数据拆分为多个TCP报文段,称之为拆包;小于时,则要考虑合并多个程序数据为一个TCP报文段,则是粘包;其中 MSS = TCP报文段长度-TCP首部长度。
  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。
拆包粘包问题的解决策略:
  1. 在数据尾部增加一个特殊字符进行分割,例如 FTP 协议;
  2. 将数据大小设置为固定的,如果数据长度不够,则使用空位补全;
  3. 将数据分为两部分,消息头和消息体;其中消息头大小固定,且包含一个字段声明内容体的大小
网络层 - IP数据报分片:
  1. MTU 是数据链路层中的网络对数据帧的一个限制(以太网中 MTU 为1500个字节),一个 IP 数据报在以太网中传输,如果它的长度大于 MTU 值,就要进行分片传输,使得每片数据报的长度小于 MTU。而分片传输的 IP 数据报不一定按序到达,但 IP 首部中的信息能让这些数据报片按序组装,IP数据报的分片与重组是在网络层进完成的
  2. 前面提到,MSS 是 TCP 数据包每次能够传输的最大数据分段,TCP 报文段的长度大于 MSS 时,要进行分段传输。TCP 在建立连接时通常会协商双方的 MSS 值(MSS 选项只出现在 SYN 报文段中,即 TCP 三次握手的前两次)。MSS 的值一般为 MTU 值减去两个首部大小(IP 数据包包头的大小 20 Bytes 和 TCP 数据段的包头 20 Bytes),TCP报文段的分段与重组是在传输层完成的

如果用链路层以太网,MSS的值往往为1460。而 Internet 上标准的 MTU(最小的 MTU,链路层网络为x2.5时)为576,那么如果不设置,则MSS的默认值就为536个字节。很多时候,MSS的值最好取512的倍数。

  1. 到这里我们就能看出,TCP 分段的原因是 MSS,IP 分片的原因是 MTU,由于一直有 MSS <= MTU,分段后的每一段TCP报文段再加上IP首部后的长度不可能超过MTU,因此也就不需要在网络层进行IP分片了,因此TCP报文段很少会发生IP分片的情况。
  2. 而由于 UDP 数据报不会自己进行分段,因此当长度超过了 MTU 时,会在网络层进行 IP 分片。同样,ICMP(在网络层中)同样会出现IP分片情况。
  3. 所以,总的来说,UDP 不会分段,就由 IP 来分,TCP会分段,当然就不用 IP 来分了!

TCP可靠性传输

TCP 如何保证可靠性传输
三次握手

在建立TCP连接之前,客户端和服务端会进行三次的握手操作,确保两端的信息传递的可靠性。

应答机制与超时重传

TCP接收端收到发送端的数据时,它将发送一个确认响应。当TCP发送端发出一个报文段后,它会启动一个定时器,等待接收端的确认报文段,如果不能及时收到一个确认响应,发送端会认为接收端没有收到数据,将重发这个报文段。

数据包校验与丢弃重复数据

TCP会检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时TCP会超时重发数据;对于重复数据,则进行丢弃;

对失序数据包进行重排序

既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层;

流量控制

TCP 连接的每一方都有固定大小的缓冲空间。TCP 规定发送端发送的数据不能超过接收端的缓冲空间大小,否则会造成接收端的缓冲区溢出。TCP使用的流量控制协议是可变大小的滑动窗口协议。

拥塞控制

网络拥塞时,减少数据的发送。

TCP的流量控制

所谓流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。因为如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据丢失。TCP的流量控制是通过大小可变的滑动窗口来实现的。接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK报文来通知发送端,滑动窗口是接收端用来控制发送端发送数据的大小,从而达到流量控制。

其实发送方的窗口上限,是取值拥塞窗口滑动窗口两者的最小值。当滑动窗口为 0 时,发送方一般不能再发送数据包,但有两种情况除外:

  • 一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。
  • 一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送数据不能超过接收方给出的接收窗口的数值。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。
在这里插入图片描述
从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在 ACK=1 时确认号字段才有意义。

TCP的拥塞控制

拥塞控制就是防止在同一时间段内过多的数据注入到网络中,使网络中的路由器或链路不致过载。发送方维持一个拥塞窗口cwnd 的状态变量。拥塞窗口的大小动态变化,取决于网络的拥塞程度,发送方让自己的发送窗口等于拥塞窗口。只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。 拥塞控制的方法主要有以下几种:慢启动拥塞避免快重传快恢复

慢开始算法

是试探一下网络的拥塞情况,由小到大逐渐增大发送窗口。在开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段,然后每经过一个传输轮次RTT,拥塞窗口 cwnd 就加倍。另外,为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量。

  1. 当 cwnd < ssthresh 时,使用上述的慢开始算法。
  2. 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
  3. 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
拥塞避免算法

让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

无论在慢开始阶段还是在拥塞避免阶段,只要网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的拥塞窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd 设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的数据量,使得发生拥塞的路由器有足够时间把队列中积压的数据处理完毕。过程图如下:
在这里插入图片描述

快重传

快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(使发送方及早知道有报文段没有到达对方)而不必等到自己发送数据时捎带确认。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
在这里插入图片描述

接收方收到了M1和M2后都分别发出了确认。现在假定接收方没有收到M3但接着收到了M4。显然,接收方不能确认M4,因为M4是收到的失序报文段。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认。但按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方。发送方接着发送了M5和M6。接收方收到这两个报文后,也还要再次发出对M2的重复确认。这样,发送方共收到了 接收方的四个对M2的确认,其中后三个都是重复确认。

快恢复

与快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减少”算法,把ssthresh门限设置为拥塞窗口cwnd的一半,但是接下去并不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法:因为如果网络出现拥塞的话,就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞,所以此时并不执行慢开始算法,而是执行拥塞避免算法。
在这里插入图片描述

拥塞控制和流量控制的差别
相同点:

拥塞控制和流量控制的相同点都是控制丢包现象,实现机制都是让发送方发得慢一点。

不同点:
  1. 拥塞控制是一个全局性的过程,防止过多的数据注入到网络中,造成网络拥塞
  2. 流量控制指点对点通信量的控制,要做的就是控制发送端发送数据的速率,以便使接收端来得及接受。

应用层

应用层主要提供应用进程间的网络通信服务,完成用户请求的各种服务。

http协议

http协议即超文本传输协议,基于TCP协议,用于从Web服务器传输超文本到本地浏览器的传送协议。http协议是无状态协议,自身不对请求和响应直接的通信状态进行保存,但有些场景下我们需要保存用户的登陆信息,所以引入了cookie 和 session 来管理状态。

无状态协议是指比如客户获得一张网页之后关闭浏览器,然后再一次启动浏览器,再登录该网站,但是服务器并不知道客户关闭了一次浏览器。

cookie 和 session 的区别:
保存位置与安全性:

cookie保存在客户端,session保存在服务端,所以在安全性上面,cookie存在安全隐患,可以通过拦截或本地文件找到cookie后进行攻击,而session相对更加安全。因此,可以将登陆信息等重要信息存放为session中;其他信息如果需要保留,可以放在cookie中。

存储容量:

单个cookie最大只允许4KB,一个站点最多保存20个Cookie;session没有大小限制,个数只跟服务器的内存大小有关。

有效期与实现机制:

cookie可长期有效存在;session依赖于cookie,过期时间默认为-1,只需关闭窗口该 session 就会失效。每个客户端对应一个session ,客户端之间的 session 相互独立;

cookie:cookie是一小段的文本信息,当客户端请求服务器时,如果服务器需要记录该用户状态,就在响应头中向客户端浏览器颁发一个cookie,而客户端浏览器会把cookie 保存 起来。当再次请求该网站时,浏览器把cookie放入请求头中提交给服务器,服务器会检查该cookie,以此来辨认用户状态。
在这里插入图片描述
在这里插入图片描述
session:当客户端请求服务器时,都会带上cookie,cookie里面一般都会有一个JSESSIONID,服务器就按照 JSESSIONID 来找到对应的 session;如果客户端请求不包含 JSESSIONID,则为此客户端 创建 session并生成相关联的JSESSIONID,并将这个JSESSIONID在本次响应中返回给客户端保存。客户端保存这个 JSESSIONID 的方式可以使用cookie机制。若浏览器禁用cookie的话,可以通过 URL重写机制 将JSESSIONID传回服务器。

一个完整的http请求是怎么样?即从输入网址到获得页面的过程:
  1. 解析url,获取 url 中包含的域名;
  2. 通过DNS系统查询域名对应的IP;
    在这里插入图片描述

DNS服务器大致分为三种类型:根DNS服务器顶级域DNS服务器权威DNS服务器,其中: 顶级域DNS服务器主要负责诸如com、org、net、edu、gov 等顶级域名。
根DNS服务器存储了所有 顶级域DNS服务器的 IP 地址,可以通过根服务器找到顶级域服务器(例如:www.baidu.com,根服务器会返回所有维护 com 这个顶级域服务器的 IP 地址)。然后你任选其中一个顶级域服务器发送请求,该顶级域服务器拿到域名后能够给出负责当前域的权威服务器地址(以 baidu为例的话,顶级域服务器将返回所有负责 baidu 这个域的权威服务器地址)。接着任选其中一个权威服务器地址查询 「www.baidu.com」 的具体 IP 地址,最终权威服务器会返回给你具体的 IP 地址。此外,本地 DNS 服务器是具有缓存功能的,通常两天内的记录都会被缓存。

所以,通过DNS系统查询域名对应的 IP 的具体步骤可以总结为:

  • 操作系统先查本地 hosts文件 中是否有记录,如果有,则直接返回相对应映射的IP地址。
  • 如果本地hosts文件中没有配置,则主机向自己的本地 DNS 服务器 发送查询报文,如果本地DNS服务器缓存中有,将直接返回结果
  • 如果本地服务器缓存中没有,则从内置在内部的根服务器列表(全球13台,固定的IP地址)中选一个发送查询报文
  • 根服务器解析域名中的后缀名,告诉本地服务器负责该后缀名的所有顶级服务器列表
  • 本地服务器选择其中一个顶级域服务器发送查询请求,顶级域服务器拿到域名后继续解析,返回对应域的所有权威服务器列表
  • 本地服务器再向返回的权威服务器发送查询报文,最终会从某一个权威服务器上得到具体的 IP 地址
  • 主机返回结果IP
    在这里插入图片描述
  1. 浏览器得到域名对应的IP地址之后,向服务器发起三次握手请求建立TCP链接;
  2. TCP链接链接建立起来后,浏览器向服务器发送http请求,如果 html文件在缓存里,浏览器则直接返回, 如果没有,则去后台拿;
  • 浏览器首次加载资源成功时,服务器返回200,此时浏览器不仅将资源下载下来,而且把response的header(里面的date属性非常重要,用来计算第二次相同资源时当前时间和date的时间差)一并缓存;
  • 下一次加载资源时,首先要经过强缓存的处理,cache-control的优先级最高,比如cache-control:no-cache,就直接进入到协商缓存的步骤了,如果cache-control:max-age=xxx,就会先比较当前时间和上一次返回200时的时间差,如果没有超过max-age,命中强缓存,不发请求直接从本地缓存读取该文件(这里需要注意,如果没有cache-control,会取expires的值,来对比是否过期),过期的话会进入下一个阶段,协商缓存
  • 协商缓存阶段,则向服务器发送header带有If-None-MatchIf-Modified-Since的请求,服务器会比较Etag,如果相同,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
  • 协商缓存第二个重要的字段是,If-Modified-Since,如果客户端发送的If-Modified-Since的值跟服务器端获取的文件最近改动的时间,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
  1. 服务器接收到请求后,根据路径参数映射到特定的处理器进行处理,并将处理结果以及相应的视图返回给浏览器。
  2. 浏览器解析视图,并根据请求到的资源、数据进行渲染页面,最终向用户呈现一个完整的页面。
  • 构建DOM树(DOM tree):从上到下解析HTML文档生成DOM节点树(DOM tree),也叫内容树(content tree);
  • 构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树;
  • 执行JavaScript:加载并执行JavaScript代码(包括内联代码或外联JavaScript文件);
  • 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree);
  • 渲染树:按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性。
  • 布局(layout):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置;
  • 绘制(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式,这一过程是通过UI后端模块完成;
http的长连接和短连接?

http的长连接和短连接本质上是TCP长连接和短连接。从http1.1开始就默认使用长连接

  • 短链接:是指客户端与服务端每进行一次请求操作,就建立一次TCP连接,收到服务器响应后,就断开连接。
  • 长连接:是指客户端和服务建立TCP连接后,它们之间的连接会持续存在,不会因为一次HTTP请求后关闭,后续的请求也是用这个连接进行通信,使用长连接的HTTP协议,会在响应头有加入:Connection:keep-alive。长连接可以省去每次TCP建立和关闭的握手和挥手操作,节约时间提高效率。但在长连接下,客户端一般不会主动关闭连接,如果客户端和服务端之间的连接一直不关闭的话,随着连接数越来越多,会对服务端造成压力

在这里插入图片描述

所以长连接多用于频繁请求资源,而且连接数不能太多的情况,例如数据库的连接用长连接。而像Web网站这种并发量大,但是每个用户无需频繁操作的场景,一般都使用短连接,因为长连接对服务端来说会耗费一定的资源。

http的断点续传是如何实现的?

HTTP请求头有个Range字段;我们下载文件的时候如果遇到网络中断,如果重头开始下载会浪费时间,所以我们可以从上一次中断处继续开始下载;具体的操作:

Range: bytes=5001-10000

或者指定5001以后的所有数据

Range: bytes=5001-

http存在的问题:
  • 通信使用明文不加密,通信内容可能被窃听;
  • 无法验证报文的完整性,数据内容可能被篡改
  • 不验证通信方身份、可能遭到伪装,无法保证数据发送到正确的机器上;

为了解决上述几个问题,那么就引入了https协议。

https协议

https 是基于tcp协议,在http的基础上加入了SSL/TLS,可看成是添加了加密认证机制的http,使用对称加密、非对称加密、证书等技术进行进行客户端与服务端的数据加密传输,最终达到保证整个通信的安全性

对称加密指加密和解密都使用同一个密钥的方式,这种方式存在如何安全地将密钥发送对方的问题;非对称加密使用两个密钥,公钥加密则需要私钥解密,私钥加密则需要公钥解密。不能私钥加密,私钥解密。非对称加密不需要发送用来解密的私钥,所以可以保证安全性,但是和对称加密比起来,速度非常的慢,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。

https的认证加密过程?如何保证内容不会被篡改的?
  • https是基于tcp协议的,首先客户端会和服务端发起链接建立
  • 服务端返回它的证书给客户端,证书中包含了服务端公钥S.pub、颁发机构和有效期等信息
  • 客户端通过浏览器内置的根证书(内部包含CA机构的公钥C.pub)验证证书的合法性
  • 客户端生成随机的对称加密密钥Z,然后通过服务端的公钥S.pub加密发送给服务端
  • 客户端和服务端之后就通过对称加密密钥Z加密数据来进行http通信
根证书如何保证签发的证书是安全有效的?
  • 服务器会预先生成非对称加密密钥,私钥S.pri自己保留,而公钥S.pub则发送给CA进行签名认证
  • CA机构也会预先生成非对称加密密钥,其私钥C.pri用来对服务器的公钥S.pub进行签名,生成CA证书
  • CA机构将签名生成的CA证书返回给服务器,也就是前面服务端给客户端那个证书
  • 因为CA机构比较权威,所以很多浏览器会内置包含它公钥C.pub的证书,称之为根证书,然后可以使用根证书来验证其颁发证书的合法性了

在这里插入图片描述
在整个过程中,一共涉及2对公私密钥对,一对由服务器产生,主要用于加密,一对由CA产生,主要用于签名。

为什么需要CA证书认证机构呢?

CA证书是为了确保服务端的公钥是准确无误、没有被修改过的。虽然https是加密的,但是请求还是可以被拦截的,假设没有CA证书,如果服务器返回的包含公钥的包被攻击者截取,然后攻击者也生成一对公私钥,他将自己的公钥发给客户端。攻击者得到客户端数据后进行解密,然后再通过服务器的公钥加密发给服务器,这样数据就被攻击者获取到了。

有了CA证书后,客户端根据内置的CA根证书,很容易识别出攻击者的公钥不合法,或者说攻击者的证书不合法。

证书通常包含这些内容:

  • 服务端的公钥;
  • 证书发行者(CA)对证书的数字签名;
  • 证书所用的签名算法;
  • 证书发布机构、有效期、所有者的信息等其他信息

http 的请求与响应

http的常见请求方式
  • get:向服务端获取资源,所以查询操作一般用get
  • post:向服务端提交请求字段,创建操作使用 post,该操作不是幂等的,多次执行会导致多条数据被创建
  • put:修改指定URL的资源,如果资源不存在,则进行创建,修改操作一般使用 put,在http中,put 被定义成幂等的,多次操作会导致前面的数据被覆盖
  • patch:局部修改URL所在资源的数据,是对put的补充
  • delete:删除指定URL的资源。
  • head:获取响应报文的首部,即获得URL资源的头部
  • options:询问服务器支持哪些方法,响应头中返回 Allow: GET、POST、HEAD
  • trace:追踪路径,主要用于测试或诊断;在请求头中在Max-Forwards字段设置数字,每经过一个服务器该数字就减一,当到0的时候就直接返回,一般通过该方法检查请求发送出去是否被篡改
get和 post 请求的区别
  • 功能:get一般用来从服务器上面获取资源,post一般用来更新服务器上面的资源。
  • 幂等性:get 是幂等的,post 为非幂等的
  • 安全性:get 请求的参数会明文附加在URL之后,而 post 请求提交的数据则被封装到请求体中,相对更安全。
  • 传输数据量的大小:get请求允许发送的数据量比较小,大多数浏览器都会限制请求的url长度在2048个字节,而大多数服务器最多处理64K大小的url;而post请求提交的数据量则是没有大小限制的。
  • 参数的数据类型:GET只接受ASCII字符,而POST没有限制。
  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • get请求可以被缓存,可以被保留在浏览器的历史记录中;post请求不会被缓存,不会被保留在浏览器的历史记录中。
http报文头分析
  1. 报文类型:报文类型分为请求报文和响应报文

请求报文包含三部分:

  • 请求行:包含请求方法、URI、HTTP版本信息
  • 请求首部字段
  • 请求内容实体

响应报文包含三部分:

  • 状态行:包含HTTP版本、状态码、状态码的原因短语
  • 响应首部字段
  • 响应内容实体

在这里插入图片描述

  1. 报文中各部分的简要描述
  • 方法(method):客户端希望服务器对资源执行的动作,是一个单独的词,比如:get 或者 post
  • 请求URL(request-URL):请求URL是资源的绝对路径,服务器可以假定自己是URL的主机/端口
  • 版本(version):报文所使用的Http版本,其格式:HTTP/<主要版本号>.<次要版本号>
  • 状态码(status-code):标识请求过程中所发生的情况
  • 原因短语(reason-phrase):数字状态码的可读版本,包含行终止序列之前的所有文本。
  • 请求头部(header):可以有零个或多个头部,每个首部都包含一个名字,后面跟着一个冒号(😃,然后是一个可选的空格,接着是一个值,>- 最后是一个CRLF首部是由一个空行(CRLF)结束的,表示了头部列表的结束和实体主体部分的开始
  • 实体的主体部分(entity-body):实体的主体部分包含一个由任意数据组成的数据块,并不是所有的报文都包含实体的主体部分,有时,报文只是以一个CRLF结束。
  1. 通用头部:既可以出现在请求报文中,也可以出现在响应报文中,它提供了与报文相关的最基本的信息:
  • Connection:允许客户端和服务器指定与请求/响应连接有关的选项,http1.1之后默认是 keep-alive
  • Date:日期和时间标志,说明报文是什么时间创建的
  • Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式
  • Cache-Control:用于随报文传送缓存指示
  1. 请求头部:请求头部是只在请求报文中有意义的头部。用于说明是谁或什么在发送请求、请求源自何处,或者客户端的喜好及能力
  • Host:给出了接收请求的服务器的主机名和端口号
  • Referer:提供了包含当前请求URI的文档的URL
  • User-Agent:将发起请求的应用程序名称告知服务器
  • Accept:告诉服务器能够发送哪些媒体类型
  • Accept-Encoding:告诉服务器能够发送哪些编码方式
  • Accept-Language:告诉服务器能够发送哪些语言
  • Range:如果服务器支持范围请求,就请求资源的指定范围
  • If-Range:允许对文档的某个范围进行条件请求
  • Authorization:包含了客户端提供给服务器,以便对其自身进行认证的数据
  • Cookie:客户端用它向服务器传送数据
  1. 响应头部:响应头部为客户端提供了一些额外信息,比如谁在发送响应、响应者的功能,甚至与响应相关的一些特殊指令
  • Age:(从最初创建开始)响应持续时间
  • Server:服务器应用程序软件的名称和版本
  • Accept-Ranges:对此资源来说,服务器可接受的范围类型
  • Set-Cookie:在客户端设置数据,以便服务器对客户端进行标识
  1. 实体首部:描述主体的长度和内容,或者资源自身
  • Allow:列出了可以对此实体执行的请求方法
  • Location:告知客户端实体实际上位于何处,用于将接收端定向到资源的位置(URL)上去
  • Content-Base:解析主体中的相对URL时使用的基础URL
  • Content-Encoding:对主体执行的任意编码方式
  • Content-Language:理解主体时最适宜使用的自然语言
  • Content-Length:主体的长度
  • Content-Type:这个主体的对象类型
  • ETag:与此实体相关的实体标记
  • Last-Modified:这个实体最后一次被修改的日期和时间
  1. 实体的主体部分:该部分其实就是HTTP要传输的内容,是可选的。HTTP报文可以承载很多类型的数字数据,比如,图片、视频、HTML文档电子邮件、软件应用程序等等。
Http 常见的状态码

1xx:请求处理中,请求已被接受,正在处理。

2xx:请求成功,请求被成功处理。

  • 200 :OK,客户端请求成功;
  • 204(请求处理成功,但是没有资源返回)

3xx:重定向,要完成请求必须进一步处理。

  • 301:永久性转移,请求的资源已经被分配到了新的地址
  • 302:暂时重定向
  • 304:已缓存。

4xx:客户端错误,请求不合法。

  • 400:客户端请求报文出现错误,通常是参数错误
  • 401:客户端未认证授权
  • 403:没有权限访问该资源
  • 404:未找到请求的资源
  • 405:不支持该请求方法,如果服务器支持GET,客户端用POST请求就会出现这个错误码

5xx:服务端错误,服务端不能处理合法请求。

  • 500:服务器内部错误。
  • 503:服务不可用,一段时间后可能恢复正常。
http/1.1和http/2.0的区别
  1. 多路复用,做到同一个连接并发处理多个请求:HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,并发请求的数量比HTTP1.1大了好几个数量级。
  2. 支持首部压缩:HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。
  3. 服务器推送:当向支持HTTP2.0的web服务器请求时,服务器会顺便把客户端需要的资源一起推送到客户端,避免客户端再次创建连接发送请求到服务器端获取,这种方式非常合适加载静态资源。
  4. http2.0采用二进制而不是文本格式
http 和 https 的区别
  1. http 和 https 都是基于 TCP 协议,但是 http 是使用明文传输,通讯内容可能被窃听和篡改,客户端也无法验证通讯方的身份,无法保证数据发送到正确的机器上;https 是在 http 的基础上加入了 SSL/TLS,可看成是添加了加密和认证机制的http,使用对称加密、非对称加密、证书等技术进行进行客户端与服务端的数据加密传输,最终达到保证整个通信的安全性。
  2. 端口不同:http 使用的是80端口,https 使用的443端口
  3. 资源消耗:和 http 通信相比,https通信会由于加解密处理消耗更多的CPU和内存资源

应用层其他相关的协议

  1. DNS域名系统:用于域名解析服务,将域名地址转换为IP地址,基于UDP服务,使用53端口。

DNS底层既使用TCP又使用UDP协议:

  1. 域名解析时使用UDP协议:客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可,不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。
  2. 区域传送时使用TCP,主要有一下两点考虑:
    ①辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
    ②TCP是一种可靠的连接,保证了数据的准确性。
  1. FTP:定义了文件传输协议,使用21端口。上传下载文件,都要用到FTP服务。
  2. Telnet:远程终端协议,它是一种用于远程登陆的端口,用户可以以自己的身份远程连接到计算机上,提供一种基于DOS模式下的通信服务。
  3. SMTP:定义了简单邮件传送协议,用于发送邮件,使用25号端口。
  4. POP3:与SMTP对应,POP3用于接收邮件。使用110端口。
  5. SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。
  6. TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在69端口上使用UDP服务。

海量数据处理

基础知识:

  • bit:位
  • byte:字节
  • 1 byte= 8 bit
  • int 类型为 4 byte,共32位bit,unsigned int也是
  • 2^32 byte = 4G
  • 1G= 2^30 =10.7亿

海量数据处理概述:

所谓海量数据处理,就是指数据量太大,无法在较短时间内迅速解决,或者无法一次性装入内存。而解决方案就是:针对时间,可以采用巧妙的算法搭配合适的数据结构,如 Bloom filter/Hashmap/bit-map/堆/数据库/倒排索引/trie树;针对空间,大而化小,分而治之(hash映射),把规模大化为规模小的,各个击破。所以,海量数据处理的基本方法总结起来分为以下几种:

  • 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
  • Trie树/Bloom filter/Bitmap
  • 数据库/倒排索引;
  • 双层桶划分;
  • 外排序;
  • 分布式处理之Hadoop/Mapreduce。

分而治之/hash映射 + hashmap统计 + 快速/归并/堆排序

这种方法是典型的“分而治之”的策略,是解决空间限制最常用的方法,即海量数据不能一次性读入内存,而我们需要对海量数据进行的计数、排序等操作。基本思路如下图所示:先借助哈希算法,计算每一条数据的 hash 值,按照 hash 值将海量数据分布存储到多个桶中。根据 hash 函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,最后做合并运算即可。
在这里插入图片描述
问题1:海量日志数据,统计出某日访问百度次数最多的那个IP

解决方式:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分解+ 分而治之 + 归并 方式:

  • 按照 IP 地址的 Hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
  • 对于每一个小文件,构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址
  • 然后再在这1024组最大的IP中,找出那个频率最大的IP

问题2:有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

解决思想: hash分解+ 分而治之 + 归并

  • 顺序读文件中,对于每个词x,按照 hash(x)/(1024*4) 存到4096个小文件中。这样每个文件大概是250k左右。如果其中的有的文件超过了1M大小,还可以按照hash继续往下分,直到分解得到的小文件的大小都不超过1M。
  • 对每个小文件,可以采用 trie树/hashmap 统计每个文件中出现的词以及相应的频率,并使用 100个节点的小顶堆取出出现频率最大的100个词,并把100个词及相应的频率存入文件。这样又得到了4096个文件。
  • 下一步就是把这4096个文件进行归并的过程了

问题3:有a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

解决方案1:如果内存中想要存入所有的 url,共需要 50亿 * 64= 320G大小空间,所以采用 hash 分解+ 分而治之 + 归并 的方式:

  • 遍历文件a,对每个 url 根据某种hash规则,求取hash(url)/1024,然后根据所取得的值将 url 分别存储到1024个小文件(a0a1023)中。这样每个小文件的大约为300M。如果hash结果很集中使得某个文件ai过大,可以在对ai进行二级hash(ai0ai1024),这样 url 就被hash到 1024 个不同级别的文件中。
  • 分别比较文件,a0 VS b0,…… ,a1023 VS b1023,求每对小文件中相同的url时:把其中一个小文件的 url 存储到 hashmap 中,然后遍历另一个小文件的每个url,看其是否在刚才构建的 hashmap 中,如果是,那么就是共同的url,存到文件中。
  • 把1024个文件中的相同 url 合并起来

解决方案2:Bloom filter

  • 如果允许有一定的错误率,可以使用 Bloom filter,4G内存大概可以表示 340 亿bit,n = 50亿,如果按照出错率0.01算需要的大概是650亿个bit,现在可用的是340亿,相差并不多,这样可能会使出错率上升些,将其中一个文件中的 url 使用 Bloom filter 映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)

问题4:有10个文件,每个文件1G,每个文件的每一行存放的都是用户的 query,每个文件的query都可能重复。要求你按照query的频度排序。

解决方案1:hash分解+ 分而治之 +归并

  • 顺序读取10个文件 a0~a9,按照 hash(query)%10 的结果将 query 写入到另外10个文件(记为 b0~b9)中,这样新生成的文件每个的大小大约也1G
  • 找一台内存2G左右的机器,依次使用 hashmap(query, query_count) 来统计每个 query 出现的次数。利用 快速/堆/归并排序 按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件c0~c9。
  • 对这10个文件 c0~c9 进行归并排序(内排序与外排序相结合)。每次取 c0~c9 文件的 m 个数据放到内存中,进行 10m 个数据的归并,即使把归并好的数据存到 d结果文件中。如果 ci 对应的m个数据全归并完了,再从 ci 余下的数据中取m个数据重新加载到内存中。直到所有ci文件的所有数据全部归并完成。

解决方案2:Trie树

  • 如果query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。在这种情况下,可以采用 trie树/hashmap 等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。

问题5:海量数据分布在100台电脑中,请高效统计出这批数据的TOP10

解决思想: 分而治之 + 归并

  • 在每台电脑上求出TOP10,采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)
  • 求出每台电脑上的TOP10后,把这100台电脑上的 TOP10 合并之后,共1000个数据,在采用堆排序或者快排方式 求出 top10
    (注意:该题的 TOP10 是取最大值或最小值,如果取频率TOP10,就应该先hash分解,将相同的数据移动到同一台电脑中,再使用hashmap分别统计出现的频率)

问题6:在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数

解决方案1:hash 分解+ 分而治之 + 归并

  • 2.5亿个 int 类型 hash 到1024个小文件中 a0~a1023,如果某个小文件大小还大于内存,进行多级hash
  • 将每个小文件读进内存,找出只出现一次的数据,输出到b0~b1023
  • 最后数据合并即可

解决方案2 : 2-Bitmap

  • 如果内存够1GB的话,采用 2-Bitmap 进行统计,共需内存 2^32 * 2bit = 1GB内存。2-bitmap 中,每个数分配 2bit(00表示不存在,01表示出现一次,10表示多次,11无意义),然后扫描这 2.5 亿个整数,查看Bitmap中相对应位,如果是00,则将其置为01;如果是01,将其置为10;如果是10,则保持不变。所描完成后,查看bitmap,把对应位是01的整数输出即可。(如果是找出重复的数据,可以用1-bitmap。第一次bit位由0变1,第二次查询到相应bit位为1说明是重复数据,输出即可)

Trie树+红黑树+hashmap

Trie树、红黑树 和 hashmap 可以认为是第一部分中分而治之算法的具体实现方法之一。

其中,Trie树适合处理海量字符串数据,尤其是大量的字符串数据中存在前缀时。Trie树在字典的存储,字符串的查找,求取海量字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。

  • 用于存储时,Trie树因为不重复存储公共前缀,节省了大量的存储空间;
  • 用于以字符串的查找时,Trie树依靠其特殊的性质,实现了在任意数据量的字符串集合中都能以O(len)的时间复杂度完成查找(len为要检索的字符串长度);
  • 在字符串统计中,Trie树能够快速记录每个字符串出现的次数

问题1:上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。

解决方案: hashmap/红黑树 + 堆排序

  • 如果是上千万或上亿的 int 数据,现在的机器4G内存能存下。所以考虑采用 hashmap/搜索二叉树/红黑树 等来进行统计重复次数
  • 然后使用包含 N 个元素的小顶堆找出频率最大的N个数据

问题2:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,并给出时间复杂度

解决思路: trie树 + 堆排序

  • 用 trie树 统计每个词出现的次数,时间复杂度是O(n*len)(len表示单词的平均长度)。
  • 然后使用小顶堆找出出现最频繁的前10个词,时间复杂度是O(n*lg10)。
  • 总的时间复杂度,是O(nle)与O(nlg10)中较大的那一个

问题3:有一千万个字符串记录(这些字符串的重复率比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个),每个查询串的长度为1-255字节。请你统计最热门的10个查询串(重复度越高,说明越热门),要求使用的内存不能超过1G。

解决方案:

  • 内存不能超过 1G,每条记录是 255byte,1000W 条记录需要要占据2.375G内存,这个条件就不满足要求了,但是去重后只有 300W 条记录,最多占用0.75G内存,因此可以将它们都存进内存中去。使用 trie树(或者使用hashmap),关键字域存该查询串出现的次数。最后用10个元素的最小堆来对出现频率进行排序。总的时间复杂度,是O(nle)与O(nlg10)中较大的那一个。

问题4:1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。

解决方案:trie树

BitMap 与 Bloom Filter

  1. BitMap 就是通过 bit 位为 1 或 0 来标识某个状态存不存在。可用于数据的快速查找,判重,删除,一般来说适合的处理数据范围小于 8bit *2^32。否则内存超过4G,内存资源消耗有点多。
  2. Bloom Filter 主要是用于判定目标数据是否存在于一个海量数据集 以及 集合求交集。以存在性判定为例,Bloom Filter 通过对目标数据的映射,能够以 O(k) 的时间复杂度判定目标数据的存在性,其中k为使用的hash函数个数。这样就能大大缩减遍历查找所需的时间。

问题1:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

解决思路:

  • 8位最多99 999 999,需要 100M个bit 位,不到12M的内存空间。我们把 0-99 999 999的每个数字映射到一个Bit位上,这样,就用了小小的12M左右的内存表示了所有的8位数的电话

问题2:2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。

解决方案:使用 2-bitmap,详情见上文

问题3:给40亿个不重复的 unsigned int 的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中

解决方案:使用 Bitmap,申请 512M 的内存,一个bit位代表一个 unsigned int 值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。

问题4:现有两个各有20亿行的文件,每一行都只有一个数字,求这两个文件的交集。

解决方案:采用 bitmap 进行问题解决,因为 int 的最大数是 2^32 = 4G,用一个二进制的下标来表示一个 int 值,大概需要4G个bit位,即约4G/8 = 512M的内存,就可以解决问题了。

  • 首先遍历文件,将每个文件按照数字的正数,负数标记到2个 bitmap 上,为:正数 bitmapA_positive,负数 bitmapA_negative
  • 遍历另为一个文件,生成正数:bitmapB_positive,bitmapB_negative
  • 取 bitmapA_positive and bitmapB_positive 得到2个文件的正数的交集,同理得到负数的交集。
  • 合并,问题解决
  • 这里一次只能解决全正数,或全负数,所以要分两次

问题5:与上面的问题4类似,只不过现在不是A和B两个大文件,而是A, B, C, D….多个大文件,求集合的交集

解决方案:

  • 依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入 Bloom Filter;
  • 如果已经存在,则在另外的集合(记为S)中记录下来;
  • 如果不存在,则插入Bloom Filter;
  • 最后,得到的S即为所有这些大文件中元素的交集

多层划分

多层划分本质上还是分而治之的思想,重在“分”的技巧上!因为元素范围很大,需要通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。适用用于:第k大,中位数,不重复或重复的数字

问题1:求取海量整数的中位数

解决方案:

  • 依次遍历整数,按照其大小将他们分拣到n个桶中。如果有的桶数据量很小,有的则数据量很大,大到内存放不下了;对于那些太大的桶,再分割成更小的桶;
  • 之后根据桶数量的统计结果就可以判断中位数落到哪个桶中,如果该桶中还有子桶,就判断在其哪个子桶中,直到最后找出目标。

问题2:一共有N个机器,每个机器上有N个数,每个机器最多存 N 个数,如何找到 N^2 个数中的中数?

解决方案1: hash分解 + 排序

  • 按照升序顺序把这些数字,hash划分为N个范围段。假设数据范围是2^32 的unsigned int 类型。理论上第一台机器应该存的范围为0(2^32)/N,第i台机器存的范围是(2^32)*(i-1)/N(2^32)*i/N。hash过程可以扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。注意这个过程每个机器上存储的数应该是O(N)的。
  • 然后我们依次统计每个机器上数的个数,依次累加,直到找到第k个机器,在该机器上累加的数大于或等于(N2)/2,而在第k-1个机器上的累加数小于(N2)/2,并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第(N2)/2-x位。然后我们对第k个机器的数排序,并找出第(N2)/2-x个数,即为所求的中位数的复杂度是O(N^2)的。

解决方案2: 分而治之 + 归并

  • 先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第(N2)/2个便是所求。复杂度是O(N2 * lgN^2)的

其他

传统的web项目中session是怎么实现?

向面试官提问

  1. 请问贵公司的这个岗位需要什么样的人才呢?
  2. 面试过程中您觉得我有哪些需要加强的方面呢?
  3. 如果有幸加入贵公司,贵公司对我有哪些方面的提升呢?
  4. 贵公司对新员工的培训体系是什么样的呢?
  5. 公司的核心价值观或者理念是什么?
  6. 贵公司是否鼓励在职进修?对于在职进修的补助办法如何?or贵公司是否鼓励考证?
  7. 您在行业里做了这么久,是怎么兼顾技术和成长的?
  8. 您觉得作为(岗位),新人应该朝哪几个方向努力呢?
  9. 在项目的执行分工上,是否有资深的人员能够带领新员工,并让新员工有发挥的机会?
  10. 贵公司能超越同业的最大原因为何?

回答薪资问题

  1. 你期望的薪资是多少呢?
    答:关于薪资这一方面,我也不知道怎么回答。首先我不知道贵公司除了薪水,还有没有其他补贴,比如:餐补、花费补贴、交通补贴、住房补贴,这些都是我需要考虑的。除此之外,贵公司有没有年终奖,年终奖是多少,我也不知道。还有五险一金,特别是住房公积金,每月交多少,以什么基数交,这些我都要考虑。
    你能为公司带来哪些价值
  2. 首先,是我能为任职岗位所带来的价值,通过个人的技术知识为公司技术上的问题提供解决方案,当然在前期需要深入了解公司的体系架构以及原有的技术储备,以在便后面能更充分的利用自己为公司创造更大的价值。
  3. 其次,在工作一定阶段后,我将为公司带来无形的价值,这种价值来源于一个员工在企业中通过正能量而在团队中相互影响的价值,这种价值是一种无形价值,是促进团队整体发展的积极价值。
  4. 最后,我在本岗位上后期的持续学习与发展中,将通过自身能力,吸纳、融合、创新工作方法、工作流程,以岗位创新影响组织发展,通过组织发展创造组织价值,实现价值转化。
评论 1 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

朝油

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值