文章目录

面试大保健

JDK 和 JRE 有什么区别?

JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。

JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。

具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。

Java 内存区域

Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存划分为若干个不同的数据区域。Java 虚拟机规范将 JVM 所管理的内存分为以下几个运行时数据区:程序计数器Java 虚拟机栈本地方法栈Java 堆元数据区

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

为什么要使用元空间取代永久代的实现?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. 将 HotSpot 与 JRockit 合二为一。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2LcH3uDC-1598187760605)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200821181230951.png)]

程序计数器:可以看作是当前线程所执行的字节码文件(class)的行号指示器,它会记录执行痕迹,是每个线程私有的;

:栈是运行时创建的,是线程私有的,生命周期与线程相同,存储声明的变量

本地方法栈:为native方法服务,native方法是一种由非java语言实现的java方法,为什么使用这种方法呢,与java环境外交互,或者与操作系统交互

:堆是所有线程共享的一块内存,是在java虚拟机启动时创建的,几乎所有对象实例都在此创建,所以经常发生垃圾回收操作;

方法区:主要存储已被虚拟机加载的类的信息,常量,静态变量和即时编译器编译后的代码等数据,该区域是被线程共享的,很少发生垃圾回收

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
谈谈面对对象的理解?

面对对象的三大特征:封装、继承和多态。封装是将一类事物的属性和行为抽象成一个类,比如学生类,老师类等等。继承则是将一类事物的共有属性和行为抽象成一个父类,子类继承父类的属性和方法,并且可以重写父类的方法。提高了代码的复用性。多态是对行为的一种抽象,多种形态,接口的不同实现方式就是多态。比如定义一个animal类有一个eat()方法,然后mat类实现animal类,eat的是鱼,dog类eat的是骨头

String 、StringBuilder 、StringBuffer 的区别?

Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它们都可以储存和操作字符串,区

别如下。

1、String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。初学者可能会有这样的误解:

String str = “abc”;

str = “bcd”; 

如上,字符串 str 明明是可以改变的呀!其实不然,str 仅仅是一引用对象,它指向一个字符串对象“abc”。第二行代码的含义是让 str 重新指向了一个新的字符串“bcd”对象,而“abc”对象并没有任何改变,只不过该对象已经成为一个不可及对象罢了。

2、 StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改。

3、 StringBuilder 是 Java5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使

用的,因为它的所有方法都没有被 synchronized 修饰,因此它的效率理论上也比 StringBuffer 要高

是否可以继承 String 类?

String类是final修饰的类,不可被继承。为什么要用final修饰呢?为了安全和效率

因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么 String interning 将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

因为字符串是不可变的,所以在它创建的时候 HashCode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象。这就是 HashMap 中的键往往都使用字符串。

== 和 equals 的区别是什么?

== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;

equals 默认情况下是引用比较(源码底层就是用的==),只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。

final 在 Java 中有什么作用?
  • final 修饰的类叫最终类,该类不能被继承。
  • final 修饰的方法不能被重写。
  • final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
String 类的常用方法都有那些?
  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。
Java 容器都有哪些?

Java 容器分为 Collection 和 Map 两大类,其下又有很多子类。

Collection:

List:arrayList、linkedList、vector

set:hashset、treeset

队列是Collection的子类**Queue:**ArrayBlockingQueue、LinkedBlockingQueue、SynchronouseQueue

Map:

HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap、Hashtable

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

是值传递。Java 语言的方法调用只支持参数的值传递。

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

方法重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,重写实现的是运行时的多态性。

重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。

重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型

无法根据返回类型区别。返回值是函数运行之后的一个状态,不能作为方法的标识,应该根据所要区分的方法的方法名是否相同并且方法中所带的参数去区分

数组与链表的区别
  • 存取方式上,数组可以顺序存取或者随机存取,而链表只能顺序存取;

  • 存储位置上,数组内存中是连续的,而链表不一定;

  • 按序号查找时,数组可以随机访问,时间复杂度为O(1),而链表不支持随机访问,平均需要O(n);

  • 按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用折半查找将时间复杂度降为O(logn);

  • 插入和删除时,数组平均需要移动n/2个元素,而链表只需修改指针即可;

  • 空间分配方面:
      数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;
      链表存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效;

Map集合几种遍历方式?

第一种:根据键找值方式遍历

第二种:获取所有的键值对对象集合,通过迭代器遍历

第三种:获取所有的键值对对象集合,通过增强for遍历

第四种:通过Map集合中values方法,拿到所有的值

什么是倒排索引?

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。

IO
  • 同步:指的是用户进程触发 IO 操作并等待或者轮询的去查看 IO 操作是否就绪 自己上街买衣服,自己亲自干这件事,别的事干不了。
  • 异步:异步是指用户进程触发 IO 操作以后便开始做自己的事情,而当 IO 操作已经完成的时候会得到 IO 完成的通知(异步的特点就是通知) 告诉朋友自己合适衣服的尺寸,大小,颜色,让朋友委托去卖,然后自己可以去干别的事。(使用异步 IO 时,Java 将 IO 读写委托给 OS 处理,需要将数据缓冲区地址和大小传给 OS)
  • 阻塞:所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止 去公交站充值,发现这个时候,充值员不在(可能上厕所去了),然后我们就在这里等待,一直等到充值员回来为止。(当然现实社会,可不是这样,但是在计算机里确实如此。)
  • 非阻塞:非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待, 银行里取款办业务时,领取一张小票,领取完后我们自己可以玩玩手机,或者与别人聊聊天,当轮我们时,银行的喇叭会通知,这时候我们就可以去了。
  • 同步阻塞 IO(JAVA BIO):同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • 同步非阻塞 IO(Java NIO) : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。用户进程也需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问。
  • 异步阻塞 IO(Java NIO):此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,而采用select 函数有个好处就是它可以同时监听多个文件句柄(如果从 UNP 的角度看,select 属于同步操作。因为select 之后,进程还需要读写数据),从而提高系统的并发性!
  • 异步非阻塞 IO(Java AIO(NIO.2)):在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为真正的 IO 读取或者写入操作已经由内核完成了。

使用场景:

BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。

AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

抽象类与接口区别?
  • 抽象类可以有构造方法,接口中不能有构造方法
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量
  • 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的
  • 抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型
  • 抽象类中可以包含静态方法,接口中不能包含静态方法
  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型
  • 一个类可以实现多个接口,但只能继承一个抽象类。
类的实例化方法调用顺序?

此题考察的是类加载器实例化时进行的操作步骤(加载–>连接->初始化)。

  1. 父类静态变量
  2. 父类静态代码块
  3. 子类静态变量
  4. 子类静态代码块
  5. 父类非静态变量(父类实例成员变量)
  6. 父类构造函数
  7. 子类非静态变量(子类实例成员变量)
  8. 子类构造函数
JAVA反射机制提供了什么功能?

Java反射是Java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等)、superclass(例如Object)、实现interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。

Java反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。

Java反射机制提供如下功能:

  • 在运行时判断任意一个对象所属的类

  • 在运行时构造任意一个类的对象

  • 在运行时判段任意一个类所具有的成员变量和方法

  • 在运行时调用任一个对象的方法

  • 在运行时创建新类对象

    在使用Java的反射功能时,基本首先都要获取类的Class对象,再通过Class对象获取其他的对象。

Stream常用方法?
  • map: 用作类型转换 如把集合里面的字符串转为大写,或者一个对象的集合取几个字段转为新的对象集合
  • filter: 过滤 符合条件的集合元素保存下来,不符合条件的去掉
  • flatMap:合并集合,比如List Album里面有一LIst 对象,这个时候就能不通过循环的方式把 List 里的每一个元素的 trasks 对象组装成一个新的集合
  • reduce: reduce可以做累加运算, .reduce(0, (a,b)-> a+b);
  • count: count和size一样返回的是元素的个数
  • max,min: 求最大值和最小值,这两个方法需要传入一个comparator比较器,Comparator比较器有一个comparing() 方法
  • anyMatch表示,判断的条件里,任意一个元素成功,返回true
  • allMatch表示,判断条件里的元素,所有的都是,返回true
// 转list
Stream<String> stream = Stream.of("I", "love", "you", "too"); 
List<String> list = stream.collect(Collectors.toList());
// 转map
        
Map<String, Integer> maps = aList.stream().collect(Collectors.toMap(Function.identity(), String::length))
// 排序sorted
aList.stream().sorted(
        (a,b)->address.IndexOf(a)-address.IndexOf(b)
).forEach(System.out :: println);//由大到小排序
JAVA io里有什么方法?怎么样的写法能保证io流任何情况都能关闭?

read(),write(),close()等等。

在try/catch/finally的finally后写close()。

常见的异常类有哪些?

ConcurrentModificationException 并发修改异常

NullPointerException 空指针异常

ClassNotFoundException 指定类不存在

NumberFormatException 字符串转换为数字异常

IndexOutOfBoundsException 数组下标越界异常

ClassCastException 数据类型转换异常

FileNotFoundException 文件未找到异常

NoSuchMethodException 方法不存在异常

IOException IO 异常

Java中异常处理机制?

什么是异常

异常指的就是程序的不正常,简单理解就是程序所发生的错误

异常的体系结构&分类

分类

  • 编译时异常:指的就是编译期间,编译器检测到某段代码可能会发生某些问题,需要程序员提前给代码做出错误的解决方案,否则编译是不通过的.(例如FileReader)
  • 运行时异常:指的是编译通过了,但运行时出现的错误.

体系结构

Throwable

​ Error:严重性错误

​ Exception:

​ RuntimeException:运行时异常

​ RuntimeException:编译时异常

异常产生的原理

java对异常默认的处理方式,是将问题抛出给上一级

抛出之前,java会根据错误产生的异常类,创建出该类的对象,底层并通过throw关键字将异常抛出给上一级,不断向上抛出,直到抛给了JVM虚拟机,虚拟机拿到问题之后,就会将错误的原因和所在的位置,打印在控制台

异常的处理方式

一、问题可以自己处理掉的

try…catch处理方式:自己将问题处理掉,不会影响到后续代码的继续执行

二、问题自己处理不掉的

throws抛出处理方式:如果发现问题自己无法完美结局,就可以通过throw关键字,将异常对象抛出给调用者,但如果使用throw抛出异常对象,则方法上面必须进行throws的声明,告知调用者此方法存在异常

细节:如果抛出的对象是RuntimeException,则方法上面无需throws声明

集合

ArrayList

ArrayList为什么线程不安全?解决方案?

因为ArrayList的add方法没有加锁。

在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

解决ArrayList线程不安全的方法?
  • 使用Vector(Vector在方法上加了锁)
  • Collections.synchronized()(在ArrayList外面包装一层 同步 机制)
  • 使用CopyOnWriteArrayList(写时复制,主要是一种读写分离的思想)
CopyOnWriteArrayList

写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

就是写的时候,把ArrayList扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList的引用指向扩容后的。

查看底层add方法源码

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

首先需要加锁

final ReentrantLock lock = this.lock;
lock.lock();

然后在末尾扩容一个单位

Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);

然后在把扩容后的空间,填写上需要add的内容

newElements[len] = e;

最后把内容set到Array中

collection集合类常用的方法?

add、clear、remove、contains、isEmpty、size、toArray

List集合的特性?

ArrayList

  1. ArrayList是List接口可变数组的实现,允许存放null
  2. 底层是使用数组实现,无参构造函数默认初始化长度为10,数组扩容是会将原数组中的元素重新拷贝到新数组中,长度为原来的1.5倍(代扩容价高)
  3. 线程非同步.

LinkedList

  1. LinkedList是List接口双向链表的非同步实现,允许存放null
  2. 底层的数据结构是基于双向链表,数据结构是节点
  3. 双向链表中每个节点分为prev,next,item,其中prev中存放上一个节点的信息,next存放下一个节点的信息,item存放该节点的值
如何实现数组和 List 之间的转换?

数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。

List如何转成Map?

1、使用for循环,依次添加进map

2、使用jdk8流的方法

@Test
public void convert_list_to_map_with_java8_lambda () { 
    
  List<Movie> movies = new ArrayList<Movie>(); 
  movies.add(new Movie(1, "The Shawshank Redemption")); 
  movies.add(new Movie(2, "The Godfather")); 
  
  Map<Integer, Movie> mappedMovies = movies.stream().collect( 
      Collectors.toMap(Movie::getRank, (p) -> p)); 

3、使用guava 工具类库

ArrayList 和 LinkedList 有什么区别?

ArrayList和LinkedList都实现了List接口,有以下的不同点:
1、ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
2、相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
3、LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

List、set和map的区别?

list和set是实现了collection接口的。

List:

  • 可以允许重复的对象。
  • 可以插入多个null元素。
  • 是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
  • 常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。

Set:

  • 不允许重复对象
  • 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
  • 只允许一个 null 元素
  • Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。

Map不是collection的子接口或者实现类。Map是一个接口。

  • Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
  • TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
  • Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
  • Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
如何边遍历边移除 Collection 中的元素?

边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
   list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

Iterator 和 ListIterator 有什么区别?
  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

聊聊HashMap

首先说一下HashMap的数据结构

在jdk1.7中,hashmap的底层数据结构是数组+链表。jdk1.8以后变成数组+链表+红黑树。当链表的长度超过8时,并且数组总容量超过64时,链表转为红黑树。而当发生扩容或remove键值对导致原有的红黑树内节点数量小于6时,则又将红黑树转换成链表。在jdk1.7中插入数据的方式是头插法,头插法会将链表的顺序翻转,这也是在多线程环境下会形成死循环的关键点。jdk1.8以后改成尾插法。

HashMap 你经常用在那个地方?

因为 HashMap 的好处非常多,我曾经在电子商务的应用中使用 HashMap 作为缓存。因为金融领域非常多的运用 Java,也出于性能的考虑,我们会经常用到 HashMap 和 ConcurrentHashMap。另外,controller 层向前台返回数据可以用封装数据,还有 mybatis 中的 map 可以作为参数或者封装结果集

接着说一下hashmap的存取

当我们put的时候,首先计算 keyhash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0wUUlCU-1598187760607)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200821113555807.png)]

h是hashcode。h >>> 16是用来取出h的高16

讲一下hashmap的扩容?

hashmap的初始容量是16,负载因子是0.75,当容量超过length*0.75时,开始扩容。每次扩容都是原长度的2倍。为什么每次扩容都是2的幂次方呢?为了能让 HashMap 存取高效,尽量较少碰撞。因为取余操作中,如果余数是2的幂次方,等同于对除数-1的&操作。 hash % length == hash & ( length - 1 )。length必须是2的幂次方。并且,二级制的&操作比取余操作要快很多,这就是为什么每次扩容都是2的幂次方的原因。

最后说一下线程安全问题

hashmap是线程不安全的。而Hashtable和ConcurrentHashMap是线程安全的。HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题(头插法),在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖的问题,即在并发执行HashMap的put操作时会发生数据覆盖的情况。例如假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完这行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

那么同样都是线程安全的,他们之间有什么区别?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RuKcvYcK-1598187760609)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200820124606006.png)]

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

  1. HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
  2. 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

CurrentHashMap了解吗?

HashMap是线程不安全的 , 但是效率高 , HashTable是线程安全的 , 但是效率低。有没有一种对象是即是线程安全的 , 同时执行效率可以达到HashMap呢?

CurrentHashMap可以做到。底层通过分段加锁进行实现 , hashmap底层是数组加上链表实现的 , 那么一个线程来操作数据,只是操作数组中一个索引的数据.。如果此时对整个数组加锁,其他线程操作不了这个数组,所以效率低。其实线程也就操作数组的一个索引,对这个索引进行加锁 ,而锁对象就是这个索引所对应的值,其他线程来修改其他索引数据时,拿到的是其他索引的锁对象,从而提高了效率。

ConcurrentHashMap 的工作原理及代码实现?

HashTable 里使用的是 synchronized 关键字,这其实是对对象加锁,锁住的都是对象整体,当 Hashtable 的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。

ConcurrentHashMap 算是对上述问题的优化,ConcurrentHashMap 引入了分割(Segment),可以理解为把一个大的 Map 拆分成 N 个小的 HashTable,在 put 方法中,会根据 hash(paramK.hashCode())来决定具体存放进哪个 Segment,如果查看 Segment 的 put 操作,我们会发现内部使用的同步机制是基于 lock 操作的,这样就可以对 Map 的一部分(Segment)进行上锁,这样影响的只是将要放入同一个 Segment 的元素的 put 操作,保证同步的时候,锁住的不是整个Map(HashTable 就是这么做的),相对于 HashTable 提高了多线程环境下的性能,因此HashTable 已经被淘汰了。

HashMap 和 Hashtable 的区别?

1.hashMap 去掉了 HashTable 的 contains 方法,但是加上了 containsValue()和 containsKey()方法。

