因为网站不支持自定md编辑器,因此标题没有序号,需要带序号的md或者pdf私信我,因为网站的资源设置了0积分也是会员才能下载的东西。

文章目录

数据结构

树:只有一个根节点;子树1个前驱,0个多个后驱。子树之间不能有交集。

概念

  • 节点的度:一个节点含有子树的个数称为该节点的度
  • 叶节点:度为为0
  • 树的度:最大节点的度
  • 节点的层次:根为1层,往下2层,以此类推
  • 树的高度或者深度:最大层数
  • 兄弟节点:同根
  • 堂兄节点:同层不同跟

二叉树:不存在节点大于2的度;二叉树有左右之分,次序不能颠倒,因此是有序树。

对于任意二叉树都是由以下几种情况复合而成的。
在这里插入图片描述

满二叉树:子节点都在一层
完全二叉树:前n-1层是满二叉树,最后一层子节点连续
平衡树:左根右递增,子树高度差小于等于1,追求绝对平衡,插入删除旋转次数未知 增删平均时间复杂度为logn
红黑树:左根右递增,根节点和叶子结点是黑色;红节点有两个黑节点;任意节点到每个叶子结点路径上的黑节点数量相同
增删查复杂度最坏为logn,最长路径不大于最短路径的两倍,追求大致平衡,插入删除最多三次旋转

树的表示

在这里插入图片描述

二叉树的性质

①若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点。\n②若规定根结点的层数为1,则深度为h的二叉树的最大结点数为2h-1个。\n③对任何一棵二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0 = n2+1。(常用这个性质解选择题)\n④若规定根结点的层数为1,则具有N个结点的满二叉树的深度h = log2(N+1)。\n⑤对于具有N个结点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点:\n1、若 i > 0,则该结点的父结点序号为:( i - 1) / 2;若 i = 0,则无父结点。\n2、若2i + 1 \u003C N,则该结点的左孩子序号为:2i + 1;若2i + 1 >= N,则无左孩子。\n3、若2i + 2 \u003C N,则该结点的右孩子序号为:2i + 2;若2i + 2 >= N,则无右孩子。

二叉树的存储结构

顺序存储一般用来存储完全二叉树,否则会造成空间浪费。现实只有堆会用数组存储。
链式存储,用链表表示二叉树。每个节点为左右指针域和数据域。
在这里插入图片描述
在这里插入图片描述

二叉树的遍历

深度遍历

  • 前序 根左右
  • 中序 左根右
  • 后序 左右跟
    广度遍历:层次遍历

排序算法,查找算法

常见面试的查找和排序算法
面试高频考点 – 常见的排序算法(7种)

  • 插入排序:从第二个元素开始,每选择一个元素,这个元素前面就是有序区间,后面就是无序区间。在有序区间选择合适位置插入。
    O(n^2) 稳定

  • 希尔排序:将数据分成n组,进行排序,逐渐缩小n值
    O(n^1.3~1.5) 不稳定

  • 选择排序:每次将最大或者最小放到最后,知道所有都排完
    O(n^2)不稳定

  • 冒泡排序:在无序区间,通过相邻数的比较,将最大的数据放入一侧,持续整个过程,直到数组整体有序。
    时间复杂度:
    最坏情况 O(N^2)
    最好情况 O(N) (一趟就有序了)
    空间复杂度: O(1)
    稳定性: 稳定

  • 快速排序(重要)
    1.找一个基准值,存储
    2.比较左边和右边,小于的放到右边,大于的放到左边
    3.然后对左右区间按同样的方式处理
    nlogn 最坏n^2

选择排序和冒泡排序的区别

  • 冒泡排序是左右两个数相比较,而选择排序是用后面的数和每一轮的第一个数相比较;
  • 冒泡排序每轮交换的次数比较多,而选择排序每轮只交换一次;
  • 冒泡排序是通过数去找位置,选择排序是给定位置去找数;
  • 当一个数组遇到相同的数时,冒泡排序相对而言是稳定的,而选择排序便不稳定;
  • 在时间效率上,选择排序优于冒泡排序。
  • 两种算法的最坏情况复杂度相同,即O(n^2),但最佳复杂度不同。冒泡排序使用n个时间顺序,而选择排序使用 n ^ 2个时间顺序

堆排序

将数组放入堆,调整每一颗子树,使之变成大根堆。
O(n * logN) O(1) 不稳定

package com.ln.mybatis.sort;

import java.util.Comparator;
import java.util.PriorityQueue;

//求前k个最小的元素
public class sortTest {
    public static void main(String[] args) {
        int test[]={10,11,12,32,434,1,2,3,4,5,6,7,8,9,10,11,12,32,434,54645,6565,757,323,32};
        int[] topk=topK(test,3);
        for (int i = 0; i < topk.length; i++) {
            System.out.println(topk[i]);
        }
//        System.out.println(topK(test,3));
    }


//    找出数组中最小的元素,建立大根堆,然后对根节点进行比较,大则不放,小则替换
    public static int[] topK(int[] arr,int k){
//        创建一个大小为k的大根堆
        PriorityQueue<Integer> maxHeap =new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return 02-01;
            }
        });

        for (int i = 0; i < arr.length; i++) {
            if(i<k){
                maxHeap.offer(arr[i]);
            }else {
                if(maxHeap.peek()>arr[i]){
                    maxHeap.poll();
                    maxHeap.offer(arr[i]);
                }
            }

        }

        int[] ret=new int[k];
        for (int i = 0; i <k ; i++) {
            ret[i]=maxHeap.poll();
        }
        return ret;
    }
}

雪花算法

组成: 1.第一位为符号位默认0不使用 占用1bit,2.时间戳:41bit 3.机器id 10bit 4.序列号 12bit
同一毫秒同一个id可以生成4096个序列号

时间回拨生成重复id的问题
1.回拨时间短不生成id
2.回拨时间长使用扩展位

java基础

==和equals的区别

== 用于比较两个引用是否指向同一个对象
而 equals() 方法用于比较两个对象的内容是否相等。
默认情况下,equals() 方法也比较引用,但许多类(如 String)重写了该方法以比较对象的实际内容。因此,使用 equals() 来检查对象的逻辑相等性通常更合适。

java1.8的特性

  1. lambda表达式(在stream和optional用方法引用)
  2. 函数式接口,如predicate,function和consumer(consumer统一处理外部调用的异常)
  3. streamAPI(集合处理)
  4. 接口可以包含默认方法
  5. Optional类,避免显示的null检查,处理可能为null的值
  6. 新日期和时间 API:java.time 包,替代了老旧的 java.util.Date 和 java.util.Calendar 类(线程安全,时区处理)

String、StringBuffer、StringBuilder的区别

在这里插入图片描述

JAVA的集合类型以及线程安全的集合

LIST SET HASH
vector,hashtable
CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentHashMap
底层大都采用Lock锁 ConcurrentHashMap不用

Java是引用传递还是值传递

是值传递
值传递:传递的是参数的拷贝,操作参数不会影像实际参数
引用传递:传递的是参数的地址,操作参数会影响实际参数

lambda

lambda 可以访问外部变量
但是变量不可变,引用不可变。

c和java的区别

1.c面向过程,执行效率高 j面向对象,执行效率低

2.j跨平台,c,c++,c#都需要在特定的系统中执行

3.c有指针,没有垃圾回收机制,j没有指针,有垃圾回收机制

4.c可以调用系统指令,j不可以,因此j中只有线程没有进程的概念,c两者都有

5.文件组织方式不一样,j是类,c中把全局变量和方法的声明放在头文件中

jvm

概念
1.JVM是java虚拟机,用来执行字节码文件(二进制 class文件)的虚拟计算机。除了java,Scala,Groovy和Python等其他语言经过处理也可以转换成字节码文件。
2.JVM运行在操作系统上,和硬件没有任何关系。

跨平台原理:编译后的字节码文件和平台无关,在java虚拟机上运行。统一的class文件结构,就是jvm的基石。
JVM分类

  • 类加载器子系统
  • 运行时数据区
  • 执行引擎
    • JIT编译器(主要影响性能):编译执行
    • 解释器(负责响应时间):逐行解释字节码

程序执行方式有三种,静态编译执行,动态编译执行,动态解释执行。
在java中,程序的执行以动态解释为主,动态编译为辅。(静态编译如C,直接编译成可执行文件exe)
机器码和字节码的区别:
机器码是CPU直接读取,速度快;字节码需要直译器转译后才能变成机器码。

JDK包括了编译器等开发工具和JRE
JRE包括了运行类库和JVM
JVM有两种运行方式 client和server Client启动快,运行慢。Server启动慢,运行快。

JVM流程 .java文件编译器解释为class文件,交给jvm执行引擎执行,执行时会用空间存储数据,就是JVM内存。
JVM内存主要为:堆,栈,方法区,本地方法区,程序计数器。

  • 程序计数器
    当前线程执行字节码的行数指示器,用来记录虚拟机字节指令地址,线程私有。执行本地方法时为空。也称为PC寄存器。
    字节码解释器在工作时,通过改变计数器的值来选取下一跳执行的代码,分支,循环,跳转,异常处理,线程恢复等功能都依赖程序计数器完成。
    Java虚拟机的多线程的实现方式:通过轮流切换并分配处理器执行时间实现。
  • 本地方法栈
    和虚拟机栈作用类似,区别是一个执行java方法,一个执行native方法。线程私有。
    与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
  • 方法区(1.8为元数据区)
    主要是存储类信息,静态变量,编译后的代码(字节码)等数据,常量池。线程共享

  • 栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。
    局部变量表:用来存储方法参数和方法中定义的局部变量
    操作数栈:用于保存计算中的临时变量和中间结果,是JVM执行引擎的一个工作区,通过入栈和出栈进行数据访问。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
    动态链接:指向方法区的运行时常量池

在Hotspot的演变过程中:

  • Java6及之前:方法区存在永久代,保存有静态变量
  • Java7:进行去永久代工作,虽然还保留着,但静态常量池,如字符串常量池,已经移动到堆中
  • Java8:移除永久代,类型信息、域(Field)信息、方法(Method)信息存放在元数据区;字符串常量池、静态变量存放在堆区

在这里插入图片描述
堆中分为老年代和年轻代,年轻代中分为eden区和存活区,区中分为s0和s1
新生成的对象在Eden区
触发Minor GC后幸存的对象存入s0,再次触发Minor GC后,eden区和s0的对象存入s1中,s0清空。
每次移动,递增计数器,超过默认值15 (通过 -XX:+MaxTenuringThreshold 设置),移动到老年代中,eden中没有足够内存分配,也会分配到老年代。
老年代靠major GC。

新生代的回收机制采用复制算法,老生代采用的回收算法是标记整理算法。

堆和栈的区别

栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。栈中数据生命周期短,出栈即失效。栈超过虚拟机允许最大深度StackOverflow
堆:存储对象,线程共享。堆中数据声明周期长,由垃圾回收机制不定期回收。空间不够扩展申请不到足够的内存,oom

垃圾回收

参考【Java】垃圾回收
作用区域:频繁发生在年轻代,较少发生在老年代,极少发生在方法区(永久代/元空间)
引用类型才需要垃圾回收,基本数据类型不需要。
内存泄漏:这个对象不再使用,但是GC没法回收。
垃圾回收分为标记和清除阶段

标记阶段

引用计数法
引用对象+1,引用失效-1。为0则认为可以进行回收。
优点:实现简单,垃圾容易辨识;判定效率高,回收没有延迟
缺点:
1.需要单独的字段存储计时器,增加空间开销
2.每次赋值都需要进行加减法,增加时间开销
3.无法处理循环引用的情况
可达性分析算法
通过被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,走过的路径被称为引用链。当一个对象到GC roots没有任何引用相连时,证明该对象不可用。
同样具备实现简单和执行高效的特点,能有效解决循环依赖的问题,防止内存泄漏的发生。

可达性分析必须在一个能保证一致性的环境下进行。这点也是导致GC必须进行"stop the world"的一个重要原因。

所谓GC roots根集合就是一组必须活跃的引用。可以是:
1.虚拟机栈中引用的对象。
2.静态变量引用的对象,除非类卸载,否则他的引用对象一直存在。
3.所有被同步锁持有的对象。(同步锁要是被销毁,同步就失效了)

清除阶段

JVM中常见的清除方法:1.标记清除法 2.标记复制法 3.标记压缩法
标记清除法:
把存活的对象进行标记,清除死亡对象。
当堆中有效空间被用完,就会stw。然后进行标记和清除。要把用户线程停止保持一致性,防止用户线程产生垃圾。
缺点:
1、效率不高,需要遍历
2、进行GC时需要停止整个应用程序,用户体验差。
3、清理出来的空闲空间不是连续的,会产生碎片。
标记复制法:
内存分为两块,每次只用其中的一块。垃圾回收时,将存活的对象复制到未使用的一块,清除不可达的对象。
年轻代S0和S1也是用的复制算法。
优点:1.没有标记和清除的过程,实现简单,运行高效;2.复制后能保证空间的连续性
缺点:损失一半空间。
适合回收对象多的场景,复制少。适用于年轻代。
标记压缩算法
1.第一阶段和标记清除算法相同,从根节点开始标记被引用对象。
2.第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间。
在这里插入图片描述
标记压缩算法等同于标记清除算法加压缩。
优点:1.没有碎片 2.不会内存减半
缺点:
1.效率低,因为会进行整理压缩
2.移动对象时,如果对象被其他对象引用,需要调整引用的地址。
3.移动过程中会stw
分代收集算法:
不同生命周期对象采用不同的算法,提高效率。
1.年轻代:区域小,生存周期短,回收频繁。
采用复制算法,内存利用率不高,hotspot中两个survivor的设计得以缓解
2.老年代:区域大,生命周期长,回收不频繁。
采用标记清除或者标记清楚整理算法。

Old GC: 只收集 old gen 的 GC。只有垃圾收集器 CMS 的 concurrent collection 是这个模式
Mixed GC: 收集整个 young gen 以及部分 old gen 的 GC。只有垃圾收集器 G1 有这个模式
年轻代的gc称为Minor GC。新生代Eden区满的时候触发Minor GC
Full GC是回收整个堆。触发条件为:
1.System.gc
2.老年代空间不足
3.方法区空间不足
4.Minor GC后进入老年代的平均大小大于老年代的可用大小
5.由Eden区,from 区向to区复制,对象大于to的内存,也大于老年代的内存。
单例模式是静态的,生命周期长,如果中间引用了别的对象,那么这个对象一直不会被回收。

Major GC通常是跟full GC是等价的,收集整个GC堆,但也有说法是old GC。

异常类型

在这里插入图片描述

为了及时有效的处理异常,java引入了异常类。所有的异常都是Thorwable的子类。

Throwable下有两个分支Excepiton和Error。

异常类主要分为三种类型:

  • 系统错误Error

    系统错误是由虚拟机抛出的,用户无法处理,如

    OutOfMemoryError :内存耗尽 ;
    NoClassDefFoundError :无法加载某个Class ;
    StackOverflowError :栈溢出

  • 编译时异常:Exception (除了其子类RuntimeException)

    ​ 在编译时期抛出的异常,在编译期间检查程序可能出现的问题,如果有提前防范,捕获处理

    应用逻辑处理:

    • NumberFormatException :数值类型的格式错误;
    • FileNotFoundException :未找到文件;
    • SocketException :读取网络失败。

    编写逻辑造成:

    • NullPointerException :对某个 null 的对象调用方法或字段;
    • IndexOutOfBoundsException :数组索引越界
  • 运行时异常 RuntimeException

    • java虚拟机正常运行期间抛出的异常。这类异常只有在运行时才能发现是否有异常。

      RuntimeException,Error以及他们的子类都被称为免检类,其他异常被称为必检类(Checked Exception),编译器会强制程序员检查并try-catch处理,或者在方法头进行声明。如数组越界和空指针。

