2023最新最全---java面试题及答案大全

java面试大全

自己辛苦整理,相对简化,适用于面试突击。
希望对初中级java开发的面试有所帮助。
毕竟现在的就业环境太差了。
有问题、有补充欢迎评论指出,虚心求教,有错麻溜改。
对你有帮助的话,记得点赞收藏。
朋友要找工作的话,记得转发给他哦~

文章目录

JAVA基础

JDK、JRE、JVM之间的区别

​ JDK:java开发工具;JRE:java运行时环境;JVM:java虚拟机。

面向对象

​ 面向对象相较于面向过程而言是两种不同的处理问题的角度。

​ 面向过程注重步骤,面向对象更注重完成这些任务的参与者(对象)。

​ 面向过程比较直接高效,而面向对象更易于复用、扩展和维护。

  • 封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。(私有的属性,共有的get、set方法)
  • 继承:继承基类的方法,并做出自己的改变和/或扩展。子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
  • 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。(父类引用指向子类对象,相同的方法调用,因为不同的子类对象的实现,执行不同的逻辑)

==和equals区别

  • ==比较的是栈中的值,包括基本数据类型的值和引用数据类型的地址。
  • equals是顶级父类object类中的方法,在不重写的情况下采用==完成比较,通常会重写,按照重写规则进行内容比较。
  • java源码中,equals被String、Integer重写了,所以比较的是对象的内容是否相等。

hashCode与equals

  • 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象 。
  • 如果两个对象的hashCode相同,不代表这两个对象⼀定是同⼀个对象,也可能是两个对象(equals不一定相等)。
  • 如果两个对象相等(equals相等),那么他们的hashCode就⼀定相同。

final

  • 修饰的类不可被继承,修饰的方法不能被子类重写,修饰的变量不能被修改(引用类型不可修改地址)。

  • 如果final修饰的是类变量(static),只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。

  • 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。

  • 系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,

    即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码

    中对final变量赋初值(仅一次)。

  • 局部内部类和匿名内部类只能访问局部final变量。(内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着 方法的执行完毕就被销毁。当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解 决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期 final变量 )

final、finally、finalize

  • Final:用于声明属性(属性不可变),方法(不能被重写),类(被final修饰的类不能被继承)。
  • Finally:处理异常时使用,表示总是执行。
  • Finalize:0bject类的一个方法,垃圾回收。

String、StringBuffer、StringBuilder

  • String是final修饰的,不可变,底层用char数组实现的。每次操作都会产生新的String对象 。

  • StringBuffer和StringBuilder都是在原对象上操作 。

  • StringBuffer是线程安全的,StringBuilder线程不安全的 。

  • StringBuffer方法都是synchronized修饰的 。

    性能:StringBuilder > StringBuffer > String 。

​ 场景:经常需要改变字符串内容时使用后面两个 。

​ 优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。

重载和重写的区别

  • 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问

修饰符可以不同,发生在编译时。

  • 重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于

等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。

接口和抽象类的区别

  • 抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法(1.8加入了默认方法)。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
  • 抽象类只能继承一个,接口可以实现多个。

接口的设计目的,是对类的行为进行约束;而抽象类的设计目的,是代码复用。

访问修饰符

  • Private:私有 出了这个类就不能被访问 当出现集成可继承父类的属性或者方法
  • Default:(包访问权限)只能在同一个包下中所有类访问,且必须是同级的包
  • Protected:(继承访问权限)只能在同一个包中所有类和不同包的子类访问
  • Public:可以再任意位置被访问

Static

静态关键字,用法包括静态变量和静态方法。

  • 静态变量:(类变量)被所有的对象所共享。
  • 静态方法:静态方法中不能访问类的非静态成员变量和非静态成员方法

String常用API

  • length(),返回当前字符串长度
  • substring():截取字符串
  • equals():比较
  • charAt():从字符串中取出指定位置的字符
  • tocharArray():将字符串变成字符数组
  • trim():去掉空格
  • split():分割字符串 数组
  • getBytes,字符串转为为byte数组

Object类API

  • getClass():返回对象的类
  • hashCode():返回对象的哈希值
  • equals():比较
  • clone():复制
  • toString():返回对象字符串
  • notify():唤醒等待的单个线程
  • notifyAll():唤醒等待的所有线程
  • wait():让线程等待
  • finalize():垃圾回收

时间常用API

  • Date
//创建一个Date日期对象:代表了系统当前此刻日期时间信息
Date d = new Date();

//获取时间毫秒值的形式:从19700101 0:0:0开始走到此刻的总毫秒值
long time = d.getTime();  // long time = System.currentTimeMillis();

time += (60 * 60 + 123) * 1000;
//把时间毫秒值转换成日期对象
Date d2 = new Date(time);
 
// 与上述代码逻辑一样,只是写法不同
Date d2 = new Date();
d2.setTime(time); // 修改日期对象成为time这个时间
  • SimpleDateFormat
//日期对象
Date d = new Date();

//开始格式化:创建一个简单日期格式化对象
// 注意:参数是格式化之后的时间形式,必须申明!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");

//开始格式化日期对象成为字符串形式
String result = sdf.format(d);

//格式化时间毫秒值----------
long time = d.getTime() + 60 * 1000;
sdf.format(time)
    
//SimpleDateFormat解析字符串时间成为日期对象
String timeStr = "2022年05月27日 12:12:12";

SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date d = sdf.parse(timeStr); // 解析
  • Calendar
// 拿到系统此刻日历对象
Calendar rightNow = Calendar.getInstance();

// 获取日历的信息:public int get(int field):取日期中的某个字段信息。
int year = rightNow.get(Calendar.YEAR);

int mm = rightNow.get(Calendar.MONTH);

int days = rightNow.get(Calendar.DAY_OF_YEAR);

//public void add(int field,int amount):为某个字段增加/减少指定的值
// 请问64天后是什么时间
rightNow.add(Calendar.DAY_OF_YEAR , 64);

//拿到此刻时间毫秒值
long time = rightNow.getTimeInMillis();
System.out.println(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time));

  • LocalDate
//获取本地日期对象。
LocalDate nowDate = LocalDate.now();

int year = nowDate.getYear();

int month = nowDate.getMonthValue();

int day = nowDate.getDayOfMonth();

//当年的第几天
int dayOfYear = nowDate.getDayOfYear();

//星期
System.out.println(nowDate.getDayOfWeek());
System.out.println(nowDate.getDayOfWeek().getValue());

//月份
System.out.println(nowDate.getMonth());
System.out.println(nowDate.getMonth().getValue());

//直接传入对应的年月日
LocalDate bt = LocalDate.of(2025, 5, 20);

//相对上面只是把月换成了枚举
System.out.println(LocalDate.of(2025, Month.MAY, 20));
  • LocalTime
//获取本地时间对象。
LocalTime nowTime = LocalTime.now();

int hour = nowTime.getHour();//时

int minute = nowTime.getMinute();//分

int second = nowTime.getSecond();//秒

int nano = nowTime.getNano();//纳秒

LocalTime time = LocalTime.of(8, 30);
System.out.println(time);//时分
System.out.println(LocalTime.of(8, 20, 30));//时分秒
  • LocalDateTime
// 日期 时间
LocalDateTime nowDateTime = LocalDateTime.now();
//今天是:
System.out.println("今天是:" + nowDateTime);
System.out.println(nowDateTime.getYear());//年
System.out.println(nowDateTime.getMonthValue());//月
System.out.println(nowDateTime.getDayOfMonth());//日
System.out.println(nowDateTime.getHour());//时
System.out.println(nowDateTime.getMinute());//分
System.out.println(nowDateTime.getSecond());//秒

//日:当年的第几天
System.out.println(nowDateTime.getDayOfYear());
//星期
System.out.println(nowDateTime.getDayOfWeek());//枚举
System.out.println(nowDateTime.getDayOfWeek().getValue());//数组
//月份
System.out.println(nowDateTime.getMonth());//枚举
System.out.println(nowDateTime.getMonth().getValue());//数组

//转日期
LocalDate ld = nowDateTime.toLocalDate();
//转时间
LocalTime lt = nowDateTime.toLocalTime();
  • DateTimeFormatter
LocalDateTime ldt = LocalDateTime.now();

//格式化器
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

String ldtStr1 = dtf.format(ldt);

//解析
DateTimeFormatter dtf1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 解析当前字符串时间成为本地日期时间对象
LocalDateTime ldt1 = LocalDateTime.parse("2022-11-11 11:11:11" ,  dtf1);
System.out.println(ldt1.getDayOfYear());

冒泡排序

for (int i = 0; i <arr.length-1; i++) { 

    //标志位
    boolean flag = true;

    for (int j = 0; j <arr.length-1-i ; j++) {

        if(arr[j] > arr[j+1]){
            int temp = arr[j+1];
            arr[j+1] = arr[j];
            arr[j] = temp;
            flag = false;
        }
    }

    //当不再发生交换时,则结束比较
    if(flag){
        break;
    }
}

集合

List和Set的区别

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出

所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。

  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元

素,在逐一遍历各个元素。

List

​ 思路:介绍list的特点–>简单介绍Arraylist,LinkedList 的底层实现–>说说Arraylist,LinkedList 的区别–>最后可以说他们不是线程安全的,引入写时复制思想。

  • List是一个有序,可重复的集合。它的实现类包括ArrayList,LinkedList,Vector。

  • ArrayList底层是动态数组实现的。动态数组就是长度不固定,随着数据的增多而变长。实例化Arraylist的时候,如果不指定长度,默认就是10。添加元素时,是按照顺序从头部开始往后添加。

    ​ 使用无参构造ArrayList()创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add(E e) 方法时,初始化定义底层数组的长度为10,之后调用add(E e)时,如果需要扩容,则调用grow(int minCapacity) 进行扩容,长度为原来的1.5倍。

    ​ 因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动,所以增删效率一般。

    ​ 但是由于每个元素占用的内存相同且是连续排列的,因此在查找的时候,根据元素的下标可以迅速访问数组中的任意元素,查询效率非常高。

  • LinkedList底层是双向链表的数据结构实现,每个节点包括:上一节点和下一节点的引用地址和data用来存储数据,双向链表不是连续排列的,是可以占用一段不连续的内存空间的。

    ​ 当有新元素插入时,只需要修改所要插入位置的前一个元素的引用和后一个元素的引用。

    ​ 删除也只需要修改两个引用,当前元素就没有指向,就成了垃圾对象,被回收。效率高。但是查询的时候需要从第一个元素开始查找,直到找到需要的数据,所以查询的效率比较低。

ArrayList和Linkedlist的区别?

  • ArrayList底层是数组实现,LinkedList底层是链表实现

  • Arraylist适合随机查找,LinkedList适合删除和添加

  • 都实现了List接口,但是LinkedList同时还实现了Deque接口,还可以作为双端队列。

  • ArrayList通过下标查询快,LinkedList通过下标查询需要遍历所有,但是查第一个和最后一个很快

  • ArrayList添加需要扩容,指定位置添加,需要数组移动元素。LinkedList添加不需要扩容,指定位置添加,需要遍历找到位置

  • ArrayList实现了Random Access接口,LinkedList没有。实现Random Access接口可以使用普通for循环遍历,没有实现的使用foreach和迭代器,ArrayList用for循环快,LinkedList用迭代器快。

  • ArrayList和LinkedList都是线程不安全的。(在添加操作时,可能是分成两步完成的: 1、在items[size]的位置存放此元素,2、增大size的值,这个时候就会引发线程安全问题。)如果想要解决当前的这个问题,可以用写时复制的CopyOnWriteArrayList。

Arraylist如何去除重复元素?

  • 可以使用set集合,因为set是不可重复的,可以把数据添加到set集合中,再转为list就可以去重。
  • 可以使用stream对象distinct去重关键字进行去重,再收集成新的list。

Arraylist中有很多空值null,如何删除?

  • 第一种:list.stream().filter(Objects::nonNull).collect(Collectors.toList());
  • 第二种:list.removeIf(Objects::isNull);

Set

​ 无序,元素不能重复。

  • HashSet:内部数据结构是哈希表(线程不安全,效率高),元素无序,唯一(存储元素类型是否重写hashCode和equals方法保证),可以存储null元素。
  • TreeSet:内部数据结构是二叉树,元素唯一,有序(线程不安全),集合元素唯一。TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素的大小关系,比较是否返回0,如果返回0则相等然后将元素按照升序排列。

Map

​ 键值对 key value的集合,可以使用任何引用类型的数据,key不能重复,通过指定的key就可以获取对应的value。