2.hashTable 同步的,而 HashMap 是非同步的,效率上逼 hashTable 要高。

3.hashMap 允许空键值,而 hashTable 不允许。

map 集合的两种取出方式?

第一种:普遍使用,二次取值。通过 Map.keySet() 遍历 key 和 value

第二种:推荐,尤其是容量大时。通过 Map.entrySet 遍历 key 和 value

Collection 和 Collections 的区别?

Collection 是集合类的上级接口,继承与他的接口主要有 Set 和 List以及Queue

Collections 是工具类,有很多对集合操作的方法

请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?

不安全。在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了 synchronized 关键字。

Collections 工具类提供了相关的 API,可以让上面那 3 个不安全的集合变为安全的。

Collections.synchronizedCollection(c) 
Collections.synchronizedList(list) 
Collections.synchronizedMap(m) 
Collections.synchronizedSet(s)
//打开源码其实实现原理非常简单,就是将集合的核心方法添加上了 synchronized 关键字

HashSet

HashSet的底层结构就是HashMap

public HashSet(){
	map = new HashMap<>();
}

但是为什么我调用 HashSet.add()的方法,只需要传递一个元素,而HashMap是需要传递key-value键值对?

首先我们查看hashSet的add方法

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

我们能发现但我们调用add的时候,存储一个值进入map中,只是作为key进行存储,而value存储的是一个Object类型的常量,也就是说HashSet只关心key,而不关心value

HashSet 是如何保证元素唯一性的呢?

是通过元素的两个方法,hashCode 和 equals 来完成

向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。

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

以下是HashSet 部分源码:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

Queue

BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

  • ArrayBlockQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
    • 有界,但是界限非常大,相当于无界,可以当成无界
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
    • 生产一个,消费一个,不存储元素,不消费不生产。每一个put操作必须等待一个take操作,否者不能继续添加元素
BlockingQueue核心方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xeqCVVMs-1598187760611)(F:/学习资料/LearningNotes/校招面试/JUC/8_阻塞队列/images/image-20200316154442756.png)]

在 Queue 中 poll()和 remove()有什么区别?
  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
阻塞队列的应用

生产者消费者 传统版

  • 题目:一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮

新版的阻塞队列版生产者和消费者,使用:volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用

多线程

解释下多线程:

首先,什么是线程,线程是程序的执行路径,或者可以说是程序的控制单元。一个进程可能包含一个或多个进程,当一个进程存在多条执行路径时,就可以将该执行方式称为多线程。线程的执行方式大致可分为就绪(wait),执行(run),阻塞(block)三个状态,而三个状态的转换实质上是在抢夺cpu 资源过程中造成的,正常情况下 cpu 资源不会被线程独自占用,因此多个线程在运行中相互抢夺资源,造成线程在上述的三个状态之间不断的相互转换。而这也是多线程的执行方式。

首先,你要知道JUC下的三个包
  • java.util.concurrent
    • java.util.concurrent.atomic
    • java.util.concurrent.locks
Volatile知道吗?
与面试官对线:

Volatile是Java虚拟机提供的轻量级的同步机制,它保证内存可见性,不保证原子性,禁止指令排序。valatile是通过内存屏障来防止指令重排序的。valatile不保证原子性,如何解决原子性呢?可以加锁或者使用atomic类。什么地方用到了volatile?最经典的单例模式用到了,双重检验并不能保证结果正确,要使用volatile禁止了指令重排序。除了单例模式,copyOnWriteArrayList底层也用到了volatile:private transient volatile Object[] array;。另外unsafe类和AtomicInteger底层同样用到了volatile。

volatile哪里用到过?

一、单例模式

/**
 * 懒汉式创建单例模式:
 * 加入了锁和volatile
 */
public class Singleton {

    // 私有构造,别人创建不了类,只能自己创建
    private Singleton() {
    }


    // 禁止指令重排
    private static volatile Singleton singleton = null;

    // 静态工厂方法
    public static Singleton getInstance() {

        if (singleton == null) {
            // 加锁,防止并发产生两个对象
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        
        return singleton;
    }
}

二、copyOnWriteArrayList类

定义一个数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k620SwYZ-1598187760613)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200816230711179.png)]

三、AtomicInteger类

定义一个int值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHcKd2I1-1598187760614)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200816231446952.png)]

为什么AtomicInteger类能保证原子性?

查看源码:

因为AtomicInteger的getAndDecrement()使用的的是unsafe类

public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

unsafe类是cas的核心,cas是一条CPU并发原语,它的功能是判断内存某个为止的值是否为预期值,如果是则更新为新的值,这个过程是和原子的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJXyEO4Z-1598187760615)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200816230913071.png)]

为什么DCL(双端检验)机制不能保证线程安全?

原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:

  • memory = allocate(); // 1、分配对象内存空间
  • instance(memory); // 2、初始化对象
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null

但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

  • memory = allocate(); // 1、分配对象内存空间
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
  • instance(memory); // 2、初始化对象

这样就会造成什么问题呢?

也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例

指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性

指令重排有三种情况

源代码——》编译器优化的重排——》指令并行的重排——》内存系统的重排——》最终执行的指令

Volatile针对指令重排做了啥

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的。

说一说CAS?

CAS的全称是Compare-And-Swap比较并交换,它是CPU并发原语。CAS底层是unsafe类和自旋。

CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。我们先将内存位置的值与A比较,如果相同,则将内存位置的值更新为B。如果不同,重复循环比较。

cas的缺点:1、因为自旋,所以增加了cpu消耗;2、只能保证一个共享变量的原子性;3、产生ABA问题;

什么是ABA问题?有两个线程12,1先获取主内存的值(假设为A)想要修改为。刚刚获取到值,2线程抢到cpu,先将主内存的值修改为B,然后又将主线程的值修改为A。修改完后,1线程拿到cpu,比较一下发现值是相同的,然后修改值。

CAS底层原理

首先我们先看看 atomicInteger.getAndIncrement()方法的源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q507jXM1-1598187760616)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200817185448939.png)]

从这里能够看到,底层又调用了一个unsafe类的getAndAddInt方法

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类。

unsafe类的getAndAddInt()方法:

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;
    }
CAS缺点
  • 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证一个共享变量的原子操作
    • 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
    • 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来ABA问题?
什么是ABA问题?

假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。

所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了。

如何解决ABA问题?

新增一种机制,也就是修改版本号,类似于时间戳的概念。

1、使用AtomicStampedReference时间戳原子引用

2、使用LongAdder(CAS机制优化)

JMM是什么

JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM关于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程解锁前,必须读取主内存的最新值,到自己的工作内存
  • 加锁和解锁是同一把锁
缓存一致性

为什么这里主线程中某个值被更改后,其它线程能马上知晓呢?其实这里是用到了总线嗅探技术

在说嗅探技术之前,首先谈谈缓存一致性的问题,就是当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一。

为了解决缓存一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议主要有MSI、MESI等等。

MESI

当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。

总线嗅探

那么是如何发现数据是否失效呢?

这里是用到了总线嗅探技术,就是每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。

总线风暴

总线嗅探技术有哪些缺点?

由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。因此不要大量使用volatile关键字,至于什么时候使用volatile、什么时候用锁以及Syschonized都是需要根据实际场景的。

创建线程的方式?线程启动的几种方法
  1. 继承Thread类,调用start方法

    优点:代码简单

    缺点:该类无法继承别的类

  2. 实现Runnable接口

    优点:继承其他类.统一实现该接口的实例可以共享资源

    缺点:代码复杂

  3. 实现Callable接口

    Callable中的call()方法有返回值,其他和Runnable的run()方法一样

  4. 线程池方式

    优点:实现自动化装配,易于管理,循环利用资源

Runnable和Callable的区别?
  • Callable重写的方法是call(),Runnable重写的方法是run()。
  • Callable的任务执行后可返回值,而Runnable的是不能返回值的。
  • Call方法可以抛出异常,run方法不可以。
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。FutureTask实现了Future接口
线程池的种类?
  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
  • 因为在项目中,线程的创建和销毁非常消耗资源,所以使用在多线程场景的时候会使用线程池,根据自己业务逻辑的需求,使用不同的线程池。
Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
  • sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,
    将执行机会(CPU)让给其他线程,不释放锁,因此休眠时间结束后会自动恢复,线程回到就绪状态
  • wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

在Java中,任意对象都可以当作锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在Object类里。
为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?其目的在于确保等待线程从Wait()返回时能够感知通知线程对共享变量所作出的修改。如果不在同步范围内使用,就会抛出java.lang.IllegalMonitorStateException的异常。

notify和notifyAll方法的区别

notify只会唤醒等待该锁的其中一个线程。notifyAll:唤醒等待该锁的所有线程。
1)永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
2)永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。

Hashtable的size()方法为什么要做同步?

对于类的非同步方法,可以多条线程同时访问。如果A线程执行了put方法,而B线程正在执行size方法,导致数据不一致。

同步方法的实现方式?

1)同步方法

即有 synchronized 关键字修饰的方法。由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。注: synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2)同步代码块

即有 synchronized 关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可。

3)使用特殊域变量(volatile)实现线程同步

  • volatile 关键字为域变量的访问提供了一种免锁机制,
  • 使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新,
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile 不会提供任何原子操作,它也不能用来修饰 final 类型的变量

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用 final 域,有锁保护的域和 volatile 域可以避免非同步的问题。

4)使用重入锁实现线程同步

在 JavaSE5.0 中新增了一个 java.util.concurrent 包来支持同步。ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁,它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力

ReenreantLock 类的常用方法有:

ReentrantLock() : 创建一个 ReentrantLock 实例

lock() : 获得锁

unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

注:关于 Lock 对象和 synchronized 关键字的选择:

a.最好两个都不用,使用一种 java.util.concurrent 包提供的机制,能够帮助用户处理所有与锁相关的代码。

b.如果 synchronized 关键字能满足用户的需求,就用 synchronized,因为它能简化代码

c.如果需要更高级的功能,就用 ReentrantLock 类,此时要注意及时释放锁,否则会出现死锁,通常在 finally代码释放锁

5)使用局部变量实现线程同步

如果使用 ThreadLocal 管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal 类的常用方法

ThreadLocal() : 创建一个线程本地变量

get() : 返回此线程局部变量的当前线程副本中的值

initialValue() : 返回此线程局部变量的当前线程的"初始值"

set(T value) : 将此线程局部变量的当前线程副本中的值设置为 value

注:ThreadLocal 与同步机制

a.ThreadLocal 与同步机制都是为了解决多线程中相同变量的访问冲突问题。

b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式

6)使用阻塞队列实现线程同步

前面 5 种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。使用 javaSE5.0 版本中新增的 java.util.concurrent 包将有助于简化开发。本小节主要是使用 LinkedBlockingQueue来实现线程的同步LinkedBlockingQueue是一个基于已连接节点的,范围任意的 blocking queue。队列是先进先出的顺序(FIFO)

LinkedBlockingQueue 类常用方法

LinkedBlockingQueue() : 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue

put(E e) : 在队尾添加一个元素,如果队列满则阻塞

size() : 返回队列中的元素个数

take() : 移除并返回队头元素,如果队列空则阻塞

7)使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作。即-这几种行为要么同时完成,要么都不完成。在 java 的 util.concurrent.atomic 包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中 AtomicInteger 表可以用原子方式更新 int 的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换 Integer;可扩展 Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger 类常用方法:

AtomicInteger(int initialValue) : 创建具有给定初始值的新的 AtomicInteger

addAddGet(int dalta) : 以原子方式将给定值与当前值相加

get() : 获取当前值

ThreadLocal

该类提供了线程局部 (thread-local) 变量,ThreadLocal会为每个线程创建变量的副本,线程之间互不影响,这样就不存在线程安全问题。ThreadLocal的底层是每个Thread维护了一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value是真正要存储的Object。用在哪些情景?spring security、spring中的事务管理器就是使用的ThreadLocal、数据库连接等

您能说说ThreadLocal常用操作的底层实现原理吗?如存储set(T value),获取get(),删除remove()等操作。
调用get()进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在:

如果存在,则以当前的ThreadLocalkey,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value值,返回结果值。

如果不存在,则证明此线程没有维护的ThreadLocalMap对象,调用setInitialValue方法进行初始化。返回setInitialValue初始化的值。

setInitialValue方法的操作如下:

1 ) 调用initialValue获取初始化的值。

2 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

3 ) 判断当前的ThreadLocalMap是否存在:

如果存在,则调用map.set设置此实体entry

如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

调用set(T value),进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在:

如果存在,则调用map.set设置此实体entry

如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

调用remove(),进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在, 如果存在,则调用map.remove,以当前ThreadLocalkey删除对应的实体entry

注意:

  • 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量

  • 在进行get之前,必须先set,否则会报空指针异常;

    如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

ThreadLocalMap 是个弱引用类,内部 一个Entry由ThreadLocal对象和Object构成,为什么要用弱引用呢?

如果是直接new一个对象的话,使用完之后设置为null后才能被垃圾收集器清理,如果为弱引用,使用完后垃圾收集器自动清理key,程序员不用再关注指针。

ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?

会内存泄露,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value不会被释放;因此只有当Current Thread销毁时,value才能得到释放。因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。我们可以手动调用ThreadLocal的remove方法进行释放!

乐观锁和悲观锁的理解及如何实现?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现,也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

ThreadPoolExecutor

为什么用线程池

线程池做的主要工作就是控制运行的线程的数量,处理过程中,将任务放入到队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用、控制最大并发数、管理线程

线程池中的任务是放入到阻塞队列中的

线程池的好处

多核处理的好处是:省略的上下文的切换开销

原来我们实例化对象的时候,是使用 new关键字进行创建,到了Spring后,我们学了IOC依赖注入,发现Spring帮我们将对象已经加载到了Spring容器中,只需要通过@Autowrite注解,就能够自动注入,从而使用

因此使用多线程有下列的好处

  • 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
底层实现

我们通过查看源码,点击了Executors.newSingleThreadExecutor 和 Executors.newFixedThreadPool能够发现底层都是使用了ThreadPoolExecutor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ytc1yChU-1598187760616)(F:/学习资料/LearningNotes/校招面试/JUC/10_线程池/images/image-20200317182004293.png)]

我们可以看到线程池的内部,还使用到了LinkedBlockingQueue 链表阻塞队列

同时在查看Executors.newCacheThreadPool 看到底层用的是 SynchronousBlockingQueue阻塞队列

最后查看一下,完整的三个创建线程的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W684LFFV-1598187760617)(F:/学习资料/LearningNotes/校招面试/JUC/10_线程池/images/image-20200317183202992.png)]

线程池的重要参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idG4h8WD-1598187760618)(F:/学习资料/LearningNotes/校招面试/JUC/10_线程池/images/image-20200317183600957.png)]

线程池在创建的时候,一共有7大参数

  • corePoolSize:核心线程数,线程池中的常驻核心线程数
    • 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
    • 当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中
  • maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1、
    • 相当有扩容后的线程数,这个线程池能容纳的最多线程数
  • keepAliveTime:多余的空闲线程存活时间
    • 当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
    • 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
    • LinkedBlockingQueue:链表阻塞队列
    • SynchronousBlockingQueue:同步阻塞队列
  • threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
  • handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的Runnable的策略

当营业窗口和阻塞队列中都满了时候,就需要设置拒绝策略

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgIAcbcc-1598187760618)(F:/学习资料/LearningNotes/校招面试/JUC/10_线程池/images/image-20200317201150197.png)]

拒绝策略

以下所有拒绝策略都实现了RejectedExecutionHandler接口

  • AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
  • CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
线程池底层工作原理
线程池运行架构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgEBO7l8-1598187760619)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200821212326139.png)]

文字说明

  1. 在创建了线程池后,等待提交过来的任务请求

  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断

    1. 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
    3. 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程运行这个任务;
    4. 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行

  4. 当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:

    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
    2. 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小

以顾客去银行办理业务为例,谈谈线程池的底层工作原理

  1. 最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理
  2. 后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
  3. 后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
  4. 假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
  5. 临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数
为什么不用默认创建的线程池?

线程池创建的方法有:固定数的,单一的,可变的,那么在实际开发中,应该使用哪个?

我们一个都不用,在生产环境中是使用自己自定义的

为什么不用Executors中JDK提供的?

根据阿里巴巴手册:并发控制这章

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
    • 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
  • 线程池不允许使用Executors去创建,而是通过ThreadToolExecutors的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
    • Executors返回的线程池对象弊端如下:
      • FixedThreadPool和SingleThreadPool:
        • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
      • CacheThreadPool和ScheduledThreadPool
        • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
手写线程池
采用默认拒绝策略