双亲委派机制

参考【Code皮皮虾】带你盘点双亲委派机制【原理、优缺点】,以及如何打破它?
双亲委派机制是在JDK1.2后才引入的。
加载类时不直接加载,委托给自己的父类加载器,递归直到加载成功。否则自己加载。
目的:1.防止类的重复加载。2.避免核心类遭到修改
Java提供四种类加载器:

  • BootStrap 启动类加载器:加载java核心类库 ,javahome/lib下的jar包,rt.jar等
  • Ext 扩展类加载器:加载java_home/ext/lib
  • Application 应用程序类加载器:主要用来加载当前应用claspath下的所有类。
  • User 用户自定义类加载器:用户自定义,加载指定路径下的类

什么时候破坏这个机制?

JDBC

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "0000");

获取连接时的DriverManager因为处于rt下,会被启动类加载器加载。
类加载时,会执行静态方法。其中会加载所有实现了Driver接口的实现类,但是这些实现类都是第三方提供的,启动类加载器无法加载,因此引入了ThreadContextClassLoader(线程上下文类加载器,默认情况下是AppClassLoader)来使用应用程序加载器,破坏双亲委派机制。
tomcat
比如tomcat web容器里面部署很多应用程序,但是每个应用依赖的第三方类库版本不同,但是类的全路径名可能相同。
双亲委派无法加载多个相同的class文件,因此tomcat给每个web容器单独同一个webAppClassLoader加载器。实现隔离性,优先加载Web应用自己定义的类,加载不到再交给CommonClassLoader加载,这和双亲委派机制恰好相反。

如何打破双亲委派机制?
1.自定义类加载器:继承ClassLoader,不想打破,只需要重写findClass,想打破,重写整个loadClass方法,设定自己的类加载逻辑。
在这里插入图片描述
2.使用线程上下文类加载器

public class Main {

    public static void main(String[] args) {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    }

}

hashmap

迭代方式
entryset
keyset
values
迭代器iterator

顺序
hashmap无序,基于哈希表
linkedhashmap,插入顺序,基于链表加哈希表
treepmap,自然排序或者比较器,基于红黑树

hashmap
1.7 数组+链表
1.8 数组+链表+红黑树
ConcurrentHashMap简介
hashmap线程不安全。
hashtable线程安全,方法直接加synchronize锁,性能低下。
concurrentHashMap
对数组增加了voliate关键字
1.7 segment+hashentry 分段锁
1.8 cas+synchronized

getSize
1.7获取三次,两次一致返回,三次拿不到加锁进行计算
1.8 获取basecount
put方法
计算哈希值;
当前map是否为空,为空先初始化;
判断哈希值所在位置是否有值,没值直接cas替换
有值判断key是否相等,相等不变
不相等判断是红黑树还是链表,进行循环,key相同替换,未找到相同的进行增加

扩容过程

1.触发扩容条件:负载因子默认0.75,当size超过的时候触发扩容
2.扩容过程:
2.1. 创建新数组,一般是容量的2倍
2.2. 更新阈值
2.3. 迁移旧数据:
遍历旧数组中的每一个桶
对于非空桶,将元素迁移到新数组中
在java8中,如果一个桶中的元素超过8,并且数组的容量大于64,链表转红黑树,否则继续使用链表
2.4.重新哈希
重新计算每个元素的哈希位置,使用新的数组长度来确定其在新数组中的位置。新的哈希位置通常通过 hash & (newCapacity - 1) 计算得到。

hashMap 存储大量数据

参考准备用HashMap存1W条数据,构造时传10000还会触发扩容吗?存1000呢?

不指定容量,会随着数据的增加不断扩容,影响性能。

在指定调用容量的构造方法时,会重新调用另一个构造方法,传入默认的负载因子0.75

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

构造方法中初始化了两个成员变量。threadHold 扩容阈值和 loadFactor 负载因子。
tableSizeFor就是找到大于入参的2的整数次方,如传入10,会得到16.
设置容量为2的整数次方是为了减少哈希冲突。

推荐在集合初始化的过程中指定集合初始化大小为 ((需要存储的元素个数)/0.75)+1

为什么HashMap中的键往往都使用String?

参考 为什么HashMap中的键往往都使用String?

1.String重写了hashCode,两个不同引用的String类型,只要值相等,hashcode就相等,而非地址相等。(设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。)
2.String不可变,每当创建一个字符串对象,他的hashcode就被缓存下来,所以存储hashMap不用重新计算,相比于其他对象更快。

hashmap红黑树转链表

resize方法:红黑树节点元素小于等于6,untreeify转化为链表
removenode方法:判断根节点和子节点是否为空来判断是否解除红黑树

为什么使用红黑树?
链表插入查询时间复杂度为O n,红黑树为O log n

hashMap和hashtable的区别

参考HashMap和Hashtable的区别

  1. hashMap线程不安全
    hashtable线程安全,用synchronized关键字实现
  2. hashMap允许null作为键或者Value,HashTable会抛出异常
  3. hashtbale使用的是key的hashcode,hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
  4. HsahMap在数组+链表的结构中引入了红黑树,Hashtable没有
    5.HashMap初始容量为16,Hashtable初始容量为11
  5. HsahMap扩容是当前容量翻倍,Hashtable是当前容量翻倍+1
  6. HsahMap只支持Iterator遍历,Hashtable支持Iterator和Enumeration
  7. linkedhashmap 保留插入顺序,允许键和值为null,treemap 基于键的compareTo方法排序,不允许null键,允许null值

使用场景:

  • 非并发场景(单线程)使用HashMap,并发场景(多线程)可以使用Hashtable,但是推荐使用ConcurrentHashMap(锁粒度更低、效率更高)。
  • 另外使用在使用HashMap时要注意null值的判断,
    Hashtable也要注意防止put null key和 null value。

ThreadLocal

可以理解为线程本地变量,每个线程中都创建一个副本,在线程之间访问内部副本变量即可,做到了线程隔离,相比于synchronized的做法是用空间换时间。
在这里插入图片描述

在使用完成之后需要remove掉,避免内存泄漏。ThreadLocal变量的key为弱引用,使用完成后TheadLocal没有使用的强引用后会释放,但是value是强引用,只要线程存活,一直存在强引用,需要通过remove删除Entry.线程池尤为严重。

单例模式

public class SingleTonObj {

  

    private static volatile SingleTonObj singleTonObj;


    private SingleTonObj() {

    }

    public static SingleTonObj getObj() {
        if (singleTonObj == null) {
            synchronized (SingleTonObj.class) {
                if (singleTonObj == null) {
                    singleTonObj = new SingleTonObj();
                }
            }
        }

        return singleTonObj;
    }
  • volatile
    其中volatile关键字是为了防止指令重排
    jvm创建对象分三步:1.分配空间,2.实例化对象,3.将对象指向空间
    如果不使用volatile,jvm会优化,将3移动到2前面,这样a线程,走到了3,还没2。b线程第一个判null已经不成立,返回了没有实例化完成的对象
  • 第二个判空的原因是 a,b都经过了第一次判空,但是a先拿到了class的锁,进行了实例化,然后进行释放锁,b拿到锁之后需要在进行判空防止重复实例化破坏单例
  • Happens-Beforene内存模型和程序模型顺序
    程序A在B前,线程中A就会在B前执行
    Happens-Beforene不会破坏代码中的先后顺序,但是在不同代码或者不同线程中的顺序无关

常量池

深度剖析Java常量池

通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class

Class常量池可以理解为Class文件中的资源仓库。Class文件中除了包含版本,字段,方法,接口,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
int a=1 1为字面量,a为符号引用
字面量是指由字母,数字构成的字符串和数值常量,字面量只可以右值出现。
符号引用是编译原理中的概念,是相对于直接引用来说的,主要包括了以下三大类:

  • 类和接口的全限定类名
  • 字段的名称和描述符
  • 方法的名称和描述符
    运行时常量池:只有运行时被加载到内存中,这些符号才有对应的内存地址,那么这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用会转变为加载到内存区域的代码的直接引用,也就是动态链接
    字符串常量池:
    jdk1.6以及之前,运行时常量池在永久代,运行时常量池包含字符串常量池。
    jdk1.7: 有永久代,但是逐渐去永久代,字符串常量池从永久代的运行时常量池分配到堆中。
    jdk1.8:无永久代,运行时常量池在元空间,字符串常量池依然在堆中。

intern
1.6 在常量池中寻找equal()相等的字符串,存在则返回常量池中的引用。不存在就在永久代的常量池中新建一个实例,放入常量池中并返回。
在这里插入图片描述

1.7:不存在永久代,常量池存在返回,不存在可以直接指向堆上的实例。
在这里插入图片描述

> String s0="zhigan";
String s1="zhigan";
String s2="zhi" + "gan";
System.out.println( s0==s1 ); //true  
System.out.println( s0==s2 );//true

字面量声明的字符串常量在编译期就能确定,会存储在常量池中,地址相同。

String s0="zhigan";
String s1=new String("zhigan");
String s2=“zhi”+new String("gan");
System . out . println ( s0 == s1 ); // false
System . out . println ( s0 == s2  ); // false
System . out . println ( s1 == s2  ); // false

new String() 的字符串不是常量,不能在编译期确定,不放入常量池,他们有自己的地址空间。

String a="a3.4"String b="a"+3.4;
System . out . println ( a == b ); // true

jvm对于加号连接,在编译器就会进行优化,将常量字符串连接。

String a="ab";
String bb="b";      
String b="a"+bb;
System . out . println ( a == b ); // false

在+中带有引用,JVM无法优化,因为引用无法在编译期确认,只能在程序运行是动态分配,并将连接后的新地址赋值给B,因此为false(如果bb是一个方法的返回结果,同样的原因)如果bb用final修饰,那么他在编译期会被解析为常量,比较的结果为true。

String s = "a" + "b" + "c" ; // 就等价于 String s = "abc";
String a = "a" ;
String b = "b" ;
String c = "c" ;
String s1 = a + b + c ;

s1这个就不一样,可以通过观察器JVM指令码发现s1的"+"操作会变成如下:
StringBuilder temp=new StringBuilder();
temp.append(a).append(b).append( c );
String s=temp.toString();

Java八大基本对象的包装类型除了两个浮点型,其他都有常量池。另外Byte,Short,Int,Long,Character这五种整形的包装类也只是对应值-128到127才可以使用对象池。

if,else嵌套优化

参考Java—优化 if-else 代码的 8 种方案
1.提前return,去除不必要的else

 if(xx){
 }
 else{
 	return
 }
 --------
 if(!xx){
 	return
 }

2.使用三元表达式
3.使用枚举类

String OrderStatusDes;
 
if(orderStatus==0){
    OrderStatusDes="订单未支付";
}else if(OrderStatus==1){
    OrderStatusDes="订单已支付";
}else if(OrderStatus==2){
    OrderStatusDes="已发货";
}

---------------------
String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc();

4.合并条件表达式
结果相同,合并表达式
5.使用Optional优化if,else

String str = "jay@huaxiao";
 
if(str != null) {
    System.out.println(str);
} else{
    System.out.println("Null");
}
-----------------------------
Optional<String> strOptional = Optional.of("jay@huaxiao");
 
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));

6.表驱动法
又称之为表驱动、表驱动方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或case)来把它们找出来的方法。以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。

if(param.equals(value1)) {
    doAction1(someParams);
} else if(param.equals(value2)) {
    doAction2(someParams);
} elseif(param.equals(value3)) {
    doAction3(someParams);
}

---------------
// 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
Map<?, Function<?> action> actionMappings = newHashMap<>(); 
 
 
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
 
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);

7.使用策略模式
例如支付场景下,支持多种支付方式

public class PaymentService {

    CreditService creditService;

    WeChatService weChatService;

    AlipayService alipayService;

    public void payment(PaymentType paymentType, BigDecimal amount) {
        if (PaymentType.Credit == paymentType) {
            creditService.payment();
        } else if (PaymentType.WECHAT == paymentType) {
            weChatService.payment();
        } else if (PaymentType.ALIPAY == paymentType) {
            alipayService.payment();
        } else {
            throw new NotSupportPaymentException("paymentType not support");
        }
    }
}

enum PaymentType {
    Credit, WECHAT, ALIPAY;
}

作者:小黑说Java
链接:https://juejin.cn/post/7030976391596212255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这种不满足开闭原则(对修改关闭,对扩展开放),修改后需要对其他支付方式进行测试。

策略设计模式是一种行为设计模式。当在处理一个业务时,有多种处理方式,并且需要再运行时决定使哪一种具体实现时,就会使用策略模式。

抽象支付方式为一个策略接口

public interface PaymentStrategy {

    public void payment(BigDecimal amount);
    
}

针对具体的支付方式做实现

public class CreditPaymentStrategy implements PaymentStrategy{
    @Override
    public void payment(BigDecimal amount) {
        System.out.println("使用银行卡支付" + amount);
        // 去调用网联接口
    }
}
public class WechatPaymentStrategy implements PaymentStrategy{
    @Override
    public void payment(BigDecimal amount) {
        System.out.println("使用微信支付" + amount);
        // 调用微信支付API
    }
}

重新实现支付服务paymentservice

public class PaymentService {
    
    /**
    * 将strategy作为参数传递给支付服务
    */
    public void payment(PaymentStrategy strategy, BigDecimal amount) {
        strategy.payment(amount);
    }
}

策略模式优化后

public class StrategyTest {

    public static void main(String[] args) {

        PaymentService paymentService = new PaymentService();

        // 使用微信支付
        paymentService.payment(new WechatPaymentStrategy(), new BigDecimal("100"));

        //使用支付宝支付
        paymentService.payment(new AlipayPaymentStrategy(), new BigDecimal("100"));

    }
}

在使用了策略模式之后,在我们的支付服务PaymentService中便不需要写复杂的if…else,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好。

数组和链表的区别

数组:随机查询快,增删慢;内存连续;
链表:随机查询慢,增删快;空间分散,不需要连续;链表内存利用率更高;
数组固定大小,在编译期间分配内存;链表动态灵活,在执行或者运行时分配内存;
链表因为要存储上一个和下一个的引用元素,因此需要更多的内存;

对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组。
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表。

ArrayList,LinkedList,Vector的区别
ArrayList动态数组,默认容量10,扩容为1.5倍,新建数组,复制数据。
LinkedList双向链表
LinkedList还实现了Deque接口,所以LinkedList还可以用作队列
Vector也是数组,线程安全,每次扩容一倍。

常用注解

@Autowired @Component @RestController @Cacheable @RequestMapping @Value @Bean @Import

@Autowired和@Resource的区别
A默认byType,可以通过@Qualify指定bean名称。是spring的注解,默认必须存在bean,可以用required=false设置
R默认ByName,有name和type两种属性,先找name,name没有匹配type。是java的注解。

set如何保证不重复

1.通过hashcode方法获取hash值
2.在hash表中查找,如果不存在则添加成功。如果hash表中含有该值,则进行equals比较,相同添加失败,不同添加到已有对象链末尾。

深拷贝和浅拷贝