HashMap 和 HashTable 有什么区别?

  • HashMap方法没有synchronized修饰,线程非安全,HashTable 是线程安全的。
  • 由于线程安全,所以 HashTable 的效率比不上 HashMap。
  • HashMap可以把null作为key或value,而 HashTable不允许。
  • HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大1倍,后者扩大1倍+1(2n+1)。
  • HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode。

Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化?

  • 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率 。
  • 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法 。
  • 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法,节省CPU资源。

说⼀下HashMap的Put⽅法

先说HashMap的Put⽅法的⼤体流程:

  • 根据Key通过哈希算法和与运算得出数组下标
  • 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中

是Node对象)并放⼊该位置

  • 如果数组下标位置元素不为空,则要分情况讨论
    • 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中
    • 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
      • 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
      • 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插 法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
      • 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法

HashMap的扩容机制原理

1.7版本

  • 先⽣成新数组
  • 遍历⽼数组中的每个位置上的链表上的每个元素
  • 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
  • 将元素添加到新数组中去
  • 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

  • 先⽣成新数组
  • 遍历⽼数组中的每个位置上的链表或红⿊树
  • 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  • 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
    • 统计每个下标位置的元素个数
    • 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置
    • 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
  • 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

迭代器在迭代过程中,修改map会出现什么问题?

​ 采用 Fail-Fast 机制,底层通过一个 modCount 值记录修改的次数,对 HashMap 的修改操作都会增加这个值。迭代器在初始过程中会将这个值赋给 exceptedModCount ,在迭代的过程中,如果发现 modCount 和 exceptedModCount 的值不一致,代表有其他线程修改了Map,就立刻抛出异常。

HashMap为什么是线程不安全的?

  • 在多线程的情况下,进行put操作的时候,如果插入的元素超过了容量的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容hash到新的扩容数组中,在多线程的环境下,存在同时进行put操作,如果hash值相同,可能出现在同一数组下用链表表示,造成闭环,导致get死循环。

怎么解决:

  • 本身hashtable就是线程安全的,因为他的所有涉及多线程的操作都加了synchronized关键字,但是效率太低。所以在多线程的状态下,建议使用ConcurrentHashMap解决多线程不安全问题。ConcurrentHashMap也分为1.7和1.8。ConcurrentHashMap不支持key和value不能为空,如果空就会报空指针异常。
  • 1.7使用的是数组+segments分段锁+HashEntry链表的数据结构,锁的实现使用的是Lock+CAS+UNSAFE类,他的扩容方式支持多个segment同时扩容。(实现原理:相当于将一个大的ConcurrentHashMap拆分成16个小的hashtable,每个hashtable中都有独立的table[],所以在put操作时,先计算存放在哪个segment对象,再计算存在对象的哪个位置,使用的是头插法,初始化默认初始segment大小为16,hashEntry对象初始容量为2)。
  • 1.8使用的是数组+链表+红黑树,直接使用node数组保存数据,取消了segment分段设计,锁机制使用的是CAS+synchronized保证并发更新,支持并发扩容。相当于是给每一个数组下标加锁。

HashMap的加载因子

​ loadFactor加载因子0.75f。

ConcurrentHashMap

1.7

​ 在 JDK7 中,ConcurrentHashMap 使用“分段锁”机制实现线程安全,数据结构可以看成是"Segment数组+HashEntry数组+链表",一个 ConcurrentHashMap 实例中包含若干个 Segment 实例组成的数组,每个 Segment 实例又包含由若干个桶,每个桶中都是由若干个 HashEntry 对象链接起来的链表。.

​ 因为Segment 类继承 ReentrantLock 类,所以能充当锁的角色,通过 segment 段将 ConcurrentHashMap 划分为不同的部分,就可以使用不同的锁来控制对哈希表不同部分的修改,从而允许多个写操作并发进行,默认支持 16 个线程执行并发写操作,及任意数量线程的读操作。

1.8

​ 在 JDK8 及以上的版本中,ConcurrentHashMap 的底层数据结构依然采用“数组+链表+红黑树”,但是在实现线程安全性方面,抛弃了 JDK7 版本的 Segment分段锁的概念,而是采用了 synchronized + CAS 算法来保证线程安全。在ConcurrentHashMap中,大量使用 Unsafe.compareAndSwapXXX 的方法,这类方法是利用一个CAS算法实现无锁化的修改值操作,可以大大减少使用加锁造成的性能消耗。这个算法的基本思想就是不断比较当前内存中的变量值和你预期变量值是否相等,如果相等,则接受修改的值,否则拒绝你的而操作。

区别

  • 数据结构:JDK7 的数据结构是 Segment数组 + HashEntry数组 + 链表,JDK8 的数据结构是 HashEntry数组 + 链表 + 红黑树,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率。
  • 锁的实现:JDK7的锁是segment,是基于ReentronLock实现的,包含多个HashEntry;而JDK8 降低了锁的粒度,采用 table 数组元素作为锁,从而实现对每行数据进行加锁,进一步减少并发冲突的概率,并使用 synchronized 来代替 ReentrantLock,因为在低粒度的加锁方式中,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中ReentrantLock 可以通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
  • 统计集合中元素个数 size 的方式:JDK7 是先尝试 2次通过不锁住 segment 的方式来统计各个 segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有Segment的大小;在 JDK8 中,对于size的计算,在扩容和 addCount() 方法中就已经有处理了,等到调用 size() 时直接返回元素的个数。

CopyOnWriteArrayList

  • ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素 时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏。
  • 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题 。
  • 写操作结束之后会把原数组指向新数组 。
  • CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应 ⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所 以不适合实时性要求很⾼的场景。

异常

Java中的异常体系

  • Java中的所有异常都来⾃顶级⽗类Throwable。
  • Throwable下有两个⼦类Exception和Error。
  • Error表示⾮常严重的错误,⽐如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError,通常这些错误出现时,仅仅想靠程序⾃⼰是解决不了的,可能是虚拟机、磁盘、操作系统层⾯出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不⼤,因为程序可能已经根本运⾏不了。
  • Exception表示异常,表示程序出现Exception时,是可以靠程序⾃⼰来解决的,⽐如NullPointerException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。
  • Exception的⼦类通常⼜可以分为RuntimeException和⾮RuntimeException两类。
    • RunTimeException表示运⾏期异常,表示这个异常是在代码运⾏过程中抛出的,这些异常是⾮检查异常,程序中可以选择捕获处理,也可以不处理。这些异常⼀般是由程序逻辑错误引起的,程序应该从逻 辑⻆度尽可能避免这类异常的发⽣,⽐如NullPointerException、IndexOutOfBoundsException、ClassCastException等。
    • ⾮RuntimeException表示⾮运⾏期异常,也就是我们常说的检查异常,是必须进⾏处理的异常,如果不处理,程序就不能检查异常通过。如IOException、SQLException、ClassNotFoundException、FileNotFoundException等以及⽤户⾃定义的Exception异常。

在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕 获异常?

​ 异常相当于⼀种提示,如果我们抛出异常,就相当于告诉上层⽅法,我抛了⼀个异常,我处理不了这个异常,交给你来处理,⽽对于上层⽅法来说,它也需要决定⾃⼰能不能处理这个异常,是否也需要交给它的上层。

​ 我们在工作当中,异常的处理应该说是因业务需求而定,比如我们在判断非法参数时就可以抛出异常提示,同时定义统一的异常处理器,对我们的异常进行统一的处理和响应。而我们在一个接口中开启了事务这个时候是不能够捕获异常的,这样会导致管理器无法通过异常信息进行回滚操作。

线程

创建线程的方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值同时可以对异常进行处理。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

线程的状态

​ 线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

​ 源码中定义的线程的状态:

public enum State {
    NEW,  //新生
    RUNNABLE,  //就绪状态
    BLOCKED,   //阻塞状态
    WAITING,   //等待
    TIMED_WAITING,   //限时等待
    TERMINATED;    //终结状态
}

sleep() 和 wait() 有什么区别?

  • sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
  • wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程

创建线程池有哪几种方式?

  • newFixedThreadPool(int nThreads):创建一个固定长度的线程池。
  • newCachedThreadPool():创建一个可缓存的线程池。
  • newSingleThreadExecutor():这是一个单线程的Executor。
  • newScheduledThreadPool(int corePoolSize):创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
  • 以上方式在实际应用中不推荐使用(因为其默认长度(最大线程数\阻塞队列长度)都是Integer.MAX_VALUE,相当于长度不受限,不利于线程管理),推荐使用自定义线程池。

自定义线程池

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • int corePoolSize:该线程池中核心线程数最大值
  • int maximumPoolSize:该线程池中线程总数最大值
  • long keepAliveTime:该线程池中非核心线程闲置超时时长
  • TimeUnit unit:时间单位
  • BlockingQueue workQueue:定义阻塞队列
  • ThreadFactory threadFactory:线程工厂,一般使用默认
  • RejectedExecutionHandler handler:拒绝策略
    • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
    • CallerRunsPolicy:“调用者运行”一种调用机制,该策略既不会抛弃任务,也不会抛异常,而是将某些任务退回到调用者,从而降低新任务流量。
    • DiscardOldestPolicy:抛弃队列中等待最久的任务。
    • DiscardPolicy:该策略默默地丢弃无法处理的任务。

线程池执行流程

在这里插入图片描述

  • 在创建线程池后,线程池中线程数为零(惰性加载)。
  • 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
  • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于corePoolSize,那么将这个任务放入阻塞队列;
  • 如果这个时候阻塞队列已满,且正在运行的线程数小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略。
  • 当一个线程完成任务时,他会从队列中取下一个任务来执行。
  • 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

线程池中 submit()和 execute()方法有什么区别?

  • 接收的参数不一样,Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。
  • submit有返回值,而execute没有。execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。
  • submit方便Exception处理。execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。

spring中使用线程的方式

  • 第一种,在配置类上添加@EnableAsync来开启异步调用,在指定方法上添加@Async注解异步执行方法

  • 第二种

    • 线程池配置
    @Configuration
    //开启异步调用
    @EnableAsync
    public class AysncConfig {
        //配置线程池, 不同的异步方法, 可以交给特定的线程池来完成
        @Bean("myThreadPool")
        public ExecutorService myThreadPool() {
    
            //这个类则是spring包下的, 是spring为我们提供的线程池类
            ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    
            //核心线程数
            threadPoolTaskExecutor.setCorePoolSize(10);
            //最大线程数
            threadPoolTaskExecutor.setMaxPoolSize(50);
            //阻塞队列大小
            threadPoolTaskExecutor.setQueueCapacity(50);
            //超时时长
            threadPoolTaskExecutor.setKeepAliveSeconds(30);
            //线程名前缀
            threadPoolTaskExecutor.setThreadNamePrefix("lz-thread-");
            //拒绝策略
            threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    
            //初始化
            threadPoolTaskExecutor.initialize();
    
            return threadPoolTaskExecutor.getThreadPoolExecutor();
        }
    }
    
    • 使用
    @Async("myThreadPool")
    public CompletableFuture<String> getString1(Integer msg) {
    
    	//方法体
        return CompletableFuture.completedFuture(String.valueOf(msg));
    }
    

ThreadLocal的原理和使用场景

​ 每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值。

​ ThreadLocalMap 由一个个 Entry 对象构成,Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。

​ 当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对

象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

​ get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap

对象。再以当前ThreadLocal对象为key,获取对应的value。由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离 。
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。

(Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离)

ThreadLocal内存泄露原因,如何避免

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

​ ThreadLocal中的Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。

​ 由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。

​ Entry对象属于ThreadLocalMap,ThreadLocalMap属于Thread,如果线程本身的生命周期很短,短时间内就会被销毁,那么「内存泄漏」立刻就会得到解决,只要线程被销毁,value也会随之被回收。问题是,线程本身是非常珍贵的计算机资源,很少会去频繁的创建和销毁,一般都是通过线程池来使用,这就将线程的生命周期大大拉长,「内存泄漏」的影响也会越来越大。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据。(建议使用)
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

synchronized 和 Lock 有什么区别?

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

Volatile

​ volatile是java提供的一种轻量级的同步机制。volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

​ 可以保证数据的可见性和有序性,无法保证原子性。

说一下 atomic 的原理?

​ Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

​ Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。unsafe类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用。Atomic就是自旋调用原子操作。

乐观锁/悲观锁

​ 乐观锁:每次拿数据的时候认为别人不会修改数据,所以不会上锁,更新的时候会判断一下这期间有没有人去更新过这个数据。适合多读的应用类型,提高吞吐量。