从上面我们知道,因为默认的Executors创建的线程池,底层都是使用LinkBlockingQueue作为阻塞队列的,而LinkBlockingQueue虽然是有界的,但是它的界限是 Integer.MAX_VALUE 大概有20多亿,可以相当是无界的了,因此我们要使用ThreadPoolExecutor自己手动创建线程池,然后指定阻塞队列的大小

下面我们创建了一个 核心线程数为2,最大线程数为5,并且阻塞队列数为3的线程池

        // 手写线程池
        final Integer corePoolSize = 2;
        final Integer maximumPoolSize = 5;
        final Long keepAliveTime = 1L;

        // 自定义线程池,只改变了LinkBlockingQueue的队列大小
        ExecutorService executorService = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

然后使用for循环,模拟10个用户来进行请求

      // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
        try {

            // 循环十次,模拟业务办理,让5个线程处理这10个请求
            for (int i = 0; i < 10; i++) {
                final int tempInt = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }

但是在用户执行到第九个的时候,触发了异常,程序中断

pool-1-thread-1	 给用户:0 办理业务
pool-1-thread-4	 给用户:6 办理业务
pool-1-thread-3	 给用户:5 办理业务
pool-1-thread-2	 给用户:1 办理业务
pool-1-thread-2	 给用户:4 办理业务
pool-1-thread-5	 给用户:7 办理业务
pool-1-thread-4	 给用户:2 办理业务
pool-1-thread-3	 给用户:3 办理业务
java.util.concurrent.RejectedExecutionException: Task com.moxi.interview.study.thread.MyThreadPoolDemo$$Lambda$1/1747585824@4dd8dc3 rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 5]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.moxi.interview.study.thread.MyThreadPoolDemo.main(MyThreadPoolDemo.java:34)

这是因为触发了拒绝策略,而我们设置的拒绝策略是默认的AbortPolicy,也就是抛异常的

触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会抛出异常从而报错退出。

线程池的合理参数

生产环境中如何配置 corePoolSize 和 maximumPoolSize

这个是根据具体业务来配置的,分为CPU密集型和IO密集型

  • CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数 + 1个线程数

  • IO密集型

由于IO密集型任务线程并不是一直在执行任务,则可能多的线程,如 CPU核数 * 2。IO密集型,即该任务需要大量的IO操作,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上,所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集时,大部分线程都被阻塞,故需要多配置线程数:

参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右

例如:8核CPU:8/ (1 - 0.9) = 80个线程数

synchronized和ReentrantLock有什么区别呢?

首先synchronized是关键字,ReentrantLock是Lock的实现类。

其次

  • ReentrantLock 使用灵活,可以对获取锁的等待时间进行设置,可以获取各种锁的信息,但是必须要释放锁;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
  • Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
  • ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
volatile与synchronized的区别?

区别:

  • Volatile本质是告诉jvm当前变量在寄存器中的值是不安全的需要从内存中读取,sychronized则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞
  • Volatile只能作用于变量,synchronized则是可以使用在变量和方法上
  • Volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化
多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 thread id 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

ReentrantLock 底层实现

ReentrantLock实现了Lock接口,是AQS的一种。加锁和解锁都需要显式写出,注意结束操作记得unlock释放锁。它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。它还提供了获取共享锁和互斥锁的方式,都是基于CAS对state操作而言的。

独占锁(写锁) / 共享锁(读锁)

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可以被多个线程锁持有

读写锁:ReentrantReadWriteLock其读锁是共享,其写锁是独占、写的时候只能一个人写,但是读的时候,可以多个人同时读

// 创建一个读写锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();

// 创建一个读锁
rwLock.readLock().lock();
// 读锁 释放
rwLock.readLock().unlock();
公平锁

是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列

非公平锁

是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

Synchronized:非公平锁

ReentrantLock:默认非公平锁

可重入锁和递归锁

可重入锁就是递归锁,ReentrantLock / Synchronized 就是一个典型的可重入锁

用作:可重入锁的最大作用就是避免死锁

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块

当我们在getLock方法加两把锁会是什么情况呢? (阿里面试)

    public void getLock() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

最后得到的结果也是一样的,因为里面不管有几把锁,其它他们都是同一把锁,也就是说用同一个钥匙都能够打开

当我们在getLock方法加两把锁,但是只解一把锁会出现什么情况呢?

public void getLock() {
    lock.lock();
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "\t get Lock");
        setLock();
    } finally {
        lock.unlock();
        lock.unlock();
    }
}

得到结果

t3	 get Lock
t3	 set Lock

也就是说程序直接卡死,线程不能出来,也就说明我们申请几把锁,最后需要解除几把锁

当我们只加一把锁,但是用两把锁来解锁的时候,又会出现什么情况呢?

    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

这个时候,运行程序会直接报错

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)
自旋锁

自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

原来提到的比较并交换,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Um1lSF3z-1598187760620)(F:/学习资料/LearningNotes/校招面试/JUC/6_Java的锁/Java锁之自旋锁/images/image-20200315154143781.png)]

优缺点

优点:循环比较获取直到成功为止,没有类似于wait的阻塞

缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源

手写自旋锁

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到

/**
 * 手写一个自旋锁
 *
 * 循环比较获取直到成功为止,没有类似于wait的阻塞
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
 * @author: 陌溪
 * @create: 2020-03-15-15:46
 */
public class SpinLockDemo {

    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {

        }
    }

    /**
     * 解锁
     */
    public void myUnLock() {

        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();


            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程,先执行
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();

    }
}

最后输出结果

t1	 come in 
.....五秒后.....
t1	 invoked myUnlock()
t2	 come in 
t2	 invoked myUnlock()

首先输出的是 t1 come in

然后1秒后,t2线程启动,发现锁被t1占有,所有不断的执行 compareAndSet方法,来进行比较,直到t1释放锁后,也就是5秒后,t2成功获取到锁,然后释放。

CountDownLatch工具类

让一些线程阻塞直到另一些线程完成一系列操作才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程就会被阻塞。其它线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会被阻塞),当计数器的值变成零时,因调用await方法被阻塞的线程会被唤醒,继续执行

场景1

现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的

解决方案

这个时候就用到了CountDownLatch,计数器了。我们一共创建6个线程,然后计数器的值也设置成6

// 计数器
CountDownLatch countDownLatch = new CountDownLatch(6);

然后每次学生线程执行完,就让计数器的值减1

for (int i = 0; i <= 6; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
        countDownLatch.countDown();
    }, String.valueOf(i)).start();
}

最后我们需要通过CountDownLatch的await方法来控制班长主线程的执行,这里 countDownLatch.await()可以想成是一道墙,只有当计数器的值为0的时候,墙才会消失,主线程才能继续往下执行

countDownLatch.await();

System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");

不加CountDownLatch的执行结果,我们发现main线程提前已经执行完成了

1	 上完自习,离开教室
0	 上完自习,离开教室
main	 班长最后关门
2	 上完自习,离开教室
3	 上完自习,离开教室
4	 上完自习,离开教室
5	 上完自习,离开教室
6	 上完自习,离开教室

引入CountDownLatch后的执行结果,我们能够控制住main方法的执行,这样能够保证前提任务的执行

0	 上完自习,离开教室
2	 上完自习,离开教室
4	 上完自习,离开教室
1	 上完自习,离开教室
5	 上完自习,离开教室
6	 上完自习,离开教室
3	 上完自习,离开教室
main	 班长最后关门
完整代码
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
    }
}
场景2

秦灭六国,而后一统天下。齐楚燕赵魏韩都要被灭后,秦国才能统一天下。

完整代码

首先要有一个枚举类,枚举类相当于一个小的数据库。One(1, “齐”),可以看做是entry,key是1,value是"齐"。

public enum CountryEnum {

    One(1, "齐"), Two(2, "楚"), Three(3, "燕"), Four(4, "赵"), Five(5, "魏"), Six(6, "韩");

    @Getter
    private Integer retCode;
    @Getter
    private String retMessage;


    CountryEnum(int retCode, String retMessage) {
        this.retCode = retCode;
        this.retMessage = retMessage;
    }

    public static CountryEnum forEach_CountryEnum(int index) {
        CountryEnum[] values = CountryEnum.values();
        for (CountryEnum value : values) {
            if (index == value.getRetCode()) {
                return value;
            }
        }
        return null;
    }
}

测试代码

/**
 * 秦灭六国,一统天下
 */
public class CountDownLatchDemo1 {

    @Test
    public void test() throws InterruptedException {
        
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 国,被灭");
                countDownLatch.countDown();
            }, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t 秦国,一统华夏");

    }
}
CyclicBarrier工具类

和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0,加到某个值的时候就执行。

案例

public class CyclicBarrierDemo {

public static void main(String[] args) {
    /**
     * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
     */
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
        System.out.println("召唤神龙");
    });

    for (int i = 0; i < 7; i++) {
        final Integer tempInt = i;
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");

            try {
                // 先到的被阻塞,等全部线程完成后,才能执行方法
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }, String.valueOf(i)).start();
    }
}
Semaphore工具类

信号量主要用于两个目的

  • 一个是用于共享资源的互斥使用
  • 另一个用于并发线程数的控制

我们模拟一个抢车位的场景,假设一共有6个车,3个停车位。前三辆先停,听完开走,后三辆再停。

Servlet 的生命周期详解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njKo7fAn-1598187760621)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200725211042305.png)]

1、Web Client 向 Servlet 容器(Tomcat)发出 Http 请求

2、Servlet 容器接收 Web Client 的请求

3、Servlet 容器创建一个 HttpRequest 对象,将 Web Client 请求的信息封装到这个对象中

4、Servlet 容器创建一个 HttpResponse 对象

5、Servlet 容器调用 HttpServlet 对象的 service 方法,把 HttpRequest 对象与 HttpResponse 对象作为参数

传给 HttpServlet 对象

6、HttpServlet 调用 HttpRequest 对象的有关方法,获取 Http 请求信息

7、HttpServlet 调用 HttpResponse 对象的有关方法,生成响应数据

8、Servlet 容器把 HttpServlet 的响应结果传给 Web Client

Servlet 的框架是由两个 Java 包组成的:javax.servlet 与 javax.servlet.http。在 javax.servlet 包中定义了所有的 Servlet 类都必须实现或者扩展的通用接口和类。在 javax.servlet.http 包中定义了采用 Http 协议通信的HttpServlet 类。Servlet 的框架的核心是javax.servlet.Servlet 接口,所有的 Servlet 都必须实现这个接口。

在 Servlet 接口中定义了 5 个方法,其中 3 个方法代表了 Servlet 的生命周期(说出这三个方法即可):

1、init 方法:负责初始化 Servlet 对象。

2、service 方法:负责响应客户的请求。

3、destroy 方法:当 Servlet 对象退出生命周期时,负责释放占用的资源。

IO对比总结?

IO 的方式通常分为几种:同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。

  • BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
  • NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
  • AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

举个例子:

  • 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着!
  • 同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
  • 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了, 类似于现在的外卖。

JVM

JAVA中垃圾回收机制?
什么样的对象会被当做垃圾回收?

当一个对象的引用(地址)没有变量去记录的时候,该对象就会成为垃圾对象,并在垃圾回收器空闲的时候对其进行清扫

如何检验对象是否被回收?

可以重写Object类中的finalize方法

这个方法在垃圾回收器执行的时候,被回收器自动调用执行的

怎样通知垃圾回收器回收对象?

system.gc()

如何判断一个对象是否可以被回收?

引用计数法:每当有一个地方引用它,计数器值加1;每当有一个引用失效,计数器值减1;任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。

枚举根节点做可达性分析:为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法:基本思路就是通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。

那些对象可以当做GC Roots
  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中的JNI(Native方法)的引用对象

JVM参数调优

JVM参数类型
  • 标配参数(从JDK1.0 - Java12都在,很稳定)
    • -version
    • -help
    • java -showversion
  • X参数(了解)
    • -Xint:解释执行
    • -Xcomp:第一次使用就编译成本地代码
    • -Xmixed:混合模式
  • XX参数(重点)
    • Boolean类型
      • 公式:-XX:+ 或者-某个属性 + 表示开启,-表示关闭
      • Case:-XX:-PrintGCDetails:表示关闭了GC详情输出
    • key-value类型
      • 公式:-XX:属性key=属性value
      • 不满意初始值,可以通过下列命令调整
      • case:如何:-XX:MetaspaceSize=21807104:查看Java元空间的值
jps -l得到进程号
jinfo -flag PrintGCDetails(参数名称) 12608(pid)

两个经典参数:-Xms 和 -Xmx,这两个参数 如何解释

这两个参数,还是属于XX参数,因为取了别名

  • -Xms 等价于 -XX:InitialHeapSize :初始化堆内存(默认只会用最大物理内存的64分1)
  • -Xmx 等价于 -XX:MaxHeapSize :最大堆内存(默认只会用最大物理内存的4分1)
JVM调优

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

  • 监控GC的状态,使用各种JVM工具,查看当前日志,并且分析当前堆内存快照和gc日志,根据实际的情况看是否需要优化。
  • 通过JMX的MBean或者Java的jmap生成当前的Heap信息,并使用Visual VM或者Eclipse自带的Mat分析dump文件
  • 如果参数设置合理,没有超时日志,GC频率GC耗时都不高则没有GC优化的必要,如果GC时间超过1秒或者频繁GC,则必须优化
  • 调整GC类型和内存分配,使用1台和多台机器进行测试,进行性能的对比。再做修改,最后通过不断的试验和试错,分析并找到最合适的参数

GC在新生区,Full GC大部分发生在养老区

类加载过程?

加载 -> 验证 -> 准备 -> 解析 -> 初始化

  • 加载:

    1.获取类的二进制字节流

    2.将字节流代表的静态存储结构转化为方法区运行时数据结构

    3.在队中生成class字节码对象

  • 验证:连接过程的第一步,确保class文件的字节流中的信息符合当前虚拟机的要求,不会危害虚拟机的安全

  • 准备:为类的静态变量分配内存并将其初始化为默认值

  • 解析:虚拟机将常量池内符号引用替换成直接引用的过程

  • 初始化:执行类构造器的init的过程

类加载的三种方式

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

双亲委派原则

他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:

可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

对象创建过程?
  1. JVM会先去方法区下找有没有所创建对象的类存在,有就可以创建对象了,没有则把该类加载到方法区
  2. 在创建类的对象时,首先会先去堆内存中分配空间
  3. 当空间分配完后,加载对象中所有的非静态成员变量到该空间下
  4. 所有的非静态成员变量加载完成之后,对所有的非静态成员进行默认初始化
  5. 所有的非静态成员默认初始化完成之后,调用相应的构造方法到栈中
  6. 在栈中执行构造函数时,先执行隐式,再执行构造方法中书写的代码
  7. 执行顺序:静态代码库,构造代码块,构造方法
  8. 当整个构造方法全部执行完,此对象创建完成,并把堆内存中分配的空间地址赋给对象名(此时对象名就指向了该空间)
方法区堆栈溢出怎么处理?

jdk1.7之前字符串常量池是方法区的一部分,方法区叫做“永久代”,在1.7之前无限的创建对象就会造成内存溢出

用jdk1.7之后,取消了永久代,添加了元数据区,就不会产生内存溢出。

在jdk1.7时出现内存溢出:

  1. 需要查看代码中是否出现死循环。

  2. 是否出现死锁现象。

  3. 在jvm运行时,提高堆内存的大小。

单例设计模式中懒汉式和饿汉式的区别?

饿汉式:

//饿汉式单例类.在类初始化时,已经自行实例化  

public class Singleton1 {  

  private Singleton1() {}  

  private static final Singleton1 single = new Singleton1();  

  //静态工厂方法  

  public static Singleton1 getInstance() {  

      return single;  

  }  

}

l 懒汉式:

//懒汉式单例类.在第一次调用的时候实例化自己  

public class Singleton {  

  private Singleton() {}  

  private static Singleton single=null;  

  //静态工厂方法  

  public static Singleton getInstance() {  

    if (single == null) {   

      single = new Singleton();  

     }   

    return single;  

  }  

}  

饿汉式就是类一旦加载,就把单例初始化完成,保证getInstance()的时候,单例就已经存在。

懒汉式比较懒,只有当调用getInstance的时候,才会去初始化这个单例

区别:

饿汉式是线程安全的,懒汉式是线程不安全的(即一个进程内有多个线程在在同时使用时可能会产生多个实例,可创建个静态内部类,产生一个单例对象,通过静态内部类返回获取这个对象)