浅拷贝 对象的引用变量还是指向原对象地址
深拷贝 对象的引用变量指向不同,会新建引用对象
一般都是浅拷贝,深拷贝的是实现方式:1.重写clone方法,克隆引用成员变量;2.字节流写入文件再读出来;3.构造函数传参等。

设计模式

工厂模式

参考工厂模式
工厂模式属于创建型模式。
意图:定义一个创建接口的接口,让其子类字节决定实例化哪个类,工厂模式使其创建过程延迟到子类执行
主要解决:接口选择的问题
如何解决:让子类实现工厂接口,返回的也是也是一个抽象的产品
关键代码:创建过程在其子类实现

在任何需要生成复杂对象的地方,都可以使用工厂方法模式。简单对象,只需要new就能完成创建的对象,无需工厂模式。使用工厂模式,需要引入一个工厂类,增加系统的复杂度。

举例,抽象一个形状接口,圆,方块实现这个接口,定义一个工厂类提供给获取形状的方法,根据入参判断实例化圆/方块
在这里插入图片描述

抽象工厂模式

在这里插入图片描述

public class AbstractFactoryPatternDemo {
   public static void main(String[] args) {
 
      //获取形状工厂
      AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
 
      //获取形状为 Circle 的对象
      Shape shape1 = shapeFactory.getShape("CIRCLE");
 
      //调用 Circle 的 draw 方法
      shape1.draw();
 
      //获取形状为 Rectangle 的对象
      Shape shape2 = shapeFactory.getShape("RECTANGLE");
 
      //调用 Rectangle 的 draw 方法
      shape2.draw();
      
      //获取形状为 Square 的对象
      Shape shape3 = shapeFactory.getShape("SQUARE");
 
      //调用 Square 的 draw 方法
      shape3.draw();
 
      //获取颜色工厂
      AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
 
      //获取颜色为 Red 的对象
      Color color1 = colorFactory.getColor("RED");
 
      //调用 Red 的 fill 方法
      color1.fill();
 
      //获取颜色为 Green 的对象
      Color color2 = colorFactory.getColor("GREEN");
 
      //调用 Green 的 fill 方法
      color2.fill();
 
      //获取颜色为 Blue 的对象
      Color color3 = colorFactory.getColor("BLUE");
 
      //调用 Blue 的 fill 方法
      color3.fill();
   }
}

在公共微服务调用模块使用,根据接口标识获取类名,从spring工厂中获取spring对象进行处理。

  • 单例模式:spring bean 默认单例
  • 代理模式:AOP的实现方式是通过代理实现,Spring主要使用JDK动态代理和CGLIB代理
  • 模板模式方法:对数据库类的操作,JDBCtemplate,数据库建立连接,执行查询,关闭连接几个过程非常适合模板方法
    redisTemplate,restFulTemplate
  • 观察者模式:spring的事件驱动模型使用的是观察者模式,常用的是listener的实现
    事件机制的实现有三个部分,事件源,事件,事件监听器。
    srping容器初始化时,会注册事件监听器。
事件源:继承ApplicationEvent 
监听器: 实现ApplicationListener<TestEvent>
事件发布器:ApplicationEventPublisher  发布时获取事件广播器ApplicationEventMulticaster,讲自定义事件告诉广播器
 getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType)
multicastEvent的方法功能是遍历事件监听器列表,逐个发布事件到监听器中。

SimpleApplicationEventMulticaster 内部维护了监听器列表,用ConcurrentHashMap管理
最终调用 SimpleApplicationEventMulticasterinvokeListener() 方法进行实质事件处理。
doInvokeListener() 最终会调用监听器的 onApplicationEvent 方法,实现监听效果。

TCP如何保证可靠性?

  1. 序列号和确认号机制:TCP发送数据,会带一个序列号,服务端在检测数据完整后会发送一个确认号表示确认收到了数据包

  2. 超时重发机制:tcp发送数据包后会启动一个定时器,如果一定时间没有接收到接收端的确认,将会重新发送

  3. 去重:从IP网络传输层到TCP层数据可能重复,TCP会对数据去重

  4. 顺序:从IP网络传输层到TCP层数据可能乱序,TCP会对数据重新排序

  5. 流量控制:客户端和服务端的缓存大小一定,为了防止数据溢出,通过滑动窗口协议保证数据大小

三次握手和四次挥手

参考

握手:c发送序列号x;

s响应 确认号x+1,序列号y;

c发送确认号y+1,x+1

挥手:c发送fin,s响应ack,s发送fin,c响应ack

第三次握手是可以发送的数据的

如果接收方没收到数据,确认号+1,否则就加上收到的数据量

HTTP

HTTP是基于TCP的应用层的超文本传输协议

优点

  1. 简单:报文格式为header+body,头部信息也是kv格式
  2. 灵活易扩展:http协议中的请求方法,url,状态码,header都没固定死,允许开发人员自定义
  3. 应用广泛跨平台

缺点:

  1. 无状态:没有记忆能力
  2. 不安全:明文传输

反射的优化

1.缓存Consturctor,Method等对象
2.setAccessible(true) 关闭安全检查
3.利用反射工具包ReflectASM,通过字节码生成的方式来实现反射机制

并发编程

synchronized

参考 面试官:请详细说下synchronized的实现原理 - 知乎
概念:在多线程情况下,多个线程访问共享资源会出现问题,而synchronized关键字则是用来保证线程同步的

synchronized解决可见性的方式是,每次都清除工作内存,从主内存中重新获取。

synchronized可以保证并发编程的三大特性:原子性,可见性,有序性。

synchronized可以实现悲观锁,非公平锁,可重入锁,独占锁或者排它锁。

实现原理:
Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。

jDK1.6对synchronized做了哪些优化?
引入偏向锁和轻量级锁,随着竞争的激烈而升级。
在这里插入图片描述

引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。

使用CAS操作将当前线程的ID记录到对象的Mark Word中。

引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。

将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。

锁消除是指java编译时,消除不可能发生共享资源竞争的锁。
锁粗化是指java编译时,将不必要的重复加锁粗化到整个操作的外部。

for(int i=0;i<n;i++){
    synchronized(lock){
    }
}
//粗化后
synchronized(lock){
    for(int i=0;i<n;i++){
    }
}

synchronize和lock的区别和使用场景

参考粗谈synchronize和Lock锁的区别及使用场景
区别

  • synchronize是java关键字,内置特性。Lock是一个接口,通过这个接口的实现类可以实现同步访问。
  • synchronize 在代码执行结束后或者代码执行异常后会自动释放;而lock必须要用户手动去释放锁,否则会造成死锁。
  • synchronize可以锁住代码块,类,对象,lock只能锁代码块
  • synchronize只能是非公平锁,而lock可以是公平锁,也能是非公平锁。
  • synchronize等待不中断,而lock可中断。
  • synchronize不知道线程有没有获得锁,但是lock可以知道。
  • synchronize是隐式锁。Lock是显示锁。显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
  • synchronize是悲观锁的一种实现.lock的实现类ReentrantLock主要用到unsafe的CAS和park两个功能实现锁,乐观锁的一种实现。
  • 性能比较:竞争不激烈时,synchronize的性能优于ReentrantLock,但是在竞争激烈的情况下,synchronize性能下降几十倍,ReentrantLock的性能可以维持常态。
    重入锁提供多样化的同步,如时间限制的同步,被打断的同步。
    在这里插入图片描述
    synchronize的释放
  • 占有锁线程代码执行完成
  • 占有锁线程出现了异常
  • 占有锁线程调用wait方法,进入waiting状态需要释放锁
  • 执行完成后可以通过notifyAll或notify等object对象的api来唤醒其他等待线程立马执行。
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

接口方法

  • lock();用来获取锁,如果锁被其他线程获取,则进行等待
Lock lock = ...; //声明锁
lock.lock(); //获得锁
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}
  • tryLock();尝试获取锁,立即返回
  • tryLock(long time, TimeUnit unit); 尝试在一定时间内获取锁
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     }
}else {
    //如果不能获取锁,则直接做其他事情
}
  • lockInterruptibly()
    它是对于那些未竞争的到锁,而 可以被外部调用interrupt()来中断,从而达到不在等候锁资源,不再去竞争锁

synchronize和reentrantlock都是可重入锁。
就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。synchronize不用手动释放。

实现

  • ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

  • Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止

LOCK的实现类

Lock定义了标准Lock的API

AQS(AbstractQueuedSynchronizer)
ReentrantLock:重入锁,支持公平和非公平
ReentrantReadWriteLock:在ReentrantLock上基础上支持读写分离是和多读少写的场景CountDownLatch:
Semphore:
Java的Lock实现类介绍

AQS

AbstractQuenedSynchronizer 抽象的队列同步器。是除了java自带的synchronized关键字之外的锁机制

核心思想是,如果共享资源空闲,请求资源的线程设置为有效线程,将共享资源设置为锁定状态,如果请求的共享资源被占用,则将线程加入到CLH队列中。
CLH是一个虚拟的双向队列,不存储队列实例,仅存储节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个节点Node,来实现锁的分配。
AQS是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功获得锁,否则加入队列中,等待被唤醒。
CLH锁是一种自旋公平锁,

AQS定义了两种资源共享方式
1.Exclusize:独占,只有一个线程能执行,如ReentranLock
2.Share 共享,多个线程可以同时执行,如Semphore,CountDownLatch,ReadWriteLock,CyclicBarrier

死锁

在 Java 开发中,死锁发生的条件通常有四个:

互斥条件:至少有一个资源必须处于非共享模式,即某一时刻只有一个线程能使用该资源。
持有并等待:至少有一个线程持有一个资源,并等待获取其他被其他线程持有的资源。
不剥夺条件:已经分配给线程的资源在其未使用完之前,不能被其他线程强制剥夺。
循环等待:存在一种线程资源的循环链,每个线程持有一个资源并等待下一个线程持有的资源。

如何避免死锁

1.死锁预防
1.1.破坏占有并且等待
1.一次性申请运行过程中需要的所有资源
2.允许只获得初期资源就开始运行,运行后逐步释放使用完毕的资源,然后再去请求新的资源。
1.2.破坏不可抢占条件
当获取锁失败,释放之前获取的资源
1.3.破坏循环等待的条件
定义资源的线性顺序来预防
2.避免死锁,在使用前进行判断,只允许不会产生死锁的进程申请资源.
一般采用银行家算法来避免。需要知道进程请求资源的最大数目。

线程池

参考并发编程(三):线程池基本面试题(必背题目)

作用

限制系统中执行线程的数量

1.降低资源消耗:复用线程。
2.提高效率,提前创建,使用从中获取节省创建时间。
3.增加线程的可管理型。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。

常见线程池

1.SingleThreadExecutor
只有一个线程,保证任务按顺序执行(FIFO,LIFO,优先级)
任务队列为链表结构的有界队列
2.FixedThreadPool
定长线程池,超出等待。只有核心线程,执行完成后回收。
任务队列为链表结构的有界队列。
3.CachedThreadPool
超出回收空闲线程,没有则创建线程。核心线程固定,非核心线程无限。使用完成后闲置10分钟回收。任务队列为延时阻塞队列。
4.ScheduledThreadPool
定时执行任务线程池
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
5.WorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。

线程池中的几个重要参数

  • corePoolSize 核心线程数量,用不到也不会回收。allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize 最大线程数量,活跃线程数量达到该值,阻塞新任务。
  • keepAliveTime 非核心线程最长存活时间
    如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory 线程工厂,指定线程池创建新线程的方式。
  • handler 拒绝策略,达到最大线程要执行的饱和策略。
//TreadPoolExecutor(自定义参数线程池)(推荐使用)
public class ThreadPoolDemo {
    public static void main(String[] args) {
        //1. 使用ThreadPoolExecutor指定具体参数的方式创建线程池
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                2, //核心线程数
                5, //池中允许的最大线程数
                2, //空闲线程最大存活时间
                TimeUnit.SECONDS, //秒
                new ArrayBlockingQueue<>(10),//被添加到线程池中,但尚未被执行的任务
                Executors.defaultThreadFactory(), //创建线程工厂,默认
                new ThreadPoolExecutor.AbortPolicy()//,如何拒绝任务
        );
        
        //2. 执行具体任务
        poolExecutor.submit(new MyRunnable());
        poolExecutor.submit(new MyRunnable());

        //3. 关闭线程池
        poolExecutor.shutdown();
    }
}

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行了");
    }
}

拒绝策略

任务不断过来,系统无法及时处理,就要拒绝。

  • AbortPolicy(默认) 抛出RejectedExecutionException
  • CallerRunsPolicy :由调用线程处理该任务。
  • DiscardOleddestPolicy: 该策略将丢弃最早的未处理任务,并尝试再次提交当前任务
  • DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。
    可以通过实现RejectedExecutionHandler接口自定义接口。

execute和submit的区别

execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。
submit方法适用于需要关注返回值的场景

线程池的关闭

shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

线程数的选择

计算密集型:应为 cpu核数+1 减少上下文切换
即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。

IO密集型:
线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
IO比cpu慢,设置过少线程数会造成cpu资源的浪费。
等待时间越长,线程越多。

什么时候使用线程池

1.任务数量大,单个任务处理时间短,频繁创建销毁线程的场景
2.线程只涉及创建没有销毁,如保持长连接,心跳,消费者线程。

线程池都有哪几种工作队列

1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,在未指定容量时,容量默认为 Integer.MAX_VALUE.此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5、DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。

线程工作流程

提交任务,线程是否达到核心线程数,未达到,创建核心线程,否则下一步
查看任务队列是否已满,未满,放入任务队列,否则,进入下一步
线程是否到达最大线程数,未到,创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。

随着任务的增加增加活跃线程数,活跃线程数=核心线程数

线程池优化

  • ThreadPoolExecutor自定义线程池,任务量不大,使用无界队列。任务量大,使用有界队列,防止OOM。
  • 如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
  • 最大线程数一般设为2N+1最好,N是CPU核数
  • 核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
  • 如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果。

线程的状态

新建 就绪 运行 阻塞 死亡

wait和sleep的区别
wait释放锁,需要notify唤醒,sleep不释放锁,自动唤醒。
run和start的区别
run直接运行,start是进入就绪状态。

  1. run() 方法
    直接调用:如果直接调用 run() 方法,它将在当前线程中执行,像普通的方法调用一样。
    无新线程:不会创建新的线程,所有代码在调用 run() 的线程中运行。
    适用场景:通常用于实现 Runnable 接口中的逻辑,或者在测试时直接调用。
  2. start() 方法
    启动新线程:调用 start() 方法会创建一个新的线程,并在新的线程中执行 run() 方法。
    异步执行:新线程会并行运行,主线程和新线程可以同时执行。
    适用场景:用于需要并发执行的场景。

线程间通信的方式

  • 通过 volatile 关键字
  • 通过 Object类的 wait/notify 方法
    wait,notify,notifyAll都必须是在同步代码块中执行
    • wait:需要先获得锁,执行wait后释放资源,进入阻塞状态,直到被notify方法唤醒
    • notify:会唤醒一个等待锁的线程,执行同步代码块后释放锁
    • notifyAll:和notify相同,但是会唤醒所有等待锁的线程,进入就绪队列
  • 通过 condition 的 await/signal 方法
    condiction是通过lock对象来创建,使用前也需要获得锁。lock替代了synchronize方法和语句的使用,condition替代了object监视器方法的使用
    • await 自主释放锁,进入沉睡状态,直到被再次唤醒
    • await(long time,TimeUnit unit) 线程自主释放锁,进入沉睡,在被唤醒和等待时间内一直处在等待状态
    • signal() 唤醒一个等待线程
    • signalAll() 唤醒所有等待线程,能够从等待方法返回的线程必须获得condition相关的锁。
  • 通过 join 的方式