​ 悲观锁:每个去拿数据的时候认为别人会修改数据,所以每次都会上锁,别人想拿的时候就会阻塞直到它拿到锁。

​ 悲观锁适合写多的场景,乐观锁适合读多的场景(不加锁提高了性能)

​ 悲观锁在java中就是利用各种锁;乐观锁在java中使用无锁编程,采用CAS算法

CAS

​ CAS (Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

​ CAS操作中包含三个操作数一一需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。

独享锁/共享锁

​ 独享锁(独占锁)是指该锁一次只能被一个线程所持有。

​ 共享锁是指该锁可被多个线程所持有。

​ 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁。其写锁是独享锁,读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

读写锁

​ ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。

​ 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

​ 互斥锁在Java中的具体实现就是ReentrantLock。

​ 读写锁在Java中的具体实现就是ReadWriteLock.

可重入锁

​ 以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取该对象上的锁,而其他线程是不可以的。可重入锁的意义在于防止死锁。

​ 实现原理:

​ 为每个锁关联一个请求计数器和一个占有他的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将请求计数器置为1。

​ 同一个线程再次请求这个锁,计数器递增;

​ 每次占用线程退出同步代码块,计数器递减。

​ 直到计数器为0,锁被释放。

公平锁/非公平锁

​ 公平锁:在锁上等待时间最长的线程将获得锁的使用权。

​ 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

​ 多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

偏向锁/轻量级锁/重量级锁

​ 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

​ 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

​ 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

​ 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

自旋锁

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

JVM

在这里插入图片描述

类加载器

​ JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

​ 继承ClassLoader可以自定义类加载器。

双亲委派机制

​ 向上委派,向下加载。

​ 双亲委派模型的好处:

  • 主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
  • 同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。

类加载的过程

在这里插入图片描述

​ 上图为类的生命周期。

​ 类加载的过程包括:加载、链接、初始化。

  • Loading(加载):通过全限定类名,找到指定的类在硬盘中的位置。通过流将该对象加载(传输)到内存中。传输完成后在内存中开空间,存放其Class信息。(从磁盘到内存)。
  • Linking(链接)
    • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。如:文件格式验证、字节码验证等
    • 准备:负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值。这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
    • 解析:将常量池内的符号引用转换为直接引用的过程
  • Initialization(初始化):初始化阶段就是执行类构造器方法()的过程,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。(给类变量赋值)在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。

运行时数据区

  • 栈:每个线程,将创建单独的运行时栈。对于每个方法的调用,将在栈存储器中产生一个条目,称为栈帧。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是共享资源。(基本数据类型的值和引用数据类型的地址)
    • 栈帧通过局部变量表、操作数栈、动态链表、返回地址组成。
  • PC寄存器:每一个线程都有单独的PC寄存器(程序计数器),用于保存当前执行指令的地址偏移量,一旦有指令执行,PC寄存器将被下一条指令更新。
  • 本地方法栈:本地方法栈保存本地方法信息,同栈。
  • 方法区:所有的类级数据将存储在这里,包括静态变量,每个jvm只有一个方法区,属于线程共享资源。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
    • 常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。
    • 方法区迭代版本变更:1.7永久代、1.8元空间(使用本地物理内存)。
  • 堆:所有对象及其对应的实例变量和数组将存储在这里,每一个jvm也只有一个堆区域,属于线程共享资源。

GC如何判断对象可以被回收

  • 引⽤计数法:每个对象有⼀个引⽤计数属性,新增⼀个引⽤时计数加1,引⽤释放时计数减1,计数为0时可以回收。(java不使用)
  • 可达性分析法:从 GC Roots 开始向下搜索,搜索所⾛过的路径称为引⽤链。当⼀个对象到 GC Roots 没有任何引⽤链相连时,则证明此对象是不可⽤的,那么虚拟机就判断是可回收对象。 (√)

GC Roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引⽤的对象
  • ⽅法区中类静态属性引⽤的对象
  • ⽅法区中常量引⽤的对象
  • 本地⽅法栈中JNI(即⼀般说的Native⽅法)引⽤的对象

说一下 jvm 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

你们项⽬如何排查JVM问题

对于还在正常运⾏的系统:

  • 可以使⽤jmap来查看JVM中各个区域的使⽤情况
  • 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
  • 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了
  • 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
  • ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表 示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年 轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明 修改有效。
  • 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存

对于已经发⽣了OOM的系统:

  • ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  • 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
  • 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
  • 然后再进⾏详细的分析和调试

JVM有哪些垃圾回收算法?

  • 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
  • 复制算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部复制到另 ⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。
  • 标记整理算法:为了解决拷⻉算法的缺陷,就提出了标记整理算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将端边界以外的所有内存直接清除。

JVM有哪些垃圾回收器?

  • 新⽣代收集器:Serial、ParNew、Parallel Scavenge
  • ⽼年代收集器:CMS(标记清除)、Serial Old、Parallel Old
  • 整堆收集器:G1

什么是三⾊标记?

三⾊标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜⾊:

  • ⿊⾊:表示⾃⼰和成员变量都已经标记完毕。
  • 灰⾊:⾃⼰标记完了,但是成员变量还没有完全标记完。
  • ⽩⾊:⾃⼰未标记完。

常用的 jvm 调优的参数都有哪些?

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

MySQL

SQL练习

http://www.cnblogs.com/qixuejia/p/3637735.html

分页查询

limit,对于小的偏移量,直接使用limit来查询没有什么问题,但是数据量增大后,limit语句的偏移量增大,查询效率会降低可通过子查询的分页方式或JOIN分页方式解决。

常见函数

  • 字符函数:concat,连接;substr,截取子串;upper,变大写;lower,变小写;replace,替换;length,获取字节长度;trim,去前后空格;lpad,左填充;rpad,右填充;instr,获取子串第一次出现的索引;
  • 数学函数:ceil,向上取整;round,四舍五入;mod:取模floor,向下取整;truncate,截断;rand,获取随机数,返回 0-1 之间的小数;
  • 日期函数:now,返回当前日期+时间;year,返回年;month,返回月;day,返回日;date format,将日期转换成字符;curdate,返回当前日期;str to date,将字符转换成日期;curtime,返回当前时间;hour,小时;minute,分钟;second,秒;datediff,返回两个日期相差的天数;monthname,以英文形式返回月;
  • 其他函数:version,当前数据库服务器的版本;database,当前打开的数据库;user,当前用户;password(字符’), 返回该字符的密码形式;md5(字符’),返回该字符的 md5 加密形式;
  • 聚合函数:avg(),平均值;count(),返回指定组中的项目个数;max(),最大值;min(),最小值;sun(), 求和;

索引

​ 索引:帮助MySQL高效检索数据的排好序的数据结构。

  • 主键索引:设定为主键后数据库会自动创建索引,InnoDB为聚簇索引。

  • 单值索引:即一个索引只包含单个列,一个表可以有多个单值索引。

  • 唯一索引:索引列的值必须唯一,但允许有空值。

  • 复合索引:即一个索引包含多个列。

  • 全文索引(Full Text):在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。

    特殊:

  • 聚簇索引:将数据存储与索引放到了一块,索引结构的子节点保存了行数据。

    • 在innodb中,默认主键为聚簇索引,不在聚簇索引上创建的索引称之为辅助索引(除了主键为,其余为辅助索引)
  • 非聚簇索引:将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置。

B+树

​ B+ 树非叶子节点上是不存储数据的,仅存储键值。而且数据是按照顺序排列的通过双向链表进行连接。三层存储大约2000W+条数据。

索引设计的原则

查询更快、占⽤空间更⼩

  • 适合索引的列是出现在where⼦句中的列,或者连接⼦句中指定的列
  • 基数较⼩的表,索引效果较差,没有必要在此列建⽴索引
  • 使⽤短索引,如果对⻓字符串列进⾏索引,应该指定⼀个前缀⻓度,这样能够节省⼤量索引空间,如果搜索词超过索引前缀⻓度,则使⽤索引排除不匹配的⾏,然后检查其余⾏是否可能匹配。
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进⾏更新甚⾄重构,索引列越多,这个时间就会越⻓。所以只保持需要的索引有利于查询即可。
  • 定义有外键的数据列⼀定要建⽴索引。
  • 更新频繁字段不适合创建索引
  • 若是不能有效区分数据的列不适合做索引列(如性别,男⼥未知,最多也就三种,区分度实在太低)
  • 尽量的扩展索引,不要新建索引。⽐如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  • 对于定义为text、image和bit的数据类型的列不要建⽴索引。

索引覆盖是什么

​ 索引覆盖就是⼀个SQL在执⾏时,可以利⽤索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL⾛完索引后不⽤回表了,所需要的字段都在当前索引的叶⼦节点上存在,可以直接作为结果返回了 。

最左前缀原则是什么

​ 当⼀个SQL想要利⽤索引是,就⼀定要提供该索引所对应的字段中最左边的字段,也就是排在最前⾯的字段,⽐如针对a,b,c三个字段建⽴了⼀个联合索引,那么在写⼀个sql时就⼀定要提供a字段的条件,这样才能⽤到联合索引,这是由于在建⽴a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段 从左往右去⽐较⼤⼩进⾏排序的,所以如果想要利⽤B+树进⾏快速查找也得符合这个规则。

索引失效

  • 对于多列索引,过滤条件要使用索引,必须按照索引建立的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
  • 计算、函数、类型转换(自动或手动)导致索引失效
  • 范围条件右边的列索引失效
  • 不等于( != 或者 <> )索引失效
  • is null 可以使用索引,is not null 无法使用索引
  • like 以通配符 % 开头索引失效
  • OR 前后存在非索引的列,索引失效
  • select *

Explain

列名描述
idid相同由上而下的执行;id不同,id越大优先级越高。
select_type查询的类型,主要是用于区别:普通查询、联合查询、子查询等复杂查询。
table表名
partitions匹配的分区信息
type针对单表的查询⽅式,其参数提供了判断查询是否高效的重要依据。效率从好到差依次是:system>const>eq_ref>ref>range>index>ALL。
possible_keys可能⽤到的索引
key实际上使⽤的索引
key_len实际使⽤到的索引⻓度
ref当使⽤索引列等值查询时,与索引列进⾏等值匹配的对象信息
rows预估的需要读取的记录条数
filtered某个表经过搜索条件过滤后剩余记录条数的百分⽐
Extra⼀些额外的信息,⽐如Using filesort、Using index、Using where

SQL优化思路

  • 开启慢日志进行慢sql筛查
  • Join语句的优化
    • 永远用小结果集驱动大的结果集
    • 保证Join语句中被驱动表上Join条件字段已经被索引
  • ORDER BY关键字优化
    • ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序
    • 尽可能在索引列上完成排序操作,遵照最佳左前缀创建索引
  • GROUP BY关键字优化
    • GROUP BY实质是先排序后进行分组,遵照最佳左前缀创建索引
    • WHERE性能高于HAVING,能写在WHERE限定的条件,就不要去HAVING限定了
  • 查询执行计划,看索引索引是否失效
    • 全值匹配
    • 最佳左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列
    • 不在索引列上做任何操作(计算,函数,(自动or手动)类型转换),会导致索引失效而转向全表扫描
    • 存储引擎不能使用索引中范围条件右边的列
    • 尽量使用覆盖索引(只访问索引的查询(索引列或查询列一致)),减少select *
    • MySQL在使用不等于(!= 或者 <>)的时候,无法使用索引会导致全表扫描
    • is null,is not null也无法使用索引
    • like 以通配符开头(%abc…),MySQL索引失效,会变成全表扫描的操作
    • 因此,可以使用like abc%,或者使用覆盖索引解决like '%字符串%'索引失效的问题
    • 字符串不加单引号,索引失效
    • 少用or,用它来连接时,会导致索引失效

MyISAM和InnoDB的区别

  • MyISAM:
    • 不⽀持事务,但是每次查询都是原⼦的;
    • ⽀持表级锁,即每次操作是对整个表加锁;
    • 存储表的总⾏数;
    • ⼀个MYISAM表有三个⽂件:索引⽂件、表结构⽂件、数据⽂件;
    • 采⽤⾮聚集索引,索引⽂件的数据域存储指向数据⽂件的指针。辅索引与主索引基本⼀致,但是辅索引不⽤保证唯⼀性。
  • InnoDb:
    • ⽀持ACID的事务,⽀持事务的四种隔离级别;
    • ⽀持⾏级锁及外键约束:因此可以⽀持写并发;
    • 不存储总⾏数;
    • ⼀个InnoDb引擎存储在⼀个⽂件空间(共享表空间,表⼤⼩不受操作系统控制,⼀个表可能分布在多个⽂件⾥),也有可能为多个(设置为独⽴表空,表⼤⼩受操作系统⽂件⼤⼩限制,⼀般为 2G),受操作系统⽂件⼤⼩的限制;
    • 主键索引采⽤聚集索引(索引的数据域存储数据⽂件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使⽤⾃增主键,防⽌插⼊数据时,为维持B+树结构,⽂件的⼤调整。

事务

​ 事务是并发控制的基本单位。一组操作要么都执行要么都不执行。

​ 维护数据库数据的一致性,每个事务结束时,保证数据的一致性。

事务四大特性

​ ACID

  • 原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
    • 实现原理undo log回滚日志。undo log,实现原子性的关键,当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或者调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。Undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作。
  • 一致性(consistency):事务将数据库从一种状态转变为另一种状态,保证事务执行前后一致。原子性,隔离性,持久性都是为了保证数据库状态的一致性。(比如:A向B转账,不可能A扣了钱,B却没有收到)
  • 隔离性(isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • 持久性(durability):一个事务一旦被提交了那么对数据库中的数据改变是永久的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
    • 为了提升性能InnoDB提供了缓冲池BufferPool。读数据:先从缓冲池中读取,如果缓冲池中没有,再从磁盘读取放入缓冲池。写数据:先写入缓冲池,缓冲池中的数据会定期同步到磁盘上(数据写到缓冲池,但还未记录到磁盘的时候,那么当前的数据库页就为脏页,将脏页记录到磁盘的过程为刷脏)。但是在mysql宕机的时候可能会丢失数据。所以使用了redo log保证数据不丢失。l redo log中只包含真正需要写入的部分,无效IO大大减少。redo log:物理日志,是InnoDB存储引擎实现的,内容基于磁盘的page,保证MySQL宕机也不影响持久性。事务提交就会同步redo log到磁盘,刷脏的时间不一定。

Spring

什么是Spring

​ Spring是一种轻量级的面向切面和控制反转开源框架,提高开发效率与维护性优点。

​ Spring能帮我们解耦合,帮助管理对象之间的依赖关系。

​ Spring 中 AOP帮助我们提高代码复用性、扩展性。(安全、事务、权限等)

​ Spring 不用关心对象的创建,只需要配置好配置文件即可,简化了开发。

​ Spring是一个简单并强大的声明式事务管理。

IOC

​ IOC:控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。

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

DI

​ 简单来说,在Spring创建对象的同时,为其属性赋值,称之为依赖注入。

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

​ 例子: 当某个 Java实例需要另一个 Java 实例时,传统的方法是由调用者创建被调用者的实例(new ),而使用 Spring框架后,被调用者的实例不再由调用者创建,而是由 Spring 容器创建,这称为控制反转。

​ Spring 容器在创建被调用者的实例时,会自动将调用者需要的对象实例注入给调用者,这样,调用者通过 Spring 容器获得被调用者实例,这称为依赖注入。

注入方式

  • Setter方法注入
  • 构造器注入
  • 自动注入(autowire)
    • byName
    • byType
  • 注解自动注入
    • @Autowired基于类型自动注入
    • @Resoure基于名称自动注入

AOP

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

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

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

动态代理

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

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

  • JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;
  • InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
  • 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

静态代理与动态代理区别

​ 区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能(他会在编译阶段将切面织入到Java字节码中,运行的时候就是增强之后的对象),但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

AOP里面的几个名词的概念

  • 连接点(Join point):指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。

  • 切面(Aspect):被抽取出来的公共模块。Aspect切面可以看成 Pointcut切点和 Advice通知的结合,一个切面可以由多个切点和通知组成。在Spring AOP中,切面可以在类上使用 @AspectJ 注解来实现。

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

    • 切点分为execution方式和annotation方式。execution方式可以用路径表达式指定对哪些方法拦截,比如指定拦截add、search。annotation方式可以指定被哪些注解修饰的代码进行拦截。
  • 通知(Advice):指要在连接点(Join Point)上执行的动作,即增强的逻辑,比如权限校验和、日志记录等。通知有各种类型,包括Around、Before、After、After returning、After throwing。

  • 目标对象(Target):包含连接点的对象,也称作被通知(Advice)的对象。 由于Spring AOP是通过动态代理实现的,所以这个对象永远是一个代理对象。

  • 织入(Weaving):通过动态代理,在目标对象(Target)的方法(即连接点Join point)中执行增强逻辑(Advice)的过程。

  • 引入(Introduction):添加额外的方法或者字段到被通知的类。Spring允许引入新的接口(以及对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

  • 代理(Proxy):将通知织入到目标对象之后形成的代理对象
    在这里插入图片描述

Advice的类型

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

Advice的执行顺序

  • 没有异常:around before advice->before advice->target method 执行->after advice->around after advice->afterReturning advice
  • 有异常:around before advice->before advice->target method 执行->after advice->around after advice->afterThrowing advice->java.lang.RuntimeException:异常发生

BeanFactory和ApplicationContext

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

​ BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:资源文件访问(ClassPathXmlApplicationContext),提供在监听器中注册bean的事件。它是在容器启动时,一次性创建了所有的Bean。在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。

Bean生命周期

​ 从对象的创建到销毁的过程。而Spring中的一个Bean从开始到结束经历很多过程,但总体可以分为六个阶段Bean定义、实例化、属性赋值、初始化、生存期、销毁。

​ 首先进行实例化bean对象,然后进入对bean的属性进行设置,然后对BeanNameAware(让Spring容器获取bean的名称)

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

​ 然后处理Aware接口

​ 实现BeanNameAware接口,让Spring容器获取bean的名称

​ 实现BeanFactoryAware接口,让bean的BeanFactory调用容器的服务

​ 实现ApplicationContextAware接口,让bean当前的applicationContext可以调用Spring容器的服务

​ 实现了BeanPostProcessor接口

​ 配置init-method属性就会自动调用初始化方法

​ 清理-销毁,调用destory()方法结束生命周期

bean的作用域

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

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

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

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

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

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

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

Spring的自动装配

  • xml配置的自动装配

    • no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
    • byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
    • byType:通过参数的数据类型进行自动装配。
    • constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
    • autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
  • 注解的自动装配

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

      如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;

      如果查询的结果不止一个,那么@Autowired会根据名称来查找;

      如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。

@Autowired和@Resource之间的区别

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

Spring事务的种类

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

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

Spring的事务传播机制

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

  • propagation_required:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
  • propagation_requires_new:无论当前存不存在事务,都创建新事务进行执行。
  • propagation_supports:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。
  • propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_nested:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。
  • propagation_mandatory:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
  • propagation_never:以非事务方式执行,如果当前存在事务,则抛出异常。

@Transactional

  • 参数一: propagation(REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、 NOT_SUPPORTED、NEVER、NESTED)
  • 参数二:事物超时设置: timeout
  • 参数三:事务隔离级别:isolation
  • 参数四: readOnly 属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,
  • 参数五:rollbackFor指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
  • 参数六: rollbackForClassName @Transactional(rollbackForClassName=“RuntimeException”)
  • 参数七:noRollbackForClassName @Transactional(noRollbackForClassName=“RuntimeException”)
  • 参数八:noRollbackFor @Transactional(noRollbackFor=RuntimeException.class)

事务不生效

  • 访问权限问题(必须public,动态代理需要)
  • 方法用 final 修饰(动态代理需要)
  • 方法内部调用,也就是发生自调用( “动态代理”,这意味着要生成一个代理类,那么我们就不能在一个类内直接调用事务方法。解决方法可以自己注入自己,在通过注入的对象调用)
  • 未被 spring 管理
  • 多线程调用
  • 表不支持事务
  • 未开启事务

事务不回滚

  • 错误的传播特性
  • 自己吞了异常
  • 手动抛了非RuntimeException的异常(因为 spring 事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。例如如果发生了checkedExcetions ,如fileNotfundException 则不会回滚
    所以自定义异常时,还是继承RuntimeException)
  • 异常不匹配

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

  • 工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
  • 单例模式:Bean默认为单例模式
  • 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

SpringMVC

mvc:是一种设计模式

m:model模型层,主要用于数据封装

v:view视图层,用于数据的显示

c:controller控制层,用于逻辑控制操作

优点:有利于开发中的分工,有利于组件的重用,解耦合,在系统中并行开发,提升开发的效率。

mvc执行流程

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

如何解决get和post乱码问题

  • 解决 post 请求乱码:我们可以在 web.xml 里边配置一个 CharacterEncodingFilter 过滤器。 设置为 utf-8.
  • 解决 get 请求的乱码:有两种方法。对于 get 请求中文参数出现乱码解决方法有两个:
    • 修改 tomcat 配置文件添加编码与工程编码一致。
    • 另 外 一 种 方 法 对 参 数 进 行 重 新 编 码 String userName = New String(Request.getParameter(“userName”).getBytes(“ISO8859-1”), “utf-8”);

全局异常处理

  • @ControllerAdvice标识一个类是全局异常处理类。
  • @ExceptionHandler标识一个方法为全局异常处理的方法。完成异常处理逻辑。

自定义拦截器

​ 自定义的Interceptor类要实现了Spring的HandlerInterceptor接口。

​ HandlerInterceptor接口中定义了三个方法,我们就是通过这三个方法来对用户的请求进行拦截处理的。

  • preHandle:Controller方法处理请求前执行,根据拦截器定义的顺序,正向执行。

  • postHandle:Controller方法处理请求后执行,根据拦截器定义的顺序,逆向执行。需要所有的preHandle方法都返回true时才会调用。

  • afterCompletion:View视图渲染后处理方法:根据拦截器定义的顺序,逆向执行。preHandle返回true就会调用。

    编写完拦截器后,我们还需要编写MVC配置类。继承WebMvcConfigurer,重写addInterceptors,添加自定义拦截器,设置拦截路径及不拦截路径。

Mybatis

什么是Mybatis?

Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。

工作原理

  • 读取 MyBatis 配置文件:mybatis-config.xml为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
  • 加载映射文件:映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL语句,需要在 MyBatis 配置文件 mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
  • 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
  • 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法,是一个既可以发送sq执行并返回结果的,也可以获取mapper的接口。
  • Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SaSession 传递的参数动态地生成需要执行的SQL 语句,同时负责查询缓存的维护。
  • MappedStatement 对象: 在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
  • 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本教据类型和 POJO 类型。输入参教映射过程类似于JDBC 对 preparedStatement 对象设置参数的过程。
  • 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型,输出结果映射过程类似于JDBC 对结果集的解析过程。

Mybatis编程步骤

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

#{}和${}的区别:

​ #{}是预编译处理,${}是字符串替换。

​ mybatis在处理#{}时,会将sql中的#{}代替为?号,再调用preparedStatement的set方法赋值;

​ mybatis在处理 时,会把 {}时,会把 时,会把{}替换成变量值。

​ 使用#{}可以防止sql注入,提高系统安全性。

mapper接口调用规范

  • Mapper接口方法名和mapper.xml中定义的每个sql的id一致。
  • Mapper接口方法的输入类型和mapper.xml中定义的每个sql的paramterType类型相同。
  • Mapper接口方法的输出类型和mapper.xml中定义的每个sql的resultType类型相同。
  • Mapper.xml文件中的anamespace是mapper接口的类路径名。

一级缓存和二级缓存

  • 一级缓存:作用域是Session,默认开启
  • 二级缓存:是mapper级别的,第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放到该mapper对应的二级缓存区域。第二次调用namespace的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存里取出结果。

Insert插入操作时返回主键ID

<insert id="方法名" parameterType="实体类路径" keyProperty="uuid" useGeneratedKeys="true"></insert>

​ keyProperty表示返回的id要保存到对象的哪个属性中;

​ useGeneratedKeys表示主键id为自动增长模式;

​ insert 方法总是返回一个int值 ,这个值代表的是插入的行数。 如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。

一对一

<association></association> 

一对多

<collection></collection>

标签

​ 除了常见的select|insert|updae|delete标签外,还、、、、,加上动态sql的9个标签 trim | where | set | foreach | if | choose | when | otherwise | bind 等,其中 为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。

springboot

​ Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用
Spring 的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。

  • 快速开发,快速整合,配置简化、内嵌服务容器

核心注解

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

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:开启自动配置的功能。
  • @ComponentScan:Spring组件扫描。

SpringBoot的自动配置原理是什么

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

  • 从配置文件META_INF/Spring.factories加载可能用到的自动配置类
  • 去重,并将exclude和excludeName属性携带的类排除
  • 过滤,将满足条件(@Conditional)的自动配置类返回

跨域

​ 直接使用:@CrossOrigin

​ 或者@Configuration

​ 实现WebMvcConfigurer重写 addCorsMapperings方法

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")//项目中的所有接口都支持跨域
                .allowedOrigins("*")//所有地址都可以访问,也可以配置具体地址
                .allowCredentials(true) //是否允许请求带有验证信息
                .allowedMethods("*")//"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"
                .allowedHeaders("*").maxAge(3600);// 跨域允许时间
    }
}

注解

  • springboot
    • @SpringBootApplication(@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan)
    • @SpringBootConfiguration:替代@Configuration
  • spring
    • @Component:泛指各种组件(@Controller、@Service、@Repository都可以称为@Component)
    • @Configuration:声明当前类为配置类
    • @Bean:声明bean
    • @Scope:设置bean的作用域
    • @Import:导入额外的配置文件
    • @EnableTransactionManagement:开启注解式事务的支持
    • @Transactional:开启事务
    • @Autowired:注入bean,类型
    • @Resource:注入bean,名称
    • @Primary:声明默认bean
    • @PostConstruct:bean的属性都注入完毕后,执行注解标注的方式进行初始化工作
    • @Lazy:使bean懒加载,取消bean预初始化
    • @Value:${}是去找外部配置的参数,将值赋过来
  • aop
    • @Aspec: 声明一个切面
    • @After:在方法执行之后执行(方法上)
    • @Before: 在方法执行之前执行(方法上)
    • @Around: 在方法执行之前与之后执行(方法上)
    • @PointCut: 声明切点
  • mvc
    • @Controller
    • @ResponseBody:返回json数据
    • @RestController:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合
    • @RequestMapping:用于映射web请求,包括访问路径和参数。
    • @RequestBody:请求参数为json数据
    • @PathVariable:用于接收路径参数
    • @ControllerAdvice:
      • 全局异常处理(常用)
      • 全局数据绑定
      • 全局数据预处理
    • @ExceptionHandler:用于全局处理控制器里的异常处理

SpringBoot事物的使用

​ 首先使用注解EnableTransactionManagement开启事物之后,然后在Service方法上添加注解Transactional便可。

多环境配置

​ 配置文件配置:spring.profiles.active

​ 可以在项目中配置多个application配置文件,根据应用场景不同,通过application-中的来决定启用某个配置文件。

常用starter

  • spring-boot-starter 核心启动器
  • spring-boot-starter-web
  • spring-boot-starter-test
  • spring-boot-starter-jdbc
  • spring-boot-starter-amqp
  • spring-boot-starter-data-redis
  • spring-boot-starter-data-elasticsearch
  • spring-boot-starter-data-mongodb
  • spring-boot-starter-freemarker
  • spring-boot-starter-mail
  • spring-boot-starter-aop

Linux

命令作用
pwd查看当前目录的路径
ls查看目录下的文件
cd切换目录
mkdir创建目录 -p级联创建
rmdir删除目录
touch创建文件
rm删除命令 -f 强制删除 -r递归删除
echo输出命令,可以输入变量,字符串的值
>和>>输出符号,将内容输出到文件中,>表示覆盖(会删除原文件内容) >>表示追加
cat查看文件的所有内容
more分页查看文件内容
tail从文件的末尾查看文件内容。-n n是一个正整数,表示查看文件的后n行数据。-f 动态的查看文件的最后几行内容
cpcp [参数] 原文件路径 目标文件路径。拷贝命令
mv移动命令,它可以移动文件,也可以给文件改名
free查看系统内存的命令
chmod用3个数字来设置文件或目录的权限,第1个数字表示用户权限,第2数字表示用户组权限,第3个数字表示其 他用户权限

Git

在这里插入图片描述

Redis

​ remote dictionary service:是用C语言开发的一个开源的基于内存的高性能键值对(key-value)数据库,它通过提供多种键值数据类型来适应不同场景下的存储需求。

应用场景

  • 缓存(数据查询、短连接、新闻内容、商品内容等等)(使用最多)
  • 分布式集群架构中的session分离
  • 任务队列(秒杀、抢购、12306等等)
  • 应用排行榜
  • 网站访问统计
  • 数据过期处理(可以精确到毫秒)时效性信息控制,如验证码控制、投票控制等
  • 分布式锁
  • 共同好友推荐

数据类型

  • String
    • incr key:自增。数据库主键自增、网站访问统计
    • setex key seconds value:设置带有过期时间的键值。验证码
    • 常用做缓存:key命名(表名:主键名:主键值:字段名),值(对象的json串)
  • Hash
    • 常用做缓存,较String来说,更方便于对象属性的更改,省去序列化步骤。
  • List
    • lpush、rpop:可实现简单的消息队列
    • lrange key start stop:朋友圈点赞,按顺序显示点赞的朋友(存放有序)
  • Set
    • sinterstore:交集。你可能认识/共同好友推荐
    • UV(网站被不同用户访问的次数)统计,统计cookie,set去重
    • IP(网站被不同IP地址访问的总次数)同上
  • Sorted_set
    • 排行榜

持久化

RDB

​ RDB(Redis DataBase)持久化方式能够在指定的时间间隔能对你的数据进行快照存储。在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。

执行方式

​ 当Redis需要保存dump.rdb文件时,服务器执行以下操作:

  • Redis调用forks(),同时拥有父进程和子进程。
  • 子进程将数据集写入到临时的RDB文件中。
  • 当子进程完成对RDB文件的写入时,Redis会用新的RDB文件替换旧的RDB文件,并删除旧的RDB文件。
触发机制
  • save:同步操作,阻塞所有客户端的请求。
  • bgsave:执行一个异步操作,以RDB文件的方式保存所有数据的快照。
  • save配置:你可以通过配置文件对 Redis 进行设置,让它在 N 秒内数据集至少有 M 个改动这一条件被满足时,自动进行数据集保存操作。同bgsave相同,都是异步操作。
优缺点

​ 优点:二进制紧凑文件,省空间。可保存不同时间的数据集。回复速度快,适用于灾难恢复。

​ 缺点:耗性能、不可精确控制、会丢失数据。

AOF

​ AOF(Append Only File),打开AOF后, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

AOF策略
  • always:每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。
  • everysec:每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。 (默认,兼顾速度与安全)
  • no:从不自己fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。
AOF重写

​ 在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 bgrewriteaof 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。Redis 2.4 则可以通过配置自动触发 AOF 重写。

​ 需要配置以下参数:
在这里插入图片描述

优缺点

​ 优点:让redis更加耐久,使用默认策略出现故障也最多丢失1秒数据,且文件可读性强。

​ 缺点:根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。

如何选择

​ 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

​ 有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

​ 由于 AOF 持久化方式在重启加载数据时效率远远不如 RDB 方式,所以 Redis 在4.0版本后引入了混合持久化方式,配置项为 aof-use-rdb-preamble,yes开启,策略为在生成或写入 AOF 文件时,将 RDB 数据写在前面,AOF 数据追加到后面,在每次启动时先加载 RDB,再加载 AOF。

redis事务

​ Redis执行指令过程中,多条连续执行的指令不得被干扰,打断,插队。一个队列中,一次性、顺序性、排他性的执行一系列命令。

  • 开启事务:multi

  • 执行事务:exec

  • 取消事务:discard

    注意:

  • 如果定义的事务中所包含的命令存在语法错误,整体事务中所有命令均不会执行。包括那些语法正确的命令。

  • 定义事务的过程中,命令执行出现错误能够正确运行的命令会执行,运行错误的命令不会被执行。已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。

锁(监控)

​ 多个客户端有可能同时操作同一组数据,并且该数据一旦被操作修改后,将不适用于继续操作。在操作之前锁定要操作的数据,一旦发生变化,终止当前操作。

  • watch key1 [key2……]:对 key 添加监视锁,在执行exec前如果key发生了变化,终止事务执行。
  • Unwatch:取消对所有 key 的监视。

删除策略

​ 对已经过期数据进行删除的策略。

  • 定时删除:创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作。
  • 定期删除:每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
  • 惰性删除:假如你的过期key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被redis给删除掉。

淘汰策略

​ 到达Redis的最大内存使用量的时候,这个时候就需要对Redis存储的数据进行清理,释放内存,将Redis内存使用量保持在容量限制以下。(也叫逐出算法)
在这里插入图片描述

  • 检测易失数据(可能会过期的数据集server.db[i].expires )
    • volatile-lru:挑选最近最少使用的数据淘汰
    • volatile-lfu:挑选最近使用次数最少的数据淘汰
    • volatile-ttl:挑选将要过期的数据淘汰
    • volatile-random:任意选择数据淘汰
  • 检测全库数据(所有数据集server.db[i].dict )
    • allkeys-lru:挑选最近最少使用的数据淘汰
    • allkeys-lfu:挑选最近使用次数最少的数据淘汰
    • allkeys-random:任意选择数据淘汰
  • 放弃数据驱逐
    • no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)

高可用

主从模式

​ 主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制。

​ 主从复制包括全量复制,增量复制两种。一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制,反之就是增量复制

全量复制
  • slave发送sync命令到master。
  • master接收到SYNC命令后,执行bgsave命令,生成RDB全量文件。
  • master使用缓冲区,记录RDB快照生成期间的所有写命令。
  • master执行完bgsave后,向所有slave发送RDB快照文件。
  • slave收到RDB快照文件后,载入、解析收到的快照。
  • master使用缓冲区,记录RDB同步期间生成的所有写的命令。
  • master快照发送完毕后,开始向slave发送缓冲区中的写命令;
  • salve接受命令请求,并执行来自master缓冲区的写命令

redis2.8版本之后,已经使用psync来替代sync,因为sync命令非常消耗系统资源,psync的效率更高。

增量复制

​ slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发增量复制。
​ 当master节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 Master节点上调用的每一个命令会使用replicationFeedSlaves()来同步到Slave节点。执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。

哨兵模式

​ 哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。

简单来说,哨兵模式就三个作用:

  • 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
  • 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
  • 哨兵之间还会相互监控,从而达到高可用。

哨兵的工作模式如下:

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

一个Master不能正常工作时,哨兵会开始一次自动故障迁移。

  • 它会将失效的Master的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master。
  • 当客户端试图连接失效的Master时,集群也会向客户端返回新的Master地址,使得集群可以使用现在的Master替换失效的Master。
  • Master和Slave服务器切换后,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件都会发生相应的改变,即,Master主服务器的redis.conf配置文件中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换。

集群模式

​ 哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。Cluster集群就解决了这个问题,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。
​ 分布式存储采用的分布式算法是Hash Slot插槽算法。

​ 插槽算法把整个数据库被分为16384个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。

缓存穿透

​ 请求查询的数据在数据库压根儿就不存在,出现非正常URL访问,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到DB数据库上面。大量的无意义的查询落在DB上,明显会增加数据库的压力,严重的可能会引起数据库宕机。

​ 解决方案:

  • 查询不到数据时(即返回null),也要将其塞入缓存中,设定短时限,例如30-60秒,最高5分钟。(临时处理方案)。
  • 设置白/黑名单(提集合set/bitmaps/布隆过滤器)。

缓存击穿

​ 在平常高并发的系统中,大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到DB数据库上面去。某一时刻数据库请求查询量过大,压力剧增,严重可能会引起数据库宕机。

​ 解决方案:

  • 加长热点数据的过期时间(淘宝活动主打产品)(提前预备)。
  • 实时监控热点key,调整过期时间(现场调整)。
  • 启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失(同时调整)。
  • 分布式锁。本质上看,其实这是高并发请求对应的多线程同时去查询数据库的这条数据。所以我们可以在第一个查询不到数据的请求上使用一个互斥锁来锁住它,其他的线程走到这一步拿不到锁就等待,直到第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有了缓存了,就直接走缓存(迫不得已)。

缓存雪崩

​ 当某一时刻发生大规模的缓存失效的情况(较短的时间内,缓存中较多的key集中过期),比如缓存服务器宕机了,会有大量的请求进来直接打到DB上面,结果就是DB扛不住,直接宕掉。

​ 解决方案:

  • 请求限流/服务降级。
  • Redis主从集群部署、设定持久化策略迅速恢复。
  • 根据业务设计不同key的不同过期时间,同类型的key固定时间+随机值间,超热数据使用永久key。
  • 淘汰策略LRU/LFU。
  • 读取key锁(×)!

MySQL与Redis 如何保证双写一致性

  • 延时双删
    • 写操作进来后,先删除缓存,在更相信数据库,休眠一会(比如1s),再删除缓存。这样第一次删除缓存后,有可能另外一个读线程读数据,会把未更新的脏数据从新写到缓存中。那么第一次延迟删除就会在写操作后,删除脏数据。(这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。)
  • 删除缓存重试机制
  • 因为延时双删可能会存在第二步的删除缓存时因为出现异常导致失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次,保证删除缓存成功就可以了。 所以可以引入删除缓存重试机制。
  • 写请求更新数据库
  • 缓存因为某些原因,删除失败
  • 把删除失败的key放到消息队列
  • 消费消息队列的消息,获取要删除的key
  • 重试删除缓存操作
  • 读取biglog异步删除缓存
    • 重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的binlog来异步淘汰key。
    • 以mysql为例:可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。

为什么Redis 6.0 之后改多线程

​ Redis6.0之前,Redis在处理客户端的请求时,包括读socket、解析、执行、写socket等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。

​ 6.0后redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

分布式锁

  • 加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁
  • 解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁

存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。

解决方案:设置锁超时时间

  • 设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间

存在问题:setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。

解决方案:redis的set命令支持在获取锁的同时设置key的过期时间

  • 使用set命令加锁并设置锁过期时间:命令格式:set <lock.key> <lock.value> nx ex

存在问题:

① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。

② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。

解决方案:

可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性(if redis.call(‘get’,KEYS[1]) == ARGV[1] then return redis.call(‘del’,KEYS[1]) else return 0 end;)

if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1{ 
    //加锁 
    try { 
        do something //业务处理 
	} catch(){ } 
    finally { 
        //判断是不是当前线程加的锁,是才释放 
        if (uni_request_id.equals(jedis.get(key))) { 
            jedis.del(key); //释放锁 
        } 
    } 
}

以上方式加上lua脚本后比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题(实际上,估算个业务处理的时间,一般没啥问题了)。

  • 锁续期(redisson):如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在JAVA的Redisson包中有一个"看门狗"机制,已经帮我们实现了这个功能。
    • redisson在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁key的生存时间
    • 线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库,整个过程有类似守护线程的“看门狗”进行监控、续期。
    • 线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
    • 看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。
    • redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。

redisson分布式锁的关键点:

  1. 对key不设置过期时间,由Redisson在加锁成功后给维护一个watchdog看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效
  2. 通过Lua脚本实现了加锁和解锁的原子操作
  3. 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。

RabbitMQ

​ MQ全称为Message Queue,”是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者模型。适用业务场景:解耦、异步、流量削峰。

​ RabbitMQ特点:基于AMQP协议、通过插件还支持JMS标准。高并发、高性能、高可用强大的社区支持。支持开发语言众多。

​ 在业务服务模块中解耦、异步通信、高并发限流、超时业务、数据延迟处理等都可以使用RabbitMQ。

核心组件

在这里插入图片描述

  • Broker:Broker(中间件)简单理解就是RabbitMQ服务器。
  • vhost:虚拟主机。用于多租户场景,提供权限范围控制,创建连接时可指定虚拟机和相对应的用户名密码。
  • Connection:链接。无论是生产者还是消费者,都需要和 Broker 建立连接,是一条 TCP 连接 ,一个生产者或一个消费者与 Broker 之间只有一个 Connection,即只有一条 TCP 连接。连接通常是长连接。
  • channel:通道。一个TCP连接下包含多个通道,实现共用TCP、减少TCP创建和销毁的开销。
  • Exchange:交换机。
    • fanout 交换机就跟广播一样,对消息不作选择地分发给所有绑定的队列。
    • 在 direct 模式里,交换机和队列之间绑定了一个 key(这个key就是Binding key),只有消息的 Routing key 与Binding key 相同时,交换机才会把消息发给该队列。
    • topic模式即主题模式,通过模式匹配来路由到队列。(通配符匹配)
    • headers不常用,headers交换机是通过Headers头部来将消息映射到队列的。
    • default Exchange,默认交换机的名字是空字符串。如果在发送消息时不指定交换机的名称,那么就会发到"默认交换机"上。默认的Exchange不进行Binding操作。
  • Queue:队列。消息队列,保存消息的地方。包含属性如下:
    • Name
    • Durable(消息代理重启后,队列依旧存在)是否持久化
    • Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除)是否独享、排外的
    • Auto-delete(当最后一个消费者退订后即被删除)自动删除
    • Arguments(队列的其他属性参数)
  • Routing key:路由规则。消息头的属性,生产者将消息发送到交换机时,会在消息头上携带一个 key,这个 key就是routing key,来指定这个消息的路由规则。
  • Binding:绑定,可以理解成一个动词,它的作用就是把exchange和queue按照路由规则绑定起来。
  • Binding key:在绑定Exchange与Queue时,一般会指定一个binding key,生产者将消息发送给Exchange时,消息头上会携带一个routing key,当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。