常见的基本排序?
冒泡排序
public void bubbleSort(int[] arr) { //从小到大

	int temp = 0;

	for(int i = 0; i < arr.length -1; i++){ //控制趟数,到倒数第二个为止

		for(int j = arr.length-1; j>i; j--){ 
        //从最后一个值开始冒泡,将后面的小值与前面的大值进行交换,并且保证循环到前面已经排序完的索引为止
			if(arr[j-1] > arr[j]){

				temp = arr[j];

				arr[j] = arr[j-1];

				arr[j-1] = temp;

			}
		}
	}
}
选择排序
public void selectionSort(int[] arr){

	int temp = 0;

	int k = 0; //存储最小值的索引

	for(int i = 0; i<arr.lengrh - 1; i++){ //控制趟数,到倒数第二个为止

		k = i;
		for(int j = i; j<arr.length;j++){  
        //将第一个数默认为最小值,将其索引赋值给k,从k索引开始,将后面每个数与k索引对应的值比较,如果值小了,就将其索引赋值给k
			if(arr[j] < arr[k]){
				k = j;
			}
		}

		//遍历完后,k就指向了最小的值,将其与i对应的值交换(也可 以先做个判断,判断k的索引是否有变化,无变化可以不交换)
		temp = arr[k];

		arr[k] = arr[i];

		arr[i] = temp;

	}
}

快速排序、简单插入排序、希尔排序、堆排序、桶排序等。。。

cookie和session的区别与联系?

一、cookie数据存放在客户的浏览器上,session数据存放在服务器上

二、很多浏览器限制站点最多保存20个cookie,单个cookie保存的数据不能超过4k

三、cookie不是很安全,考虑安全应当使用session

四、可以考虑将登录信息等重要信息存放为session,其它信息如果需要保留,可以放在cookie中

五、session会在一定时间内保存在服务器上

六、session会在浏览器关闭或者一段时间内销毁,也可以通过setMaxInactiveInterval(int)方法进行设置,或是通过invalidate()方法强制结束当前会话,cookie可以通过setMaxAge(int)方法设置缓存在客户端的时间

一般情况下,session生成的sessionid都是保存在cookie中

总结:

cookie:在客户端保存数据,不安全。只能保存字符串,且是少量数据

session:在服务器端保存数据,安全。可以保存对象数据,数据无限制

如果客户端禁止 cookie ,session 还能用吗?

可以用,session 只是依赖 cookie 存储 sessionid,如果 cookie 被禁用了,可以使用 url 中添加 sessionid 的方式保证 session 能正常使用。

Get方法和Post方法区别?怎么用java客户端发起post请求?

get请求的数据会附在URL之后,数据不安全,且传输数据量一般限制在2kb。

post请求的数据放在http请求体中,数据安全,且传输数据量无限制。

Post用URLConnection里的OutputStream对象写入数据,服务端用HttpServletRequest的getParameter(key)方法得到属性值value。

servlet的生命周期及常用方法?
  • init()方法: 在servlet的生命周期中,仅执行一次init()方法
  • service()方法: 它是servlet的核心,每当客户请求一个httpservlet对象,该对象的service()方法就要调用,而且传递给这个方法一个”请求”对象和一个”响应”对象作为参数
  • destory()方法: 仅执行一次,在服务器端停止且卸载servlet时执行该方法

解决servlet线程安全

  • 继承SingleThreadModel,消耗服务器内存,降低性能。并且过时,不推荐
  • 尽量避免使用全局变量,推荐
  • 通过使用ThreadLocal
过滤器有哪些作用,以及过滤器的生命周期?

生命周期:每个Filter在tomcat启动时进行初始化,每个Filter只有一个实例对象

Init:在服务器启动时会创建Filter实例

doFilto:这个方法会在用户每次访问“目标资源”时执行

destroy:服务器关闭时销毁Filter对象

作用:

  • 验证客户是否来自可信网络
  • 对客户提交的数据进行重新编码
  • 过滤掉客户的某些不应该出现的词汇
  • 验证用户是否可以登录
  • 验证客户的浏览器是否支持当前的应用
  • 记录系统日志
转发和重定向的区别?

一、重定向是浏览器发送请求并收到响应以后再次向一个新地址发请求;转发是服务器收到请求后为了完成响应转到另一个资源

二、重定向中有两次请求对象,不共享数据;转发只产生一次请求对象且在组件间共享数据

三、重定向后地址栏地址改变,而转发不会

四、重定向的新地址可以是任意地址;转发必须是同一个应用内的某个资源

如何防止表单重复提交?

网络延迟时,重复点击提交按钮,有可能发生重复提交表单问题

解决方案:

一、数据库主键唯一

二、提交成功后重定向

三、使用JavaScript解决,使用标记位,提交后隐藏或不可用提交按钮

使用session解决:

生成唯一的Token(uuid)给客户端,客户端第一次提交时带着这个Token,后台与session中的进行对比

常见的http返回状态码?
  • 100:告诉客户端应继续发送请求

  • 200:请求响应成功

  • 202:请求已被受理还未做出响应

  • 301:永久重定向

  • 302:暂时重定向

  • 400:请求无效,常见的情况是请求参数有误,http头构建错误

  • 404:访问不到资源

  • 500:服务器后端错误

  • 1开头的状态码是消息类型的.

  • 2开头的状态码表示成功.

  • 3开头的状态码表示需要重定向.

  • 4开头的状态码表示请求错误.

  • 5开头的状态码表示服务器错误.

TCP和UDP的区别,HTTP协议?
  • TCP协议提供安全可靠的网络传输服务,它是一种面向连接的服务.类似于打电话,必须先拨号.双方建立一个传递信息的通道传输
  • UDP协议是一种数据报协议,它传输的数据是分组报文,它是无连接的,不需要和目标通信方建立连接,类似于写信,所以它的传输不保证安全可靠.但适合大数据量的传输
  • HTTP协议是超文本传输协议,是一种相对于TCP来说更细致的协议,TCP以及UDP协议规范的是网络设备之间的通信规范,HTTP实在TCP协议的基础上针对用户的协议,用户服务具体体现在应用程序之间的交互,比如javaweb中客户端服务端体系就要用http协议来规范通信

TCP和UDP在开发中很少见到,但是网络底层都有他们的影子,正常的会话级别的服务:如客户端服务器体系底层就说基于TCP协议.而邮件发送,短信发送等底层使用的是UDP协议

HTTP协议,客户端/服务器体系的程序都使用HTTP协议来规范通信

tcp为什么要三次握手,两次不行吗?为什么?

如果采用两次握手,那么只要服务器发出确认数据包就会建立连接,但由于客户端此时并未响应服务器端的请求,那此时服务器端就会一直在等待客户端,这样服务器端就白白浪费了一定的资源。若采用三次握手,服务器端没有收到来自客户端的再此确认,则就会知道客户端并没有要求建立请求,就不会浪费服务器的资源

tomcat 如何调优,涉及哪些参数?

硬件上选择,操作系统选择,版本选择,jdk选择,配置jvm参数,配置connector的线程数量,开启gzip压缩,trimSpaces,集群等
可参考:http://blog.csdn.net/lifetragedy/article/details/7708724

数据库

事务的特性和隔离级别?

事务的特性:

  • 原子性(Atomicity)

    ​ 原子性指事务是不可分割的工作单位,事务中的操作要么都发生,要么都不发生

  • 一致性(Consistency)

    ​ 事务必须使数据库从一个一致性状态变换到另外一个一致性状态

  • 隔离性(Isolation)

    ​ 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

  • 持久性(Durability)

    ​ 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

隔离级别:

  • 未提交读read uncommitted

    ​ 会发生 脏读、不可重复读、虚读

  • 已提交读read committed //Oracle SQL Server(系统事务)

    ​ 会发生不可重复读和虚读

  • 重复读repeatable read //Mysql

    ​ 会发生虚读

  • 串行化serializable

    ​ 解决所有

脏读:脏读是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。例如:小明给小红转1000元钱,小红收到后,小明回滚了事务,此时小红的1000元不存在,形成了虚读。

不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。例如:张秘书看了一眼今天的营业额是100万,然后走去老总办公室跟老板说:“今天营业额是100万哦”。恰好此时,李总监又拿到了50万订单,更新了营业额。老板又恰好想亲自查询一遍营业额,发现营业额是150万,老板很生气。这就是不可重复读。

幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。

InnoDB,MyISAM存储引擎特性?

InnoDB

InnoDB存储引擎是Mysql的默认存储引擎。InnoDB存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全。 但是对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引。

MyISAM

MyISAM 不支持事务、也不支持外键,其优势是访问的速度快,对事务的完整性没有要求或者以SELECT、INSERT 为主的应用基本上都可以使用这个引擎来创建表。

Char和Varchar的区别?
  • CHAR和VARCHAR类型在存储和检索方面有所不同
  • CHAR列长度固定为创建表时声明的长度,长度值范围是1到255
  • 当CHAR值被存储时,它们被用空格填充到特定长度,检索CHAR值时需删除尾随空格
SQL中用到了小于号大于号怎么写?

小于号 &lt

大于号 &gt

delete、drop、truncate区别?

truncate 和 delete只删除数据,不删除表结构 ,drop删除表结构,并且释放所占的空间。

删除数据的速度,drop> truncate > delete

delete属于DML语言,需要事务管理,commit之后才能生效。drop和truncate属于DDL语言,操作立刻生效,不可回滚。

使用场合:

当你不再需要该表时, 用 drop;

当你仍要保留该表,但要删除所有记录时, 用 truncate;

当你要删除部分记录时(always with a where clause), 用 delete.

mysql 中 in 和 exists 区别?

**mysql中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。**一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

  • 如果查询的两个表大小相当,那么用in和exists差别不大。
  • 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in:
  • not in 和not exists如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。
  • EXISTS只返回TRUE或FALSE,不会返回UNKNOWN。
  • IN当遇到包含NULL的情况,那么就会返回UNKNOWN。
数据库的三范式是什么?

第一范式:数据库表的每一列都是不可再被拆分

第二范式:在第一范式的基础上,非主键完全依赖主键

第三范式:在第二范式的基础上,非主键只依赖于主键

Sql优化?
  • 通过慢查询日志去寻找,哪些sql执行效率低

  • 使用explain分析低效率的sql执行计划 explain+sql查询一些参数

  • 没有索引:

    ​ 针对查询的列创建索引,提高查询效率,但是索引太多,mysql也会出现选择困难,所以建立索引要有效,无效的索引需要删除。

  • 索引失效:

    ​ 尽量选择较小的列

    ​ 将where中用的比较频繁的字段建立索引

    ​ select子句中避免使用‘*’

    ​ 避免在索引列上使用计算,notin和<>等操作

    ​ 当只需要一行数据的时候使用limit 1

    ​ 保证表单数据不超过200w,适时分割表

    ​ 针对查询较慢的语句,可以使用explain来分析该语句具体的执行情况

    ​ 避免查询时判断null,否则可能会导致全表扫描,无法使用索引;

    ​ 避免like查询,否则可能导致全表扫描,可以考虑使用全文索引

    ​ 能用union all的时候就不用union,union过滤重复数据要耗费更多的CPU资源

  • 数据量太大:

    ​ 分页查询优化

    ​ 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

    ​ Select * from table t where t.id in(select id from table order by id limit 100000,10) ;

    ​ 对于主键自增的表,可以把limit查询缓存某个位置的查询

    ​ Select * from table where id > 100000 limit 10;

  • 分库分表:

    ​ 使用MyCat中间件实现。

  • 全文索引技术:

    ​ ElasticSearch , solr

  • 非关系型数据库:

    ​ 不需要像关系型数据库一样维护表于表之间的关系,而是使用json这种灵活多变的形式,效率比Mysql提高很多

索引是什么?MySQL为什么使用B+树,而不是使用其他?B+树的特点

索引是帮助MySQL高效获取数据的数据结构。索引会影响where后面的查找,和order by 后面的排序。
B+Tree索引(平衡多路查找树)
是B-Tree的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。B-Tree需要获取所有节点,相比之下B+Tree效率更高。B+树索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问;

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

二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。

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

创建索引时需要注意什么?

唯一、不为空、经常被查询的字段、字段越小越好 的字段适合建索引。

MySQL数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?

a. 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。
b. 选择合适的表字段数据类型和存储引擎,适当的添加索引。
c. mysql库主从读写分离。
d. 找规律分表,减少单表中的数据量提高查询速度。
e. 添加缓存机制,比如memcached,apc等。
f. 不经常改动的页面,生成静态页面。
g.书写高效率的SQL。

各种索引的概念:索引,主键,唯一索引,联合索引,索引分类

索引分类: Mysql常见索引有:主键索引、唯一索引、普通索引、全文索引、组合索引.{按聚集分类:聚集索引和非聚集索引}
索引( 普通索引):不允许有空值,指字段 唯一、不为空值 的列
唯一索引:唯一索引可以保证数据记录的唯一性,在为这个数据列创建索引的时候就应该用关键字UNIQUE把它定义为一个唯一索引。唯一索引允许空值( 索引列的所有值都只能出现一次,即必须唯一)
主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。
外键:表的外键是另一表的主键,
组合索引:索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引。
CREATE INDEX index_name ON table_name (column_list)
全文索引: 全文索引的索引类型为FULLTEXT, 可以在VARCHAR或者TEXT类型的列上创建。
单列索引与多列索引

你们项目中使用到的数据库是什么?你有涉及到关于数据库到建库建表操作吗?数据库创建表的时候会有哪些考虑呢?

项目中使用的是MySQL 数据库, 数据库创建表时要考虑

a、大数据字段最好剥离出单独的表,以便影响性能

b、使用varchar,代替 char,这是因为 varchar 会动态分配长度,char 指定为 20,即时你存储字符“1”,它依然是 20 的长度

c、给表建立主键,看到好多表没主键,这在查询和索引定义上将有一定的影响

d、避免表字段运行为 null,如果不知道添加什么值,建议设置默认值,特别 int 类型,比如默认值为 0,在索引查询上,效率立显。

e、建立索引,聚集索引则意味着数据的物理存储顺序,最好在唯一的,非空的字段上建立, 其它索引也不是越多越好,索引在查询上优势显著,在频繁更新数据的字段上建立聚集索引, 后果很严重,插入更新相当忙。

mysql中哪些情况下可以使用索引,哪些情况不能使用索引?mysql索引失效的情形有哪些?

使用索引:where条件后涉及到的列;最常用到的列要创建索引。

不能使用索引:数据唯一性差;频繁更新的字段不要使用索引;where 子句里对索引列使用不等于(<>),使用索引效果一般

索引失效:如果条件中有or;like以%开头;where中索引列有运算;where中索引列使用了函数;

sql中查询语句的优化是怎么做的?
  • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
  • 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
  • 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  • 尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
  • in 和 not in 也要慎用,否则会导致全表扫描
  • 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
  • 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
  • 任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
  • 尽可能的使用 varchar/nvarchar 代替 char/nchar
  • 使用join代替子查询。
  • 使用联合查询union代替手动创建临时表
数据库优化

分为索引优化和sql优化。

总结一下就是where子句不能使用or,in,not in,!=,不能对null值做判断,不能对索引列进行运算,不能对索引列做函数,like查询不能使用%开头,不能使用select *。

应该使用between and 来代替in,只查询一条数据的时候要用limit 1,使用join代替子查询,在经常查询的列上创建索引,使用varchar来代替char,使用联合查询代替手动创建临时表。

了解过数据库的表级锁和行级锁吗?乐观锁和悲观锁有哪些了解?

MySQL 的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM 和 MEMORY 存储引擎采用的是表级锁(table-level locking);InnoDB存储引擎既支持行级锁( row-level locking),也支持表级锁,但默认情况下是采用行级锁。

MySQL 主要的两种锁的特性可大致归纳如下:

表级锁: 开销小,加锁快;不会出现死锁(因为 MyISAM 会一次性获得 SQL 所需的全部锁);锁定粒度大,发生锁冲突的概率最高,并发度最低。

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

乐观锁:通过 version 版本字段来实现

悲观锁:通过 for update 来实现

分库分表方案?

一开始上来就是32个库,每个库32个表,1024张表这个分法,

基本上国内的互联网肯定都是够用了,无论是并发支撑还是数据量支撑都没问题

如果每个库正常承载的写入并发量是1000,那么32个库就可以承载32 * 1000 = 32000的写并发,如果每个库承载1500的写并发,32 * 1500 = 48000的写并发,接近5万/s的写入并发,前面再加一个MQ,削峰,每秒写入MQ 8万条数据,每秒消费5万条数据。1024张表,假设每个表放500万数据,在MySQL里可以放50亿条数据。每秒的5万写并发,总共50亿条数据,对于国内大部分的互联网公司来说都够了。

此方案最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到1024个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表么。

服务器升级流程

  • 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库 * 32表,对于大部分公司来说,可能几年都够了
  • 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
  • 扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,4台服务器,扩到8台服务器,16台服务器,由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移,比较便捷,我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址,重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务
MySQL 索引是怎么实现的?

索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。

具体来说 MySQL 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是 B+ 树实现的,B+ 树的搜索效率,可以到达二分法的性能,找到数据区域之后就找到了完整的数据结构了,所有索引的性能也是更好的。