A.start();
B.start();
B.join();

join方法本质上是调用wait方法,让当前线程阻塞,直到另一个线程执行完毕。(当前线程wait后,执行join方法的线程大概率抢到锁资源,而且当一个线程执行完毕后,会默认调用notifyAll方法。)

volatile 如何保证可见性和指令重排

造成可见性的原因是JAVA内存模型JMM,在java内存模型中,共享变量存放在主内存中,每个线程都有自己的工作内存,操作共享变量需要从主内存中获取,但是何时写回主内存不可预知,这就导致每个线程变量的操作是封闭的,其他线程不可见的。
CPU快内存慢,一般都用寄存器解决。

可以加synchronized关键字,进入synchronize代码块,会清除缓存,从主内存中获取共享变量,进行操作,刷新回主内存,然后释放锁。
效率低下,出现了缓存一致性协议,有MSI,MESI,MOSI等,最出名的是Intel的MESI协议,保证了每个缓存中使用的共享变量的副本是一致的。

使用volatile等于告诉CPU需要MESI协议和嗅探机制来保证可见性。

MESI机制:
1.Modify:当缓存中的数据被修改时,该缓存设置为M状态
2.Eclusive(独占):当只有一个缓存使用某行数据时,设置为E状态
3.Share(共享):当多个CPU有数据的缓存,该数据的缓存设置为S状态
4.Invalid(无效):当某个数据的缓存修改时,其他持有该数据的缓存更新为I状态

核心思想:CPU修改数据,发现该数据是共享变量,会发出通知让其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量的时候,发现自己的缓存行是无效的,那么他就会重新读取。
监听和通知基于总线机制。总线嗅探机制就是一个监听器。

2.有volatile修饰的共享变量在写之前会多出一条lock指令
lock前缀会触发
1.将当前缓存行的数据写会主内存
2.这个写回操作会使其他CPU中缓存了该内存地址的数据无效。

JMM:
1.lock前缀会将线程工作内存中的缓存数据写回主内存
2.通过缓存一致性协议,其他线程如果工作内存中使用了该变量的值,就会失效
3.其他线程会重新从主内存获取新的值、

大量使用volatile会导致总线风暴。
volatile保证数据的可见性,但不保证数据操作的原子性。在多线程环境下,使用volatile变量是线程不安全的,可以使用锁机制或者原子类。

禁止指令重排
编译器不会对volatile读以及volatile后面的任务内存操作重排序。
通过内存屏障来实现。

异步开发

  1. 线程异步,可以使用线程池
  2. CompletableFuture异步,它是基于异步函数式编程。相对阻塞式等待返回结果,CompletableFuture 可以通过回调的方式来处理计算结果,实现了异步非阻塞,性能更优。
  3. SpringBoot @Async 异步

并发编程/多线程开发

继承Thread,实现runnable,实现callable,使用线程池

callable和runnable的区别

1.runnable通过创建线程执行 start
callable通过executorservice执行 submit 或者作为FeatureTask的参数
2.r没有返回值,c有返回值,可以通过泛型指定
3.r不能异常处理,c的call()方法可以抛出异常,并由其执行者Handler进行捕获并处理
4.r适用于不需要返回值,不会抛出异常的场景,c适用于需要返回值,或者需要抛出异常的场景

CAS

compare and swap

乐观锁,假设不会发生冲突去完成操作,因为冲突失败就重试,知道成功为止。

CAS中使用了3个基本操作数:

共享变量的内存地址V

工作内存中共享变量的副本值,也就是旧的预期值A

更改的值B

只有当内存地址V的值和预期值A相等的时候才进行更新,否则提交失败,重新尝试,这个过程称为自旋。

缺点:

  • ABA问题:通过增加版本号解决,如AtomicStampedReference类使用pair内部类实现,包括版本号和引用,都相等才更新

  • 竞争激烈,自旋可能会消耗较高的CPU。可以使用AtomicLong的替代类:LongAdder。

  • 不能保证代码的原子性:只能保证共享变量操作的原子性,而不能保证代码块的原子性

优点

  • 保证变量操作的原子性
  • 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高
  • 在线程对共享资源占用时间较短的情况下,使用CAS效率也会较高

Java提供的CAS操作类,unsafe类,Atomic系类底层调用unsafe的API

CAS使用场景:

  • 使用变量统计网站访问量
  • Atomic类操作
  • 数据库乐观锁更新

数据库

mysql和oracle的区别

1.类型 oracle是一个对象关系数据库管理系统
mysql是关系数据库管理系统
2.存储 mysql没有表空间,角色管理,同义词,快照和包以及自动存储管理
3.安全 mysql使用用户名,密码和位置验证,oracle更多安全功能,用户名,密码,配置文件,高级安全增强等
4.事务支持 innodb支持事务,oracle完全支持事务
5.性能诊断 mysql 慢查询日志 oracle 有很多工具,如awr,addm,sqltrace

如何优化联表查询

1.使用正确的连接类型,join最快
2.使用合适的索引,确保连接字段上有索引,考虑联合索引和查询结果使用覆盖索引
3.减少扫描的行数,where后的条件
4.避免在连接字段上使用聚合函数,计算和类型转换
5.使用合适的连接顺序,小表在from后面第一个
6.使用临时表
比如有订单表和客户表,直接查询,在每次连接中查询中进行聚合计算
可以创建临时表

CREATE TEMPORARY TABLE customer_order_summary AS
SELECT customer_id, COUNT(order_id) AS order_count, SUM(order_amount) AS total_amount
FROM orders
GROUP BY customer_id;

一次聚合计算,后续直接连接,也减少了连接的行数
7.分批次查询
8.在a表中加入b表的字段,单查a表

char和varchar的区别

  1. 长度
    varchar存储可变长度字符串,仅使用必要的空间。
    char存储定长字符串,会占用多余的空间
    mysql和oracle都会在字符长度不够时在后面补全空格,区别是oracle在查询时会返回空格,mysql不会。
    char会默认去掉字符串末尾的空格
  2. 存储方式
    varchar需要1个或者2个字节来记录字符串的长度,长度小于等于255用一个字节表示,否则用2个字节表示。在更新时可能会使行比原来更长,导致需要做额外的工作。MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页使行可以放进页内。
    char适合用来存储密码的MD5值。
  3. 存储容量
    char最大255 2^8
    varchar 65535 2^16

sql优化方案

  • 不使用select * ,避免多余的网络开销和回表操作
    当以主键索引为查询条件时,select * 不会触发回表,因为已经主键索引的B+树已经包含了所有数据;当以非主键索引作为查询条件时,查询结果只有查询条件和主键,还需要根据主键再去查询其他字段,就是回表查询。
    覆盖索引是指select到from的查询列都是主键或者索引。
  • 能union all,就不用union。去重的过程中需要遍历,排序和比较
  • 小表驱动大表: in 左大右小 exist 左小右大
  • 联合索引的使用遵循左前缀法则

1.索引优化
创建索引:
在查询,分组,排序字段上建立索引;对于经常连接用的字段建立索引;
避免过多索引,影响写性能。
复合索引:
对于经常作为复合条件查询的字段,增加复合索引。注意字段顺序。
2.查询优化
避免select * 增加I/O开销,浪费内存和带宽
采用连接查询代替子查询
避免不必要的聚合操作,先过滤条件,在聚合
limit分页优化,避免一次性查询大量数据
3.表结构优化
反规范化:合适的场景使用反规范化减少连接操作,注意一致性和数据冗余的问题
分区表:对于大表考虑使用分区,分区可以加速查询,减少扫描使用的数据量
合理使用数据类型:小数据不要用大类型
4.缓存机制
查询缓存,redis,memcached
数据库缓存,mysql的innodb Buffer Pool,定期检查数据库缓存大小,查看是否合理
5.数据归档和分表
6.执行计划分析与优化
7.数据库配置优化:内存配置,连接池配置
8.批量操作和并行处理
9.优化事务和锁机制

如何查看有没有走索引

mysql

参考MySQL如何查看SQL查询是否用到了索引?

explain+sql
EXPLAIN select * from tb_brand where id='1';

在这里插入图片描述
type 性能
由好到坏:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
至少range,最好ref
possible_keys 查询用到的索引,没有的话值为null
key 实际决定查询结果使用的索引,没有的话值为null
rows为执行查询时必须检查的行数

oracle

参考Oracle通过执行计划查看查询语句是否使用索引

explain plan for +sql
select * from table(dbms_xplan.display)

TABLE ACCESS FULL为全表扫描;
index range scan为索引范围扫描;
常见的索引类型扫描:

  • index unique scan 索引唯一扫描
    当查询条件可以用到主键,唯一键,具有外键约束的键,或者只是访问索引所在的数据的时候,优化器会选择这种扫描类型
  • index range scan 索引范围扫描
    当优化器发现在唯一列上使用了>,<,between会使用范围扫描,在组合列上只使用部分进行查询,导致查询出多行数据。
  • index full scan 全索引扫描
    查询的数据可以全部从索引中获取,则使用全索引扫描
  • index fast full scan 索引快速扫描
    与全索引扫描类型,区别是返回的数据是不排序的

慢查询SQL

是指超过指定时间的sql。在mysql中默认关闭,手动开启

show variables like 'slow_query%';
show variables like 'long_query_time';

参数说明如下:
slow_query_log:慢查询开启状态
slow_query_log_file:慢查询日志存放的位置(一般设置为 MySQL 的数据存放目录)
long_query_time:查询超过多少秒才记录

mysql> set global slow_query_log=ON;
Query OK, 0 rows affected (0.05 sec)

mysql> set global long_query_time=0.01;
Query OK, 0 rows affected (0.00 sec)

超过指定时间就会记录在sql中

建表

尽量避免用text类型,采用es或者oss存储
字段尽量设置非null,会影响索引稳定
记得写注释comment
数据重要的情况下采用innodb,支持事务操作
减少索引大小可以采用前缀索引,但是前缀索引不能消除group by ,order by带来排序开销

经验:新核心迁移,编码不一致,扩容 gbk 到utf-8

索引的建立规则

1.选择索引的列:
查询,排序,连接,聚合的列
2.避免过度索引,重复索引
3.高选择性,显著减少数据扫描的数量,如学号和性别
4.左前缀匹配法则
5.索引维护:定期重建,监控性能,删除未使用的索引
7.索引的事务性:考虑索引的设计对事务的影响

索引以及实现方式

索引是帮助数据库高效获取数据的数据结构。
数据库除了维护数据,还维护着满足特定查找算法的数据结构,数据结构以某种方式指向引用数据,称为索引。
MySQL索引的数据结构
MySQL索引的数据结构

Oracle是B树,Mysql是B+树

参考面试官:你知道多少种索引?
索引,目录,提高查询效率。
索引常用的实现方式 B树,B+树 。hash也可以。
B树就是平衡树,时间复杂度为logn

  • 多路查找树,叶子节点位于同一层
  • 每个节点不仅包含数据的键值,还包括data值
  • 每个节点相当于一个磁盘块
    在这里插入图片描述

B+树基于B树实现,是有序的。

  • 每个叶子节点存储字段键值以及对应的数据
  • 非叶子节点只存储索引键值以及指向子节点的指针,不存储数据
  • 每个节点相当于一个键盘块
  • 同一层级的叶子节点之间以双向链表的形式相连
  • 在这里插入图片描述

为什么有B树,还要有B+树?
B+树的叶子节点会指向下一节点,遍历查找更快。
非叶子节点不存储数据,key更紧密,数据查询更稳定和迅速,因为更好的利用空间局部性原理。

为什么使用B+树?
在这里插入图片描述
数据库访问通过页,尽量减少IO操作次数,因此树的层级要尽可能的少。
B树相比于二叉树,B树的非叶子结点可以有多个子树,因此B树高度远远小于AVL树和红黑树,磁盘IO数大大减少。

B+树相比于B树
1.非叶子节点不存储数据,存储的数据更多,因此B+树的高度更低,更少的磁盘IO操作。由于每个节点存储的记录更多,对局部性原理的利用更好,缓存的命中率更高。
2.更适合范围查找:B+树只需要对链表进行遍历,但是B树需要找到查询下限,然后进行中序遍历,直到找到查询的下限。
3.更稳定的查询效率:B+树的查询复杂度稳定为树高,因为所有数据都在叶子节点。

哈希也可以,但是会有两个缺点:1.哈希冲突。2.哈希计算的是个值,无法进行范围查询。
Mysql的innodb使用的是B+树。

索引的类型

1,普通索引:普通索引是最基本的索引,它没有任何限制,值可以为空;仅加速查询。
2,唯一索引:唯一索引与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
3,主键索引:主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。
4,组合索引:组合索引指在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
5,全文索引:应用场景为百度和淘宝的搜索框。关键字查找,不支持大小写,索引创建慢。mysql中的MyIsam支持,innnodb不支持。不推荐使用,一般用es实现。

6.空间索引:专为地理数据设计,用于加速空间数据的查询,通常用于 GIS 数据。

索引失效的情况

  1. 函数
  2. 运算
  3. 类型转换
  4. 模糊查询前置通配符
  5. or语句必须都有索引才会走索引
  6. != 和数据量和数据趋势有关,返回结果<20%,走索引,>20%不走索引,选择更有效率的全表扫描
  7. 范围查询不一定走索引,优化器会计算,结果集占总数<30% 才会走索引 比例值会随着版本,服务器,IO,数据量,数据重复情况而不同。
  8. 存在null值 <>、NOT、in、not exists
  9. 复合索引未遵循左前缀匹配法则

聚簇索引和非聚簇索引

mysql默认引擎innodb分为两种索引,聚簇索引和非聚簇索引,每个索引对应一颗b+树,两者的区别主要是叶子节点存储的数据不同,聚簇索引存储行数据,非聚簇索引存储聚簇索引,因此需要进行第二次查询,称为回表查询。
主键索引的叶子节点储存的是一行完整的数据,非主键索引的叶子结点存储的是主键值 和 非主键索引的对应的列值
聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引; 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针。
聚簇索引一般是指主键索引,一张表只能有一个。查询效率高。
非聚簇索引也称二级索引,数量不限制,查询效率低。

聚簇索引数据和索引都在一块存储。非聚簇索引叶子节点指向存储节点的位置,数据和索引分开存储。
在innodb中,在聚簇索引上创建的索引称为辅助索引,像非聚簇索引,复合索引,前缀索引,唯一索引。辅助索引叶子结点不存储数据,存储聚簇索引,总是需要二次查找。
聚簇索引具有唯一性,因为索引和数据在一块存储。
表中行的物理顺序和索引中的行物理顺序是相同的。
聚簇索引默认是主键,如果没有主键,会选择一个非空的列来做聚簇索引(类似oracle中的rowid)。如果想指定非主键做聚簇索引,删除主键,设置聚簇索引,然后重新设置主键即可。

InnoDB存储引擎使用聚簇索引来组织数据,也就是说表中的数据实际上是按照主键顺序存储的。因此,使用主键进行查询可以获得良好的性能。
MyISAM存储引擎使用非聚簇索引,也就是索引和数据是分开存储的,因此查询的性能可能会受到影响,特别是对于范围查询或者覆盖查询。