RabbitMQ的模式有哪几种

​ 简单队列模式、工作队列模式、订阅模式(fanout)、路由模式(direct)、主题模式(topic)、RPC模式、发布确认模式。

简单应用(以路由模式为例)

  • 发布方
public static final String DIRECT_EXCHANGE_NAME = "direct.exchange";
public static final String DIRECT_ROUTING_KEY_NAME = "direct.routing.key";

@ApiOperation("路由模式(direct)发送消息")
@GetMapping("/sendDirectMessage/{msg}")
public String sendDirectMessage(@PathVariable String msg) {

    //发送消息到交换机
    //参数: 交换机、路由键、消息体
    rabbitTemplate.convertAndSend(DirectConfig.DIRECT_EXCHANGE_NAME, DirectConfig.DIRECT_ROUTING_KEY_NAME, msg);
    return "success";
}
  • 订阅方
/**
 * 步骤:
 *      1.配置交换机
 *      2.配置队列
 *      3.绑定队列到交换机
 */
@Configuration
public class DirectConfig {

    public static final String DIRECT_EXCHANGE_NAME = "direct.exchange";
    public static final String DIRECT_QUEUE_NAME = "direct.queue";
    public static final String DIRECT_ROUTING_KEY_NAME = "direct.routing.key";


    //配置交换机
    @Bean
    DirectExchange directExchange() {

        //参数: 交换机名、持久化、自动删除、参数
        //return new DirectExchange(DIRECT_EXCHANGE_NAME,true,false,null);
        return new DirectExchange(DIRECT_EXCHANGE_NAME);
    }