Mysql支持的索引类型?
  • index ---- 普通索引,数据可以重复,没有任何限制。
  • unique ---- 唯一索引,要求索引列的值必须唯一,但允许有空值;如果是组合索引,那么列值的组合必须唯一。
  • primary key ---- 主键索引,是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值,一般是在创建表的同时创建主键索引。
  • 组合索引 ---- 在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。
  • fulltext ---- 全文索引,是对于大表的文本域:char,varchar,text列才能创建全文索引,主要用于查找文本中的关键字,并不是直接与索引中的值进行比较。
接口和抽象类的区别?
  • 抽象类可以有构造方法,接口中不能有构造方法。

  • 抽象类中可以有普通成员变量,接口中没有普通成员变量

  • 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

  • 抽象类中的抽象方法的访问类型可以是public,protected和默认类型,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。

  • 抽象类中可以包含静态方法,从Java8开始,接口也是可以有静态方法的,接口增加了对 default method的支持。Java9之后增加了对private default method的支持。也就是说在Java8中接口default方法和static方法都可以有方法体

  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

  • 一个类可以实现多个接口,但只能继承一个抽象类。

session和cookie有什么区别?

**Cookie和Session出现的原因:**由于http 协议是无状态的,服务器无法确定这次请求和上次的请求是否来自同一个客户端。利用session和cookie可以让服务器知道不同的请求是否来自同一个客户端。

Cookies是保存在客户端上的一小段文本信息,其主要内容有名字,值,过期时间,路径等等。会话cookie:不设置过期时间,只要关闭浏览器窗口cookie就消失了。会话cookie不保存在硬盘上,保存在内存里。持久cookie:设置过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
Session代表服务器与浏览器的一次会话过程,Session是一种服务器端的机制,Session 对象用来存储特定用户会话所需的信息。

区别

1、数据存储位置:cookie数据存放在客户的浏览器上,session数据放在服务器上。Session由服务端生成,保存在服务器的内存、缓存、硬盘或数据库中。

2、安全性:cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。

3、服务器性能:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。

4、数据大小:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

5、信息重要程度:可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

索引失效问题?
  • 如果条件中有or,即使其中有部分条件带索引也不会使用(这也是为什么尽量少用or的原因),例子中user_id无索引。注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
  • like查询是以%开头
  • 存在索引列的数据类型隐形转换,则用不上索引,比如列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
  • where 子句里对索引列上有数学运算
  • where 子句里对有索引列使用函数
  • 如果mysql估计使用全表扫描要比使用索引快,则不使用索引
左连接 ,右连接,内连接和全外连接的4者区别?
  • left join (左连接):返回包括左表中的所有记录和右表中连接字段相等的记录。
  • right join (右连接):返回包括右表中的所有记录和左表中连接字段相等的记录。
  • inner join (等值连接或者叫内连接):只返回两个表中连接字段相等的行。
  • full join (全外连接):返回左右表中所有的记录和左右表中连接字段相等的记录。
一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 MySQL 数据库,又插入了一条数据,此时 id 是几?

表类型如果是 MyISAM ,那 id 就是 8。

表类型如果是 InnoDB,那 id 就是 6。

InnoDB 表只会把自增主键的最大 id 记录在内存中,所以重启之后会导致最大 id 丢失。

说一下 MySQL 的行锁和表锁?

MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。

  • 表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
  • 行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。
说一下乐观锁和悲观锁?

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。

悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。

数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。

高并发下,如何做到安全的修改同一行数据?

使用悲观锁 悲观锁本质是当前只有一个线程执行操作,结束了唤醒其他线程进行处理。也可以缓存队列中锁定主键。

数据库会死锁吗,举一个死锁的例子?

会,一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。

**解决方法:**这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。

聚集索引和非聚集索引的区别?

聚簇索引就是索引和记录紧密在一起。
非聚簇索引 索引文件和数据文件分开存放,索引文件的叶子页只保存了主键值,要定位记录还要去查找相应的数据块。

innodb为什么要用自增id作为主键?

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页
如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置, 频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE(optimize table)来重建表并优化填充页面。

说说分库与分表设计

分库与分表的目的在于,减小数据库的单库单表负担,提高查询性能,缩短查询时间。
通过分表,可以减少数据库的单表负担,将压力分散到不同的表上,同时因为不同的表上的数据量少了,起到提高查询性能,缩短查询时间的作用,此外,可以很大的缓解表锁的问题。
分表策略可以归纳为垂直拆分和水平拆分。
水平分表:取模分表就属于随机分表,而时间维度分表则属于连续分表。
如何设计好垂直拆分,我的建议:将不常用的字段单独拆分到另外一张扩展表. 将大文本的字段单独拆分到另外一张扩展表, 将不经常修改的字段放在同一张表中,将经常改变的字段放在另一张表中。
对于海量用户场景,可以考虑取模分表,数据相对比较均匀,不容易出现热点和并发访问的瓶颈。

库内分表,仅仅是解决了单表数据过大的问题,但并没有把单表的数据分散到不同的物理机上,因此并不能减轻 MySQL 服务器的压力,仍然存在同一个物理机上的资源竞争和瓶颈,包括 CPU、内存、磁盘 IO、网络带宽等。

分库与分表带来的分布式困境与应对之策
数据迁移与扩容问题----一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。
分页与排序问题----需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。
分布式全局唯一ID—UUID、GUID等

与面试官对线:

分库分表可以减小单表单库的压力,提高查询性能,分表减少查询时间。也可以缓解表锁的问题。分库是解决数据库压力,库内分表是解决了表单数据过大的问题。

分表:就是将不常用的字段单独拆分到同一张表中,将经常改变的字段放假另一张表中。

框架

Spring

什么是spring?

spring是一个轻量级的,开源的,模块化的,一站式业务层框架,它能够整合其他主流框架

spring的实质就是实现了工厂模式的工厂类,在配置文件中,通过添加标签,来创建实例对象

spring的核心?

spring的核心分别是IOCAOP

什么是控制反转(IOC)?什么是依赖注入(DI)?

**控制反转(Inversion of Control)**不是一种技术,是一种思想,是面向对象编程中的一种设计原则。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

依赖注入(Dependency Injection)由容器动态的将某个依赖关系注入到组件之中依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

依赖注入的三种方式分别是:接口注入,构造注入,setter方法注入

AOP是指面向切面编程,就是在不修改代码的前体下,对程序进行加强,例如加入日志,权限判断,异常处理等等

AOP的底层使用的是代理技术,分别为jdkProxy动态代理和CGLIB动态代理,jdk动态代理是需要被代理的类实现接口的,而CGLIB则不需要被代理的类实现接口

关于动态代理,它的底层实现是利用java反射来实现

Proxy主要使用 Proxy.newProxyInstance方法

CGLIB主要使用 Enhancer.create方法

MethodHandler主要使用 MethodHandles.lookup().findVirtual().bindTO()方法

静态代理:是实现已经编写好,在程序运行前代理类的.class文件就已经存在

动态代理:在程序运行时运用反射机制动态创建而成

静态代理虽然执行效率要比动态代理高,但需要事先编写好代理类的java文件,工程量大,由于代理类和委托类实现了相同接口,会出现大量重复代码,而且后期不易于维护;

而动态代理则是随着程序的运行而创建代理对象,比较灵活,但是动态代理的底层是反射机制,比较消耗资源

AOP能做什么:

  • 降低模块的耦合度
  • 使系统容易扩展
  • 避免修改业务代码,避免引入重复代码,更好的代码复用

AOP怎么用:

  • 前置通知:某方法调用前发出通知
  • 后置通知:某方法完成之后发出通知
  • 返回后通知:方法正常返回后,调用通知.在方法,正常退出发出通知
  • 异常通知:抛出异常后通知:在方法抛出异常退出时执行的通知.在方法调用时,异常退出发出通知
  • 环绕通知:通知包裹在被通知的方法的周围
spring 有哪些主要模块?
  • spring core:框架的最基础部分,提供 ioc 和依赖注入特性。
  • spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法。
  • spring dao:Data Access Object 提供了JDBC的抽象层。
  • spring aop:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
  • spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。
  • spring Web mvc:spring 中的 mvc 封装包提供了 Web 应用的 Model-View-Controller(MVC)的实现。
spring的scope有几种?

在Spring 2.0之前,有singletonprototype两种;

在Spring 2.0之后,为支持web应用的ApplicationContext,增强另外三种:requestsessionglobal session类型,它们只适用于web程序,通常是和XmlWebApplicationContext共同使用。

singleton类型的bean定义从容器启动到第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活,典型单例模式,如同servlet在web容器中的生命周期

spring容器在进行输出prototype的bean对象时,会每次都重新生成一个新的对象给请求方,虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不在拥有当前对象的引用,请求方需要自己负责当前对象后继生命周期的管理工作,包括该对象的销毁。

  • singleton(单例模式)

    IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例

  • prototype(原型模式)

    IOC容器仅创建多个Bean实例,IOC容器每次返回的是一个新的实例

  • request(HTTP请求)

    该属性仅对HTTP请求产生作用,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext

  • session(会话)

    该属性仅用于HTTP Session,同一个session共享一个Bean实例.不同session使用不同的实例

  • global-session(全局会话,在spring5.x中已移除)

    该属性仅用于HTTP Session,同session作用域不同的时候,所有session共享一个Bean实例

Bean的生命周期

实例化 -> 属性赋值 -> 初始化 -> 销毁

Spring框架实现实例化和依赖注入的方式?

实例化:

一、构造器实例化Bean

二、静态工厂方式实例化Bean

三、实例工厂方式实例化Bean

依赖注入:

一、基于构造函数的注入

二、基于set方法的注入

三、接口注入

四、基于注解的依赖注入

springmvc执行流程?
  • 用户发送请求至前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping处理器映射器
  • 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  • DispatcherServlet通过HandlerAdapter处理器适配器调用处理器
  • HandlerAdapter执行处理器(handler,也叫后端控制器)
  • Controller执行完成返回ModelAndView
  • HandlerAdapter将handler执行结果ModelAndView返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  • ViewReslover解析后返回具体View对象
  • DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
  • DispatcherServlet响应用户
SpringMVC 怎么样设定重定向和转发的?

在返回值前面加”forward:” 就可以让结果转发。 ”forward:user.do?name=method4”

在返回值前面加”redirect:”就可以让返回值重定向

如果在拦截请求中,我想拦截 get 方式提交的方法,怎么配置? 可以在@RequestMapping 注解里面加上 method=RequestMethod.GET

SpringMvc 里面拦截器是怎么写的?

一种是实现接口,另外一种是继承适配器类,然后在 SpringMvc 的配置文件中配置
拦截器即可。

spring bean 的生命周期
  1. Spring 容器根据配置中的 bean 定义中实例化 bean。
  2. Spring 使用依赖注入填充所有属性,如 bean 中所定义的配置。
  3. 如果 bean 实现 BeanNameAware 接口,则工厂通过传递 bean 的 ID 来调用 setBeanName()。
  4. 如果 bean 实现 BeanFactoryAware 接口,工厂通过传递自身的实例来调用 setBeanFactory()。
  5. 如果存在与 bean 关联的任何 BeanPostProcessors,则调用 postProcessBeforeInitialization() 方法。
  6. 如果为 bean 指定了 init 方法( 的 init-method 属性),那么将调用它。
  7. 最后,如果存在与 bean 关联的任何 BeanPostProcessors,则将调用postProcessAfterInitialization() 方法。
  8. 如果 bean 实现 DisposableBean 接口,当 spring 容器关闭时,会调用 destory()。
  9. 如果为 bean 指定了 destroy 方法( 的 destroy-method 属性),那么将调用它
简述 Spring IoC 的实现机制?

简单来说,Spring 中的 IoC 的实现原理,就是工厂模式反射机制

Spring 中有多少种 IOC 容器?

Spring 提供了两种IOC 容器,分别是 BeanFactory、ApplicationContext 。

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

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

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

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

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

BeanFactory 最常用的是 XmlBeanFactory 。它可以根据 XML 文件中定义的内容,创建相应的 Bean。

请介绍下常用的 ApplicationContext 容器?

XmlWebApplicationContext :由 Web 应用的XML文件读取上下文。例如我们在 Spring MVC 使用的情况。

Spring中涉及的设计模式
  • 代理模式 — 在 AOP 和 remoting 中被用的比较多。
  • 单例模式 — 在 Spring 配置文件中定义的 Bean 默认为单例模式。
  • 模板方法 — 用来解决代码重复的问题。比如 RestTemplate、JmsTemplate、JdbcTemplate 。
  • 依赖注入 — 贯穿于 BeanFactory / ApplicationContext 接口的核心理念。
  • 工厂模式 — BeanFactory 用来创建对象的实例。
  • 适配器模式 — SpringMVC中的适配器HandlerAdatper。
  • 观察者模式 — spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现。
Spring 的 IOC支持哪些功能

Spring 的 IoC 设计支持以下功能:

  • 依赖注入
  • 依赖检查
  • 自动装配
  • 支持集合
  • 指定初始化方法和销毁方法
  • 支持回调某些方法(但是需要实现 Spring 接口,略有侵入)
Spring框架中的单例bean是线程安全的吗?

不是,Spring框架中的单例bean不是线程安全的。

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

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

  • 有状态就是有数据存储功能。
  • 无状态就是不会保存数据。
Spring如何处理线程并发问题?

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。

Spring以及SpringMVC常用注解?

@Component:用于标记在一个类上,表示当前类是spring的一个组件,是ioc的一个容器.他有三个衍生注解:@Controller、@Service、@Repository

@Controller:用于标记在一个类上,代表这个类是控制层组件.

@Service:用于标记在一个类上,代表这个类是业务层组件.

@Repository:用于标记在一个类上,代表这个类是数据访问层组件.

@RequestMapping:是一个用于处理请求地址映射的注解,可用于类或方法上.用于类上,表示类中所有响应请求的方法都是以该地址作为父路径

@RequestParam:用于将指定的请求参数赋给方法中的形参.

@PathVariable:可以获取URL中的动态参数.

@RequestBody:用于读取request请求的body部分数据.

@ResponseBody:用于将controller方法返回的对象,用流响应给客户端.

@RestController:@Controller+@ResponseBody,用于标记在一个类上.

@Transactional:写在类上用于指定当前类中的方法支持事务,写在方法上表示当前的方法支持事务

@Required 注解有什么作用?

这个注解表明bean的属性必须在配置的时候设置。如果没有设置,则抛异常BeanInitializationException。

@Autowired和@Resource之间的区别?

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

@Autowired和@Resource之间的区别

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

当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。

@RequestMapping 的作用是什么?

将 http 请求映射到相应的类/方法上。

Spring支持的事务管理类型, spring 事务实现方式有哪些?

编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。

声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。

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

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

说一下Spring的事务传播行为?

spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。

① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

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

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

④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

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

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

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

什么是AOP?

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

Spring AOP and AspectJ AOP 有什么区别?AOP 有哪些实现方式?

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

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

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

JDK动态代理和CGLIB动态代理的区别?

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

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

(1)切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。

(2)连接点(Join point):指方法,在Spring AOP中,一个连接点 总是 代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

(3)通知(Advice):在AOP术语中,切面的工作被称为通知。

(4)切入点(Pointcut):切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

(5)引入(Introduction):引入允许我们向现有类添加新方法或属性。

(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。它通常是一个代理对象。也有人把它叫做 被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。

(7)织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入:

Spring通知有哪些类型?
  1. 前置通知(Before):在目标方法被调用之前调用通知功能;
  2. 后置通知(After-returning ):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  3. 返回通知(After):在目标方法成功执行之后调用通知;
  4. 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
@Value 注解的作用是什么?

基于 @Value 的注解可以读取 properties 配置文件

切面是如何织入的?

InvocationHandler是JDK动态代理的核心,生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法。

Spring的异步方法?

背景:前几周,公司的一个项目需要发送邮件,起初并没有考虑时间的影响,就未采用同步的方式进行发送。到了测试环境,发现需要发送邮件的地方耗时过久,因此研究了一下spring的异步方法支持—@Async。

@Async用法

类: 如果该注解应用在类上,表示该类所有方法是异步的;

方法: 如果该注解应用在方法上,表示该方法是异步的。

注意: @Async、@Transactional等注解采用的是代理模式,如果在同一个类的某个方法上,调用本类带有@Async等注解的方法是,该注解会失效。

Spring的事务失效?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQ14oCQG-1598187760622)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200821100043097.png)]

Spring MVC的异常处理 ?

在 Spring MVC的Web应用程序中,可以存在多个实现了HandlerExceptionResolver的异常处理类,他们的执行顺序,由其order属性决定, order值越小,越是优先执行, 在执行到第一个返回不是null的ModelAndView 的Resolver时,不再执行后续的尚未执行的Resolver的异常处理方法。