聚簇索引优势
1.数据和索引叶子结点存储在一起,每页有多行数据,查询其中一条会加载到buffer(缓存器)中,访问同页其他行就不会访问磁盘直接返回,查询更快。
(一次iO读写会获取16k的资源,读取到的数据区域称为Page,b+树,一个叶子节点上有多条数据和索引值,因此数据在叶子节点上不需要重复查询,走缓存。只有页分裂,即数据不存在才重新申请IO,)
2.当行数据发生变化,只需维护聚簇索引。非聚簇索引叶子节点存储主键,更节约空间。
3.myisam使用非聚簇索引,地址凌乱,拿到地址,按照合适的算法进行IO读取,聚簇索引只需一次。
4.涉及大数据量的排序,全表扫描,count,非聚簇索引更快,因为索引小,这些操作都是在内存中完成的。

聚簇索引中物理顺序和索引顺序一致,因此建议使用自增主键,不用uuid。默认会在索引树的末尾增加主键值,对索引树的结构影响最小。索引紧凑,磁盘碎片少,效率高。
主键大小也会影响,因为辅助索引中存储的主键,导致索引存储内存增多。影响IO操作读取的数据量。

优点:
1.查询快,索引和数据在一块
2.主键排序和范围查找很快
缺点:
1.插入速度依赖于插入顺序,一般主键自增
2.更新主键代价高,需要移动行
3.二级索引访问需要进行两次索引查找

mylsam查询比innodb速度快的原因

1.innodb不会压缩索引 查询时需要缓存数据块,而mylsam数据和索引是分开的,可以压缩索引,在相同容量的内存加载更多的数据
2.innodb寻址要映射到块,再到行(个人猜想主要是查询行的版本号),MYISAM记录的直接是文件的OFFSET,定位比INNODB要快
(注释: SELECT InnoDB必须每行数据来保证它符合两个条件: 1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除)
3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护MVCC (Multi-Version Concurrency Control)多版本并发控制

联合索引的命中规则

在test表上对列a,b,c建立联合索引
联合索引采用左前缀命中规则
从左到右匹配直到匹配终止条件。
终止条件为范围操作符,函数等不能应用索引的情况。
和顺序无关,mysql优化器会进行优化。

a‹› range索引
or 不使用索引
order by 没有影响
a= and b‹› 走索引

数据库事务

数据库事务是一个不可分割的数据库操作序列,也是数据库控制并发的基本单位,其执行结果为使数据库从一种状态到另一种状态
事务的特性 ACID
原子性 Atomicity:事务中的操作要么全部成功,要么全都失败
一致性 Consistency:事务执行前后,数据库中的数据具有一致性
隔离性 Isolation:多个事务之间不会相互影响
持久性 Durability:事务提交后,对数据库的改变是永久性的

脏读:读到了未提交的数据
不可重复读:两次读取到的数据内容不一致,这是update引起的
幻读:两次读取到的数据数量不一致,这是insert或者delete引起的

事务的隔离性
读取未提交:会发生脏读、不可重复度、幻读
读取已提交:避免脏读 Oracle默认
可重复读:避免脏读,不可重复读 Mysql默认
可串行化:最高隔离级别,事务顺序执行,不会相互影响
注意:

  • Mysql默认采用MyIsam,不支持事务
  • 事务隔离机制的实现基于锁机制和并发调度。
  • 隔离级别越低,事务请求的锁越少。MySQL的innodb默认使用可重复读并不会有性能损失。
  • innoDB存储引擎在分布式事务下一般会用到可串行化隔离级别
--mysql
start transaction; -- 开启事务

savepoint abc;--设置回滚点


rollback to abc;--回滚到abc
commit; -- 提交事务
rollback; -- 回滚事务

--查看MYSQL中事务是否自动提交
show variables like '%commit%';
--关闭自动提交
set autocommit = 0;-- 0:OFF  1:ON

set session transaction isolation level  --隔离级别;
--eg: 设置事务隔离级别为:read uncommitted,read committed,repeatable read,serializable
set session transaction isolation level read uncommitted;
--查询当前事务隔离级别
select @@tx_isolation;

select for update

对行加锁,不能增删改,其他人也不能select for update

for update 使用场景:高并发情况下对数据有很强的要求
排他锁的申请前提,没有线程对结果集的数据进行排他锁和行级锁,否则会申请阻塞

共享更新锁的释放条件:1.commit 2.推出数据库 log off 3.程序停止运行

数据一致性
悲观锁:先锁后更 乐观锁:先更后比较 悲观锁适合频繁更新的情况,乐观锁适合频繁查询的情况
行锁和表锁
innoDb默认行级锁,指定主键是行级锁,否则是表级锁
for update仅适用于InnoDB,必须在实务块中才能生效(Begin Commit)

总结
1.innodb行锁是根据索引项加锁实现的,只有通过索引查询数据才会产生行锁,否则只会产生表锁
2.由于mysql的行锁是针对索引加的锁,不是对记录进行加锁,因此即便查询不同行的记录,但是索引相同也会发生锁冲突
3.当表有多个索引,不同的事务可以用不同的索引锁定不同的行。不论是使用主键索引,普通索引还是唯一索引,innodb都会使用行锁来进行加锁
4.即便查询条件中用到了索引,但是因为sql执行计划不一定采用索引,因此分析锁冲突时,需要检查sql执行计划,判断是否真的走了索引

oracle
select for update 当发现有人修改时,等待修改完成之后再去查询
for update nowait 发现有人修改,直接异常
for update wait 3 超过三秒未更新结束,抛出异常

delete和truncate的区别

1.d删除记录日志,可以使用where语句
t不记录日志,重置表结构和属性(如自增列的计时器)
2.性能:t不记录日志,更快
3.事务处理 在事务中使用,d能回滚,t一般不能回滚,因为是ddl语句
4.delete会触发触发器,t不会
5.delte会删除有外键约束表的数据,前提是满足约束条件。
t如果表与其他表有外键关系,通常会被制止

什么情况下会发生锁表

insert,delete,update,select for update 未提交事务的情况下
alter table 修改表结构
truncate table清空表数据
mysql手动加锁
GET_LOCK()和RELEASE_LOCK()函数来获取和释放表锁。

锁表发生在并发而非并行。
减少锁表概率:缩短数据库事务开启和提交时间:具体:批量执行改为单个执行,优化sql执行速度。

幻读

什么时候发生幻读,如何解决?
幻读产生的条件:
在可重复读的隔离界别下,普通的查询都是快照读,查询不到别的事务新插入的数据,所以幻读是在当前读的状态下产生。

快照读:读取的是快照数据,不加锁的查询读取的都是快照数据
当前读:读取数据库最新数据,加锁的查询和增删改都会进行行当前读

解决幻读,innodb加入了间隙锁,间隙锁不仅可以锁住数据行的实体,也可以锁住数据行之间的间隙。
会造成并发下降。
解决方案:
1.降低为读取已提交,但是为了解决数据和日志不一样的问题,需要把binlog格式设置为row。
–日志不一样的原因:binlog日志是以commit顺序为准,如果第一个事务更新数据但未提交,第二个事务更新数据满足第一个事务中的更新条件但是提交了,日志中第二个日志在前,第一个日志在后。看着就是把第一个事务中的数据也更新了
RC情况下默认是statement,记录顺序是以commit方式提交的。
row会记录每一列之后的值。
2.判断有无数据,有则删除,无数据不删除,因为删除不存在的数据一定要加间隙锁
3.间隙锁只针对写锁,只要两个线程锁定的区间有交叉就会出现死锁。可以根据条件查询出所有主键,根据主键删除数据,这样只会加行锁。

间隙锁产生的条件:
执行当前读,where条件没有命中索引(命中索引加行锁,没有索引加表锁)
innodb_locks_unsafe_for_bin_log 默认false 启用间隙锁。

间隙锁和行锁的组合称为netxt-key lock 临间锁,每个next-key lock 临间锁 是前开后闭区间,n行数据,产生n+1个next-key 锁

数据 1,2,3 select * from table for update 产生四个next-key锁,(-∞,1],(1,2],(2,3],(4,+∞],此时另一个事务进行插入是无法插入的,避免了幻读的产生

间隙锁是在可重复读的隔离级别下生效。
只有可重复读的隔离界别的当前读才会出险幻读。
增加排他锁时,会对结果集的行增加间隙锁。

mysql共享锁 sql + lock in share mode
1.允许其他事务增加共享锁
2.不允许其他事务增加排他锁
3.多个事务共同添加,必须等待先执行的事务commit后才行。
排他锁 sql + for update
1.事务之间不允许其他排他锁和共享锁读取,修改更不允许
2.所有事务只有一个排他锁执行commit后,其他事务才可以执行

mvcc
多版本并发控制,解决读写时的线程安全问题,线程不用去争抢读写锁。
隔离性:通过加锁(当前读)和MVCC(快照读)实现。
一致性:通过undolog,redolog,隔离性共同实现。
mvcc的实现,基于undolog,版本链,readView。

在mysql存储过程中,会隐式的定义几个字段:
trx_id:事务id,每次进行一次事务操作,自增1
roll_pointer:回滚指针,用于找到上一个版本的数据,结合undolog进行回滚

使用select读取数据,这是时刻的数据会有多个版本,通过readview来判断能够读取哪个版本
readview中包含以下字段:
m_ids:活跃的事务id列表 活跃的事务是指还没有commit的事务
min_trx_id_:活跃事务中的最小值
max_trx_id_:下一个事务id
ctreator_trx_id:执行select读这个操作的事务id

readview如何判断哪个版本可用
trx_id==ctreator_trx_id 可以访问这个版本 :读取自己创建的记录
trx_id<min_trx_id_ :可以访问这个版本 :要读取的事务已提交,可以访问
trx_id>max_trx_id_:不可访问 :读取的事务id已经不在该版本链中,故无法访问
min_trx_id_<=trx_id<=max_trx_id_: trx_id在当前事务在活跃事务中不可以访问这个版本,反之可以 :读取的事务id不在活跃列表可以读取

mvcc如何实现RC和RP的隔离级别
1.rc时,每个快照读,都会生成并获取最新的readview
2.rr时,只有在同一个事务的第一个快照读才会创建readview

幻读问题:
快照读:通过mvcc,rr的隔离界别解决了幻读问题,因为每次都是同一个readview
当前读:通过临键锁,rr隔离级别并不能解决幻读问题

mysql redo和undo

mysql redo和undo
事务的原子性,隔离性由锁机制实现;
事务的一致性和持久性由redo和undo日志来保证
redo log是重做日志,保证持久性。
undo log是回滚日志,保证事务的一致性。

redo是记录尚未完成的操作,数据库崩溃则用其重做。
redo log分为两部分

  • 内存中的重做日志缓冲,易失的
  • 硬盘中的重做日志文件,持久的

innodb采用write ahead log (预先日志持久化策略),先写入日志,在写入磁盘

第一步:InnoDB 会先把记录从硬盘读入内存
第二部:修改数据的内存拷贝
第三步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第四步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第五步:定期将内存中修改的数据刷新到磁盘中(注意注意注意,不是从redo log file刷入磁盘,而是从内存刷入磁盘,redo log file只在崩溃恢复数据时才用),如果数据库崩溃,则依据redo log buffer、redo log file进行重做,恢复数据,这才是redo log file的价值所在

在commit时写入redo日志
为什么不直接写,而是先写日志?
磁盘写入时间长,先写入日志,日志只需要考虑修改的数据。
如何保证每次都写入redo log file?
没开启0_direct 直接写磁盘操作。每次写到内存,在刷到磁盘。

每次将redo buffer写入os cache 文件缓存,innodb都调用fsync操作将缓存写入redo log file。

buffer pool中的数据未刷新到磁盘,称为脏页。
redo log满时,会把脏页刷入磁盘。
除了redo满时,什么时候刷脏页?
系统内存不足,淘汰数据页为脏页时。
mysql认为空闲时。
mysql正常关闭前,会把所有的脏页刷到磁盘。

脏页刷入会带来性能问题吗?
在生产环境中,如果我们开启了慢 SQL 监控,你会发现偶尔会出现一些用时稍长的 SQL。**这是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,**导致数据库操作抖动。

2.5 参数innodb_flush_log_at_trx_commit
上面提到的Force Log at Commit机制就是靠InnoDB存储引擎提供的参数innodb_flush_log_at_trx_commit来控制的
该参数控制 commit提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。
1、当设置参数为1时,(默认为1,建议),表示事务提交时必须调用一次 fsync 操作,最安全的配置,保障持久性
2、当设置参数为2时,则在事务提交时只做 write 操作,只保证将redo log buffer写到系统的页面缓存中,不进行fsync操作,因此如果MySQL数据库宕机时 不会丢失事务,但操作系统宕机则可能丢失事务
3、当设置参数为0时,表示事务提交时不进行写入redo log操作,这个操作仅在master thread 中完成,而在master thread中每1秒进行一次重做日志的fsync操作,因此实例 crash 最多丢失1秒钟内的事务。(master thread是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性)

undo log
用于记录更改的前一份copy

undo log的存储位置
在InnoDB存储引擎中,undo存储在回滚段(Rollback Segment)中,每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo 页的申请,在5.6以前,Rollback Segment是在共享表空间里的,5.6.3之后,可通过 innodb_undo_tablespace设置undo存储的位置。

purge线程的作用,清除undo页和清除page中带delete_bit的标识。数据删除只是打标记,并不是真的删除。
undo日志两种,insert和update
insert执行后就删除。
update对应delete和insert操作,需要提供mvcc操作,执行并不删除,等待purge线程操作。

undo log是逻辑日志。redo log是物理日志。

undo和rollback的区别,undo会使用rollback。

myisam和InnoDB的区别

  1. InnoDB支持事务,MyISAM不支持,InnoDB每条sql语句都默认封装成事务进行提交,影响速度,优化方式是将多条sql放入begin和commit之间,组成一个事务。
  2. InnoDB支持外键,而MyISAM不支持
  3. innodb支持行锁,表锁,myisam只支持表锁
  4. innodb必须有主键,myisam可以没有
  5. innodb聚簇索引,myisam非聚簇索引
  6. innodb不会保留表的行数,myisam会保存表的行数

一张表修改要求比较高的事务处理选择InnoDB,查询要求比较高,选择MyISAM

mybatis的条件语句

MyBatis动态SQL 多条件查询(if、where、trim标签)
if where 判断非空
choose、when、otherwise标签 多条件选择
foreach 拼接list

数据库非空字段默认值可以通过<if>或者sql  nvl()/ifnull()  来处理

parameter 入参类型

1.基本数据类型:int,string,long,Date
#{value}或${value} 获取参数中的值
2.复杂数据类型:类或者Map
#{属性名}或{属性名} ,map中则是#{key}或
{key}

for each的collection属性

单个List<Long> idList,为list
单个array数组,属性为array
map中放array或者list时,属性为数组的key
java对象中放array或者list时,属性为对象的属性名

resultType
1.基本数据类型
2.pojo类型

mybatis的缓存机制

一级缓存 sqlsession共享
二级缓存 同一个命名空间共享

mybatis如何封装返回结果

resultType设置为java对象:
1.反射创建对象
2.同名的列赋值给同名的属性(通过反射,找到set方法赋值)
3.得到java对象,如果是集合放入list中

当列名和java对象属性名不一致时,使用resultMap建立结果映射

数据库死锁

出现原因:
1.外键未加索引
在更新从表会对主表加写锁,在更新主表时,如果从表没有索引,会对整个表加锁。
容易发生死锁
2.用户1锁A,请求锁B,用户2锁B请求锁A
多表操作尽量按照相同的顺序进行处理。