    //配置队列
    @Bean
    Queue directQueue() {

        //参数: 队列名、持久化、是否独占、自动删除、参数
        //return new Queue(DIRECT_QUEUE_NAME,true,false,false,null);
        return new Queue(DIRECT_QUEUE_NAME);
    }

    //绑定队列到交换机
    @Bean
    Binding directBinding() {
        //参数:队列名、目的类型(使用默认)、交换机名、路右键名、参数
        //return new Binding(DIRECT_QUEUE_NAME, Binding.DestinationType.QUEUE, DIRECT_EXCHANGE_NAME,DIRECT_ROUTING_KEY_NAME,null);
        return BindingBuilder.bind(directQueue()).to(directExchange()).with(DIRECT_ROUTING_KEY_NAME);
    }

}
@Component
//也可应用到方法上
@RabbitListener(queues = DirectConfig.DIRECT_QUEUE_NAME)
public class DircetRecive {

    private static final Logger log = LoggerFactory.getLogger(DircetRecive.class);

    @RabbitHandler
    public void recive(String msg){

        log.info("我收到的消息是:{}",msg);
    }

}

消息可靠投递

​ 配置文件配置:

spring:
  # 添加配置
  rabbitmq:
    # 发布方return回调确认开启
    publisher-returns: true
    # 开启是否达到交换机的回调(相关联的)
    publisher-confirm-type: correlated