Springboot

为什么要用 spring boot?
  • 配置简单
  • 独立运行
  • 自动装配
  • 无代码生成和 xml 配置
  • 提供应用监控
  • 易上手
  • 提升开发效率
spring boot 与spring的比较?

部署方面:spring boot一键启动 java -jar 不需要tomcat,因为springboot底层有tomcat

监控方面:springboot有Spring-boot-start-actuator:可以查看属性配置、线程工作状态、环境变量、JVM 性能监控

配置方面:springboot省略了spring大量的xml配置,采用约定大于配置的思想,只需要写一个application.xml

什么是热部署?

所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。

spring boot 有哪些方式可以实现热部署?
  • 使用 devtools 启动热部署,添加 devtools 库,在配置文件中把 spring. devtools. restart. enabled 设置为 true
  • 使用 Intellij Idea 编辑器,勾上自动编译或手动重新编译。
SpringBoot 的常用注解有哪些?
  • @SpringBootApplication包含**@Configuration**、@EnableAutoConfiguration@ComponentScan通常用在主类上
  • @ComponentScan:组件扫描。个人理解相当于,如果扫描到有@Component ,@Controller ,@Service等这些注解的类,则把这些类注册为bean
  • @Configuration:指出该类是 Bean 配置的信息源,相当于XML中的,一般加在主类上
  • @Bean:相当于XML中的,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理
  • @Import:用来导入其他配置类
  • @ImportResource:用来加载xml配置文件
  • @ControllerAdvice:包含@Component。可以被扫描到。统一处理异常。控制器增强器
  • @ExceptionHandler(Exception.class):用在方法上面表示遇到这个异常就执行以下方法
SpringBoot 有哪几种读取配置的方式?

方式一: 通过@Value("${spring.datasource.url}")这样的方式读取

**方式二:**通过@ConfigurationProperties(prefix = “spring.datasource”)这种写法系统会依据prefix前缀自动注入配置数据到数据实体变量,这种方式不错,但是存在缺陷,我们编写的PropertyPlaceholderConfigurer扩展字段会无效,所以如果只是单纯的读取配置而不需要额外操作时可使用这方式最简单

方式三: 我们可以直接注入Environment对象示例并读取properties对象属性environment.getProperty(“spring.datasource.database”);与方式一的本质差不多,我们不需要编写对应字段的模型对象,但是对于程序可阅读性不好友,复用率不高

方式四: 通过系统启动时候初始化Listener,使用PropertiesLoaderUtils工具类读取指定配置文件并获得Properties配置对象,我们可以随时随地使用该对象的属性,这种方式比较少用,针对比较自定义的配置数据可使用该方式

SpringBoot 配置加载顺序?

在不指定要被加载文件时,默认的加载顺序:由里向外加载,所以最外层的最后被加载,会覆盖里层的属性,加载顺序依次为:

  • 位于与jar包同级目录下的config文件夹,
  • 位于与jar包同级目录下
  • idea 环境下,resource文件夹下的config文件夹
  • idea 环境下,resource文件夹下
Spring Boot 如何定义多套不同环境配置?

一、Spring Boot 环境设置机制

spring.profiles.active 属性可以为我们指定当前设置的环境,以此来选择我们的配置文件。例如我们有配置文件

  • application.yml
  • application-dev.yml
  • application-test.yml
  • application-prod.yml

当执行 java -jar xxx.jar --spring.profiles.actvie=test 此时,系统将启用 application.yml 和 application-test.yml配置文件。

当执行 java -jar xxx.jar --spring.profiles.actvie=prod 此时,系统将启用 application.yml 和 application-prod.yml 配置文件。

二、配置多环境

正如第一点所述,我们配置不同的配置文件

application.yml

application-dev.yml(开发环境)

application-test.yml(测试环境)

application-uat.yml(预发布环境)

application-prod.yml(生产环境)

三、指定环境

1 、在 cmd 命令中指定

java -jar xxx.jar --spring.profiles.actvie=dev

2 、在 application.yml 中指定

spring: profiles: active: dev

springcloud如何实现服务的注册和发现?

服务在发布时 指定对应的服务名(服务名包括了IP地址和端口) 将服务注册到注册中心(eureka或者zookeeper)

这一过程是springcloud自动实现 只需要在main方法添加@EnableDisscoveryClient 同一个服务修改端口就可以启动多个实例

调用方法:传递服务名称通过注册中心获取所有的可用实例 通过负载均衡策略调用(ribbon和feign)对应的服务

Ribbon和Feign的区别

Ribbon和Feign都是用于调用其他服务的,不过方式不同。

1.启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeignClients。

2.服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。

3.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。

springcloud断路器的作用Hystrix?

当一个服务调用另一个服务由于网络原因或者自身原因出现问题时 调用者就会等待被调用者的响应 当更多的服务请求到这些资源时,导致更多的请求等待,这样就会发生连锁效应(雪崩效应)断路器就是解决这一问题

断路器有:

  • 完全打开:一定时间内达到一定的次数无法调用,并且多次检测没有恢复的迹象,断路器完全打开,那么下次请求就不会请求到该服务
  • 半开:短时间内有恢复迹象断路器会将部分请求发给该服务,当能正常调用时断路器关闭
  • 关闭:当服务一直处于正常状态能正常调用断路器关闭
什么是spring cloud?

spring提供了一个与外部系统集成的系统。它是一个敏捷的框架,可以短平快构建应用程序。与有限数量的数据处理相关联,它在微服务体系结构中起着非常重要的作用。

spring的核心特性:版本化/分布式布置、服务注册与发现、服务与服务之间的调用、路由、断路器和负载均衡、分布式消息传递

为什么考虑用spring cloud?

Spring Cloud来源于Spring,质量、稳定性、持续性都可以得到保证

Spirng Cloud天然支持Spring Boot,更加便于业务落地。

Spring Cloud是Java领域最适合做微服务的框架。

相比于其它框架,Spring Cloud对微服务周边环境的支持力度最大。

对于中小企业来讲,使用门槛较低。

springcloud与dubbo的区别?

1、dubbo由于是二进制的传输,占用带宽会更少

2、springCloud是http协议传输,带宽会比较多,同时使用http协议一般会使用JSON报文,消耗会更大

3、dubbo的开发难度较大,原因是dubbo的jar包依赖问题,很多大型工程无法解决

4、springcloud的接口协议约定比较自由且松散,需要有强有力的行政措施来限制接口无序升级

5、dubbo的注册中心可以选择zk,redis等多种,springcloud的注册中心只能用eureka或者自研

Mybatis

Mybatis中使用#和$书写占位符有什么区别?
  • #{}传参能防止sql注入
  • ${}传参是字符串拼接
如果有些场景非要用$拼接sql,我们怎么防止sql注入?

如果我们order by语句后用了${},那么不做任何处理的时候是存在SQL注入危险的。你说怎么防止,那我只能悲惨的告诉你,你得手动处理过滤一下输入的内容。如判断一下输入的参数的长度是否正常(注入语句一般很长),更精确的过滤则可以查询一下输入的参数是否在预期的参数集合中。

1、(简单又有效的方法)PreparedStatement

2、使用正则表达式过滤传入的参数

3、字符串过滤

4、jsp中调用该函数检查是否包函非法字符

mybatis里foreach可以用在什么场景,类似这样的标签还有?

spu表内有sku表集合

动态SQL?

所谓SQL的动态和静态,是指SQL语句在何时被编译和执行,二者都是用在SQL嵌入式编程中的

SQL语句的主体结构,在编译时尚无法确定,只有等到程序运行起来,在执行的过程中才能确定,这种SQL叫做动态SQL

静态SQL语句的编译是在应用程序运行前进行的,编译的结果会存储在数据库内部

程序运行时,数据库将直接执行编译好的SQL语句,降低运行时的开销.

MyBatis中用于实现动态SQL的元素主要有:if、where、foreach

<where>
<if test="username != null">
	and username = #{username}
</if>
</where>
Mapper动态代理规范
  • xml映射文件中的namespace与mapper接口的全类名相同
  • mapper接口方法名和xml映射文件中定义的每个statement的id相同
  • mapper接口方法的输入参数类型和xml映射文件中定义的每个sql的parameterType的类型相同
  • mapper接口方法的输出参数类型和xml映射文件中定义的每个sql的resultType的类型相同
  • mybatis中的mapper动态代理是不支持方法重载的dao接口里的方法,因为是全类名+方法名的保存和寻找策略
  • mapper接口的工作原理是JDK动态代理,mybatis运行时就是用JDK动态代理为mapper接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行mappedStatement所代表的sql,然后将sql执行结果返回
说一下 MyBatis 的一级缓存和二级缓存?

一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,它的声明周期是和 SQLSession 一致的,有多个 SQLSession 或者分布式的环境中数据库操作,可能会出现脏数据。当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认一级缓存是开启的。

二级缓存:也是基于 PerpetualCache 的 HashMap 本地缓存,不同在于其存储作用域为 Mapper 级别的,如果多个SQLSession之间需要共享缓存,则需要使用到二级缓存,并且二级缓存可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态)。

MyBatists的好处?
  • 避免创建、释放频繁造成系统资源浪费 :在 SqlMapConfig.xml 中配置数据链接池,使用连接池管理数据库链接。
  • 将 Sql 语句配置在 XXXXmapper.xml 文件中与 java 代码分离。
  • Mybatis 自动将 java 对象映射至 sql 语句。因为向 sql 语句传参数麻烦,因为 sql 语句的 where 条件不一定,可能多也可能少,占位符需要和参数一一对应
  • Mybatis 自动将 sql 执行结果映射至 java 对象。因为 对结果集解析麻烦, sql 变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成 pojo 对象解析比较方便

Rabbit MQ

RabbitMQ是什么?

MQ 全称为 Message Queue,即消息队列,RabbitMQ 是由 Erlang 语言开发,基于 AMQP(Advanced Message Queue Protocol ,高级消息队列协议)协议实现的消息队列。

AMQP是什么?

AMQP是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开发标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端中间件不同产品,不同的开发语言等条件的限制

简单的说,AMQP是一个高级队列消息协议,基于此协议的客户端与中间件不受产品与开发语言的限制。

为什么使用MQ

  • 异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。
  • 应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
  • 流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。
  • 日志处理 - 解决大量日志传输。
  • 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

详细回答:

起到了解耦,异步处理,削峰填谷的作用。以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。

RabbitMQ的特点

  • 可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  • 灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
  • 消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
  • 高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  • 多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
  • 多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
  • 管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
  • 跟踪机制(Tracing) 如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
  • 插件机制(Plugin System) RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

市场上有哪些MQ

ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ、Redis

RabbitMQ的基本概念

  • Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到 MQ

  • Consumer:消息消费者,即消费方客户端,接收 MQ 发的消息

  • Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过滤

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

  • Broker:消息队列服务进程,此进程包括两个部分:Exchange 和 Queue

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

  • Routing_key: Exchange和Queue绑定(binding)时的一个额外参数,目的是防止避免basic_publish的参数混淆

  • Vhost:虚拟主机,即消息队列服务器实体

常用的交换器主要分三种:

  • Fanout:如果交换器收到消息,将会广播到所有绑定的队列上
  • Direct:如果路由键完全匹配,消息就被投递到相应的队列
  • Topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符

消息基于什么传输?

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

RabbitMQ的六种工作模式

1.simple简单模式

img

2.work工作模式

img

消费者C1和C2同时监听一个队列,谁先抢到消息,谁负责消费。在高并发的情况下,会发生一条消息被多个消费者消费,造成多次消费。

3.publish/subscribe发布订阅

img

引入交换机Exchange,消息通过Exchange分发给不同队列,分发顺序是轮询。

4.routing路由模式

img

交换机根据消息的路由键匹配相应的消息队列。

5.topic 主题模式

img

交换机根据路由键的表达式匹配消息队列。*、#代表通配符, *代表一个词,#代表0个或多个词。

6.RPC

待查资料。。。。。O(∩_∩)O

你们公司生产环境用的是什么消息中间件?

比如说ActiveMQ是老牌的消息中间件,国内很多公司过去运用的还是非常广泛的,功能很强大。但是问题在于没法确认ActiveMQ可以支撑互联网公司的高并发、高负载以及高吞吐的复杂场景,在国内互联网公司落地较少。而且使用较多的是一些传统企业,用ActiveMQ做异步调用和系统解耦。

然后你可以说说RabbitMQ,他的好处在于可以支撑高并发、高吞吐、性能很高,同时有非常完善便捷的后台管理界面可以使用。另外,他还支持集群化、高可用部署架构、消息高可靠支持,功能较为完善。而且经过调研,国内各大互联网公司落地大规模RabbitMQ集群支撑自身业务的case较多,国内各种中小型互联网公司使用RabbitMQ的实践也比较多。

除此之外,RabbitMQ的开源社区很活跃,较高频率的迭代版本,来修复发现的bug以及进行各种优化,因此综合考虑过后,公司采取了RabbitMQ。但是RabbitMQ也有一点缺陷,就是他自身是基于erlang语言开发的,所以导致较为难以分析里面的源码,也较难进行深层次的源码定制和改造,毕竟需要较为扎实的erlang语言功底才可以。

然后可以聊聊RocketMQ,是阿里开源的,经过阿里的生产环境的超高并发、高吞吐的考验,性能卓越,同时还支持分布式事务等特殊场景。而且RocketMQ是基于Java语言开发的,适合深入阅读源码,有需要可以站在源码层面解决线上生产问题,包括源码的二次开发和改造。

另外就是Kafka。Kafka提供的消息中间件的功能明显较少一些,相对上述几款MQ中间件要少很多。但是Kafka的优势在于专为超高吞吐量的实时日志采集、实时数据同步、实时数据计算等场景来设计。因此Kafka在大数据领域中配合实时计算技术(比如Spark Streaming、Storm、Flink)使用的较多。但是在传统的MQ中间件使用场景中较少采用。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

MQ 有哪些常见问题?如何解决这些问题?

消息的顺序消息的重复问题。

消息顺序

消息有序指的是可以按照消息的发送顺序来消费。

消息M1先发送,消息M2后发送。如何保证M1先于M2被消费?

img

解决方案:

方案一:保证M1和M2被保存在同一个队列。

img

缺点:

  • 并行度就会成为消息系统的瓶颈(吞吐量不够)
  • 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。

方案二:

拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

消息重复

造成消息重复的根本原因是:网络不可达。

所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?

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

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

先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;

但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。

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

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

假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。

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

发送方确认模式

将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。

一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。

如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

接收方确认机制

消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。

这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性。

下面罗列几种特殊情况:

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

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

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

生产者丢失消息:从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction机制就是说:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降。

confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了。

如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

消息队列丢数据:消息持久化。

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。

这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢?这里顺便说一下吧,其实也很容易,就下面两步

将queue的持久化标识durable设置为true,则代表是一个持久的队列,发送消息的时候将deliveryMode=2
这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据

消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!

消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;

解决方案:处理消息成功后,手动回复确认消息。

如何保证高可用的?RabbitMQ 的集群

RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。

单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式

普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。

如何保证消息的顺序性?

rabbitMQ为多个消费者开辟多个queue队列(先进先出),将保证操作顺序的消息发布到同一个队列中去,操作这个队列的消费者会一个一个消息去处理,因为队列这种结构是先进先出的类型,所以保证的数据的顺序性。

消息大量积压怎么解决?

  • 临时启动多个消息者,并发处理消息
  • 临时启动多个消息者,接受消息之后,不处理。暂时把消息写到文件中。消息中间件中的消息处理的完了。关闭临时消费者。单独写个离线程序,处理文件中的消息
  • 临时启动多个消息者,接受消息之后,直接丢弃。 可以让生产者的源头恢复数据

RabbitMQ 节点的类型有哪些?

磁盘节点:消息会存储到磁盘。

内存节点:消息都存储在内存中,重启服务器消息丢失,性能高于磁盘类型。

Redis

Redis 持久化机制

RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

缓存穿透
是什么?

查询一个数据库不存在的数据。

redis缓存流程?

1、首先去redis缓存查询数据

2、如果数据存在则直接返回缓存数据

3、如果数据不存在,就对数据库进行查询,并把查询到的数据放进缓存

4、如果数据库查询数据为空,则不放进缓存

造成缓存穿透的例子?

例如我们的数据表中主键是自增产生的,所有的主键值都大于0。此时如果用户传入的参数为-1,程序就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有人恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮我们的数据库。

为了防止有人利用这个漏洞恶意攻击我们的数据库,我们可以采取如下措施:

如果从数据库查询的对象为空,也放入缓存,key为用户提交过来的主键值,value为null,只是设定的缓存过期时间较短,比如设置为60秒。这样下次用户再根据这个key查询redis缓存就可以查询到值了(当然值为null),从而保护我们的数据库免遭攻击。