避免方法
1.所有的update和delete必须走唯一索引
schema的操作
2.sql语句中不要有太复杂的关联操作,使用explain sql检查sql,全表扫描尽量优化为索引
3.把select放在update之前
4.避免事务中的用户相互等待。

现场解决方法:
1.重启系统
2.撤销进程,剥夺资源。终止参与死锁的进程,收回资源,从而解除死锁。
3.进程回退策略

级联删除和更新
ON DELETE CASCADE ON UPDATE CASCADE
Oracle不支持,只能用触发器实现

oracle函数

ws_concat 返回类型为clob,需要to_char,支持distinct,不支持排序
listagg( xx,‘,’) within GROUP(ORDER BY xx) 支持排序,不支持distinct,自定义分隔符

递归查询

父子结构的数据,可以用递归查询查询出具有层级结构的数据
SELECT …
FROM table
START WITH … 起始条件
CONNECT BY PRIOR … 父节点=子节点

SELECT * FROM LLClaimUser start with usercode =‘chenhw3’ connect by prior usercode=upusercode;
查询chenhw3的所有下级,下级的下级也算。

优化

  • 使用嵌套查询代替
  • 使用MATERIALIZED VIEW优化查询
    MATERIALIZED VIEW是一种允许预计算结果并将其存储在磁盘上的数据库对象。通过使用MATERIALIZED VIEW存储递归查询的结果,可以有效地提高查询速度。

CREATE MATERIALIZED VIEW dept_mv

AS SELECT DEPTNO, DEPTNAME, PARENTDEPTNO

FROM DEPT

START WITH PARENTDEPTNO = 1

CONNECT BY PRIOR DEPTNO = PARENTDEPTNO;

SELECT * FROM dept_mv WHERE PARENTDEPTNO = 1;

单引号和双引号的区别

单引号用于围绕字符串文字
双引号用于区分大小写和特殊符号的列名

mysql和oracle分页

  • mysql
select * from a limit m,n   

m=(startpage-1*pagesize) n=pagesize

  • oracle
select * from (select rownum rn,a.* from table a where rownum<=x) where rn >=y

y= (startpage-1)pagesize+1 ,x=startpagepagesize
rownum只能小于不能大于,因为rownum是查询后进行排序的,如果大于会一直不符合条件。因此需要使用别名,在查询后通过别名进行大于判断

in和not in对sql性能的影响

在这里插入图片描述

写sql

mysql 执行顺序 from-join-where-group by-having -select -order by-limit
oracle 执行顺序 from-where-group by -having-select-order by

1.查询总分大于200的学生

SELECT sum(realpay) as sum1,caseno FROM llclaimdetail group by caseno having sum(realpay)>10000;

where筛选行,having筛选组

spring

spring

IOC和AOP
IOC:控制反转,自己new对象改为由Spring注入,解耦。
AOP:面向切面编程,提取公共代码,进行增强处理,解耦。

springboot

  • 内嵌servlet服务器,Tomcat,Jetty等,不需要打成war包部署到容器中,只需要打成可执行的jar包即可

  • 简化配置:添加对应功能的starter依赖简化xml配置

springboot核心注解

SpringBootApplication 是SpringBootConfiguration,EnableAutoConfiguration 和ComponentScan三个注解的组合
EnableAutoConfiguration 会导入AutoConfigurationImportSelector,这个类会将所有符合条件的@Configuration配置都进行加载,开启springBoot自动配置,根据当前类路径下的包或者类来配置Spring Bean
@Configuration 用来代替 applicaitonContext.xml配置文件,所有配置文件中处理的类都可以通过这个注解所在的类进行注册

springboot自动装配过程

spring启动类中包括了@EnableAutoConfiguration注解 他会扫描meta-INF下的spring.factories文件指定的类 根据配置类的条件,自动装配Bean

spring定时任务

java timer
spring task
quartz xxl-job

JDK动态代理和CGLIB代理有什么区别

参考 java动态代理

静态代理:代理类和被代理类实现相同的接口,代理类持有被代理对象,调用接口方法中进行前置和后置操作,调用被代理类的方法。
类已经写好,编译后就能生成class文件。

public interface Person {

	//租房
	public void rentHouse();
}


public class Renter implements Person{

	@Override
	public void rentHouse() {
		System.out.println("租客租房成功!");
		
	}

}


public class RenterProxy implements Person{
	private Person renter;
	public RenterProxy(Person renter){
		this.renter = renter;
	}
	@Override
	public void rentHouse() {
		System.out.println("中介找房东租房,转租给租客!");
		renter.rentHouse();
		System.out.println("中介给租客钥匙,租客入住!");
		
	}

}

public class StaticProxyTest {

	public static void main(String[] args) {
		Person renter = new Renter();
		RenterProxy proxy = new RenterProxy(renter);
		proxy.rentHouse();
	}

}

动态代理:
代理类在程序运行时创建的代理方式被称为动态代理。动态代理相较于静态代理可以对代理类的所有函数进行统一管理,不需要对每个方法都写一遍。
通过reflect包下的proxy类和invocationHandler
生成jdk动态代理类和代理对象。

public interface Person {

	//租房
	public void rentHouse();
}

public class Renter implements Person{

	@Override
	public void rentHouse() {
		System.out.println("租客租房成功!");
		
	}

}

创建代理类实现invocationHandler接口,需要实现invoke方法,代理类中存在被代理对象。
调用代理类的方法都会调用invoke方法,在invoke中通过反射调用被代理对象的方法。
在执行被代理对象的方法调用前后增加自己的处理,这就是spring aop的主要原理。

public class RenterInvocationHandler<T> implements InvocationHandler{
	//被代理类的对象
	private T target;
	
	public RenterInvocationHandler(T target){
		this.target = target;
	}

	/**
     * proxy:代表动态代理对象
     * method:代表正在执行的方法
     * args:代表调用目标方法时传入的实参
     */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		//代理过程中插入其他操作
		System.out.println("租客和中介交流");
		Object result = method.invoke(target, args);
		return result;
	}

}

public class ProxyTest {

	public static void main(String[] args) {

		//创建被代理的实例对象
		Person renter = new Renter();
		//创建InvocationHandler对象
		InvocationHandler renterHandler = new RenterInvocationHandler<Person>(renter);
		
		
		//创建代理对象,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
		Person renterProxy = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class<?>[]{Person.class}, renterHandler);
		renterProxy.rentHouse();
		
		//也可以使用下面的方式创建代理类对象,Proxy.newProxyInstance其实就是对下面代码的封装
		/*try {
			//使用Proxy类的getProxyClass静态方法生成一个动态代理类renterProxy 
			Class<?> renterProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[]{Person.class});
			//获取代理类renterProxy的构造器,参数为InvocationHandler
			Constructor<?> constructor = renterProxyClass.getConstructor(InvocationHandler.class);
			//使用构造器创建一个代理类实例对象
			Person renterProxy = (Person)constructor.newInstance(renterHandler);
			renterProxy.rentHouse();
			//
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}*/
	}

}

JDK动态代理生成的代理类中的方法都会调用RenterInvocationHandler中的invoke方法,而invoke方法中调用了被代理对象的指定方法。

没有实现接口采用cglib代理
通过继承目标类,在子类中采用方法拦截的方式拦截所有父类方法的调用,然后加入自己需要的操作。因为使用的是继承,因此类不能为final。

创建被代理类

public class UserService {
	
	public void getName(){
		System.out.println("张三!");
	}

}

创建代理工厂类ProxyFactory

public class ProxyFactory<T> implements MethodInterceptor {

	private T target;

	public ProxyFactory(T target) {
		this.target = target;
	}

	// 创建代理对象

	public Object getProxyInstance() {

		// 1.cglib工具类
		Enhancer en = new Enhancer();
		// 2.设置父类
		en.setSuperclass(this.target.getClass());
		// 3.设置回调函数
		en.setCallback(this);

		return en.create();
	}

   //拦截方法
	@Override
	public Object intercept(Object obj, Method method, Object[] args,
			MethodProxy methodProxy) throws Throwable {
		System.out.println("开始事务...");

		// 执行目标对象的方法
		Object result = method.invoke(target, args);

		System.out.println("提交事务...");
		return result;
	}

}

JDK和CGLib动态代理都是实现SpringAOP的基础。如果加入容器的目标对象有实现接口,用动态代理,如果目标对象没有实现接口,用CGlib代理。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP是动态代理,运行时增强
AspectJ属于编译增强
Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢,而 AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。

spring事务

Spring事务基于数据库事务
Spring隔离级别和数据库事务隔离级别一样,多了个默认,采用数据库默认的隔离界别
Spring事务的两种方式,编程式事务,声明式事务
编程式事务精确到代码级别,声明式事务精确到方法级别。

Spirng事务的传播机制
a调用b

  • propagation_required a没有事务,b就开启事务,a有事务,b就和他合并到一起。
  • propagation_requires_new a没有事务,b新建事务,a有事务,挂起,b新建事务。
    b影响a,a不影响b
  • propagation_nested 有事务就加入,没有事务就新建。a require b nested b异常,a不回滚,a影响b,b不影响a
  • propagation_supports 如果a有事务就加入事务,没有事务,就以非事务的方式进行
  • propagation_not_support 有事务挂起,以非事务运行
  • propagation_mandatory 有事务就加入,a没有事务就报错
  • propagation_never 以非事务的方式运行存在事务报错
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

 Propagation propagation() default Propagation.REQUIRED;
 Isolation isolation() default Isolation.DEFAULT;
 //事务超时时间,超时回滚
  int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
  //如果事务只读,可以利用事务的只读属性开启优化措施
  boolean readOnly() default false;

}
//事务的回滚策略,指定异常回滚
@Transactional(rollbackFor= MyException.class)
//事务的回滚策略,指定异常不回滚
@Transactional(noRollbackFor= MyException.class)

Spring事务失效的场景

  • 作用于非public方法中,声明式事务基于aop,会进行判断是否是公共方法
  • 数据库不支持事务
  • 事务方法未被Spring管理
  • 同一个类中的方法调用 a调用b,b添加事务注解,a不添加事务注解 是由于使用 Spring AOP 代理自调用造成的。为解决这两个问题,使用 AspectJ 取代 Spring AOP 代理。
  • 未配置事务管理器 DataSourceTransactionManager
  • 方法的事务传播类型不支持事务
  • 不正确的捕获异常,在执行sql时,即使发生异常但是由于捕获异常而不会回滚
  • 错误的标注异常类型,Spring事务回滚的异常类型为RuntimeException,如果在事务方法中捕获并抛出Exception异常,回滚会失效

spring对象生命周期

ApplicaitonContext代表spring的IOC容器,负责实例化,配置,装配bean。

  • 生产
    • 加载BeanDefinition:
      Spring启动,调用loadBeanDefinition方法,通过注解,xml等方式扫描BeanDefinition到beanDefinitionMap,然后遍历map中的对象:
    • createBean创建Bean对象
      • 实例化:createBeanInstance 通过反射获取构造方法进行构造实例,优先选择使用@Autowired注解和无参构造方法。参数从单例池中查找,先根据类型查找,多个实例用参数名匹配。
      • 填充属性:populateBean方法 通过三级缓存进行属性注入;
      • 初始化Bean对象:initializeBean方法进行初始化:
        1.初始化容器相关信息,invokeAwareMethods方法,为实现了各种aware接口的Bean设置注入BeanName,beanFactory等容器信息。
        2.invokeInitMethods方法执行bean的初始化方法,是通过实现initializingBean接口而实现的afterPropertiesSet方法,之后执行Bean上自定义的initMethod方法,
        3.在执行执行初始化方法initMethod之前,执行BeanPostProcessor方法进行初始化前后的处理,包括负责AOP处理的AnnotationAwareAspectJAutoProxyCreator,负责构造后@PostContstruct和销毁前@PreDestory方法系统级处理器。通过实现PriorityOrdered接口来执行顺序。
      • 注册销毁方法registerDisposableBean,在销毁时调用destory方法
    • addSingleTon:将完整的bean对象放入单例池singleTonObject中。
  • 使用
  • 销毁
    close,销毁前执行postProcessorBeforeDestruction 会执行@preDestroy方法;
    通过destoryBeans方法逐一销毁所有的Bean,会执行destory方法;
    destroy执行之后,通过客户销毁方法 invokeCustomDestoryMethod方法,来执行丁自定义的Destroy方法

SpringBoot启动原理

1.首先从main找到run()方法,在执行run()方法之前new一个SpringApplication对象
2.进入run()方法,创建应用监听器SpringApplicationRunListeners开始监听
3.然后加载SpringBoot配置环境(ConfigurableEnvironment),然后把配置环境(Environment)加入监听对象中
4.然后加载应用上下文(ConfigurableApplicationContext),当做run方法的返回对象
5.最后创建Spring容器,refreshContext(context),实现starter自动化配置和bean的实例化等工作。

Spring Factories

在META-INF下,META-INF目录是提供jar文件描述信息的文件目录。

SpringFactories模仿java SPI机制
SPI机制是service provice interface,面向对象基于接口编程,如JDBC,java定义接口,数据库公司实现接口,换数据库不需要修改java代码。类似还有xml解析,日志模块的方案。
java SPI 就是为某个接口寻找服务的实现的机制。

对于在maven中引用的其他外部包加入容器的过程,需要用到spring.factories。
在SpringBoot启动时,SpringFactoriesLoader类会通过loadFactories或者loadFactoryNames寻找每个jar包下的META-INF/spring.factories,
AutoConfigurationImportSelector的selectImports方法返回的类名,来自spring.factories文件内的配置信息。通过该方法加载外部的Bean

拦截器和过滤器

  • 实现原理:过滤器基于回调函数;拦截器基于java反射
  • 使用范围:Filter在servlet中定义,依赖servlet,只能在web中使用。拦截器可以在web,application,swing
  • 触发时机不同:Filter在请求进出servlet前后。拦截器可以深入到方法前后,异常抛出前后。
  • 范围不同:过滤器过滤所有请求,拦截器只对action请求起作用。(只会对Controller中请求或访问static目录下的资源请求起作用。)
  • 进入Bean的情况不同:拦截器先于ApplicationContext加载,所以拦截器无法注入Spring容器管理的bean。
    解决办法:拦截器不使用@Component加载,改为使用@Configuration+@Bean加载。
  • 执行顺序不同:过滤器用@Order控制级别,级别越小越先执行;拦截器默认执行顺序为注册顺序,也可以通过@Order手动执行,级别越小越先执行;

Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码、做一些业务逻辑判断等。
SpringMVC 中的Interceptor 拦截器的主要作用就是拦截用户的 url 请求,并在执行 handler 方法的前中后加入某些特殊请求,比如通过它来进行权限验证,或者是来判断用户是否登陆。

拦截器的实现方式:实现HandlerInterceptor 或者WebRequestInterceptor

过滤器:用于属性甄别,对象收集(不可改变过滤对象的属性和行为)
拦截器:用于对象拦截,行为干预(可以改变拦截对象的属性和行为)

过滤器是JavaWeb的三大组件之一,是实现Filter的java类。
过滤器实现对请求资源的过滤功能,在请求资源前,响应前进行操作。
过滤器主要用来如参数过滤、防止SQL注入、防止页面攻击、过滤敏感字符、解决网站乱码、空参数矫正、Token验证、Session验证、点击率统计等。

过滤器 Filter
init():该方法在容器启动初始化过滤器时被调用,它在Filter的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。