​ 启动MQ的回调ConfirmCallback和ReturnCallback。

​ ConfirmCallback是消息无论有没有到达交换机都会触发,到达交换机ack为true、未到达为false。

​ ReturnCallback是exchange到queue成功则不触发,不成功则触发。(该回调多出现在开发阶段)

  • 在发送前,对消息信息(包括交换机、路由键)都持久化到数据库且设置状态为-1尝试次数为0。
  • 使用自定义的RabbitTemplate,配置ConfirmCallback和ReturnCallback。
  • ConfirmCallback中ack为true,则改变状态为1,代表发送成功。
  • ReturnCallback监听到达交换机但未到达queue的信息,这种情况多出现在开发,路由键配置错误的情况,新增消息信息到数据库,状态为4,方便程序员/运维人员查看。
  • 配置定时任务,查询数据库中状态为-1的消息,重新进行发送。每次尝试次数加1,如果尝试次数超过5,不再查询(运维人员进行处理)。
  • 以上过程+运维人工兜底,保证消息投递的可靠性。

消费端消息重试

​ 在默认情况下,消息消费时出现异常情况,消息会重新入队进行再次消费。如果异常不能及时得以解决,就会出现消息会无限制的投递,占用服务器资源。此时可以开启消费端,消息重试机制,对消息重试进行控制。

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          #开启消息重发控制
          enabled: true
          #重发次数
          max-attempts: 3
          #间隔时间
          initial-interval: 3000

消费端手动应答

​ 根据不同的业务场景,也可以通过编码的方式对消费端消息进行手动应答,来处理异常问题。

spring:
  rabbitmq:
    listener:
      simple:
        # 开启手动ack
        acknowledge-mode: manual
@Component
public class DirectReliableRecive {

    private static final Logger log = LoggerFactory.getLogger(DirectReliableRecive.class);

    @RabbitListener(queues = DirectReliableConfig.DIRECT_RELIABLE_QUEUE_NAME)
    public void revice(Message message,Channel channel) {

        try {
            log.info("我收到的消息是:{}",new String(message.getBody()));
            int i = 1/0;
            //响应成功   第一个参数:消息标识   第二个参数: 是否多个应答
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //响应不成功    第一个参数:消息标识   第二个参数: 是否多个应答    第三个参数:是否重新入队
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
                //消息拒绝   第一个参数:消息标识    第二个参数:是否重新入队
                //channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

}

死信队列

​ 死信队列:DLX,dead-letter-exchange,利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。

​ 死信的来源:

  • 消息被拒绝(basic.reject或basic.nack)并且requeue=false
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加数据到mq中)

​ 死信的处理方式

  • 丢弃,如果不是很重要,可以选择丢弃
  • 记录死信入库,然后做后续的业务分析或处理
  • 通过死信队列,由负责监听死信的应用程序进行处理

​ 根据消息TTL过期会变为死信的特性,可在延时任务中进行使用(取消未付款订单),在消息发送是指定TTL,不做正常消费。等其变为死信,在数据库中,查询该消息的状态,根据业务进行状态变更。

​ RabbitMQ中,安装延时队列插件,也可以直接使用延时队列。

消息幂等性处理

​ 在消息的可靠投递中,一个消息发送成功但还没有来的及进行回调处理。定时任务此时查询到该消息状态为-1,则进行了重新发送。那么这个正常的消息,就被发送了两次。出现了幂等性问题。

​ 为了解决幂等性问题,在消息发送时,在Message对象中存储一个消息唯一标识符。

​ 在消费端消费的时候,首先取出标识符,查看在redis中有没有,如果没有就存入到redis中,再进行业务处理。如果有则代表该消息已经被消费过,直接return。

消费端限流(削峰)

spring:
  rabbitmq:
    listener:
      simple:
        # 公平分发
        prefetch: 1
        # 开启手动ack
        acknowledge-mode: manual
        max-concurrency: 1 #每次最多拿一条消息

Nginx

​ Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发。

Nginx怎么处理请求的?

​ nginx接收一个请求后,首先由listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址。

server {            						# 第一个Server区块开始,表示一个独立的虚拟主机站点
    listen       80;      					# 提供服务的端口,默认80
    server_name  localhost;       			# 提供服务的域名主机名
    location / {            				# 第一个location区块开始
        root   html;       				# 站点的根目录,相当于Nginx的安装目录
        index  index.html index.htm;      	# 默认的首页文件,多个用空格分开
    }          								
}

什么是正向代理和反向代理?

​ 正向代理:一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

​ 正向代理总结就一句话:代理端代理的是客户端。用户是有感知的。

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

​ 反向代理总结就一句话:代理端代理的是服务端。用户是无感知的。

Nginx应用场景?

  • http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
  • 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
  • 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
  • nginx 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。

Nginx虚拟主机怎么配置

#当客户端访问www.xxxx.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080
server {
    listen       80;
    server_name  www.xxxx.com;
    location / {
    proxy_pass http://127.0.0.1:8080;
    index  index.html index.htm;
	}
}

Nginx负载均衡算法