或者使用布隆过滤器。

缓存雪崩
是什么?

在某一个时间段,缓存集中过期失效。在缓存集中失效的这个时间段对数据的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

如何解决?

为了避免缓存雪崩的发生,我们可以将缓存的数据设置不同的失效时间,这样就可以避免缓存数据在某个时间段集中失效。例如对于热门的数据(访问频率高的数据)可以缓存的时间长一些,对于冷门的数据可以缓存的时间段一些。甚至对于一些特别热门的数据可以设置永不过期。

缓存击穿
是什么?

一个key非常热点(例如双十一期间进行抢购的商品数据),在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求到数据库,给数据库造成压力。

如何解决?

我们同样可以将这些热点数据设置永不过期就可以解决缓存击穿的问题了。

Redis集群方案
1.主从复制Replication
是什么?

在主从复制模式下Redis节点分为两种角色:主节点(也称为master)和从节点(也称为slave)。这种模式集群是由一个主节点和多个从节点构成。原则:Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TmPYFnmP-1598187760623)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200627221850202.png)]

主节点可以进行读写操作(为了缓解主节点的压力,一般让主节点只进行写操作),从节点只能进行读操作。

有什么缺点?

在redis主从模式下,一旦主节点宕机,就无法再进行写操作了。

2.哨兵sentinel
是什么?

sentinel(哨兵)是用于监控redis集群中Master状态的工具,其本身也是一个独立运行的进程,是Redis的高可用解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。

怎么干?

sentinel可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求,并且其余从节点开始从新的主节点复制数据。在redis安装完成后,会有一个redis-sentinel的文件,这就是启动sentinel的脚本文件,同时还有一个sentinel.conf文件,这个是sentinel的配置文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ad1SEhOR-1598187760624)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200627223012607.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dCJ1OCtF-1598187760624)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200627223308271.png)]

如果sentinel挂了怎么办呢?其实sentinel本身也可以实现集群,也就是说sentinel也是高可用的。

高可用是什么?

高可用(HA)是分布式系统架构设计中必须考虑的因素之一,它是通过架构设计减少系统不能提供服务的时间。保证高可用通常遵循下面几点:

  1. 单点是系统高可用的大敌,应该尽量在系统设计的过程中避免单点。

  2. 通过架构设计而保证系统高可用的,其核心准则是:冗余。

  3. 实现自动故障转移。

3.Redis内置集群cluster
是什么?

Redis Cluster是Redis的内置集群,在Redis3.0推出的实现方案。在Redis3.0之前是没有这个内置集群的。Redis Cluster是无中心节点的集群架构,依靠Gossip协议协同自动化修复集群的状态。

Redis cluster在设计的时候,就考虑到了去中心化,去中间件,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZ9yg0zG-1598187760625)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200627223453443.png)]

需要注意的是,这种集群模式下集群中每个节点保存的数据并不是所有的数据,而只是一部分数据。那么他们是如何分配的呢?

哈希槽方式如何分配数据?

Redis 集群是采用一种叫做 哈希槽 (hash slot) 的方式来分配数据的。redis cluster 默认分配了16384 个slot,当我们set一个key 时,会用 CRC16 算法来取模得到所属的 slot ,然后将这个key 分到哈希槽区间的节点上,具体算法就是: CRC16(key) % 16384。

Redis cluster的主从模式

redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会在这些从节点中选取一个来充当主节点,从而保证集群不会挂掉。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvGv5Vrz-1598187760626)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200628085521336.png)]

缓存预热

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

缓存更新

(1)定时去清理过期的缓存;

(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

解决方案

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

单线程的redis为什么这么快

(一),纯内存操作
(二),单线程操作,避免了频繁的上下文切换
(三),采用了非阻塞I/O多路复用机制

redis的数据类型,以及每种数据类型的使用场景

(一)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
(二)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以用户名作为大key,jti作小key,jwt做value,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
(四)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是
利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

(五)sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

redis的过期策略以及内存淘汰机制

redis采用的是定期删除+惰性删除策略
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除+惰性删除是如何工作的呢?

定期删除,redis默认每隔100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

Redis 为什么是单线程的

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。Redis利用队列技术将并发访问变为串行访问

有没有尝试进行多机redis 的部署?如何保证数据一致的?

主从复制,读写分离
一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

对于大量的请求怎么样处理

redis是一个单线程程序,也就说同一时刻它只能处理一个客户端请求;
redis是通过IO多路复用来处理多个客户端请求的;

为什么Redis的操作是原子性的,怎么保证原子性的?

Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.

Redis事务

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3.如果在一个事务中出现运行错误,那么正确的命令会被执行。

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作

解锁:使用 del key 命令就能释放锁
解决死锁:
1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。
2) 使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。

Linux

linux常用命令?

项目

分布式事务解决方案?

基于XA协议的两阶段提交 2PC

二阶段协议:

第一阶段TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经完成的工作后,就可以丢弃这个事务分支信息了。

第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。

也就是TM与RM之间是通过两阶段提 交协议进行交互的.

优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

TCC补偿机制

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

优点: 相比两阶段提交,可用性比较强

缺点: 数据的一致性要差一些。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

消息最终一致性

基本思路就是:

消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。

缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

基于Seata实现分布式事务

Seata(原名Fescar) 是阿里18年开源的分布式事务的框架。Fescar的开源对分布式事务框架领域影响很大。作为开源大户,Fescar来自阿里的GTS,经历了好几次双十一的考验,一经开源便颇受关注。后来Fescar改名为Seata。

实现原理

Fescar将一个本地事务做为一个分布式事务分支,所以若干个分布在不同微服务中的本地事务共同组成了一个全局事务,结构如下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cFQzGvpe-1598187760626)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200820220841335.png)]

Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

一个典型的分布式事务过程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
  2. XID 在微服务调用链路的上下文中传播。
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
基于消息队列实现分布式事务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WN0oSTX-1598187760627)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200820220938841.png)]

超时未支付订单处理

如何获取超过60分钟的订单?我们可以使用延迟消息队列(死信队列)来实现。

所谓延迟消息队列,就是消息的生产者发送的消息并不会立刻被消费,而是在设定的时间之后才可以消费。

我们可以在订单创建时发送一个延迟消息,消息为订单号,系统会在60分钟后取出这个消息,然后查询订单的支付状态,根据结果做出相应的处理。

一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。

(1) 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。

(2)上面的消息的TTL到了,消息过期了

(3)队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。

Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

如何解决数据库缓存一致性?
Cache Aside Pattern 缓存旁路模式( Facebook 就是采用的这种策略。)

缓存旁路模式说白了就是“先更新数据库,再删缓存”这种套路。

这种策略是失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。命中:应用程序从 cache 中取数据,取到后返回。更新:先把数据存到数据库中,成功后,再让缓存失效。

也会产生不一致问题:缓存刚好失效;线程B读取缓存无效;线程B读取数据库旧值;线程A更新数据库;线程A删除缓存;线程B把旧值写入缓存;

使用重试机制保证一致性:更新数据库,缓存因为种种问题删除失败,将需要删除的key发送至消息队列,自己消费消息,获得需要删除的key,继续重试删除操作,直到成功。

还可以用阿里的cannal中间件,启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。更新数据库数据,数据库会将操作信息写入 binlog 日志当中,订阅程序提取出所需要的数据以及 key,另起一段非业务代码,获得该信息,尝试删除缓存操作,发现删除失败,将这些信息发送至消息队列,重新从消息队列中获得该数据,重试操作。

先删除缓存,再更新数据库

也会产生脏数据,如何解决呢?延时双删策略:先删除缓存,再跟新数据库,过1-2秒后再删除缓存。这么做,可以将1秒内所造成的缓存脏数据,再次删除!

项目疑难杂症?

预约订单防止超卖?select for update 或者 where +条件判断

哪些情况用到了rabbitMQ

商品上架、更新静态页、发送短信

秒杀的时候,只有最后一件物品,该怎么去抢或者分配?

秒杀商品的库存都会放到 redis 中,在客户下单时就减库存,减完库存会判断库存是否为大于 0,如果小于 0,表示库存不足,刚才减去的数量再恢复,整个过程使用 redis 的 watch 锁

你项目对于订单是怎么处理的,假如一个客户在下订单的时候没有购买怎么办?对于顾客在购买商品的时候你们怎么处理你们的库存?

订单表中设置了一个过期时间,每天会有定时任务来扫描订单表数据,如果到达预订的过期时间没有付款就会取消此订单交易。

关于库存的设计是这样的:

普通商品在发货时才去更新库存,如果库存不足商家会马上补货

秒杀的商品会在客户下单时就减库存,如果在规定时间(半个小时)没有付款,会取消此订单把库存还原

插入商品的话,要求级联插入几张表,你们当时是怎么实现的?

三张表 商品表、商品描述表、sku 表,他们的关系是一对一对多

模板表的设计思想?

模板表设计主要是为了商品表与品牌表、规格表进行数据关联。方便商品录入时, 选择对应的品牌和规格数据。

简单介绍一下你的这个项目以及项目中涉及到的技术框架以及使用场景以及你主要负责项目中的哪一块?
商品的价格变化后,如何同步redis中数以百万计的购物车数据。

解决方案:

购物车只存储商品id,到购物车结算页面将会从新查询购物车数据,因此就不会涉及购物车商品价格 同步的问题。

系统中的钱是如何保证安全的。

1) 钱计算方面

浮点数计算类型存储钱的额度,否则计算机在计算时可能会损失精度。

2) 事务处理方面

在当前环境下,高并发访问,多线程,多核心处理下,很容易出现数据一致性问题,此时必须使用 事务进行控制,访问交易出现安全性的问题,那么在分布式系统中,可以使用分布式锁。

订单中的事物是如何保证一致性的。

使用分布式事务来进行控制,保证数据最终结果的一致性。

为什么要有网关?

安全校验、统一出入口、过滤器、限流、认证判断、授权、隔离内外网、日志记录、实现微服务负载均衡、监控

常见的网关:nginx、zuul、Gateway

一共有七张表,角色表、权限表、用户表、菜单表、以及三张中间表。角色表是核心,关联着其他几张表。

面对项目高并发,如何做出优化?
  • 集群
  • 开启索引
  • redis缓存
  • 分库分表
  • sql优化
  • 页面静态化
  • 分布式架构,分担服务器压力
  • 用rabbit MQ解耦,提高业务处理能力
  • nginx服务器做负载均衡
角色权限控制

在每个微服务中,需要获取用户的角色,然后根据角色识别是否允许操作指定的方法,Spring Security中定义了四个支持权限控制的表达式注解,分别是@PreAuthorize@PostAuthorize@PreFilter@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。在需要控制权限的方法上,我们可以添加@PreAuthorize注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法。

@Service(dubbo的@service)与@Transactional同时使用,dubbo无法发布。

原因:

事务控制的底层原理是为服务提供者类创建代理对象,而默认情况下Spring是基于JDK动态代理方式创建代理对象,而此代理对象的完整类名为com.sun.proxy.$Proxy42(最后两位数字不是固定的),我们在配置中进行包扫描不是com.sun.proxy,导致Dubbo在发布服务前进行包匹配时无法完成匹配,进而没有进行服务的发布。

解决方案:

(1)修改applicationContext-service.xml配置文件,开启事务控制注解支持时指定proxy-target-class属性,值为true。其作用是使用cglib代理方式为Service类创建代理对象。cglib创建的代理对象,包名与我们的包名一致。

<!--开启事务控制的注解支持-->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

(2)在Service注解中加入interfaceClass属性,值为**要指定的的服务接口.class**,作用是指定服务的接口类型。否则会导致发布的服务接口为SpringProxy,而不是需要的接口。

@Service(interfaceClass = HelloService.class)
@Transactional
public class HelloServiceImpl implements HelloService {
    public String sayHello(String name) {
        return "hello " + name;
    }
}

Stringboot自动配置原理(面试回答)

本文摘自圣斗士Morty博客(https://me.csdn.net/u014745069)https://blog.csdn.net/u014745069/article/details/83820511。

Springboot的启动类上有一个**@SpringBootApplication注解,它是一个派生注解,在它内部有一个@EnableAutoConfiguration**,翻译叫做开启自动配置。这个注解也是一个派生注解,其中关键的功能由**@Import提供。@Import注解导入的AutoConfigurationImportSelector.class类中有一个方法叫selectImports()。该方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表。这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(…)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。当然并不是所有的自动配置类都会被加载。如果在自动配置类上有条件判断的@Conditional**,需要先满足@Conditional的条件。常见的条件注解有:

@ConditionalOnBean:当容器里有指定的bean的条件下。

@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。

@ConditionalOnClass:当类路径下有指定类的条件下。

@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。

@ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true

全局配置的属性如何生效呢?比如:server.port=8081。

ServletWebServerFactoryAutoConfiguration类上,有一个**@EnableConfigurationProperties**注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。

img

在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上。

简略回答:通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。

Spring Cloud流程图

五大神兽

Spring Cloud是一个全家桶式的技术栈,包含了很多组件。本文将详细讲讲Spring Cloud的五大神兽:EurekaRibbonFeignHystrixGateway

业务场景

假设现在有一个电商网站,要实现支付订单功能,流程如下:

  • 创建一个订单之后,如果用户立刻支付了这个订单,我们需要将订单状态更新为“已支付”
  • 扣减相应的商品库存
  • 通知仓储中心,进行发货
  • 给用户的这次购物增加相应的积分

针对上述流程,我们需要订单服务库存服务仓储服务积分服务

大家肯定一眼就看得懂流程是怎样运作的,那么先来讲讲Eureka

Eureka

Eureka 是 Netflix 公司开源的一个服务注册与发现的组件 。它包括两个组件,Eureka Server (注册中心)Eureka Client (服务提供者、服务消费者)

我们把Eureka加入流程图中,看看它到底起了什么作用?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C2p9lxga-1598187760628)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706102920021.png)]

我更觉得Eureka更像是一家婚介所。。。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lq6omHuR-1598187760628)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706104420529.png)]

接下来我们用图片来简单的分析一下Eureka如何使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJyPrEyH-1598187760629)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200711143739462.png)]

实际上为了保证Eureka的高可用性,我们通常会搭建集群。例如创建几个Eureka Server模块,在各自的配置文件中配置url。再将Eureka Client 分别注册到这几个 Eureka Server中。

讲解完Eureka后,再简单了解两个服务发布和注册服务软件,ConsulNacos

Consul

Consul 是由 HashiCorp 基于 Go 语言开发的,支持多数据中心,分布式高可用的服务发布和注册服务软件。
• 用于实现分布式系统的服务发现与配置。
• 使用起来也较为简单。具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署 。
• 官网地址: https://www.consul.io

Consul的使用步骤:
  • 添加依赖坐标
  • 配置application.yml
  • 启动consul的安装包
  • 使用 RestTemplate 完成远程调用

Nacos

Nacos(Dynamic Naming and Configuration Service) 是阿里巴巴2018年7月开源的项目。
• 它专注于服务发现和配置管理领域 致力于帮助您发现、配置和管理微服务。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理。
• 一句话概括就是Nacos = Spring Cloud注册中心 + Spring Cloud配置中心。
• 官网:https://nacos.io/
• 下载地址: https://github.com/alibaba/nacos/releases

Nacos的使用步骤与Consul相同,很简单。

但是!!!我们每次调用服务都在代码中拼字符串url,调用RestTemplate方法,实在是很麻烦。既然如此,那怎么办呢?别急,Feign早已为我们提供好了优雅的解决方案。

Feign

Feign 是一个声明式的 REST 客户端,它用了基于接口的注解方式,很方便实现客户端配置。

  • Feign 底层依赖于 Ribbon 实现负载均衡和远程调用。
  • Ribbon默认1秒超时,因此Feign的默认超时时间也为1秒。
  • Feign 只能记录 debug 级别的日志信息。

我们来聊一聊,为什么Feign只需要用注解定义一个接口,然后调用接口,就会代替你发送请求,获得响应结果呢?很简单,Feign用到了动态代理

img

Feign通过**@FeignClient**(value = “FEIGN-PROVIDER”)、@RequestMapping("/goods/findOne/{id}")、@PathVariable(“id”) int id三个注解,拼凑成访问地址,例如http://FEIGN-PROVIDER/goods/findOne/"+id

Ribbon

现在我们已经通过feign得到了url,假如库存服务部署了五台服务器,那么feign如何选择呢?此时我们需要用到Ribbon

Ribbon是Netflix提供的一个基于HTTP和TCP的客户端负载均衡工具。主要作用是简化远程调用负载均衡。我们已经知道Feign其实是在Ribbon的基础上封装,并且在远程调用方面比Ribbon更加简单,因此我们着重讲解它的负载均衡。