doFilter():容器中的每一次请求都会调用该方法,FilterChain用来调用下一个过滤器Filter。

destroy():容器销毁时被调用。一般在方法中销毁或关闭资源,也只会被调用一次。

拦截器核心API
SpringMVC拦截器提供三个方法分别是preHandle、postHandle、afterCompletion,我们就是通过重写这几个方法来对用户的请求进行拦截处理的。

preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行,在整
个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。

如何解决循环依赖

一级缓存:实例化,初始化的对象
二级缓存:实例化,还未初始化的对象
三级缓存:对象工厂,用来创建二级缓存中的对象

AB相互依赖
实例化A,放入三级缓存,属性注入时发现需要B,去实例化B,发现需要依赖A,从缓存中依次查找,从三级缓存中删除,放入二级缓存,完成初始化后,将B放入一级缓存。接着初始化A,完成后删除二级缓存中的A,放入一级缓存。

使用三级缓存的原因是:保证使用的是同一个对象。
单用二级缓存,在二级缓存中放入一个普通Bean之后,BeanPostPorcessor生成代理对象覆盖,多线程环境,取不到一致的对象。

保证获取的是同一个对象。A3和A2都依赖A1,A1依赖A2,A3,在进行A1的依赖注入时,创建A2时,给A2注入了A1的代理对象,但是A3进行依赖注入时,如果不缓存A1的代理对象,aop会重新生成代理对象,导致单例被破坏

Spring Filter 怎么写

参考 SpringBoot——SpringBoot使用过滤器Filter
1.注解实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
编写一个过滤器,在启动类上添加ServletComponentScan注解扫描过滤器的对应包

2.Spring Boot 的配置类实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
新增过滤器不添加注解,通过FilterRegistrationBean注册自己的过滤器

获取Spring容器对象

实现BeanFactoryAware ,ApplicationContextAware,ApplicationListener接口

获取spring容器对象

SpringAOP

参考:Spring AOP代码实现:实例演示与注解全解
Aspect Oriented Programming 面向切面编程,用于处理代码中公共非业务逻辑,如日志,鉴权等。

  • PointCut:在什么时候切入 分为execution和annotation方法。前者用路径表达式指定那些类织入切面,后者指定被那些注解修饰的代码织入切面。
  • Advice: 处理时机和处理内容,在什么时间做什么事。
  • Aspect:切面,即PointCut和Advice
  • JointPoint:连接点,是程序执行的一个点。例如一个方法的执行或者一个异常的处理
  • Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

在这里插入图片描述
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();//拿到方法
Object[] allArgs = joinPoint.getArgs();//拿到参数

@Aspect
@Component
public class LogAdvice {
    // 定义一个切点:所有被GetMapping注解修饰的方法会织入advice
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    // @Pointcut("execution(* com.example.demo.controller..*(..))")
    private void logAdvicePointcut(){}

    @Before("logAdvicePointcut()")
    public void logAdvice(){
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("get请求的advice触发了");
    }
}

SpringCloud

Spring SpringBoot SpringCloud的区别

Spring IOC和AOP
SpringBoot 约定大于配置,快速开发
SopringCloud 微服务治理,提供解决方案
SpringBoot可以依赖SpringCloud开,SpringCloud不能离开SpringBoot,属于依赖关系。

springCloud是一系列架构的有序集合。基于springboot简化分布式基础设置的开发,如服务发现eureka,配置中心,路由zuul,gateway,消息总线,服务远程调用feign,负载均衡ribbon,断路器hystrix,数据监控等。

spring,springmvc,springboot

m是web框架,b是自动化配置框架,内置tomcat
m打包需要引入大量xml配置,b通过引入starter依赖,自动下载相关依赖打包

spring需要大量配置,b约定大于配置,通过自动配置和默认属性减少手动配置,开箱即用
b提供了固定版本的依赖管理,统一spring相关的依赖库,避免了版本冲突的问题

分布式和微服务的区别

水平拆分:应用分为表示层,数据访问层,业务逻辑层,可以将这三层拆分到不同的服务器上

垂直拆分:根据业务逻辑拆分

分布式:拆,将项目拆分为多个模块,分开部署,就是分布式

微服务:细粒度的垂直拆分,不一定在不同的服务器上。

无状态微服务

无状态微服务:可以从任何客户端接受任何请求,没有会话的概念。
有状态微服务:必须在客户机和指定微服务实例之间建立会话。进入服务器场所并属于特定会话的请求必须路由到特定的微服务实例。
客户端和服务端之间保持状态,如果状态破坏,请求失败

为什么使用netty

NIO的好处

  • 多线程的socket
    1.内存占用高
    2.线程上下文切换成本高
    3.只适合连接少的场景
  • 线程池
    1.阻塞模式下,线程只能处理一个socket连接
    2.仅适用于短连接的情况
  • 多路复用selector
    1.selector管理多个channel,获取channel上发生的事件。channel工作在非阻塞模式下。
    2.适合连接数特别多,流量低的场景。

不使用NIO

  • 类库的API繁杂,需要掌握Selector,ServerSocketChannel,SocketChannel,ByteByffer等。
  • 需要熟悉JAVA多线程编程。因为NIO涉及Reactor模式,必须对多线程和网络编程非常熟悉。
  • epoll bug 会导致selector 空轮询,最终导致CPU 100% 。直到JDK1.7还没根本解决。

Netty

  • API简单,多
  • 功能强大,多种解码编码器,支持多种协议
  • 性能高。比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
  • 社区活跃
  • Dubbo,ES都采用了Netty

RPC

rpc:远程过程调用,通过网络从远程计算机上请求服务
应用层:阿里 Dubbo,Google gRPC,SpringBoot
远程通信协议:RMI,Socket,Soap,REST
通信框架:MINA和NETTY

RPC使用场景,包含服务发现,负载,容错,网络传输,序列化等组件。
其中RPC协议指定了程序如何序列化,协议编码,网络传输。

重要组成:
客户端(Client):服务调用方。
客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
服务端(Server):服务的真正提供者。
Network Service:底层传输,可以是 TCP 或 HTTP。

RPC比HTTP快的原因

  • 序列化方式:rpc针对二进制协议来进行序列化和反序列化;http基于文本进行。
  • 报文长度:rpc自定义传输协议,http包含多余内容,如http header中的accept,referer等
  • RPC基于TCP/IP的,是长连接。http服务都是短连接,http1.1支持长连接,但是取决于服务端是否支持长连接,不可控。

追求性能,使用RPC;追求兼容性,使用Rest。

redis

redis的特性

在这里插入图片描述
在这里插入图片描述

redis的使用场景

  1. 消息队列:使用 LIST 或 PUB/SUB 机制来实现消息传递和队列功能。
  2. 实时分析:利用 Sorted Sets 和 HyperLogLog 进行实时数据分析和计数。
  3. 会话存储:存储用户会话数据,例如登录状态和用户偏好。
  4. 分布式锁:利用 Redis 的 SETNX 命令实现分布式锁,以确保资源的互斥访问。
  5. 排行榜:使用 Sorted Sets 创建和管理排行榜或评分系统。
  6. 计数器:使用 INCR 命令实现高效的计数器功能,例如访问计数。
  7. 数据结构存储:利用 Redis 提供的各种数据结构(如 HASH、SET、LIST 等)来存储复杂数据类型。

哪里用到的redis,时效

  • 数据库存储 存储登录信息 token 30分钟 实现value的序列化,存储的对象
  • 缓存 路由转发,接口匹配 redismanager默认30天
    工作流暂存 10分钟:为了缓解数据库压力,开始的流程是异步进程,但是要全量进程的结果
  • 幂等性的实现 在消费消息时,将消息的uuid存储到redis中,防止重复消费

redis数据结构

String 动态字符串,类似于ArrayList
List 双向链表
Hash 数组加链表 等价于Java语言的HashMap
Set HashMap 只不过所有value都指向同一个对象
ZSet 压缩链表或者跳表

Stirng 底层实现

动态字符串sds
sds结构一共有五种header定义,目的是为了满足不同长度字符串使用,节省内存。
header主要包括:

  • len长度
  • alloc字符串最大容量,不包括header和最后的空终止字符
  • flags标志位,第三位表示类型,其余五位未使用
  • buf:字符数组
  • encoding
    • int:可以使用long类型的整数表示的会用long型来存储
    • raw:长度大于44字节的字符串,使用sds存储
    • embstr:长度小于等于44字节的字符串,效率比较高,且数据都保存在一块内存区域。

ZSet的实现方式:
ziplist压缩链表:

  • 元素数量小于128
  • 每个元素长度小于64

skiplist跳跃链表:不满足上述条件就采用跳表,具体来说组合了map和skiplist

  • map用来存储 member到score的映射, 时间复杂度为O(1)
  • skiplist按照从小到大存储分数,每个元素的值都是[score,value]对

跳表就是在有序节点上增加多级索引
在这里插入图片描述

n个节点,第一层索引为n/2,第二层索引为n/4,第k层索引为n/2^k
空间换时间的折半查找

redis排行榜 zset
添加
zadd key score value
ZADD broadcast:20210108231 1 lisi
加分
zincrby key increment member
ZINCRBY broadcast:20210108231 2 lisi
排名
ZRANGE broadcast:20210108231 0 -1 WITHSCORES

redis遍历key

keys pattern 一次性匹配所有key,当key比较多时,对内存消耗和redis都是隐患
SCAN cursor [MATCH pattern] [COUNT count] 2.8.0版本之后加入
scan只会返回少量元素,每次调用用上次调用返回的游标,以此延续迭代过程,返回为0标识迭代结束。
count表示从数据集返回多少个匹配元素

redis的淘汰策略

过期键淘汰策略是定期删除和惰性删除
定期删除是指:redis服务器定期操作redis.c/serverCron函数执行时,redis.c/activeExpireCycle会被调用。actvieExpire函数在规定时间内,分多次遍历服务期内的多个数据库,从过期字典中检查一部分key的过期时间并删除。
current_db记录当前检查数据库,函数处理2号数据库时间超限,返回后下次检查会从3号数据库开始检查。所有数据库检查完毕current_db重置为0,然后再次开启一轮的检查工作。
惰性删除是指用户请求,此时会检查过期,过期清除不会返回

大量key堆积,内存耗尽如何处理?
内存淘汰机制:
在这里插入图片描述

缓存击穿,缓存雪崩

  • 缓存穿透:缓存和数据库中都没有数据,多次重复访问导致数据库崩溃
    解决
    • 业务层校验:对于不合规的入参,直接返回
    • 不存在数据设置短过期时间
    • 布隆过滤器
  • 缓存击穿:热点key失效同时,大量请求进入,从而全部到达数据库,压垮数据库
    解决
    • 永不过期
    • 定时更新:过期时间1h,每到59min去更新
    • 互斥锁:redis根据key获取到的value为空时,先加锁,去数据库加载,加载完毕,释放锁。其他线程请求发现获取锁失败,则睡眠一段时间。
  • 雪崩:缓存大面积失效,或者Redis宕机,大量请求进入数据库,压垮数据库
    解决
    • 设计有效期均匀分布:避免缓存设置相近的有效期,可以设置有效期时增加随机值,或者统一规划有效期,使得过期时间均匀分布。
    • 缓存预热:流量大时,提前访问一遍,将数据存储到redis中并且设置不同的过期时间
    • 保证Redis的高可用

redis的锁机制

redis一般用作缓存,多读少写,只支持乐观锁
Redis事务命令主要包括 WATCH, EXEC, DISCARD, MULTI。
事务使用MULTI开启,这时可以执行多条命令,Redis在这些事务中加入命令,当用户执行Exec命令时才真正的执行队列当中的命令。执行discard,丢弃队列中的命令。
Watch命令是Exec执行的条件,watch的key没有修改则执行事务,否则事务不会被执行。
Watch命令可以被调用多次,一个watch命令可以监控多个key。watch命令调用则开启监视功能,直到exec命令终止。
Redis的watch命令给事务CAS机制,如果key在执行exec前有变动,则整个事务被取消。
事务中,采用watch加锁,unwatch解锁,执行EXEC命令或者Discard命令后,锁自动释放,不需要进行unwatch操作。

redis分布式锁

参考:Redis实现分布式锁的7种方案
分布式锁需要保证:
1.互斥性:只能有一个客户端获得锁
2.安全性:锁只能被持有该锁的客户端删除
3.死锁:锁超时释放,避免死锁
4.容错:当redis部分节点宕机,客户端仍能获取锁和释放锁

1.一种实现
setnx k v; 成功返回1 失败返回0;
getset k v;返回key的旧值;
expire k secondsl;给key设置超时时间
del key [key …]

1.1setnx key 当前时间+过期时间
1.2. 失败获取锁失败,成功 expire 超时时间
1.3.执行业务 del key
问题:如果获取锁成功未设置超时时间的时候进行重启,会产生死锁。
(如果不是kill线程,而是shutdown可以用springBean的predestory注解,进行删除key操作)
优化:获取锁失败后,查看当前key的value值,如果不为空,并且当前时间大于该值,说明当前锁失效,通过getset获取值。如果getset获取的值,和一开始的旧值相同,则获取锁成功。
问题:多节点时间需要尽可能的保持一致。getset即便失败也会延长之前锁的时间。

2.使用lua脚本
保证setnx和expire的原子性
3.set扩展命令
SET key value[EX seconds][PX milliseconds][NX|XX]

  • NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds :设定key的过期时间,时间单位是秒。
  • PX milliseconds: 设定key的过期时间,单位为毫秒
  • XX: 仅当key存在时设置值

问题:业务没执行完,锁就释放了;锁被别的线程误删。
4.SET EX PX NX + 校验唯一随机值,再删除
还是会存在业务没有执行完成,锁释放的问题。
5.Redisson框架
开源框架Redisson
加锁后启动一个watchdog线程,每隔10s检查是否持有锁,持有锁就延长,防止锁过期提前释放。

6.redlock+redisson
redis节点全为master,避免master加锁后未同步到slave然后宕机导致加锁失败的情况

按顺序向5个master节点请求加锁
根据设置的超时时间来判断,是不是要跳过该master节点。
如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!

持久化方案

在这里插入图片描述

redis线程模型

参考 深入学习redis 的线程模型

redis内部采用事件处理器是单线程的,因此redis叫做单线程模型。采用IO多路复用机制同时监听多个socket,将产生的事件的socket压缩到内存队列中,事件分派器根据事件不同的类型选择对应的事件处理器进行处理。

文件事件处理器的结构

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)

线程模型
多个socket可能并发产生不同的操作,每个操作对应不同的事件,但是IO多路复用程序会监听多个socket,将产生事件的socket放入队列中排队,事件分派器每次从队列中取出一个socket,根据socket对应的事件类型交给对应的事件处理器进行处理。
在这里插入图片描述
建立连接

  • redis服务端进程初始化时,将server socket 的AE_READABLE事件与连接应答处理器关联。
  • 客户端socket01向redis进程serverSocket请求建立连接,此时server socket 产生一个AE_READBLE事件,IO多路复用程序监听到server socket产生的事件后,将socket压入队列。
  • 文件事件分派器从队列中获取socket,交给连接应答处理器。
  • 连接应答处理器会创建一个能与客户端通信的socket01,并将socket01的AE_READABLE事件与命令请求处理器关联
    执行一个set请求
  • 客户端发送了一个set key value请求,此时redis中的socket01会产生AE_READABLE事件,IO多路复用程序,将socket01压入队列
  • 此时事件分派器从队列中获取到socket01产生的AE_READABLE事件,由于前面socket01的AE_READABLE事件已经和命令请求处理器关联
  • 事件分派器将命令交给请求处理器来处理。命令处理器读取k v并在自己内存中完成设置
  • 操作完成后,将socket01的AE_WRITEABLE事件与命令回复处理器关联
  • 如果此时客户端准备好了接受数据,那么redis中的socket01会产生一个AE_READABLE事件,同样压入队列中
  • 事件派分器找到相关联的命令回复处理器,由命令回复处理器对socket01输入本次操作的一个结果,比如ok,之后解除socket01的AE_READABLE事件与命令回复处理器的关联。
    在这里插入图片描述