  • 轮询
  • 加权轮询
  • ip_hash
  • fair(插件):根据响应速度进行分配。
  • url_hash(第三方插件)

nginx的四大功能

  • 正向代理: 在客户端(浏览器)配置代理服务器,通过代理服务器进行互联网访问。
  • 反向代理: 我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。
  • 负载均衡 : 单个服务器解决不了,我们增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡。
  • 动静分离 : 为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度。降低原来单个服务器的压力

SpringCloud

​ Spring cloud 是一个基于 Spring Boot 实现的服务治理工具包,用于微服务架构中管理和协调服务的。Spring Cloud 是一系列框架的有序集合。它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。通过 Spring Boot 风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。有了 Spring Cloud 之后,让微服务架构的落地变得更简单。

微服务的理解

​ 其实和SOA 架构类似,微服务是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。服务做到:单一职责、面向服务,对外暴露RestAPI、服务自治。

Eureka

​ eureka就是服务注册中心(可以是一个集群),对外暴露自己的地址。服务提供者启动后向eureka注册自己的信息(ip、地址、服务名等)。消费者从eureka拉取注册中心维护的服务列表的副本,通过列表上的服务信息,完成服务调用。服务提供者会定期向eureka发送心跳续约。
在这里插入图片描述

注解:
    //标明该服务为Eureka的服务端
    @EnableEurekaServer
    //标明该服务为Eureka的客户端
    @EnableEurekaClient
    //标明该服务为Eureka的客户端,替代上
    @EnableDiscoveryClient

自我保护机制:默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。

​ 自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。

自我保护机制的工作机制是如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制。

Ribbon

​ Ribbon是SpringCloud的负载均衡组件。

​ 我们通过服务名配置好负载均衡算法,就可以直接请求到对应的实例上。底层是通过LoadBalancerInterceptor这个类会在对请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务名。
在这里插入图片描述

负载均衡策略

IRule:这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例。
在这里插入图片描述

策略说明
com.netflix.loadbalancer.RoundRobinRule 轮询策略启动的服务被循环访问
com.netflix.loadbalancer.RandomRule 随机选择随机从服务器列表中选择一个访问
com.netflix.loadbalancer.RetryRule 重试选择先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
BestAvailableRule 最大可用策略先过滤出故障服务器,再选择一个当前并发请求数最小的服务(nacos是NacosRule(com.alibaba.cloud.nacos.ribbon.NacosRule))
WeightedResponseTimeRule 带有加权的轮询策略对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
AvailabilityFilteringRule 可用过滤策略先过滤出故障的或并发请求大于阈值的服务实例,再选择并发较小的实例
ZoneAvoidanceRule 区域感知策略默认规则,复合判断server所在区域的性能和server的可用性选择服务器

配置文件配置负载进程策略

# 这里使用服务提供者的instanceName
# 通过服务名,来进行不同服务的个性化配置
nacos-producer:
  ribbon:
    # 代表Ribbon使用的负载均衡策略      (nacos的权重负载策略)
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #通过配置文件,配置负载均衡策略
    # 同一台服务器上的最大重试次数(第一次尝试除外)
    MaxAutoRetries: 1
    # 重试的下一个服务器的最大数量(不包括第一个服务器)
    MaxAutoRetriesNextServer: 1
    # 是否可以为此客户端重试所有操作
    OkToRetryOnAllOperations: true
    # 从源刷新服务器列表的时间间隔
    ServerListRefreshInterval: 2000
    # Apache HttpClient使用的连接超时
    ConnectTimeout: 3000
    # Apache HttpClient使用的读取超时
    ReadTimeout: 3000
# 另一个服务名
nacos-producer1:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #通过配置文件,配置负载均衡策略

懒加载配置

# 预加载配置,默认为懒加载。我们在这里开启预加载。
# 一般在服务多的情况下,懒加载有可能在第一次访问时造成短暂的拥堵,有可能造成生产故障。
ribbon:
  eager-load:
    enabled: true
    clients: nacos-producer #这里添加的是预加载的服务名

OpenFeign

​ Spring Cloud OpenFeign对Feign进行了增强,是声明式、模板化的HTTP客户端。用于远程服务调用。

​ OpenFeign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给OpenFeign去做。

  • 启动类
//开启feign客户端
@EnableFeignClients
  • feign接口
    • 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
    • @FeignClient ,声明这是一个Feign客户端,同时通过 value 属性指定服务名称
    • 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
//指定接口代理nacos-producer客户端,path为当前服务接口前缀。
@FeignClient(value = "nacos-producer", path = "/product")
public interface ProductFeignService {
}

​ OpenFeign中本身已经集成了Ribbon依赖和自动配置,因此不需要额外引入依赖,也不需要再注入 RestTemplate 对象。

Hystrix

​ hystrix是Netlifx开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控(Hystrix Dashboard)等功能。

  • 熔断机制:当失败率达到阀值自动触发熔断(如因网络故障、超时造成的失败率)。熔断的含义是直接忽略该服务,返回兜底数据;
  • 降级机制:超时降级、资源不足时(线程或信号量)降级 、运行异常降级等,降级后可以配合降级接口返回托底数据。

单独使用

  • 启动类
// 添加断路器支持
@EnableCircuitBreaker
降级Fallback
@RestController
@RequestMapping("h1")
@DefaultProperties(defaultFallback = "classFallBack")
public class HystrixController01 {

    @RequestMapping("/method01/{id}")
    //添加到方法上,表明该方法开启服务降级、熔断
    @HystrixCommand(fallbackMethod = "methodFallBack")
    //@HystrixCommand
    public String method(@PathVariable String id) {

        try {
            TimeUnit.SECONDS.sleep(5);//出现sleep interrupted睡眠中断异常
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "hello world!";
    }


    public String methodFallBack(String id) {
        return "系统异常,请及时联系客服人员。";
    }

    /**
     * com.netflix.hystrix.contrib.javanica.exception.FallbackDefinitionException:
     * fallback method wasn't found: classFallBack([])
     * 这里要注意,我们在class的回调方法中,形参列表必须为空。
     * 不然会出现以上错误。
     * @return
     */
    public String classFallBack(/*String id*/) {
        return "系统异常,请及时联系客服人员。";
    }
    
}
线程隔离

​ 服务雪崩效应产生与服务堆积在同一个线程池中有关,因为所有的请求都是同一个线程池进行处理,这时候如果在高并发情况下,所有的请求全部访问同一个接口,这时候可能会导致其他服务没有线程进行接受请求,这就是服务雪崩效应。

@RestController
@RequestMapping("h2")
public class HystrixController02 {

    @GetMapping("/method01/{id}")
    @HystrixCommand(fallbackMethod = "methodFallBack",
            //测试thread和semaphore 两种隔离策略的异同
            // execution.isolation.strategy 默认为thread
            commandProperties = {
                    //@HystrixProperty(name= HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "THREAD")}
            @HystrixProperty(name= HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE")}
    )
    public String method01(@PathVariable String id){
        //xxxx
    }

    @GetMapping("/method02/{id}")
    @HystrixCommand(fallbackMethod = "methodFallBack",
            commandProperties = {
                    设置超时的时候不中断线程,默认为true
                    @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_INTERRUPT_ON_TIMEOUT,value="false")}
    )
    public String method02(@PathVariable String id){
        //xxxx
    }

    @GetMapping("/method03/{id}")
    @HystrixCommand(fallbackMethod = "methodFallBack",
            commandProperties = {
                    //设置熔断策略为semaphore,并且最大连接为1  (可以通过该思路来实现,限流)
                    @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
                    @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value="1")}
    )
    public String method03(@PathVariable String id){
        //xxxx
    }

    @GetMapping("/method04/{id}")
    @HystrixCommand(fallbackMethod = "methodFallBack",
            commandProperties = {
                    //设置是否超时,默认为true
                    @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_TIMEOUT_ENABLED,value="false")}
    )
    public String method04(@PathVariable String id){
        //xxxx
    }

    @GetMapping("/method05/{id}")
    @HystrixCommand(fallbackMethod = "methodFallBack",
            commandProperties = {
                    //设置过期时间,单位:毫秒,默认:1000
                    @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,value="6000")}
    )
    public String method05(@PathVariable("id")String id){
        //xxxx
    }

    public String methodFallBack(String id) {
        return "系统异常,请及时联系客服人员。";
    }

}
服务熔断

​ 服务在高并发的情况下出现进程阻塞,导致当前线程不可用,慢慢的全部线程阻塞 ,导致服务器雪崩。这时直接熔断整个服务,而不是一直等到服务器超时。

  • 断路器全开时:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务
  • 半开:短时间内有恢复迹象断路器会将部分请求发给该服务,正常调用时断路器关闭
  • 关闭:当服务一直处于正常状态能正常调用
/**
 * circuitBreaker.enabled :true 打开熔断 默认开启
 * circuitBreaker.requestVolumeThreshold: 当在配置时间窗口内达到此数量的失败后,进行短路。默认20个
 * circuitBreaker.sleepWindowInMilliseconds:短路多久以后开始尝试是否恢复,默认5s
 * circuitBreaker.errorThresholdPercentage:出错百分比阈值,当达到此阈值后,开始短路。默认50%
 */
@GetMapping("/method06/{id}")
@HystrixCommand(fallbackMethod = "methodFallBack",
        commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")}
)
public String method06(@PathVariable String id){
    //xxxx
}

整合Feign

  • 启动类
@EnableFeignClients
//@SpringBootApplication
//@EnableCircuitBreaker //开启熔断器
//@EnableDiscoveryClient 
@SpringCloudApplication  //以上三个注解的组合注解
  • 配置
feign:
  hystrix:
    #feign是否开启熔断
    enabled: true
  • Fallback类,相比于FallbackFactory,工厂可以用来获取到触发断路器的异常信息。建议使用FallbackFactory。

  • FallbackFactory

    • feign接口
    /**
     * fallbackFactory 指定一个fallback工厂,与指定fallback实现类不同,
     * 此工厂可以用来获取到触发断路器的异常信息,
     * ProductFeignFallBack需要实现FallbackFactory类
     * 指定服务的serviceName
     */
    @FeignClient(value = "rest-template-user",fallbackFactory = ProductFeignFallBack.class)
    @Component
    public interface Product02Feign {
    
        @GetMapping("/product/getString")
        public String getString();
    
    }
    
    • 回退工厂实现类
    @Component
    @Slf4j
    /**
     * 必须实现FallBackFactory,且它的泛型必须是你要指定地点feign的接口。
     */
    public class ProductFeignFallBack implements FallbackFactory<ProductFeign> {
        @Override
        public ProductFeign create(Throwable throwable) {
            return new ProductFeign() {
                @Override
                public String getString() {
                    log.error("fallback reason:{}",throwable.getMessage());
                    return "我犯错了,我知道。";
                }
            };
        }
    }
    

Gateway

​ Spring Cloud Gateway是建立在Spring生态系统之上的API网关,Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到api。所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。

​ gateway相当于所有服务的门户,将客户端请求与服务端应用相分离,客户端请求通过gateway后由定义的路由和断言进行转发,路由代表需要转发请求的地址,断言相当于请求这些地址时所满足的条件,只有同时符合路由和断言才给予转发。最后过滤器是对请求的增强。

核心概念

  • 路由(route):路由信息的组成是由一个id、一个目的url、一组谓词匹配、一组Filter组成的。如果路由的谓词为真,说明请求url和配置路由匹配。
  • 谓词(predicate)(断言): 谓词函数允许开发者去定义匹配来自于Http Requeset中的任何信息比如请求头和参数。
  • 过滤器(Filter): 过滤器Filter将会对请求的请求和响应进行修改处理。Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。

工作流程

  • Gateway Client向Gateway Server发送请求
  • 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
  • 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
  • RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
  • 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
  • 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应

应用