负载均衡
  • 服务端负载均衡:负载均衡算法在服务器,由负载均衡器维护服务地址列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pDjGqQl-1598187760630)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706165447065.png)]

  • 客户端负载均衡:负载均衡算法在客户端,客户端维护服务地址列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5dnScbKC-1598187760631)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706165457078.png)]

Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法。其负载均衡策略有:

  • 随机:RandomRule
  • 轮询:RoundRobinRule
  • 最小并发:BestAvailabelRule
  • 过滤:AvailabilityFilteringRule
  • 响应时间:WeightedResponseTimeRule
  • 轮询重试:RetryRule
  • 性能可用性:ZoneAvoidanceRule

此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:

  • 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
  • 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
  • Feign就会针对这台机器,构造并发起请求。

对上述整个过程,再来一张图,帮助大家更深刻的理解:

img

在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。这样会导致什么问题呢

  • 如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的100个线程都会卡在请求积分服务这块。导致订单服务没有一个线程可以处理请求
  • 然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了

这种问题我们称之为服务雪崩。这时就轮到Hystrix闪亮登场了。

Hystrix

Hystrix是隔离、熔断以及降级的一个框架

Hystix 主要功能

  • 隔离:线程池隔离、信号量隔离
  • 降级:异常,超时
  • 熔断
  • 限流

Hystrix 熔断机制,用于监控微服务调用情况,当失败的情况达到预定的阈值(5秒失败20次),会打开
断路器,拒绝所有请求,直到服务恢复正常为止。

断路器三种状态:打开、半开、关闭

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zkYOw57-1598187760632)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706172004863.png)]

Hystrix 提供了 Hystrix-dashboard 功能,用于实时监控微服务运行状态。但是Hystrix-dashboard只能监控一个微服务。Netflix 还提供了 Turbine ,进行聚合监控。

Hystrix起到什么作用呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。

这时我们回头再看那个问题,如果积分服务挂了,会咋样呢?

如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。**但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!**所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!

**那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?**没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。

为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:

img

说完了Hystrix,接着给大家说说最后一个组件:Gateway,也就是微服务网关。这个组件是负责网络路由的。不懂网络路由?行,那我给你说说,如果没有Gateway的日常工作会怎样?

Gateway

网关就是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等网关旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。在目前的网关解决方案里,有Nginx+ Lua、Netflix Zuul 、Spring Cloud Gateway等等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7u9gHslw-1598187760633)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200706172900449.png)]

Gateway 支持过滤器功能,对请求或响应进行拦截,完成一些通用操作。

Gateway 提供两种过滤器方式:“pre”和“post

  • pre 过滤器,在转发之前执行,可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
  • post 过滤器,在响应之前执行,可以做响应内容、响应头的修改,日志的输出,流量监控等。

Gateway 还提供了两种类型过滤器

  • GatewayFilter:局部过滤器,针对单个路由(遵循约定大于配置的思想,只需要在配置文件配置局部过滤器名称,并为其指定对应的值,就可以让其生效。)
  • GlobalFilter :全局过滤器,针对所有路由(不需要在配置文件中配置,系统初始化时加载,并作用在每个路由上)

如果没有Gateway,假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!

上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。

而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全等等。

总结

最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:

  • Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
  • Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
  • Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
  • Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
  • Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

以上就是我们通过一个电商业务场景,阐述了Spring Cloud微服务架构几个核心组件的底层原理。

**文字总结还不够直观?没问题!**我们将Spring Cloud的5个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理:

img

此外,Spring Cloud Config实现配置中心

配置文件是我们再熟悉不过的了,尤其是 Spring Boot 项目,除了引入相应的 maven 包之外,剩下的工作就是完善配置文件了,例如 mysql、redis 、security 相关的配置。除了项目运行的基础配置之外,还有一些配置是与我们业务有关系的,比如说七牛存储、短信相关、邮件相关,或者一些业务上的开关。

对于一些简单的项目来说,我们一般都是直接把相关配置放在单独的配置文件中,以 properties 或者 yml 的格式出现,更省事儿的方式是直接放到 application.properties 或 application.yml 中。但是这样的方式有个明显的问题,那就是,当修改了配置之后,必须重启服务,否则配置无法生效。

目前有一些用的比较多的开源的配置中心,比如携程的 Apollo、蚂蚁金服的 disconf 等,对比 Spring Cloud Config,这些配置中心功能更加强大。有兴趣的可以拿来试一试。

Spring Cloud Config 解决了在分布式场景下多环境配置文件的管理和维护

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新
  • 配置信息改变时,不需要重启即可更新配置信息到服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9HdLkJf-1598187760634)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200711101626826.png)]

Spring Cloud Bus

如果只有一个 client 端的话,那我们设置手动刷新不算太费事,但是如果client端比较多的话呢,一个一个去手动刷新未免有点复杂。这样的话,我们可以借助 Spring Cloud Bus 的广播功能,让 client 端都订阅配置更新事件,当配置更新时,触发其中一个端的更新事件,Spring Cloud Bus 就把此事件广播到其他订阅端,以此来达到批量更新。

Spring Cloud Bus 是用轻量的消息中间件将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管理。关键的思想就是,消息总线可以为微服务做监控,也可以实现应用程序之间相通信。

Spring Cloud Bus 可选的消息中间件包括 RabbitMQ 和 Kafka 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mc0yoLdN-1598187760635)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200711101751663.png)]

bus不是一个服务,而是一个插件。config server和config client都添加了bus的依赖坐标。bus只是将这些服务连接起来,提供广播功能。

例如当外部配置文件更改了数据,config server会强制收到修改的数据。如果没有bus,A,B,C服务不知道数据已经更改,还会按照原来的数据执行。有了bus以后,bus会通知到A,B,C服务,说:哥们,我们的数据已经更改啦,我带来了新数据,不要用原来的了~。而rabbitMQ就是携带数据的中间件。

Spring Cloud Stream

我们在项目中经常会用到消息中间件,比如我们在传统的企业中常使用ActiveMQ做异步调用和系统解耦,假如公司发展壮大了,用户非常多,这时ActiveMQ无法支撑高并发、高负载以及高吞吐的复杂场景,我们就必须切换消息中间件:RabbitMQ。换中间件就无法避免重构原先的代码,这样会非常耗费人力时间,且程序的拓展性不是很好。Spring Cloud Stream很好地解决了这种问题:

  • Spring Cloud Stream 是一个构建消息驱动微服务应用的框架。
  • Stream 解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
  • Spring Cloud Stream目前支持两种消息中间件RabbitMQ和Kafka

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cmu9GeMb-1598187760636)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200711170231500.png)]

Spring Cloud Stream 构建的应用程序与消息中间件之间是通过绑定器 Binder相关联的。绑定器对于应用程序而言起到了隔离作用, 它使得不同消息中间件的实现细节对应用程序来说是透明的。

• binding 是我们通过配置把应用和spring cloud stream 的 binder 绑定在一起

• output:发送消息 Channel,内置 Source接口

• input:接收消息 Channel,内置 Sink接口

Sleuth+Zipkin

随着业务发展,系统拆分导致系统调用链路愈发复杂,一个前端请求可能最终需要调用很多次后端服务才能完成。当整个请求变慢或不可用时,我们是无法得知该请求是由某个或某些后端服务引起的,这时就需要解决如何快读定位服务故障点,以对症下药。于是Sleuth+Zipkin就闪亮登场啦。

一般的,一个分布式服务跟踪系统,主要有三部分:数据收集数据存储数据展示。根据系统大小不同,每一部分的结构又有一定变化。譬如,对于大规模分布式系统,数据存储可分为实时数据和全量数据两部分,实时数据用于故障排查(troubleshooting),全量数据用于系统优化;数据收集除了支持平台无关和开发语言无关系统的数据收集,还包括异步数据收集(需要跟踪队列中的消息,保证调用的连贯性),以及确保更小的侵入性;数据展示又涉及到数据挖掘和分析。虽然每一部分都可能变得很复杂,但基本原理都类似。

服务追踪的追踪单元是从客户发起请求(request)抵达被追踪系统的边界开始,到被追踪系统向客户返回响应(response)为止的过程,称为一个“trace”。每个 trace 中会调用若干个服务,为了记录调用了哪些服务,以及每次调用的消耗时间等信息,在每次调用服务时,埋入一个调用记录,称为一个“span”。这样,若干个有序的 span 就组成了一个 trace。在系统向外界提供服务的过程中,会不断地有请求和响应发生,也就会不断生成 trace,把这些带有span 的 trace 记录下来,就可以描绘出一幅系统的服务拓扑图。附带上 span 中的响应时间,以及请求成功与否等信息,就可以在发生问题的时候,找到异常的服务;根据历史数据,还可以从系统整体层面分析出哪里性能差,定位性能优化的目标。

Spring Cloud Sleuth

Spring Cloud Sleuth 其实是一个工具,它在整个分布式系统中能跟踪一个用户请求的过程,捕获这些跟踪数
据,就能构建微服务的整个调用链的视图,这是调试和监控微服务的关键工具。
• 耗时分析
• 可视化错误
• 链路优化

Zipkin

Zipkin 是 Twitter 的一个开源项目,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包
括数据的收集、存储、查找和展现。

spring cloud sleuth结合zipkin,将信息发送到zipkin,利用zipkin的存储来存储信息,利用zipkin ui来展示数据。

你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。

为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:

[外链图片转存中…(img-kYAjHGmB-1598187760633)]

说完了Hystrix,接着给大家说说最后一个组件:Gateway,也就是微服务网关。这个组件是负责网络路由的。不懂网络路由?行,那我给你说说,如果没有Gateway的日常工作会怎样?

Gateway

网关就是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等网关旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。在目前的网关解决方案里,有Nginx+ Lua、Netflix Zuul 、Spring Cloud Gateway等等。

[外链图片转存中…(img-7u9gHslw-1598187760633)]

Gateway 支持过滤器功能,对请求或响应进行拦截,完成一些通用操作。

Gateway 提供两种过滤器方式:“pre”和“post

  • pre 过滤器,在转发之前执行,可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
  • post 过滤器,在响应之前执行,可以做响应内容、响应头的修改,日志的输出,流量监控等。

Gateway 还提供了两种类型过滤器

  • GatewayFilter:局部过滤器,针对单个路由(遵循约定大于配置的思想,只需要在配置文件配置局部过滤器名称,并为其指定对应的值,就可以让其生效。)
  • GlobalFilter :全局过滤器,针对所有路由(不需要在配置文件中配置,系统初始化时加载,并作用在每个路由上)

如果没有Gateway,假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!

上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。

而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全等等。

总结

最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:

  • Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
  • Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
  • Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
  • Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
  • Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

以上就是我们通过一个电商业务场景,阐述了Spring Cloud微服务架构几个核心组件的底层原理。

**文字总结还不够直观?没问题!**我们将Spring Cloud的5个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理:

[外链图片转存中…(img-aFRRbjhW-1598187760634)]

此外,Spring Cloud Config实现配置中心

配置文件是我们再熟悉不过的了,尤其是 Spring Boot 项目,除了引入相应的 maven 包之外,剩下的工作就是完善配置文件了,例如 mysql、redis 、security 相关的配置。除了项目运行的基础配置之外,还有一些配置是与我们业务有关系的,比如说七牛存储、短信相关、邮件相关,或者一些业务上的开关。

对于一些简单的项目来说,我们一般都是直接把相关配置放在单独的配置文件中,以 properties 或者 yml 的格式出现,更省事儿的方式是直接放到 application.properties 或 application.yml 中。但是这样的方式有个明显的问题,那就是,当修改了配置之后,必须重启服务,否则配置无法生效。

目前有一些用的比较多的开源的配置中心,比如携程的 Apollo、蚂蚁金服的 disconf 等,对比 Spring Cloud Config,这些配置中心功能更加强大。有兴趣的可以拿来试一试。

Spring Cloud Config 解决了在分布式场景下多环境配置文件的管理和维护

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新
  • 配置信息改变时,不需要重启即可更新配置信息到服务

[外链图片转存中…(img-l9HdLkJf-1598187760634)]

Spring Cloud Bus

如果只有一个 client 端的话,那我们设置手动刷新不算太费事,但是如果client端比较多的话呢,一个一个去手动刷新未免有点复杂。这样的话,我们可以借助 Spring Cloud Bus 的广播功能,让 client 端都订阅配置更新事件,当配置更新时,触发其中一个端的更新事件,Spring Cloud Bus 就把此事件广播到其他订阅端,以此来达到批量更新。

Spring Cloud Bus 是用轻量的消息中间件将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管理。关键的思想就是,消息总线可以为微服务做监控,也可以实现应用程序之间相通信。

Spring Cloud Bus 可选的消息中间件包括 RabbitMQ 和 Kafka 。

[外链图片转存中…(img-mc0yoLdN-1598187760635)]

bus不是一个服务,而是一个插件。config server和config client都添加了bus的依赖坐标。bus只是将这些服务连接起来,提供广播功能。

例如当外部配置文件更改了数据,config server会强制收到修改的数据。如果没有bus,A,B,C服务不知道数据已经更改,还会按照原来的数据执行。有了bus以后,bus会通知到A,B,C服务,说:哥们,我们的数据已经更改啦,我带来了新数据,不要用原来的了~。而rabbitMQ就是携带数据的中间件。

Spring Cloud Stream

我们在项目中经常会用到消息中间件,比如我们在传统的企业中常使用ActiveMQ做异步调用和系统解耦,假如公司发展壮大了,用户非常多,这时ActiveMQ无法支撑高并发、高负载以及高吞吐的复杂场景,我们就必须切换消息中间件:RabbitMQ。换中间件就无法避免重构原先的代码,这样会非常耗费人力时间,且程序的拓展性不是很好。Spring Cloud Stream很好地解决了这种问题:

  • Spring Cloud Stream 是一个构建消息驱动微服务应用的框架。
  • Stream 解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
  • Spring Cloud Stream目前支持两种消息中间件RabbitMQ和Kafka

[外链图片转存中…(img-Cmu9GeMb-1598187760636)]

Spring Cloud Stream 构建的应用程序与消息中间件之间是通过绑定器 Binder相关联的。绑定器对于应用程序而言起到了隔离作用, 它使得不同消息中间件的实现细节对应用程序来说是透明的。

• binding 是我们通过配置把应用和spring cloud stream 的 binder 绑定在一起

• output:发送消息 Channel,内置 Source接口

• input:接收消息 Channel,内置 Sink接口

Sleuth+Zipkin

随着业务发展,系统拆分导致系统调用链路愈发复杂,一个前端请求可能最终需要调用很多次后端服务才能完成。当整个请求变慢或不可用时,我们是无法得知该请求是由某个或某些后端服务引起的,这时就需要解决如何快读定位服务故障点,以对症下药。于是Sleuth+Zipkin就闪亮登场啦。

一般的,一个分布式服务跟踪系统,主要有三部分:数据收集数据存储数据展示。根据系统大小不同,每一部分的结构又有一定变化。譬如,对于大规模分布式系统,数据存储可分为实时数据和全量数据两部分,实时数据用于故障排查(troubleshooting),全量数据用于系统优化;数据收集除了支持平台无关和开发语言无关系统的数据收集,还包括异步数据收集(需要跟踪队列中的消息,保证调用的连贯性),以及确保更小的侵入性;数据展示又涉及到数据挖掘和分析。虽然每一部分都可能变得很复杂,但基本原理都类似。

服务追踪的追踪单元是从客户发起请求(request)抵达被追踪系统的边界开始,到被追踪系统向客户返回响应(response)为止的过程,称为一个“trace”。每个 trace 中会调用若干个服务,为了记录调用了哪些服务,以及每次调用的消耗时间等信息,在每次调用服务时,埋入一个调用记录,称为一个“span”。这样,若干个有序的 span 就组成了一个 trace。在系统向外界提供服务的过程中,会不断地有请求和响应发生,也就会不断生成 trace,把这些带有span 的 trace 记录下来,就可以描绘出一幅系统的服务拓扑图。附带上 span 中的响应时间,以及请求成功与否等信息,就可以在发生问题的时候,找到异常的服务;根据历史数据,还可以从系统整体层面分析出哪里性能差,定位性能优化的目标。

Spring Cloud Sleuth

Spring Cloud Sleuth 其实是一个工具,它在整个分布式系统中能跟踪一个用户请求的过程,捕获这些跟踪数
据,就能构建微服务的整个调用链的视图,这是调试和监控微服务的关键工具。
• 耗时分析
• 可视化错误
• 链路优化

Zipkin

Zipkin 是 Twitter 的一个开源项目,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包
括数据的收集、存储、查找和展现。

spring cloud sleuth结合zipkin,将信息发送到zipkin,利用zipkin的存储来存储信息,利用zipkin ui来展示数据。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