为什么redis效率高?
1.纯内存操作
2.核心是基于非阻塞的IO多路复用机制
3.C语言实现,语言更接近操作系统,执行速度相对会快
4.单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

redis的吞吐量:
单点TPS达到8万/秒,QPS达到10万/秒
qps是指每秒最大能接受的用户访问量,tps是指每秒钟最大能处理的请求数。

部署方式

单机,主从,哨兵

kafka

使用消息队列的原因

1.流量削峰(消息过多,处理有限) 落库采用异步消息的方式,减缓数据库压力
2.服务解耦(接口故障不影响生产和消费)消息发出去,发消息的应用down了,也不会影响后序微服务的消费
3.异步(提高响应速度,用户无感知)

kafka 如何解决消息重复

  • 生产者:生产者发送的消息没有收到正确的broker响应,导致producer重试。

    以及 ack=all 以及 retries > 1 。ack=0,不重试。可能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集

    启动kafka的幂等性要启动kafka的幂等性,设置: enable.idempotence=true幂等生产者的原理:每个生产者都有唯一Pid,发次发送消息会累加seq。broker为每个topic的每个分区都维护了一个当前当前写成功的消息最大PID-seq,消息落盘+1,当收到小于当前最大PID-seq时就会丢弃该消息。

  • 消费者:

    offset手动提交,业务成功处理后,提交offset

    幂等性:多次操作结果一致
    获取消息的唯一id,会将id存入redis,记录为处理中,以后的消息就不会进行重复处理

kafka如何保证消息不丢失

1.生产者发送给服务器
acks机制 0 发送就成功,1 生产者收到leader分区的响应则认为成功 -1 当所有ISR中的副本全部收到消息,生产者才认为是成功的
2.服务器通过副本保存消息
3.消费者,关闭自动提交,在接收到消息,进行业务处理完毕后再提交偏移量

kafka什么时候进行rebalance

参考Kafka的Rebalance机制可能造成的影响及解决方案
每当有新的消费者加入或者订阅的topic数发生变化,会触发rebalance(再均衡:在同一个消费组当中,分区的所有权从一个消费者转到另一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。
过程如下:
1.所有消费者向coordinator发送请求,请求加入comsumer group。一旦所有成员都发送了请求,Coordinator会从中选择一个consumer作为leader,并将组员信息发给leader
2.leader分配消费方案,指定哪个消费者消费哪些topic的哪些分区。发给coordinator,coordinator发送给消费者,组内成员知道自己该消费哪些分区了。

coordinator:每个consumer group会选择一个服务器作为自己的coordinator,负责监控整个消费者组内各个分区的心跳,以及判断是否宕机和开启rebalance的
partition:每个topic分区,备份在不同的服务器上

如何选择coordinator?
对groupid进行hash,然后对_consumer_offsets的分区数量进行取模,默认分区数量为50,可以配置。

发生的时机:
1.分区数量增加。
2.对topic的订阅发生变化
3.消费者组成员的加入或者离开。
影响:1.重复消费;2.集群不稳定;3.影响消费速度

kafka 消息积压怎么办

参考

1.修复消费者,然后停掉所有消费者
2.临时建立10倍或者20倍的queue数量(新建topic,分区是原来的10倍)
3.写一个临时分发消息的consumer,消费挤压数据不作处理,均匀写入临时建好10数量的queue中
4.征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的消息
5.这种做法相当于临时将queue资源和consumer资源扩大10倍,以10倍速度来消费消息
6.消费完成之后,回复原有的部署架构,重新用原来的consumer机器来消费消息。

kafka分区和消费者的关系

1.生产者中的分区合理消费,消费者的线程对象和分区保持一致,多余的线程不会进行消费
2.消费者默认即为一个线程对象
3.消费者服务器数*线程数 = partition个数

分区数量

分区多可以增加吞吐量

分区过多:

  • 客户端服务端需要的内存变多,客户端生产者有个参数batchsize,默认16kb。为每个分区缓存消息,满了打包发送。
    分区越多,消费者获取数据所需的内存就增多。同时消费者线程数要匹配最大分区数,线程切换开销很大。
    服务器端维护许多分区界别的缓存。
  • 文件句柄的开销:每个分区在底层文件系统都有属于自己的目录。分区越多,要同时打开的文件句柄数就越多。
  • 降低高可用性:kafka通过副本来保证高可用。分区多,服务器上的副本就多,服务器挂掉,在副本之间进行重新选举的消耗就大。

目标吞吐量TT,消费者吞吐量TC,生产者吞吐量TP,分区数= TT/max(TP,TC)

分区和消费者的关系是多对一
一个分区只能对应一个消费者的原因是保证消息顺序性。
因此消息挤压,部署多台消费者实例是不能加快消费的,最多增加到和分区数量一致,超过的组员只会占有资源二不起作用。

消费者分区分配策略

1.range策略

  • 将消费者按名称排序,分区按照数字排序
  • 分区总数/消费者,除得尽均匀分配,除不尽位于前面的消费多负责分区

2.roundrobin 轮询
为了保证均匀分配,需要满足两个条件
1.同一个消费者组里每个消费者订阅的主题必须相同
2.同一个消费者里面所有消费者的num.streams必须相等

3.sticky分配策略
主要实现两个目的,如果发生冲突,优先实现第一个
1.分区分配尽可能均匀
2.分区的分配尽可能的与上次分配的保持相同

0.11.X版本引入,最复杂最优秀
区别于轮询,在消费者挂掉之后,不会全部分区重新轮询分配,而是将挂掉消费者消费的分区进行重新分配。

其他微服务组件

eureka特性

  • cap
    Consistency(一致性)
    Availability(可用性)
    Partition tolerance(分区容忍性)
    任意组合,不能兼得 eureka ap zk cp
    注册慢:ap特性,client延迟注册 30s;server 响应缓存 30s;server 缓存刷新 30s

  • 自我保护机制: eureka会检查最近15分钟服务正常心跳占比,低于85%出发自我保护模式,不剔除服务。但是服务也会过期,eureka在启动完成后,每隔60s检查服务健康状态,如果被保护的服务过一段时间默认90s还没有恢复就会把服务剔除。
    在此期间高于85%,自动关闭自我保护机制。
    自我保护原因:避免因为网络通信故障降低可用性。
    一般开发关闭,因为开发经常重启。生产开启。生产环境相对稳定,让eureka管理。

  • 服务注册:启动注册,发送主机名,端口号等

  • 服务续约:30s一次,90s没收到进行服务剔除

  • 服务下线:client优雅退出发送cancel命令,server接到命令删除节点

  • 获取注册列表:第一次全量更新,后续增量更新,30s一次

eureka注册中心机制

参考高频面试之Eureka
注册中心,包括服务发现,治理等功能。
@EnableEurekaServer作为注册中心,@EnableEurekaClient作为服务的提供者或消费者。

  • 注册服务
    EurekaClient将服务信息封装成Intanceinfo对象,通过EurekaHttpClient调用register方法发送post请求执行服务注册;
    EurekaServer提供了基于Jersey的Rest风格接口,在ApplicationResource类中提供了addInstance方法来接受注册信息。如果注册信息通过校验,将服务信息保存到本地注册表。数据结构为双层HashMap,key为应用名,内层map,Key是应用实例信息编号,value是InstanceInfo
    EurekaClient接受并解析注册结果,判断httpResponse的statusCode,如果是204则代表注册成功
    调用replicateToPeers方法将此次注册信息复制到对等的Eureka节点
  • 定时任务
    • 拉取服务器注册实例
      任务通过ScheduledExecutorService来实现任务调度,执行周期默认为60秒一次
      获取方式有两种,全量获取和增量获取。第一次全量获取,后序增量获取;获取到服务器注册实例信息后,保存或更新到本地
      - 续约
      任务通过ScheduledExecutorService来实现任务调度,执行周期默认为30秒一次
      通过Renew方法发起续约请求。将appname,appid以及intanceinfo作为参数,通过EurekaHttpClien发送Http请求
      EurekaServer通过renewLease()方法接受续约请求
      根据AppName从注册表获取对应的服务信息,并更新一些属性如renewsLastMin,lastUpdateTimestamp
      EurekaServer返回结果200或者204
      EurekaClient接受续约结果;如果是404重新发起,如果是200则表示续约成功,更LastSuccessHeartBeatTimestamp变量
      • 剔除服务
        通过 JDK 自带的 Timer 来实现任务调度,通过 evict() 方法执行具体的操作
        首先判断是否开启了实例自我保护机制,如果开启自我保护,则不做任何操作
        如果未开启,根据 lastUpdateTimestamp 收集已过期的服务,加入到List集合中
        通过 internalCancel() 方法,在该方法中从 registry 中剔除已经过期的实例。具体的剔除过程会通过打乱过期服务列表,并通过 Random 随机剔除,保证服务器剔除的均匀性

Eureka

feign ribbon eureka

feign在调用时会被ribbon拦截,RibbonLoadBalancerClient中构建了一个ILoadBalancer来确定应用名对应的真实ip,
内部调用eureka的服务发现获取服务发现缓存的服务信息。
更新是在RibbonClientConfiguration创建定时任务30s一次

限流算法

一致性哈希

一致性哈希相较于普通哈希具有更好的可扩展性和容错性。
哈希是对节点个数取模,在进行扩容或者节点下线时,需要重新映射所有数据。
一致性哈希是对2^32取模,将哈希值空间映射到虚拟的闭环上,称为哈希环。取模所得值,进行顺时针寻找到的第一个节点为哈希环中的位置。节点扩容和下线时只需要迁移相邻节点的数据。
当分布不均衡时,采用虚拟节点来解决问题。

无状态的服务,消费降级

linux命令

cat 文件 | grep 关键字 | wc -l
参考 Linux系统中统计文件中某个字符出现次数命令详细教程

  • 不分大小写统计
    grep -o -i ‘a’ aaa.txt | wc -l
  • 统计多个文件中某个字符出现次数总和
    grep -o -i ‘a’ aaa.txt bbb.txt | wc -l

其他

分布式锁

1.数据库
在数据库中以资源唯一号为主键插入数据,处理完成后删除数据
问题:

  • 释放锁失败会导致死锁,需要定时任务定时清理
  • 锁的可靠性依赖数据库,建议设置备库,避免单点
  • 锁是非阻塞的,因为插入失败会报错;如果需要阻塞式的设置for,while循环等
  • 非可重入的锁,需要在数据库中添加字段获取主机信息,匹配的话重新分配给他。

乐观锁:查询数据和版本号,进行更新,更新成功加锁成功
问题:

  • 对表增加额外字段,增加数据库冗余
  • 并发量高的时候,会导致大量请求失败,以及大量请求对数据库的压力。

适合并发量不高,且操作不频繁的场景

悲观锁:增加排它锁 select for update;更新数据。
问题: 每次请求都会都会产生额外的加锁开销并且阻断等待锁的获取,高并发的情况下,容易造成大量请求阻塞,影响可用性。
2.缓存 redis redlock和redission实现,所有节点均为master,加锁轮询请求,超过半数即为加锁成功,否则加锁失败
3.zookeeper:
zk是一个为分布式提供一致性服务的开源软件。
它内部是一个分层的文件系统目录结构,规定同一个目录下只能有一个唯一文件名。
基于zookeeper实现分布式锁的步骤:

  1. 创建一个目录myLock
  2. 线程A获取锁就在myLock下创建临时顺序节点
  3. 获取目录下所有子节点,找到比自己小的,如果不存在,获得锁
  4. 线程B获取所有节点,判断不是最小节点,设置监听比自己小的节点。
  5. A处理完毕后,删除节点,线程B监听到变更事件,判断自己是不是最小节点,如果是则获得锁。
    可以使用zookeeper第三方库Curator客户端
    问题:
  • 性能没有缓存高,每次加锁释放锁都需要动态的创建,销毁临时节点来实现锁功能。zk中创建和删除节点都需要通过leader来执行,同步到fllower上。
  • zookeeper也可能有并发问题。当获取锁的客户端和zk的session连接断了,就会删除临时节点,其他线程就会获得锁。不常见是因为zk有重试机制,Curator客户端支持多种重试策略的配置。

参考堆的应用 – Top-K问题(巨详细)
堆是二叉树,一般的二叉树用链表存储,用数组存储会浪费空间,但是堆是完全二叉树,用数组存储。
在这里插入图片描述
在这里插入图片描述
已知父节点下标n, 他的右节点为2n+2,左节点2n+1
已知子节点n,他的父节点为(n-1)/2

大根堆和小根堆
大根堆:根节点大于左右孩子节点
小根堆:根节点小于左右孩子节点

优先级队列

java提供这种数据结构,每次添加或者删除元素,都会变成小跟堆。

// 默认得到一个小根堆
PriorityQueue<Integer> smallHeap = new PriorityQueue<>();
smallHeap.offer(23);
smallHeap.offer(2);
smallHeap.offer(11);
System.out.println(smallHeap.poll());// 弹出2,剩余最小的元素就是11,会被调整到堆顶,下一次弹出
System.out.println(smallHeap.poll());// 弹出11

 // 如果需要得到大根堆,在里面传一个比较器
 PriorityQueue<Integer> BigHeap = new PriorityQueue<>(new Comparator<Integer>() {
     @Override
     public int compare(Integer o1, Integer o2) {
         return o2 - o1;
     }
 });

top-K问题
数组,找出前三个最小元素
1.排序
2.放入小跟堆
3.在大根堆中放三个元素,循环数组每次往里放一个数据,弹出最大的数据,最后堆里就是结果。

堆的向下调整算法、堆的向上调整算法、堆的实现、Topk问题

数据格式标准化

xml到标准对象 thoughtworks.xstream 流
json到标准对象 hutools工具,底层反射调用set方法

javaBean
java语言中的可重用组件。
满足:1.类是公共的;2.有一个无参的公共构造器;3.有属性,且有对应的get和set方法。
其他开发者通过JSP,Servlet 其他JavaBean,applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地复制粘贴的功能。

javaBean的任务就是一次性编写,任何地方执行,任何地方重用。

synchronized在哪用到

createMaxNo 开启数据库事务,sf for update 增加数据库排他锁
同一个应用有一个缓存,加同步锁,避免并发重复增加步长,造成浪费。
应用之间由数据库的排他锁协调。

ConditionalOnMissingBean注解

在beanFactory中没有指定的bean才能匹配,主要用来做自动配置,当没有指定类的时候,使用默认配置
确保使用注解的bean在指定bean的后面运行,否则会失效。
ConditionalOnBean
指定类存在时,实例化当前Bean
ConditionalOnClass
指定类名在类路径上存在,实例化当前Bean
ConditionalOnMissingClass
指定类名在类路径上不存在,实例化当前Bean

actuator

找到HealthContributor类型bean进行健康注册,健康检查的时候检查这些bean

redis池化

JedisPoolConfig

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