spring:
  cloud:
    gateway:
      # 默认全局过滤器,对所有路由生效
      default-filters:
        # 响应头过滤器,对输出的响应设置其头部属性名称为
        # X-Response-Default-MyName,值为 lz
        # 如果有多个参数多则重写一行设置不同的参数
        - AddResponseHeader=X-Response-Default-MyName, lz
      # 路由
      routes:
        - id: feign-consummer-route
          uri: lb://feign-consummer
          # 谓词
          predicates:
            - Path=/**
          # 过滤器
          filters:
            - PrefixPath=/feign

自定义全局过滤器

@Component
public class AuthMyFilter implements GlobalFilter, Ordered {

    /**
     * @description  过滤器执行方法
     * @param exchange 前后端交互信息,包括request与response
     * @param chain 过滤器链
     * @return 下个过滤器直到过滤器结束或者校验不通过返回结果
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //开始执行鉴权方法
        //获取请求参数
        String token = exchange.getRequest().getHeaders().getFirst("token");

        if (StringUtils.isEmpty(token)) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            String msg = "token is null!";

            DataBuffer wrap = response.bufferFactory().wrap(msg.getBytes());
            return response.writeWith(Mono.just(wrap));
        }

        // 调用下个过滤器(过滤器是基于函数回调)
        return chain.filter(exchange);
    }

    /**
     * @description 定义过滤器优先级,数字越小优先级约高,可以为负数
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

跨域配置

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          #允许跨域的请求路径
          '[/**]':
            #允许的来源
            allowedOrigins: "*"
            #允许的方法
            allowedMethods:
              - GET
              - POST
            #是否允许携带cookie
            allowCredentials: true
            #允许http请求携带的header
            allowedHeaders:
              - Content-Type
            #response应答可以暴露的header
            exposedHeaders:
              - Content-Type

Config

​ 配置中心。

​ 分布式系统中,由于服务数量非常多,配置文件分散在不同的微服务项目中,config支持配置文件放在远程Git仓库(GitHub、码云),对配置文件集中管理。

​ Bus是消息总线可以为微服务做监控,也可以实现应用程序之间相互通信。 Spring Cloud Bus可选的消息代理有RabbitMQ和Kafka。对配置文件进行监控和通知。

Nacos

​ 致力于服务发现、配置和管理微服务。

​ 相比于eureka来说,nacos除了可以做服务注册中心。其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。

基本概念

  • **服务注册:**Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接受到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
  • **服务心跳:**在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
  • **服务同步:**Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
  • **服务发现:**服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面的注册清单,并且缓存在Nacos Client本地, 同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存。
  • **服务健康检查:**Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。

Sentinel

​ 分布式容错机制。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。相比于hystrix功能更加丰富,还有可视化配置的控制台。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

​ 使用时先定义好资源埋点。只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。

注解实现流控

  • @SentinelResource注解实现
  • @SentinelResource 注解用来标识资源是否被限流、降级。
    • blockHandler: 定义当资源内部发生了BlockException应该进入的方法(捕获的是Sentinel定义的异常)
    • fallback: 定义的是资源内部发生了Throwable应该进入的方法
    • exceptionsToIgnore:配置fallback可以忽略的异常

可配置的参数:

  • 流控规则配置
    • 流控阈值类型
      • QPS
      • 线程数
    • 流控模式
      • 直接
      • 关联
      • 链路
    • 流控效果
      • 快速失败
      • Warm Up(激增流量)
      • 排队等待
  • 降级规则配置
    • 慢调用比例
    • 异常比例
    • 异常数
  • 热点参数限流
  • 授权控制规则(可设置黑名单、白名单)

整合Feign

  • yml
#对Feign的支持
feign:
  sentinel:
    enabled: true # 添加feign对sentinel的支持
  • feign
@FeignClient(value = "nacos-producer", path = "/product", fallback = ProductFeignFallback.class)
public interface ProductFeignService {
	//xxxxx
}
  • fallback
@Component
public class ProductFeignFallback implements ProductFeignService {
   //xxxx
}

Seata

​ Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

CAP定理

  • Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
  • Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
  • Partition tolerance (分区容错性):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

​ 在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。

​ 如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。

​ 如果此时要保证可用性,就不能等待网络恢复,那节点之间就会出现数据不一致。

​ 也就是说,在P一定会出现的情况下,A和C之间只能实现一个。

分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:

  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

  • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • **Soft State(软状态):**在一定时间内,允许出现中间状态,比如临时的不一致状态。
  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

Seata中的三大角色

  • **TC (Transaction Coordinator) - 事务协调者:**维护全局和分支事务的状态,驱动全局事务提交或回滚。这个就是我们的Seata服务器,用于全局控制。

  • **TM (Transaction Manager) - 事务管理器:**定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • **RM (Resource Manager) - 资源管理器:**管理分支(本地)事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

    其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

Seata支持4种事务模式

  • AT:本质上就是2PC的升级版,在 AT 模式下,用户只需关心自己的 “业务SQL”,对业务无侵入。
  • TCC:和我们上面讲解的思路是一样的。
  • XA:同上,但是要求数据库本身支持这种模式才可以。
  • Saga:用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作。

AT模式设计思路

​ AT模式的核心是对业务无侵入,是一种改进后的两阶段提交。

​ 简单总结:

  • 一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。提交至数据库。
  • 二阶段如果确认提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可,当然如果需要回滚,那么就用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

Dubbo

​ Dubbo是一款高性能、轻量级的开源RPC框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和Spring框架无缝集成。

Dubbo核心组件有哪些

在这里插入图片描述

节点角色说明:

节点角色说明
Provider暴露服务的服务提供方。
Consumer调用远程服务的服务消费方。
Registry服务注册与发现的注册中心。
Monitor统计服务的调用次数和调用时间的监控中心。

调用关系说明:

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

Dubbo的注册中心集群挂掉,发布者和订阅者之间还能通信么?

​ 可以通讯。启动Dubbo 时,消费者会从Zookeeper拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。

Dubbo集群提供了哪些负载均衡策略?

  • Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。

  • RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题。

  • LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求。

  • ConstantHash LoadBalance: 一致性Hash策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。

    默认为Random随机调用

Dubbo的集群容错方案有哪些?

  • Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。
  • Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。
  • Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。

默认的容错方案是Failover Cluster。

Dubbo超时设置有哪些方式?

​ Dubbo超时设置有两种方式:

​ 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。

​ 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。

​ 因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。
​ dubbo在调用服务不成功时,默认是会重试两次。

Zookeeper

​ ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

​ 它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

Zookeeper集群中的角色

  • leader 处理所有的事务请求(写请求),可以处理读请求,集群中只能有一个leader。
  • follower只能处理读请求,同时作为leader的候选节点,即如果leader宕机,follower节点要参与到新的leader选举中,有可能成为新的leader节点。
  • observer只能处理读请求,不能参与选举。

四种类型的 znode

​ ZooKeeper 是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。

​ 这里面的每一个节点都被称为: ZNode,每个节点上都会保存自己的数据和节点信息。

​ 节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。

​ 节点可以分为四大类:

  • PERSISTENT 持久化节点:客户端与 zookeeper 断开连接后,该节点依旧存在。
  • EPHEMERAL 临时节点 :-e,客户端与 zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号。
  • PERSISTENT_SEQUENTIAL 持久化顺序节点 :-s,客户端与 zookeeper 断开连接后,该节点被删除。
  • EPHEMERAL_SEQUENTIAL 临时顺序节点 :-es,客户端与 zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号。

分布式锁

​ 有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。

​ 我们可以基于zookeeper的两种特性来实现分布式锁,首先我们来看第一种:

唯一节点特性

​ 我们可以基于唯一节点特性来实现分布式锁的操作。多个应用程序去抢占锁资源时,只需要在指定节点上创建一个 /Lock 节点,由于Zookeeper中节点的唯一性特性,使得只会有一个用户成功创建 /Lock 节点,剩下没有创建成功的用户表示竞争锁失败。

​ 这种方法虽然能达到目的,但是会有一个问题,假设有非常多的节点需要等待获得锁,那么等待的方式自然是使用watcher机制来监听/lock节点的删除事件,一旦发现该节点被删除说明之前获得锁的节点已经释放了锁,那么此时剩下的B、C、D节点会同时收到删除事件从而去竞争锁,这个过程会产生惊群效应。

​ 什么是“惊群效应”呢?简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候zookeeper会在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对zookeeper服务器的性能产生比较的影响。

有序节点

​ 为了解决惊群效应,我们可以采用Zookeeper的有序节点特性来实现分布式锁。

​ 每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致惊群效应,因为每个客户端只需要监控一个节点。
在这里插入图片描述

Docker

​ Docker 是一个开源的应用容器引擎。

​ Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

常用命令

  • docker images 查看本地dockers镜像
  • docker search tomact 在Docker Hub上搜索tomcat镜像
  • docker pull tomcat[:version] 从Docker Hub上下载特定版本的镜像到本地
  • docker rmi -f [镜像id] 删除镜像
  • docker run --name mynginx -d nginx:latest 将镜像放入容器中启动
  • docker ps 查看运行中的容器
  • docker ps -a 查看所有容器
  • docker stop [容器id]or[容器name] 暂停容器
  • docker start [容器id]or[容器name] 启动容器
  • docker rm [容器id]or[容器name] 删除容器
  • docker cp [容器id]or[容器name]:/文件路径 从容器拷贝文件到宿主机
  • docker logs -f -t --tail 10 [容器id]or[容器name] 打印日志

DockerFile中常见的指令

​ Dockerfile 是一个文本文件,其中包含我们需要运行以构建 Docker 映像的所有命令。Docker 使用 Dockerfile 中的指令自动构建镜像。我们可以docker build用来创建按顺序执行多个命令行指令的自动构建。

  • From 指定基础镜像,该脚本从什么基础上创建
  • Label 指定镜像标签
  • Run docker脚本执行到这一步要运行那些指令
  • CMD docker脚本执行完之后,容器生成好了要执行的命令,一个DockerFile脚本中只能有一条CMD指令有效

Vue

​ Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。

生命周期

vue的生命周期通常有8个,分别是创建前后,挂载前后,更新前后,销毁前后,分别对应的钩子函数有beforeCreate创建前,created创建后,beforeMount挂载前,moubted挂载后,beforeUpdate更新前,updated更新后,beforeDestory销毁前,destoyed销毁后。

Vue指令

  • v-text渲染数据,不解析标签。
  • v-html不仅可以渲染数据,而且可以解析标签。
  • v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
  • v-show:根据表达式之真假值,切换元素的 display CSS 属性。
  • v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key值 使用。
  • v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
  • v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
  • v-model:实现表单输入和应用状态之间的双向绑定
  • v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
  • v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

v-if与v-show区别

​ 此元素进入页面后,此元素只会显示或隐藏不会被再次改变显示状态,此时用v-if更加合适。当用v-if来隐藏元素时,初次加载时就不用渲染此dom节点,提升页面加载速度

​ 此元素进入页面后,此元素会频繁的改变显示状态,此时用v-show更加合适。当用v-show来隐藏元素时,只会在初次加载时渲染此dom节点,之后都是通用display来控制显隐。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
很抱歉,我无法提供完整的2024年的Java面试题答案,因为我无法预测未来的情况。但是,我可以给你提供一些常见的Java面试题答案,帮助你准备面试。 1. Java中的基本数据类型有哪些? 答案Java中的基本数据类型包括byte、short、int、long、float、double、boolean和char。 2. Java中的包装类是什么? 答案:包装类是一种将基本数据类型封装成对象的类。例如,Integer是封装int类型的包装类。 3. Java中的String类是可变的吗? 答案:String类是不可变的,一旦创建就不能被修改。如果需要修改字符串,可以使用StringBuilder或StringBuffer类。 4. Java中的继承和接口有什么区别? 答案:继承是指一个类从另一个类获取属性和方法的过程,通过extends关键字实现。接口是一种规范,定义了一组方法的集合,通过implements关键字实现。 5. Java中的异常处理机制是什么? 答案Java中的异常处理机制通过try-catch-finally语句块来实现。try块中包含可能抛出异常的代码,catch块用于捕获并处理异常,finally块用于执行无论是否发生异常都需要执行的代码。 6. Java中的多线程是如何实现的? 答案Java中的多线程可以通过继承Thread类或实现Runnable接口来实现。另外,还可以使用线程池来管理和调度线程。 7. Java中的反射是什么? 答案:反射是指在运行时动态获取和操作类的信息。通过反射,可以获取类的属性、方法和构造函数等信息,并且可以在运行时调用这些方法。 8. Java中的泛型是什么? 答案:泛型是一种参数化类型的机制,可以在编译时检查类型的安全性。通过使用泛型,可以使代码更加灵活和可重用。 9. Java中的集合框架有哪些? 答案Java中的集合框架包括List、Set、Map等接口和它们的实现类。这些集合类提供了一组用于存储和操作对象的方法。 10. Java中的内存管理是如何工作的? 答案Java中的内存管理由Java虚拟机(JVM)负责。JVM使用垃圾回收机制来自动管理内存,当对象不再被引用时,垃圾回收器会自动回收该对象所占用的内存空间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值