Java基本知识
jdk和jre的区别?✅
jdk里面包含了jre,负责创建和编译软件,然后交给jre去运行java程序。
jre是运行时环境,里面包含了jvm,是java程序运行所必须的软件环境。
Java中的八种基本数据类型以及占用的字节
byte 1 short 2 int 4 long 8 float 4 double 8 char 2
boolean:使用boolean表达式虚拟机会将其转成int数据类型的值,占4个字节;如果是boolean数组的话,将转成byte数组,每个boolean元素占一个字节。
面向过程和面向对象(OOP)的区别?✅
面向过程就是把问题分成一个一个的步骤,每个步骤使用函数进行实现,解决问题用函数之间的调用即可,最简单的实现就是写一个算法。
面向对象就是把这个问题抽象成不同的对象进行组合,建立对象的目的不是为了完成一个个步骤,而是为了描述解决这个问题的过程中发生的行为,面向对象有三大特性:封装、继承、多态。
举个简单的例子,比如说造一辆车,车具有方向盘,轮胎等属性,跑起来,转弯这些行为,我们将这些属性和行为进行抽象并且组合成一个Car类,这就是面向对象编程。
面向对象的三大特性?
封装:将属性私有,同时对外提供了set和get方法。
继承:子类可以继承父类的属性和方法,这里需要注意的是父类中使用private修饰符修饰的属性和方法子类是无法继承的。
多态:指的是父类中定义的方法可以被子类重写,同时在运行的时候根据实际类型调用子类中重写的方法。
多态的实现需要满足以下两方面:
类之间有继承关系;子类重写了父类方法。
多态的目的?解决了什么问题?
多态的目的主要是为了实现接口的统一,提高程序的灵活性和可扩展性。
解决的问题:
代码复用:将相同的方法定义在父类或者接口中,然后不同的子类实现各自的方法逻辑,避免了重复的代码。
扩展性:当添加了新的子类之后,只需要继承或者实现父类或者接口即可,而不需要修改已有的代码。
解耦:通过多态,调用方只需要关心接口和父类中的方法,而不需要关心子类的实现逻辑,降低了组件之间的耦合度。
灵活性:在运行时动态的确定对象的类型,并调用该对象的方法,提高了程序的灵活性。
Java中==和equals的区别
==如果比较基本数据类型比较的是变量值,比较引用类型的话比较的是堆中的内存地址。 equals如果不重写和==是一样的,重写之后比较的是内容是否相同。
说一下final关键字
final在java里面可以修饰类,方法以及变量。
修饰类:该类不能被继承
修饰方法:该方法不可以被子类重写,但是可以重载。
修饰变量:如果是基本数据类型,一旦被赋值之后不能修改值,如果是引用类型,引用的值可以修改,但是不能修改指向的对象。
static关键字的作用?
- 修饰类的成员变量,不能修饰局部变量
被static修饰的成员变量称为静态变量,只在类加载的时候初始化一次,通过类名. 变量名访问。
- 修饰方法
称为静态方法,通过类名. 方法名进行调用。静态方法中不能直接使用实例变量,而需要通过创建类的实例对象进行访问。
- 修饰代码块
称为静态代码块,在类加载的时候执行,通常用来初始化静态变量。
String, StringBuffer, StringBuilder区别?
- String是用final修饰的终结类,一旦创建后就不能被修改,每次修改都是返回新的字符串对象,原字符串对象不变;而StringBuffer和StringBuilder允许在其内部字符数组上进行修改。
- String进行字符串拼接,实际上是new出来一个StringBuilder对象,调用了append和toString方法返回一个新的字符串对象;而StringBuffer和StringBuilder进行字符串拼接是在原对象上操作。
- StringBuffer是线程安全的,它的所有方法都使用了synchronized关键字保证线程安全,而StringBuilder不是线程安全的。
String类型的常用方法?
- length:返回字符串的长度。
- charAt:返回指定索引位置的字符。
- indexOf:返回执行字符的索引位置。
- equals:判断两个字符串是否相等。
- substring:截取指定区间的字符串。
- replace:替换字符串中的字符。
- matches:根据正则表达式判断字符串是否符合要求。
- split:根据给定的正则表达式拆分字符串。
- trim:去掉首位空格。
- toUpperCase:将字符串转成大写。
- toLowerCase:将字符串转成小写。
重载和重写的区别?✅
重载:在一个类中,多个方法有相同的方法名,但是参数列表不同,这种同名不同参的方法叫做重载。
重写:子类在继承父类方法的基础上(这就要求方法名和参数列表必须相同),对父类实现的方法进行覆盖。
区别:
- 重载发生在同一个类中,而重写发生在父子类中。
- 重载的参数列表不能相同,里面包括参数类型,个数和顺序不同;而重写的参数列表必须相同。
- 重载对于访问权限没有要求,而子类重写方法的访问权限不能小于父类方法。
- 重载对于返回值没有要求,而子类重写方法的返回值必须和父类方法的返回值相同或者是其子类。
接口和抽象类的区别?
- 最明显的区别就是接口中只定义了一些方法,在不考虑jdk8中支持默认方法的实现的情况下,接口中是没有方法的实现的。
- 接口不允许有构造方法,而抽象类可以有构造方法。
- 抽象类中允许有public,protected,default修饰符,而接口中默认是public修饰符,其他修饰符不允许出现。
- 一个类可以实现多个接口,但是只能继承一个抽象类。
- 接口是用来指定规范的,我们在项目中提倡的是面向接口编程,而抽象类是用来实现代码的复用的。在写项目的时候,我们先将接口暴露给外部,让其他类实现这个接口,如果我们发现有多个实现类中有相同且可复用的代码,则可以在接口和实现类中间加一层抽象类,将这些代码放到抽象类中,再让对应的实现类进行继承。
List和Set的区别?🌟
- list是有序的集合,可以存储重复的元素,可以根据索引访问元素;set是无序的集合,不可存储重复元素,不支持根据索引访问元素。
- list通常用于存储需要按照顺序访问的元素,如历史记录;而set通常用于去重和判重操作。
- list支持添加、删除和修改操作,但是添加和删除操作可能会影响其他元素的位置;而set不支持修改操作,只能进行添加和删除操作。
hashCode和equals方法
我们在比较两个对象是否相等的时候,一般都会重写hashcode和equals方法,hashcode主要作用就是减少equals比较的次数,提高性能;equals方法如果没有重写的话默认是object中的equals方法,和==一样,如果是基本数据类型比较的是变量值是否相等,如果是引用类型则比较的是指向的内存地址是否相同,重写之后比较的是两个对象的内容是否相同。
以HashSet为例,我们先通过hashcode方法获取hashcode值,找到对应的存放位置,如果该位置有元素,则通过equals方法判断两个元素是否相同,如果相同的话,则添加操作不会成功,如果不同的话,则会重新散列到其他位置。
有以下几个注意点:
- 如果两个对象相同,则hashcode值相等,调用equals方法返回true。
- 如果两个对象的hashcode值相等,则这两个对象不一定相等。
ArrayList和LinkedList的区别?🌟
- ArrayList底层是基于动态数组实现的,内存空间是连续的,可以支持按照下标查询,时间复杂度为O(1),如果未给定索引查找的话需要遍历,时间复杂度为O(n),当在尾部进行插入和删除操作时,时间复杂度为O(1),其他部分涉及到数组的移动,时间复杂度为O(n)。
- LinkedList底层是基于双向链表实现的,内存空间不是连续的,不支持按照下标查询,查找元素的话需要遍历,时间复杂度为O(n)。当在头部和尾部进行插入和删除操作时,时间复杂度为O(1),其他部分需要遍历,时间复杂度为O(n)。
- ArrayList适合查询操作比较多的场景,因为支持下标查询;而LinkedList适合增删操作比较多的场景,可以直接改变指针增删节点就行了。
- ArrayList和LinkedList都不是线程安全的。
如何保证集合的线程安全?
- 使用的时候优先在方法内使用,声明为局部变量,这样的话就不存在线程安全的问题。
- 使用Collections.synchronizedList将ArrayList和LinkedList包装成线程安全的集合。
- 使用线程安全的集合类,如ConcurrentHashMap。
HashMap和HashTable的区别?
- HashMap不是线程安全的,而HashTable是线程安全的,因为它的每一个方法都使用了synchronized修饰。
- HashMap允许key和value为null,而HashTable不允许。
- HashMap扩容是数组容量翻倍,而HashTable是数组容量翻倍+1。
HashMap的底层实现原理?
HashMap的底层是使用的散列表的数据结构,即数组+链表+红黑树。
在jdk1.8之后,HashMap中引入了红黑树,当链表长度大于8,并且数组长度大于64时,就会将链表转为红黑树,当红黑树的结点数小于等于6个就会退化成链表。
当我们使用put方法向HashMap中放入元素的时候:
- 先得到key的hashCode值,然后再重新hash得到当前元素在数组中的下标。
- 判断该位置是否有元素,如果没有则新建节点添加数据。
- 如果有元素的话,则通过equals方法判断是否相同,如果相同则进行覆盖,如果不同则将这个元素放入当前位置的链表或者红黑树中。
- 如果key为null的话,则放入下标为0的位置,因为null的hash值不好计算。
当我们使用get方法获取元素的时候,先通过key的hashCode重新hash得到hash值,然后再通过equals方法进一步判断key是否相同从而获取指定的元素。
HashMap为什么链表长度为8转成红黑树呢?
这是做了效率的权衡才决定长度为8的,如果hash函数设计合理的话,根据泊松分布,那么长度为8的概率非常小,所以选择8,而16的话,查询效率就比较低了。
HashMap中put方法的具体流程?
- 当创建一个HashMap的时候,如果指定了容量,那么就会初始化一个指定容量的数组,如果调用的是无参构造方法,那么数组是空的,当添加第一个元素的时候会扩容成长度为16的数组。
- 当我们继续添加元素时,先通过key的hash值判断当前位置是否有元素,如果没有则直接新建节点添加数据。
- 如果有的话则使用equals方法判断是否相同,如果相同则覆盖value,如果不同则判断当前位置是否是红黑树,如果是则添加到红黑树中。
- 如果不是红黑树则遍历链表,将该元素插入到链表尾部,并判断链表长度是否大于8,如果大于8则转换成红黑树。
- 插入成功后,判断HashMap中存在的键值对数量是否超过了扩容阈值(数组长度*0.75),如果超过则进行扩容,扩容成原来容量的2倍。
hashmap如果出现哈希冲突,有什么解决办法?
- 拉链法:hash值相同,内容不同的节点通过单向链表进行连接。
- 开放定址法:如果当前位置不为空,则遍历散列表找到下一个空的位置,把值存入空的位置。
- 双哈希法:有多个不同的hash函数,如果hash值相同,则进行第二次,第三次hash...直到计算的索引位置没有哈希冲突。
怎么样尽量的避免哈希冲突?
- 选择合适的哈希函数。
- 增加哈希表的大小。
- 对于热点数据,可以考虑将它们单独处理。
- 定期清理哈希表中不再使用的数据。
HashMap的扩容机制?
当我们添加元素或者初始化的时候都会调用resize方法进行扩容,以后每次扩容都是需要超过扩容阈值才进行扩容。
扩容的容量为原来容量的2倍,然后初始化一个扩容后容量大小的新数组,并遍历旧数组中的数据添加到新数组中。
- 如果当前位置只有一个元素,那么 该元素的hash值 & 新容量-1 就是新数组中对应的下标值。
- 如果当前位置是红黑树,则直接走红黑树的添加逻辑。
- 如果当前位置是链表,则遍历链表,判断 该元素的hash值 & 旧容量 是否等于0,如果等于0,则下标位置不变,否则下标位置是旧位置 + 增加的容量大小。
HashMap在JDK1.7 和1.8中有什么变化?
- 1.7中使用的数据结构是数组+链表,1.8中使用的是数组+链表+红黑树,当链表长度超过8就会转换成红黑树。
- 1.7扩容时重新计算哈希值和索引位置,1.8扩容时不需要重新计算哈希值,而是与扩容后的容量进行&得到新的索引位置。
- 1.7采用的是头插法插入链表,在扩容时会改变链表元素位置,所以在并发场景下有可能导致链表成环的问题;1.8采用的是尾插法插入链表,不会改变链表元素位置,也就不会出现成环的问题。
HashMap的寻址算法?
先得到key的hashCode值,然后将hashCode值右移16位再与原来的hashCode进行异或运算得到最后的hash值。
为什么说HashMap不是线程安全的?
- jdk1.7 扩容导致链表成环
因为使用的头插法,比如旧数组一个位置的链表有元素abc,其中a指向b,b指向c。当进行扩容的时候,先把c元素迁移到新数组中,再把b元素迁移过来,这时c指向b,然而另一个线程扩容进行数据迁移还是b指向c,那么这样就形成了环形链表的问题。
多个线程同时进行put操作可能导致数据丢失
两个线程同时执行put方法,如果put元素计算的索引位置相同的话,那么后放入的元素可能会覆盖之前放入的元素。
get可能为null
一个线程put,另一个线程同时get,当put元素导致扩容时,因为hashmap进行扩容需要重新计算索引位置,那么放入新数组后不再原位置的元素,可能get方法就获取不到元素值。
为什么string通常作为hashmap的key?
易于理解和维护:string作为一种普通的数据类型,可以直接使用字符串常量进行赋值,而不需要额外的转换和处理。
高效的hash运算:string类型的hash值计算方式简单且高效,因此可以快速定位到对应的位置,提高了查找效率。
字符串常量池的优势:使用string类型作为key,对应的值会存放在常量池中,这样的话相同的key直接拿来进行比较就可以了。
字符串的不可变性:由于string类型一旦被赋值就无法被修改,所以我们无法在hashmap中修改已存在的key值,这样就会避免使用可变对象作为key引发的问题。
ConcurrentHashMap的实现原理?
它是一种乐观锁,通过CAS操作来实现并发更新。
ConcurrentHashMap是一种高效的,线程安全的HashMap集合。
在jdk1.7中的数据结构是ReentrantLock + Segment + HashEntry。
ConcurrentHashMap中是一个Segment数组,每个Segment中有一个HashEntry数组,HashEntry数组是一个数组加链表的结构,当我们要修改HashEntry中的数据时,需要获取到对应的Segment的锁,因为锁的粒度是一个Segment,而当我们读取元素的时候是不需要加锁的,因为它使用了volatile关键字保证可见性。
在jdk1.8中做了很多优化,比如它放弃了Segment臃肿的设计,使用node+cas+synchronized+红黑树来更高效的保证并发安全。锁住的粒度更细了,它在修改数据的时候只需要锁住当前链表或者红黑树的首节点即可。在读数据的时候同样不需要加锁,也是使用了volatile关键字保证了可见性。
Java中的异常✅
对于Java中的异常,主要分为两大类,检查型异常和非检查型异常。
检查型异常需要在编译阶段进行捕获或者向上抛出,否则编译不通过,这种异常在IO操作中比较常见,比如说FileNotFoundException,并不是说一定会有这个异常,只是说可能会运行不成功,需要对这种情况做特殊处理。
非检查型异常,一般是RuntimeExeption,它不需要显式的进行捕获或者抛出,但是运行的时候如果出现异常程序也会挂掉。对于这种异常,一般是代码本身出现问题,比如说数组越界,空指针异常等。如果代码写的健壮性足够高,这种异常是可以避免的。
常见的运行时异常:类型转换异常,空指针异常,数组越界异常。
Java创建对象的几种方式?
- 使用new关键字,这是最常用的创建对象的方式,可以调用任意构造函数。
- 使用反射的方式创建对象,调用newInstance方法。
- 使用clone方法,这种方式不会调用任何构造函数。需要先实现Cloneable接口并重写其定义的clone方法。
- 使用反序列化,不会调用任何构造函数,需要先让类实现Serialiable接口。
什么是泛型?泛型的好处有哪些?
泛型是jdk5之后引入的一个新特性,它允许在定义类和接口的时候使用类型参数,在使用的时候再用具体的类型进行替换,泛型最主要的应用是在集合类框架中,
泛型的好处主要有两个:
- 方便,可以提高代码的复用性:比如我们将String类型和Integer类型放入List集合中,放String类型的时候需要定义一个List接口,而放Integer类型需要再定义一个接口,那么这样的话代码比较冗余,使用泛型就可以很好的解决这个问题。
- 安全:在没有泛型之前,使用Object进行类型转换需要在运行时检查,如果类型转换出错则程序直接挂掉,这对程度影响非常大。而使用泛型就可以在编译时做类型检查,这样就提高了程序的安全性。
什么是泛型擦除?
在编译时限定加入集合的类型,在运行时将类型擦除,比如一个List< String>和一个List< Integer>在运行期会被擦除为List。
泛型中上下界限定符 extends 和 super 有什么区别?
<? extends T>限定上界,表示类型化参数是T或者T的子类;<? super T>限定下界,表示类型化参数是T或者T的父类。- 如果想从集合中读取数据而不写入数据,可以使用 ? extends 通配符(集合相当于生产者),如果想要向集合中写数据而不读数据,则使用 ? super 通配符(集合相当于消费者),如果既要存又要读,则不能使用通配符。
常见的数据结构
常见的数据结构一共有6种,分别是:数组,链表,栈,队列,哈希表和树。
- 数组:是一组相同类型的数据按照连续的内存空间存储,查找块,增删慢。
- 链表:由节点组成的数据结构,节点包含数据和指向下一个节点的引用。增删快,查找较数组慢。
- 栈:是一种遵循后进先出原则的数据结构,可以使用push(入栈)和pop(出栈)操作。
- 队列:是一种遵循先进先出原则的数据结构,可以使用入队和出队操作。
- 哈希表:根据键直接访问值的数据结构,通过哈希函数将键映射到存储位置。
- 树:是一种非线性的数据结构,由节点和边组成。常见的树结构包括二叉树、二叉搜索树等。
队列的使用场景
- 任务调度:线程池中放在工作队列中的任务按照先来先执行的顺序。
- 消息队列:生产者将消息发送到队列中,然后消费者从队列中获取消息。
- 日常生活中的排队场景。
String s =“abc“与String s = new String(“abc“)的区别
答:String s =“abc“会创建1个或者0个对象,而String s = new String(“abc“)会创建1个或者2个对象。
对于String s =“abc“,如果字符串常量池中已经存在值为“abc”的字符串对象,那么变量s会指向已存在的对象;否则会在字符串常量池中新创建一个字符串对象,并将它赋值给变量s。
对于String s = new String(“abc“),不管字符串常量池中是否存在值为“abc”的字符串对象,它都会在堆中创建一个字符串对象。如果常量池中没有则创建一个字符串对象,如果有则不创建。
说几个常见的语法糖?
语法糖就是在计算机语言中添加某种语法,这些语法对语言的功能不造成影响,但是更方便程序员的使用。
常见的语法糖有:switch支持枚举和字符串,泛型,自动装箱和拆箱,增强for循环和lambda表达式等。
lambda是如何实现的?
lambda表达式其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部的api。
什么是反射机制?反射为什么这么慢?
反射机制指的是在程序运行时可以获取自身的信息。在java中,只需要给定类名,就可以通过反射机制获取该类所有的属性和方法。
使用反射的好处是提高了程序的灵活性和扩展性,但是也会带来一些问题,比如:
- 代码的可读性和可维护性变低。
- 反射代码的执行性能低。
- 反射破坏了封装性。
所以一般我们在业务代码中尽量避免使用反射,但是如果要成为一名合格的java程序员,我们要能够做到读懂中间件和框架中的反射代码,并在某些场景下利用反射解决一些问题。
那么反射为什么这么慢呢?
主要有以下几个原因:
- 反射需要动态的解析类,所以java虚拟机的一些优化就不会起作用。
- 反射需要包装和拆包参数,这个过程中可能会产生大量的对象,触发gc,那么包装和拆包的过程以及gc都是需要耗时的。
- 反射获取方法时,需要遍历方法数组,并且需要对方法的可见性以及对应的参数等进行额外的检查,这些操作都是耗时的。
java中的注解的作用?
注解是jdk5中引入的,它的作用是给java代码提供元数据,不会对代码的执行造成影响。注解简单来说就是一种标识,可以放在类和字段上,常常是和反射、AOP结合起来使用。
元注解,定义在注解上的注解,常见的元注解有两种,一个是@Target,标明该注解用在什么地方,另一个是@Retention,标明在什么阶段保留该注解。
什么是序列化和反序列化?
为什么使用?
- 网络通信的数据都是二进制数据,而java中的都是类对象,所以需要把类对象序列化转成字节数组进行传输,同时这个过程应该是可逆的,也就用到了反序列化。
- 对象持久化:将java中的类对象持久化保存到文件或者数据库中。
如何实现?
- java提供的原生序列化,需要被序列化的类实现Serializable接口,但是这种方式效率低,并且序列化后的流数据比较大,所以不推荐使用。
- 使用第三方序列化方式,比如JSON。
注意:
transient(串森它)和 static 修饰的属性不能被序列化。
serialVersionUID 有何用途? 如果没定义会有什么问题?
它的用途主要是为了对比对象序列化后内容是否被篡改,如果被篡改了,那么这个id就会不一样,当进行反序列化的时候就会报异常。
什么是浅拷贝和深拷贝?
浅拷贝是拷贝的原对象的地址,和原来的对象使用的是同一个内存地址。
深拷贝要求拷贝的类需要实现Cloneable接口,并重写clone方法,它是会创建一个全新的对象,并将原对象所有的属性拷贝一份。也就是说,原对象修改,进行深拷贝的对象不受影响。
比如说有这样一个例子,定义一个老师类,它有姓名,年龄以及一个学生类。当进行浅拷贝后,修改这个老师类中的学生属性,那么拷贝的对象中的学生属性也会被修改。而进行深拷贝的话,那么修改学生的属性,拷贝的对象不会被修改。
栈和堆的区别?
- 存储内容不同:栈里存放的是基本类型的变量以及对象的引用;而堆里存放的是创建出来的实例对象。
- 共享性不同:栈是线程私有的,而堆是所有线程共有的。
- 大小不同:栈的空间大小远远小于堆。
- 抛出的错误类型不同:栈和堆空间不足都会抛出异常,栈抛出StackOverFlowError,堆抛出的是OutOfMemoryError。
看到这里
静态内部类实现单例模式
是一种线程安全的单例实现方式。
class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}JDK8新特性
1.lambda表达式
为了简化代码而生。
引出:定义一个thread类,需要传入runnble接口的实现类,但是不想定义实现类,所以使用匿名内部类并重写里面的方法,但是由于我们只关心方法体中的内容,所以为了更加简化引出了lambda表达式。
lambda表达式其实就是一个匿名内部类,只不过更加简洁了。
lambda表达式不关注接口名和方法名,关注参数列表和返回值,由三部分组成:参数列表、->、方法体。
使用lambda表达式的前提:
方法中的参数或者局部变量必须是一个接口,并且只能有一个抽象方法(可以使用@FunctionalInterface修饰)。
lambda表达式和匿名内部类的比较:
- 所需类型不同
匿名内部类可以是类,抽象类或者接口;而lambda表达式的类型必须是一个接口。
- 抽象方法个数不同。
匿名内部类所需接口对抽象方法个数没有限制;而lambda表达式所需接口只能有一个抽象方法。
- 实现原理不同
匿名内部类是在编译后生成一个class;而匿名内部类是在程序运行时动态的生成class。
2.接口的增强
在jdk8之前,接口中只能由静态常量和抽象方法,在jdk8之后,接口中新增了默认方法和静态方法。
为什么引入默认方法和静态方法?
答:在jdk8之前接口中新增一个抽象方法,那么它的实现类都需要重写这个方法,不利于接口功能的扩展。
引入了默认方法,那么接口中就可以实现默认方法,接口的实现类不强制要求重写默认方法,使得接口功能的扩展性增强。
静态方法实现后,它的实现类不能够继承静态方法,只能通过接口名.静态方法名调用。
3.函数式接口
因为lambda表达式不关心接口名和方法名,只关心返回值和参数列表,所以为了更方便的使用lambda表达式,引入了函数式接口。共分为以下几类:
- supplier
无参有返回值的接口
- consumer
有参无返回值的接口
- function
有参有返回值的接口
- predicate
有参且返回值为boolean类型的接口
4.方法引用
符号 :: 称为方法引用运算符,用在lambda中称为方法引用。如果lambda表达式中的方案在其他方法中已经实现了,那么就可以使用方法引用进行简化。
常见格式:
- 对象名
::方法名 - 类名
::静态方法名 - 类名
::实例方法名 - 类名
::构造器 - 数组
::构造器
5.Stream API
特性:
- Stream不存储数据,只是对数据进行加工,最终返回一个新的值或者集合。
- Stream不会改变原始数据源,返回一个新的值或者集合。
- 如果不调用终结方法,那么中间的操作不会执行
获取Stream的两种方式:
- Collections接口中的stream方法。
- Stream接口中的静态方法of。
- 使用Arrays.stream方法使用数组创建流。
分类:分为中间操作和终结操作。
中间操作每次都返回一个新的流。
每个Stream流只能调用一次终结操作,调用后流无法再使用,终结操作会生成一个新的值或者集合。
函数:
forEach(终结方法):遍历流中的数据并打印输出。
count(终结方法):统计流中的元素个数。
filter:过滤数据,返回符合条件的元素组成的流。
limit(截取前几个元素)、skip(跳过前几个元素)。
map:进行元素的映射。
sorted:默认升序排序,也可自定义降序排序。
distinct:去重。
6.Optional类
主要是解决空指针的问题。
Optional是一种使用final修饰,可以存放null的容器对象,主要作用是为了避免null检查,出现空指针异常。
Optional对象的创建方式:
- optional.of() 这种方式不能存放null
- optional.ofNullable() 可以存放null,避免了null检查
- optional.empty() 创建一个空的optional对象
7.新时间日期API
jdk8之前的日期时间API存在一些问题:
- 设计不合理:比如指定时间日期是从1900年开始计算。
- 线程不安全:多线程下时间日期的格式化和解析操作线程不安全。
- 无法指定时区:日期类并不提供国际化,不支持指定时区。
针对以上问题,jdk8中提供了一套全新的时间日期API,这些类都位于java.time包下,以下是一些关键的类:
- LocalDate、LocalTime、LocalDateTime
- DateTImeFormatter:日期时间格式化类
- Instant:时间戳
- Duration:用于计算两个LocalTime之间的距离
- Period:用于计算两个LocalDate之间的距离
- ZonedDateTime:包含时区的时间
读写文件API及常用步骤
文件读取:FileInputStream(字节流,多处理音视频,二进制数据)、BufferedInputStream、FileReader(字符流,多处理文本文件)。
文件写入:FileOutputStream、BufferedOutputStream、FileWriter。
步骤:
- 选择合适的文件读取方法。
- 选择好后创建文件输入流对象。
- 读取目标文件内容,同时使用对应的文件输出流对象将读取到的内容写入到指定的文件中。
- 读取并写入完毕后,关闭文件输入流和文件输出流对象。
java中的juc包
juc包提供了处理并发场景的类和接口。
常用的类和接口:
- lock接口:常见的实现类是ReentrantLock。
- Atomic包:里面提供了一系列原子操作的类,用于实现线程安全的原子操作,比如AtomicInteger、AtomicLong。
- 并发集合类:ConcurrentHashMap。
- 线程池类:ThreadPoolExectutor,可以通过指定核心参数从而更加灵活的管理线程。
集合和数组的区别?
数组的内存空间是连续的,而集合的内存空间不一定连续。
数组的大小是固定的,在创建集合的时候需要指定出来;而集合的大小不是固定的,可以动态的扩展。
数组只能存储相同类型的数据,而集合可以存储不同类型的数据。
字符集
字符集是一种针对字符的编码规范,字符可以通过这些规范使得计算机能够识别,存储以及显示这些字符。
常见的字符集有: ASCII是美国人发明的,针对英文字符,数字和一些特殊的标点符号进行了编号,使用一个字节表示,二进制首位是0,总共可以表示128个字符。
GBK因为ASCII不能表示中文,所以中国人发明了一种GBK编码,中文用两个字节表示,首位是1,非中文用一个字节表示,首位是0。
Unicode编码也成为万国码,是由国际组织制定的,它基本了涵盖上世界上所有国家的字符,常见的方案有UTF-8。UTF-8中一个中文占3个字节。
token是什么?
token是令牌的意思,可以用于进行用户身份的验证以及判断用户是否登录。
当用户登录之后,后端可以生成一个随机字符串作为token,同时和用户相关身份信息保存在redis中,后端将token返回给前端,前端将token保存在sessionStorage中,每次发送请求将token放在请求头中,后端通过请求头中的token,在redis中取出值进行验证身份信息,同时也可以通过判断token是否有判断用户是否登录或者登录过期。
java常用容器
list、set、map、queue、deque、stack。
关于try , catch , finally 代码块的分析:
不管程序有无异常,finally中的代码是一定会执行的。
如果try catch 里面有return时,会先计算return表达式后面的值,将它保存起来,然后执行finally中的代码,执行结束后返回之前保存起来的值(不管finally中的代码如何都是返回之前保存的值)。
注意:
- finally中不能有return,如果有的话,那么程序会提前退出,就不能得到预期结果了。
- 有种情况finally中的代码不一定会执行,当程序调用system.exit方法时,程序会立即退出。
int i = 1;
try{
return i++;
}catch{
}finally{
++i;
}
//返回结果是1什么是内存溢出?什么是内存泄漏?怎么避免?
内存溢出:当可用内存空间不足以存放新创建的对象时,这时候就会发生内存溢出。
内存泄漏:当我们执行完业务代码后,使用的对象应该被回收,但是由于还有别的对象引用了它,所以它不能被GC自动回收,这样就占了额外的内存空间,长时间就有可能导致内存溢出。
避免:我们使用完了对象后,手动的去关闭流或者释放内存。
怎么判断一个字符串是不是整数?
可以利用String中的matches使用正则表达式判断是不是整数。
String str = "12345";
if(isInteger(str)){
sout("str是一个整数");
}else{
sout("str不是一个整数");
}
private boolean isInteger(String str){
return str.matches("^-?\\d+$");
}^表示匹配字符串的开始位置-?表示匹配一个可选的负号\\d+表示匹配一个或多个数字$表示匹配字符串的结束位置
Java从编码到项目运行经历了哪些阶段?
- 根据业务进行编码。
- 用java编译器将源代码编译成字节码文件。
- 使用构建工具(如Maven),将项目打包成jar包。
- 使用命令行 java -jar 或者 tomcat 服务器运行 jar 包启动项目。
- 在测试环境下进行项目的调试和测试,确保项目能够正常运行且符合预期功能。
- 测试环境下没有问题,则部署到生产环境中,但是仍然需要对项目进行监控,如果出现问题或者功能需要更新则需要维护。
Select语句从java客户端发送到mysql客户端返回结果经过了哪些步骤?
- java客户端与mysql进行交互底层使用的是JDBC技术。
- JDBC加载驱动,获取连接对象,获取statement对象,编写sql语句,然后执行,这个时候会将sql语句交由mysql执行。
- mysql客户端收到sql语句时,先对该sql语句进行语法检查,语义分析,以确保查询语句是正确的。
- 接下来mysql将对查询进行优化,尝试去找到最佳的执行计划,这里面涉及到索引的选择,连接顺序等优化工作。
- 执行查询,根据查询条件,扫描对应的表并生成结果集。
- 返回给java客户端。
- java客户端获取到结果集进行处理。
- 处理完毕之后,java客户端关闭连接、statement以及结果集对象。
Java中的new操作在内存中是怎么分配的?
- 首先声明一个引用变量,用来存储对象在堆中的内存地址。
- 在堆中分配内存,用来存储对象的实例数据。
- 分配好内存空间后,会调用对象的构造函数给对象初始化,为类中的实例变量赋值。
- 最后,new操作符会返回堆中的内存地址,并将它赋值给对象引用变量。
类的实例化顺序
父类的静态成员和静态方法块;(静态成员和静态代码块的加载顺序按照定义顺序来)
子类中的静态成员和静态方法块;
父类中的成员变量和方法块;
父类构造函数;
子类中的成员变量和方法块;
子类构造函数。
调试
条件断点:在所在的行打一个断点,然后鼠标右击显示一个框,里面有Condition,可以在这里面写条件,这样的话在运行调试的时候,只有满足条件的时候才会停下来。
进行debug:
异常断点:当出现异常后,可以自己配置异常断点,这样在debug模式下,会自动停在出现该异常的行上。
方法断点:
在方法上打一个断点,以debug运行,程序执行到打断点的方法时就会进入方法,这样就可以观察方法的参数以及方法的执行过程。
JIT和AOT的区别?
JIT是即时编译,会在程序运行过程中对代码进行编译,会根据运行情况针对性的进行编译。
AOT是预先编译,是在程序执行前就编译好了,所以启动速度更快,无需在程序运行时进行即时编译。
计算机网络
cookie和session的区别?
由于http请求是无状态的,而cookie和session是用来弥补无状态的手段。cookie在浏览器端以键值对的形式保存用户信息。而使用session依赖cookie,用户第一次登录会将sessionid保存到cookie中,而用户信息保存到服务器端,后续发送请求将cookie中的sessionid带着从而获取用户信息,如果浏览器禁用了cookie,也可以将sessionid放在url上,但是建议进行加密。
- cookie是存放在客户端的,而session是存放在服务器端的。
- cookie因为存储在客户端,所以有被窃取和篡改的风险,而session因为存储在服务端,相对更安全一些。
- 存储数据大小不同:单个cookie存储数据大小不能超过4k,浏览器的很多站点也限制cookie不能超过20个;而session存储到服务端,浏览器对其没有限制。
- 生命周期不同:cookie的生命周期是累计计时的,而session是间隔计时。以20分钟举例,cookie从创建开始计时,到了20分钟就会销毁;而session在20分钟内没有被访问也会被销毁,如果被访问了,那么生命周期就会重新计时。session在浏览器关闭时就会失效,而cookie可以长期保存。
使用场景:
cookie可以保存少量的文本信息,并在每次请求时将这些信息发送给服务端,常见的用途包括:用户认证,跟踪用户行为以及记录用户偏好等。
session通常用于在服务端存放用户的状态信息,比如用户登录状态,购物车管理等。
HTTP的特点?
基于TCP协议,TCP是一种面向连接的,可靠的,基于字节流的协议,传输数据的时候不会丢包。
基于请求-响应模型:一次请求对应一次响应,如果没有请求则也没有响应。
HTTP协议是无状态的协议。
无状态指的是每次请求和响应都是独立的,后一次请求不会记录前一次请求的数据。
缺点:多次请求之间不能进行数据共享。
优点:速度快。
常见的HTTP状态码有哪些?分别代表什么含义?
200 表示服务器处理请求成功
301 永久重定向,当客户端请求一个网址的时候,服务器会将其重定向到另一个网址,搜索引擎会抓取重定向后网页的内容并将旧网址替换成重定向后的网址。
302 临时重定向,与永久重定向不同的是,搜索引擎抓取重定向后网页的内容并保留旧网址,因为搜索引擎认为重定向后的网址是暂时的。
304 未修改,当客户端向服务端发送请求后,服务端发现请求的内容未发生修改,则返回304状态码,这样客户端就可以直接使用之前缓存的内容,而不需要再从服务端下载内容,节省了网络带宽和提高了响应速度,它是HTTP协议中用于缓存控制的一种常见状态码。
400 客户端请求错误,一般是参数不合法导致服务器校验参数失败。
401 认证失败,用户未登录。
403 授权失败,客户端访问该资源没有对应的权限。
404 客户端请求的资源找不到。
500 服务器处理客户端请求的时候发生错误。
504 网关超时。
1xx:表示信息性状态码,表示服务器正在处理请求。
2xx:表示请求成功处理。
3xx:重定向,表示客户端需要进一步的操作以完成请求。
4xx:表示客户端错误,通常是客户端的请求存在问题。
5xx:表示服务器错误,意味着服务器处理请求时发生了错误。
HTTP协议报文格式
请求报文格式:
请求行(请求方法,请求URL,HTTP协议版本)、请求头(包含请求的相关信息,包括Content-Type,Content-Length等)、空行(请求头结束的标志,用于分离请求头和请求体)、请求体(请求的相关信息,通常post请求携带内容)。
响应报文格式:
状态行(HTTP协议版本,状态码以及状态信息)、响应头(包含服务器响应的信息,如Content-Type)、空行(响应头结束的标志,用于分离响应头和响应体)、响应体(包含服务器返回的实际内容,如html页面,图片数据等)。
HTTP常见字段有哪些?(HTTP报文头)
Host:客户端在发送请求时,用于指定服务器的域名,有了Host字段后,我们就可以将请求发送到同一台服务器上的不同网站。
Content-Length:表示服务器返回数据的数据长度。
Http协议是基于TCP协议的,而TCP在传输数据的时候,存在一个“粘包”问题,Http协议通过设置回车符、换行符作为Http header 的边界,通过Content-Length字段作为Http body的边界,这两个方式就是为了解决“粘包”的问题。
"粘包":TCP在传输数据的时候,会将消息分成多个报文段进行传输,当多个消息的一部分在一个报文段中,那么就出现了“粘包”问题,需要通过一些手段规定消息的边界,从而能够让接收方获取到有效的消息。
- Connection:用于客户端请求服务器使用Http长连接,以便其他请求复用。
Http长连接的特点:只要任意一端没有明确提出要断开连接,那么就一直保持TCP连接。
Http/1.1默认的连接就是长连接,但是为了兼容Http/1.0,需要指定Connection字段的值为Keep-Alive。
- Content-Type:服务器做出响应后,告知客户端返回的数据格式。
- Accept:客户端发送请求时,可以设置Accept字段的值告知自己可以接收的数据格式。
- Content-Encoding:服务器做出响应后,告知客户端数据采用的压缩格式。
- Accept-Encoding:客户端发送请求时,可以设置Accept-Encoding字段的值告知自己可以接收的数据压缩格式。
TCP的keepalive和Http的Keep-Alive是一个东西吗?
不是一个东西。
Http的Keep-alive是标识当前的Http连接是长连接,只要任意一端没有提出断开连接,那么TCP连接就可以复用,避免了频繁的建立TCP连接所带来的开销。
如果Http发送一次请求之后不再发送请求,并且也没有提出断开连接,那么TCP连接一直保持存活就会造成资源的浪费,而TCP的keepalive就是用来解决这个问题的,通过设置一个超时时间,内核发送探测报文来判断对方是否存活,如果超时时间已过,还是没有其他的Http请求,那么就会关闭这个TCP连接。
Forward和Redirect的区别?
- forward是服务器内部的重定向,url地址不变;redirect是客户端和服务器之间的重定向,url地址发生变化。
- forward中的request在servlet之间是共享的,而redirect重定向前后是两个不同的request。
- forward只会发送一次请求获取请求的servlet的响应内容,而redirect会发送两次http请求。
Get和Post请求方式的区别?
用途不同
get请求是用来获取数据的,而post请求是用来提交数据的。
表单的提交方式
get请求是将表单中的数据以 k1 = v1 & k2 = v2的形式拼接到url地址栏上的,而post请求是将表单中的数据添加到请求头上或者请求的消息体中。
传输数据的大小限制
get请求传递的数据大小受url长度的限制,因为浏览器会对url长度做出限制,而Http协议本身不会对url长度做出限制,而post请求是没有限制的。
参数的编码
get请求传递的参数在url地址栏上明文显示,只能传递ASCII字符。
post请求使用的是二进制数据多重编码传递参数。
缓存
get请求可以被浏览器缓存并收藏为标签。
post请求不能被浏览器缓存,也不能被收藏为标签。
从浏览器地址栏中输入URL到展现页面的全过程
- 用户输入url并回车后,会先通过DNS协议将域名解析成对应的IP地址,查找IP地址的流程一般会先从浏览器缓存,路由器缓存,host文件中查找是否有对应的域名和IP地址的映射,如果有则直接返回;如果没有则查找本地域名服务器,没有则继续递归查询根域名服务器,顶级域名服务器以及权威域名服务器,这样肯定能找到对应的映射关系然后逐层返回。
- 建立TCP连接。
- 客户端发送Http请求,经过路由器转发,服务器防火墙到达服务器。
- 服务器处理该Http请求,返回一个Html文件。
- 浏览器解析该Html文件,并且显示在浏览器端。
输入url的全流程,如果我输入某个域名,想让他不访问这个ip要怎么办?
- 修改本地hosts文件,改变该域名和对应IP的映射关系。
- 使用网络防火墙,阻止该域名对指定IP的访问。
说说TCP和UDP的区别?以及各自的优缺点?
- TCP是面向连接的,即TCP传输数据前需要先建立连接;而UDP是无连接的,传输数据前不需要建立连接。
- TCP提供可靠的数据传输,它使用序号,确认和重传机制来确保数据的完整性和准确性;UDP不提供可靠的数据传输,数据传输没有确认和重传。
- UDP具有较高的实时性,传输效率也比TCP高,适用于高速传输和实时性较高的通信或者广播通信。
- 每一条TCP连接只能是点对点通信,而UDP支持一对一,一对多,多对一以及多对多的交互通信。
- TCP需要维护连接状态和传输控制信息,因此消耗的系统资源较多;UDP则简单高效,消耗的系统资源较少。
TCP适用于对通信数据的完整性和准确性要求较高的场景,比如重要文件的传输,邮件发送等。
UDP适用于对通信速度要求较高,对通信数据的完整性和准确性要求较低的场景,比如网络电话,视频会议,直播等场景。
TCP是如何保证可靠传输的?
- 连接管理:TCP建立连接时需要进行三次握手,断开连接时需要四次挥手,保证建立连接的双方能够收发正常。
- 序列号和确认应答机制:TCP会给每个字节分配一个序列号,发送方根据序列号将数据分割成多个报文段,并发送到网络中。接收方通过确认应答机制告诉发送方已经成功接收到数据,如果发送方在一定时间内未接收到确认应答,则会重新发送数据。
- 重传机制:当发送方长时间得不到确认应答,则认为数据包丢失,触发超时重传;如果得到一个数据包的多次确认应答,则也会认为数据包丢失,会触发快速重传。
- 流量控制:接收方通过滑动窗口告知发送方可以接收的数据量,发送方根据窗口大小进行流量控制,确保不会发送超过接收方处理能力的数据。
- 拥塞控制:根据网络拥塞情况,有一个拥塞窗口,它和滑动窗口比较,较小值作为发送方传输数据的速率的依据。
TCP建立连接好之后,如果写数据太快会怎么样?
- 接收方缓存区溢出:如果接收方无法及时处理接收到的数据,可能导致接收方缓存区溢出,丢失部分数据。
- 流量控制:如果发送方发送数据过快,超过接收方的接收窗口大小,那么接收方会发送接收窗口大小为零的反馈,从而限制发送方的发送速率。
- 拥塞控制:过快的发送速率可能导致网络阻塞,进一步影响数据的传输速率。
- 丢包和重传:由于接收方无法及时处理数据,可能会导致数据丢失,这样发送方需要重传,进一步增加网络开销和延迟。
TCP的三次握手四次挥手
TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。
当TCP建立连接时,需要通过三次握手建立连接:
- 一开始客户端和服务端都处于closed状态,先是服务端主动去监听某个端口,进入listen状态。
- 客户端随机初始化序列号,并将这个序列号放入TCP首部的序列号字段中,同时把SYN置为1,表示SYN报文,接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于syn-sent状态。
- 服务端接收到客户端的SYN报文后,会随机初始化序列号,并将该序列号放入TCP首部的序列号字段中,其次把客户端发送的序列号+1放到TCP首部的确认应答号字段中,接着把SYN和ACK的标志位置为1,最后把报文发送给客户端,该报文不包含应用层数据,之后服务端处于syn-reveived状态。
- 客户端收到服务端的报文后,还要向服务端回应最后一个应答报文,首先把ACK标志位置为1,接着把服务端发送的序列号+1放入TCP首部的确认应答号字段中,最后把报文发送给服务端,这次报文可以携带客户端到服务端的数据,之后客户端处于
ESTABLISHED(因斯谈破类时的)状态。 - 服务端收到客户端的应答报文后,也进入
ESTABLISHED状态。 - 完成三次握手,就成功建立连接了,客户端和服务端可以收发数据。
当TCP断开连接时,需要通过四次挥手断开连接:
- 客户端想要断开连接,此时会将一个TCP首部FIN置为1的FIN报文发送给服务端,之后客户端进入FIN_WAIT_1状态。
- 服务端收到客户端的FIN报文后,回应一个ACK报文,之后服务端进入CLOSE_WAIT状态。
- 客户端收到服务端的ACK报文后,进入FIN_WAIT_2状态。
- 等待服务端处理完数据之后,也向客户端发送FIN报文,之后服务端进入LAST_ACK状态。
- 客户端收到服务端的FIN报文后,回一个ACK报文,之后进入TIME_WAIT状态。
- 服务端收到客户端的ACK的应答报文后,进入CLOSE状态,至此服务端完成连接的关闭。
- 客户端在经过2MSL一段时间后,自动进入CLOSE状态,至此客户端完成连接的关闭。
值得注意的是,主动关闭连接的才有TIME_WAIT状态。
FIN_WAIT和CLOSE_WAIT
两者是连接关闭的过渡状态,确保数据的完整性和连接的正确关闭。
fin_wait_1表示发送方发送了fin包,告知对方自己以及完成了数据的发送,等待关闭连接,在收到接收方的ack包进入fin_wait_2状态。
close_wait表示接收方收到了发送方的fin包,并返回一个ack包,但是此时还有数据没有处理完,需要进入close_wait状态处理剩余的数据,剩余数据处理完会发送一个fin包给发送方,此时进入last_ack状态。
为什么TIME_WAIT等待的时间是2MSL?
MSL是报文最长生存时间,它是任何报文在网络上的最长存活时间,超过这个时间那么这个报文就会被丢弃,设置为2MSL的目的是为了允许报文可以丢弃一次,如果客户端发送的ACK报文没有到达服务端,那么服务端就会触发超时重传FIN报文,客户端收到FIN报文后,会重发ACK给服务端,一来一回正好是2MSL。
为什么需要TIME_WAIT状态?
避免历史连接中的数据,因为是相同序列号被接受方接收导致数据错乱。
序列号并不是无限递增的,会发生回绕成初始值的情况。如果发送方发送数据包因为网络延迟没有到达接收方,这时新打开了一个连接,发送相同序列号的数据,那么接收方会先拿到旧的数据包,但是无法判断是新的还是旧的,有可能引发数据错乱。设置成2MSL可以保证之前连接中的数据包都能够被丢弃。
保证被动关闭连接的一方能够正确的关闭。
主动关闭连接的一方如果发送ACK报文后就进入CLOSE状态,那么如果ACK报文丢失,被动关闭连接的一方则无法关闭连接,如果有TIME_WAIT状态,那么ACK报文丢失后,被动关闭连接的一方会超时重发FIN报文,主动关闭连接的一方在接收到FIN报文后,会重发ACK报文给被动关闭连接的一方。
为什么是三次握手,而不是两次、四次?
- 避免历史连接:
如果采用两次握手,那么客户端发送SYN后,因为宕机并且网络阻塞导致报文没有到达服务端,这时客户端重启,发送另一个SYN报文,那么旧的SYN报文先到服务端,服务端建立连接,发送ACK报文给客户端,客户端接收到ACK报文之后发现和预期的不一样,就会发送RST报文断开连接,当服务端收到RST报文后才会断开连接。很显然,两次握手没有避免历史连接,导致白白建立连接,浪费资源。
而三次握手客户端发现ACK报文与预期的不一致时,发送RST报文释放连接,后续新的SYN报文到达服务端后,就能够正常的完成三次握手了。
- 同步双方初始序列号:
序列号是保证TCP可靠传输的关键因素,有了序列号可以保证客户端有序发送数据,并且确定哪些数据包被接受了。
客户端发送SYN报文,服务端发送ACK报文作为回应,同时服务端的SYN报文,客户端同样发送ACK做出回应,这样一来一回就能同步双方的初始序列号了,而四次握手因为第二次和第三次可以变为一次,两次握手只能保证一方的初始序列号被接收,没办法保证双方都能够接收对方的初始序列号。
- 避免资源浪费:
如果没有三次握手,那么服务端接收到一个客户端的SYN报文就会建立连接,那么由于网络阻塞客户端发送多个SYN报文就会让服务端建立多个无效的连接,造成资源的浪费。而三次握手可以丢弃历史SYN报文(发送RST报文释放连接)。
为什么使用四次挥手而不是三次呢?
因为如果采用三次挥手的话,是把第二和第三次挥手合成一次,但是当客户端发送一个FIN后,客户端收到断开连接的请求需要处理一些数据,这个时间间隔可能好几分钟,那么客户端由于没有收到确认,所以就会一直重发,这样就会造成资源的浪费。而采用四次挥手,当服务端收到断开连接的请求后,会立即发送一个ACK给客户端,这样既能保证接收双方的确认应答,也不会造成资源的浪费。
TCP报文头部结构
- 源端口号:发送方的端口号
- 目标端口号:接收方的端口号
- 序列号:tcp发送数据的时候会给每个字节都打上一个序列号,比如说发送500个字节,那么序列号范围就是0~499,用来解决网络包乱序的问题。
- 确认应答号:序列号+1 返回给发送方,那么确认应答号是500。这里的序列号和确认应答号保证了TCP连接的可靠性,用来解决丢包的问题。
- 控制位:当ACK为1的时候确认序列号才有效,ACK为0那么确认序列号就是无效的数字。SYN表示建立连接,而FIN表示断开连接。
- 窗口大小:类似于一个缓冲区,它的作用是建立连接后一次性把多个数据段发送给接收方,接收方只需要返回一次ACK即可,提高了数据传输速率。
TCP/IP网络模型有哪几层?
一共分为4层,从上到下分别是应用层,传输层,网络层,网络接口层。
- 应用层
手机或者电脑上的应用软件就是属于应用层的,当两个不同的设备之间的应用需要通信的时候,这时候需要将数据传输给下一层,也就是传输层。本层常用协议HTTP、DNS协议。
应用层工作在操作系统的用户态,而传输层及以下工作在内核态。
传输层
传输层为应用层提供网络支持。
传输层是应用进行数据传输的媒介,帮助实现应用到应用的通信,真正的数据传输需要交给下一层处理,也就是网络层。本层协议**:TCP协议和UDP协议**。
网络层
负责在网络中传输数据包,并进行数据包的路由和转发。
本层常用协议**:IP协议**。
IP地址是为了区分设备的,而因为IP地址寻址麻烦,所以将IP地址分为网络号(标识该IP地址是属于哪个子网的)和主机号(标识同一子网下的不同主机)。
需要配合子网掩码才能算出IP地址的网络号和主机号。
比如10.100.122.0/24,那么它的子网掩码是255.255.255.0,将IP地址与子网掩码按位与得到网络号,将子网掩码取反和IP地址按位与得到主机号。
IP地址寻址先找到目标子网,再去找对应的主机,IP地址还有一个作用就是路由,两台设备连接的线路是错综复杂的,IP地址可以根据目标地址选择合适的路径。
- 网络接口层
在IP头部的前面加上MAC头部,并封装成数据帧发送到网络上。
实际应用分为5层,将网络接口层分为了数据链路层和物理层。
如何基于UDP实现可靠传输?
为啥TCP能够实现可靠传输,还有UDP干什么?
因为TCP有几个痛点:1. 存在队头阻塞;2. TCP建立连接的延迟。
所以为了追求更快的速度以及解决队头阻塞问题,就有了基于UDP协议的QUIC协议,目前已经应用在了HTTP/3上。
QUIC是如何实现可靠传输的?
解决队头阻塞。
QUIC传包时如果丢包,那么重传的数据包的序列号是递增的,这也就意味着QUIC不需要像TCP那样有序确认数据包,否则应用层无法获取数据,QUIC可以乱序确认数据包,前面的数据包被确认那么窗口就会向右滑动,这就解决了TCP队头阻塞的问题。
QUIC更快的连接确认。
在HTTP/2中,实现加密传输需要先TCP握手,再TSL握手,所以连接延迟比较大,而HTTP/3中的QUIC协议里面包含了TSL协议,并且使用的是TSL1.3,所以只需要一次握手就可以建立连接和密钥协商,延迟比较低。
OSI七层网络模型?
OSI网络模型是为了解决不同主机之间的网络通信问题。
应用层:负责给应用程序提供统一的接口。
表示层:负责把数据转换成兼容另一个系统能识别的格式。
会话层:负责建立、管理和终止表示层实体之间的通信会话。
传输层:负责端对端的数据传输。
段是传输层里数据的名字。
网络层:负责数据的路由、转发、分片。
路由器(网络层的核心)
IP地址进行寻址和路由转发。
这里的数据叫做包。
数据链路层:负责数据的封装成帧和差错检测,以及MAC寻址。
这一层比特会被封装成帧,在封装的时候加上MAC地址(物理地址--跳到跳传输)
物理层:负责在物理网络中传输数据帧。
计算机网络中两个常用的体系为OSI七层网络模型和TCP/IP四层网络模型
IP协议
IP位于TCP/IP网络模型中的第三层,也就是网络层。
一般说的IP地址通常指的是IPv4地址,它是由32比特位组成,共分为4组,每组8位,组间使用 . 进行分割。
IP地址根据是否分类分为了分类地址和无分类地址。
分类地址:
分为了5类,分别是A , B , C , D , E类。
其中A,B,C类主要分为了两部分,分别是网络号和主机号
而D类和E类是没有主机号的,不能用于主机IP,D类常被用于多播,而E类暂未使用。
但是IP分类有缺点,就是某个分类下可分配的主机数和显示不匹配,比如A类地址可分配主机数过多,造成了地址的浪费。基于这种情况提出了无分类地址。
无分类地址:
这种方式没有分类地址的概念,而是将32比特位的IP地址分为了两部分,前面是网络号,后面是主机号。在地址后面用 /x 表示前x位是网络号,这就使得IP地址更加得灵活。
为什么用IP协议可以进行通讯了,还要有TCP和UDP?
IP协议中有源ip和目的ip,可以通过路由和寻址找到目的主机;而TCP和UCP里面有源端口号和目的端口号,可以确认是交给主机的哪个进程。
HTTP的请求过程
- 客户端首先和服务器发送连接请求,进行TCP三次握手建立连接。
- 发送HTTP请求,请求中带有请求方法,URL,请求头以及可能的请求体。
- 服务器接收到请求之后,根据请求的内容做出相应的处理,比如查询数据库。
- 服务器根据处理的结果生成包含响应状态码,响应头以及响应体的HTTP请求。
- 服务器通过已建立的TCP连接将请求发送回客户端。
- 客户端接收到响应之后,会根据响应状态码和内容做出相应的处理,比如显示页面,处理数据等。
- 如果连接是持久化的,那么该连接就会复用,直到一方想主动关闭连接才进行TCP四次挥手;如果是非持久化的,那么直接进行TCP四次挥手。
HTTP/1.0、HTTP/1.1、HTTP/2和HTTP/3的区别?
HTTP/1.0:
每次请求都需要建立TCP连接,返回响应结果后断开TCP连接。
HTTP/1.1:
引入了长连接,只要发送两端任意一方没有明确提出要断开连接,那么TCP连接可以复用,而不需要再重新建立连接。
支持管道网络传输,可以一次发送多个请求,但是服务器是按照请求的顺序进行响应的,这样可以减少整体的响应时间。
只有当服务器将一次发送的请求全部相应完,客户端才能继续发送下一批次的请求,这个过程中如果服务器响应请求发生了阻塞,那么客户端无法继续发送请求,这就造成了**“队头阻塞”**的问题。
所以说HTTP/1.1解决了请求的队头阻塞问题,但没有解决响应的队头阻塞问题。
需要注意的是HTTP/1.1的管道化技术不是默认开启的,并且浏览器基本都不支持,所以我们知道有这个功能,但是没有被使用。
HTTP/2:
它是基于HTTPS协议的,所以它的安全性有保障。
头部压缩:如果同时发送多个请求,它们的头部是一样的或者相似的,那么协议就会消除掉重复的部分。
二进制格式:HTTP/2不再像HTTP/1.1那样以明文的形式发送报文,而是使用了二进制格式,头消息和数据体都是二进制,并且统称为帧,这样的话计算机就无需将明文转成二进制,而是直接解析二进制报文,提高了数据传输的速率。
并发传输:HTTP1.1是基于请求-响应模型的,只有当服务器响应完全部请求才能接着发送请求,这样就有可能出现响应的队头阻塞问题。
而HTTP/2引入了Stream的概念,将不同的请求根据Stream ID进行区分,服务器可以根据Steam ID有序组装HTTP消息,不同Stream的帧可以乱序发送,也就是HTTP/2可以交错并行的发送请求和响应。
服务器推送:服务器可以在客户端发送请求之前将资源推送给客户端,加快页面的加载速度。
HTTP/2的缺陷?
HTTP/2虽然使用了Stream提高了并发能力,解决了HTTP/1.1的响应队头阻塞问题,但是它仍然存在队头阻塞问题,只不过不是在HTTP这一层,而是在TCP这一层。
因为HTTP/2是基于TCP协议的,而TCP协议是字节流协议,需要保证收到的字节数据是完整且连续的,这样内核才会将缓冲区中的数据返回给HTTP应用。那么当第一个字节数据没有达到之前,它后面的字节数据只能存放在缓冲区中,只有当第一个字节数据到达,HTTP应用才会从内核中拿到数据,这就是HTTP/2的队头阻塞问题。
比如说发送方发送了多个包,其中一个包在网络传输中丢了,导致内核中的TCP数据不是连续的,那么HTTP应用就无法获取到内核中的数据,直到TCP重传将丢失的包重新发送成功,这些数据才能被HTTP应用读取到,期间同一个TCP连接中的所有HTTP请求都必须等待这个丢了的包重传回来,这就造成了队头阻塞问题。
HTTP/3
因为TCP协议存在队头阻塞问题,所以HTTP/3将HTTP下层的TCP替换成了UDP,这样就解决了HTTP/2中的队头阻塞问题。
基于UDP的QUIC协议可以实现类似TCP的可靠传输
HTTP 和 HTTPS 的区别
https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
http是超文本传输协议,信息是明文传输的,存在一定的安全问题,而https则是为了解决这个问题,在tcp和http之间加入了ssl/tsl安全协议,使得报文能够加密传输。
http的连接建立相对简单,只需要进行tcp三次握手就可以进行http报文的传输,而https在tcp三次握手之后,还需要进行ssl/tsl的握手过程,才能进行加密报文的传输。
http使用的是80端口,而https使用的是443端口。
HTTPS是如何保证数据安全传输的?
- 采用混合加密的方式实现信息的机密性,防止信息被窃听。
在建立连接前使用非对称加密的方式交换密钥。在通信的过程中使用对称加密的方式加密数据。
对称加密:客户端与服务器使用同一个密钥进行加密和解密数据。
非对称加密:服务器将自己私钥对应的公钥颁布给客户端,然后客户端利用公钥对数据进行加密,服务器利用自己的私钥对数据进行解密。
- 使用摘要算法 + 数字签名保证数据的完整性。
摘要算法就是针对发送的内容计算一个hash值,连通内容一块发送到接收方,接收方可以针对内容计算hash值,并与发送方的hash值进行比较来判断内容是否被篡改。
但是只有摘要算法是不行的,因为内容和hash值都可以被劫持,然后篡改内容并重新计算hash值发送给接收方,接收方仍然不知道内容已经被篡改了,这个时候就需要使用数字签名了。
数字签名就是使用私钥加密数据,因为私钥没有对外公布,所以内容无法被篡改,然后接收方使用私钥对应的公钥进行解密,这样就可以保证数据的完整性了,但是实际的数据传输一般不会使用非对称加密的方式来加密传输数据,因为这种方式效率比较低。
- 通过数字证书,防止他人冒充网站。
有了摘要算法和数字签名后,可以保证加密数据的完整性,但是如果我将服务器颁布的公钥进行替换发送给客户端,然后伪造一份数据发送给客户端,那么客户端肯定是能够解密出来的,这样还是缺乏了安全性。
为了解决这个问题,服务器可以将公钥放到权威机构CA中,由CA颁布数字证书给客户端(这个数字证书里面包含有服务器的公钥以及CA自己的数字签名),当客户端拿到数字证书之后,首先会去CA上验证这个数字证书的合法性,CA会用自己的公钥对这个数字证书进行解密,如果解密成功,那么说明这个数字证书是合法的,客户端可以拿到其中的公钥加密需要传输的数据,这样就避免了公钥被伪造的问题。
对称加密算法:DES、AES算法
非对称加密算法:RSA算法
HTTPS建立连接的过程?
tcp三次握手以及tsl四次握手。
使用不同的密钥交换算法,握手的流程也是不一样的。
下面是基于RSA加密算法的握手流程:
客户端首先发起加密通信的请求:这一阶段客户端主要向服务器发送:
自己支持的TSL协议版本,一个随机数(用于后面生成会话密钥)以及自己支持的加密算法。
服务器收到客户端的请求之后,向客户端做出回应,回应的内容包括:
确认TSL协议版本,如果浏览器不支持,那么关闭加密通信 一个随机数(后面生成会话密钥)
确认使用的加密算法以及服务器的数字证书。
客户端收到服务器做出的回应之后,首先会用浏览器或者操作系统的CA公钥验证数字证书的合法性,如果证书没问题,会从证书中取出公钥用来加密报文,并向服务器发送以下内容:
一个随机数,该随机数会用公钥加密;加密算法改变通知,表示以后使用会话密钥加密数据;客户端握手结束通知,这一项同时会把之前的内容做个摘要供服务器检验。
客户端和服务器有了这三个随机数,就会用之前协商的加密算法生成本次通信的会话密钥。
服务器收到客户端的随机数后,会根据之前协商的加密算法生成本次通信的会话密钥,并向客户端发送以下信息:
加密算法改变通知;服务器握手结束,并收集内容供客户端校验。
这样就完成了握手的过程,客户端和服务器进入加密通信,使用的是普通的HTTP协议,不过使用会话密钥加密内容。
Https一定安全可靠吗?
至少目前是没有任何漏洞的,即使使用一个假基站去转发客户端的全部信息,也需要伪造一个假的数字证书去完成和客户端的TSL握手,如果非要说的话,那么就是用户在浏览器弹出该网站的数字证书不可信并点击继续导致的通信过程被监听了。
既然有HTTP协议,为什么还要有RPC?
因为使用纯裸TCP协议进行网络通信存在一个问题,那就是“粘包”问题,而为了解决这个问题,需要在应用层定义消息格式从而确定消息边界,于是就有了各种协议,而HTTP和各类PRC协议就是建立在TCP之上的应用层协议。
RPC本质上不是一种协议,它叫做远程过程调用,是一种调用方式,而像gRPC这样的具体实现才叫做协议。RPC可以让我们像调用本地方法那样去调用远端的服务方法,因此能够屏蔽掉网络通信细节,同时RPC有很多种实现方式,并不一定基于TCP协议,它也可以基于UDP和HTTP协议。
从发展历史来讲,HTTP主要用于B/S架构,而RPC则更多用于C/S架构,但是现在也不分这么清了,B/S和C/S正在慢慢融合。现在很多软件支持多端,比如支持网页版的同时,也支持手机端和PC端,对外一般使用HTTP协议,而内部集群的服务调用一般使用RPC协议进行通讯。
RPC出现的更早,而且RPC比目前主流的HTTP/1.1性能要好,所以大部分公司内部还在使用RPC。
HTTP/2在HTTP/1.1基础上进行了优化,性能要比RPC要好,但是因为是近几年才出的,而RPC已经跑了很多年了,所以一般也没必要进行替换。
为什么RPC比HTTP/1.1性能好?
因为HTTP在发送请求的时候,消息头和消息体每次的内容都有相同的部分,所以比较冗余,而RPC的定制化程度高,可以使用一些手段去保存体积更小的数据,同时也不需要像HTTP协议那样考虑各种浏览器行为,比如302重定向跳转啥的,所以性能上更好。
RPC的实现原理
RPC称为远程过程调用,它可以让我们像调用本地方法那样取调用远端的服务方法,因此可以屏蔽掉网络通信的细节,它的实现主要有以下几个步骤:
建立连接
如果RPC是基于TCP协议实现的,那么需要先通过三次握手建立连接才能进行数据通信。
服务寻址
服务提供方需要暴露自己的IP,端口以及方法的地址,然后服务调用方依靠这些在调用方法的时候进行寻址。
网络传输
由于进行网络通信只能是二进制数据,所以进行传输的对象需要实现Serializable接口进行序列和反序列化。
服务调用
服务调用方调用方法并获取返回数据,接着对数据进行处理完成远程调用的过程。
既然有HTTP协议,为什么还要有WebSocket?
- TCP本身是全双工的,而HTTP/1.1这种基于TCP的协议是半双工的,对于扫码登录这种简单的场景还可以用,但是像网页游戏这种,需要服务端需要频繁推送数据给客户端的场景就不太友好,所以我们就需要用到支持全双工的WebSocket协议。
全双工:同一时间双方都可以主动发送数据。
半双工:同一时间只有一方可以主动发送数据。
- 正因为各个浏览器都支持HTTP协议,所以WebSocket协议会先利用HTTP协议加上一些特殊的header头进行握手升级操作,升级成功之后就和HTTP没有关系了,之后就用WebSocket的数据格式进行收发数据。
可以使用定时轮询和长轮询的方式完成扫码登录场景:
定时轮询:每隔一段时间客户端发送一次HTTP请求到服务器判断用户是否扫码。
长轮询:设置一个较长时间,发送一次HTTP请求,并在这个时间内等待服务器响应,如果用户扫码,那么直接返回,响应速度快。
这两种方式都是对用户无感的。
建立WebSocket连接的过程?
- 客户端先使用普通的HTTP请求发送服务端,不过需要加上一些特殊的请求头,包括Connection: Upgrade,Upgrade: WebSocket表示浏览器想要升级协议,并且是WebSocket协议,同时也会发送一段随机生成的base64码。
- 如果服务端刚好支持WebSocket协议,就会走WebSocket的握手流程,同时根据客户端发送的base64码,用某个公开的算法变成另一段字符串,放在HTTP的响应头里,同时携带上101状态码(101这里指的是协议切换)返给客户端。
- 之后,客户端用同样的公开算法将base64码转成一段字符串,并于服务端返回的进行比对,如果一致则握手成功,只有就可以使用WebSocket的数据格式进行收发数据了。
跨域请求是什么?有什么问题?怎么解决?
跨域指的是浏览器在发起请求时,会检查该请求对应的协议、域名以及端口号是否与当前网页一致,如果不一致则浏览器会进行限制。比如在百度的某个网页中,使用ajax访问京东是不行的。之所以浏览器会做这层限制,是为了保证用户的信息安全。
解决方案:
- response添加请求头,比如response.setHeader("Access-Control-Allow-Origin","*"),表示可以访问所有网站,不再受是否同源的限制。
- 使用代理服务器:通过在前端设置代理,将请求转发到同源的服务器,再由该服务器处理请求的资源。
怎么避免CSRF攻击?
CSRF(跨站请求伪造)攻击是一种常见的网络安全攻击方式,攻击者可以利用用户的登录凭证,在用户不知情的情况下执行恶意操作。常见的避免方式如下:
- 同源检测:对每一个请求进行同源检测,以防止跨域攻击。
- 添加Token:在请求参数或者请求头中添加token,并且每个token只能使用一次,攻击者无法仿造token,从而避免csrf攻击。
- 对请求方法进行限制:对post,delete,put类型的请求进行限制,只允许特定来源的请求使用这些类型。
计算机操作系统
线程和进程的区别?✅
- 进程是一个运行的程序,是系统进行资源调度和分配的一个独立单位,进程中包含了线程,每个线程执行不同的任务。
- 不同进程使用的内存空间是不同的,同一个进程下的线程共享内存空间。
- 线程更加轻量,上下文切换的成本比进程要低。
程序、进程、线程和协程的区别?
程序是静态的概念,它是由代码编译成的二进制文件,比如桌面上的微信,我们没有运行它,那么它就是一个程序。
进程是动态的概念,它需要占用系统资源,当微信运行起来,那么它就是一个进程,它具有独立的内存空间,是系统进行资源分配和调度的一个独立单位。
进程中包含了线程,每个线程执行不同的任务,同一进程下的线程共享内存空间,它是CPU调度的一个独立单位。
协程是更加轻量的线程,一个线程中可以由多个协程,协程之间是交替执行的,它是由程序员手动调度的,没有优先级之分。
不同的进程之间地址空间独立的,同一个锁对象如何在不同的地址空间传递?
将锁对象放入共享内存,然后进程在共享内存中获取锁对象。
操作系统根据什么判断CPU使用率?
- CPU时间片统计:操作系统会跟踪进程占用CPU的时间,如果占用时间越长,那么CPU使用率就会越高。
- 上下文切换:操作系统会通过监视上下文切换的数量来评估CPU使用率。
- 空闲时间:操作系统可以检测CPU的空闲时间来判断CPU使用率。
- 中断处理:进行中断处理会耗费CPU,所以操作系统可以根据中断处理时间来评估CPU使用率。
CPU飙高,有什么原因?
- 计算机运行大量的程序或者进程。
- 计算机上可能存在病毒在后台运行,并占用大量的CPU资源。
- 计算机运行了大型的任务或者程序,这里面需要大量的运算,比如视频剪辑和3D渲染。
- 某些发生故障的硬件或者驱动可能会导致CPU升高。
CPU飙高,怎么排查?
- 使用top命令,找到占用CPU高的进程的id。
- 通过这个pid,找到耗费CPU的线程,通过 top -H -p 进程pid 命令。
- 将找到的线程id转成16进制,通过 printf '0x%x\n' 线程pid命令。
- 通过jstack命令查看堆栈信息,通过 jstack 进程pid | grep 16进制线程pid -A 20 查看前20条记录,就能够定位问题所在的行号。
进程之间如何进行通信?
因为进程之间是互相独立的,在进程执行任务的时候,就需要用到进程通信,否则执行任务的效率将大打折扣。
1.管道
管道分为命名管道和匿名管道。它是一种半双工的通信方式,数据只能单向流动,管道里的通信数据遵循先进先出的原则;匿名管道只能用在父子进程之间的通信,而命令管道可以用在不相关的进程之间的通信。管道的通信效率低,不适合频繁的交换数据。
2.消息队列
消息队列是保存在内核中的消息链表,按照消息的类型进行消息传递,具备较高的可靠性和稳定性。消息队列的消息体有一个最大长度的限制,所以不适合比较大的数据的传输。
3.共享内存
一个进程在内存中开辟一块共享空间,这块空间可以被其他线程访问,它是最快的通信方式。但是当多个进程同时竞争一个共享资源时,就有可能发生数据错乱的问题。
4.信号量
它是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,与共享内存搭配起来使用。
5.信号
它是一种异步通信方式,用来通知接收进程的某个事件已经发生。比如使用kill命令,就是给进程发信号。
6.socket套接字
它不仅可以用于不同主机间的进程间通信,也可以用于本地主机上的进程间通信。
线程之间是如何进行通信的?
共享内存
将工作内存中的值传回主存,然后从主存中取出。
消息传递
常见的方式有wait和notify。
什么是孤儿进程和僵尸进程?
孤儿进程:父进程退出,它的子进程仍然在执行自己的任务,这些子进程称为孤儿进程。孤儿进程会被init进程收养,并会收集它们的状态信息。
僵尸进程:当一个进程fork出子进程,子进程退出后,而父进程没有调用wait或者waitpid获取子进程的状态信息,那么子进程的进程描述符仍然存在于系统中,这种进程称为僵尸进程。
孤儿进程因为有init进程善后,所以并不会对系统有什么危害;而僵尸进程虽然运行实体已经消失,但是它仍然在内核的进程表中存在一条记录,长期下去会造成系统资源的浪费。
怎么处理僵尸进程?
- 通过信号机制:子进程退出时,向父进程发送sigchild信号,父进程处理sigchild信号并调用wait或者waitpid方法,阻塞等待僵尸进程的出现,处理完僵尸进程后父进程继续运行。
- 杀死父进程:杀死父进程后,僵尸进程就会变为孤儿进程,由init进程来回收。
- 重启系统:关闭系统时所有的进程也会被停止,开启后init进程会重新加载其他的进程。
什么是零拷贝?
零拷贝指的是应用程序需要将一块内核区域中的数据转移到另外一块内核区域中时,不需要先复制到用户空间,再转移到目标内核区域了,而是直接转移。
产生死锁的四个必要条件?
互斥:当资源被一个线程占有了,那么其他线程就无法访问这个资源。
不剥夺:线程占有的资源没有被释放,其他线程不能剥夺该资源。
请求与保持:当线程请求其他资源时,已持有的资源不会被释放。
循环等待:多个线程循环等待对方手中持有的资源,形成一个闭环。
怎么避免死锁?
前三个条件是锁的必要条件,要避免死锁就需要破坏第4个条件:
- 线程加锁要按照顺序。
- 给锁设置一个超时时间,并且使用lock接口中的trylock方法非阻塞式的获取锁。
- 要进行死锁检查,争取在死锁发生的第一时间去查看原因,从而解决死锁。
可以使用jdk自带的工具进行排查。
先通过jps命令获取这个运行的进程的进程号,然后通过jstack获取该进程的堆栈信息,找到出问题的行号进行修改就可以了。
用户态和内核态
这两者是操作系统的两种不同运行级别。
不同点:
- 权限不同:内核态具有更高的权限,可以访问系统内的任何资源,而用户态只能访问部分系统资源。
- 运行环境不同:内核态运行在操作系统内核中,而用户态则运行在用户空间中。
- 应用场景不同:内核态主要用于系统管理和资源分配等任务,而用户态主要去执行应用程序。
用户态和内核态的切换通常由系统调用,中断或异常等事件触发。系统调用是应用程序请求内核服务,这样应用程序就可以访问内核提供的各种功能。中断和异常则是由硬件设备或者软件错误导致的,中断处理程序会在内核态中运行,从而去处理中断和异常。
操作系统的进程管理
操作系统可以对进程的创建,调度,执行以及终止进行统一管理,能够使进程并发执行,提高系统资源的利用率以及合理分配。
它还涉及到了进程之间的通信,进程状态的转换以及同步机制等方面的内容。
进程状态的转换
- 新建态:操作系统新建一个进程,需要给进程分配资源并进行一些初始化操作。
- 就绪态:进程准备就绪,等待CPU调度。当CPU空闲时,操作系统会在就绪态中选择一个进程并将其转成运行态。
- 运行态:CPU执行该进程,这个时候进程会占用CPU,同时执行一些指令和计算。
- 等待态:进程等待某个资源或者条件完成,这个时候进程没有占用CPU,当等待的资源释放或者条件满足时,就会转成就绪态。
- 终止态:进程已经执行完成或者发生了错误,这个时候操作系统会把它删除,并释放掉它占用的资源。
进程同步机制:
- 信号量:通过信号量可以控制进程对共享资源的访问。
- 互斥锁:可以保证同一时间只能有一个进程访问共享资源。
- 条件变量:可以协调多个进程之间的执行顺序。
- 管程:一种集中管理共享资源的机制。
设计模式
单例模式
单例模式分为饿汉单例和懒汉单例,饿汉单例在类加载的时候就初始化了对象,不存在线程安全问题,而懒汉单例在需要用到该对象的时候才去初始化对象,所以存在线程安全问题(两个线程同时判断对象为空,同时去初始化)。
饿汉单例:
public class Singleton{
private Singleton{}
private static fianl Singleton SINGLETON = new Singleton();
public static Singleton getInstance(){
return SINGLETON;
}
}懒汉单例:
public class Singleton{
private Singleton{}
private volatile static fianl Singleton SINGLETON; //保证指令的有序性,因为创建对象这一句,在编译成字节码的时候是三句,需要保证顺序性。
public static Singleton getInstance(){
if(SINGLETON == null){ // 这里使用到了双重检查锁
synchronized(SINGLETON){
if(SINGLETON == null){ // 这里需要再次判空是因为,如果另外一个线程拿到锁了,不判空会再次创建对象。
SINGLETON = new Singleton();
}
}
}
return SINGLETON;
}
}数据结构
红黑树
红黑树的本质是一种自平衡的二叉搜索树,在二叉搜索树的基础上引入了一些额外的规则:
二叉搜索树:对于树中的任意一个节点,其左子树的值小于当前节点的值,右子树的值大于当前节点的值,同时它的左子树和右子树也是二叉搜索树。
- 所有节点要么是黑色,要么是红色。
- 根节点是黑色,叶子节点也是黑色。
- 红色节点的子节点都是黑色。
- 从任意节点到其子树每个叶子节点的路径中都包含相同数量的黑色节点。
这些规则确保了红黑树的平衡性。
为什么不采用普通的二叉搜索树?
因为如果插入的元素依次递增的话,那么这棵树就会退化成链表,这样的话查询时间复杂度为O(n),而红黑树具有平衡性,最差情况的时间复杂度为O(logn)。
为什么不采用二叉平衡树?
二叉平衡树也是一种特殊的二叉搜索树,它要求任意节点的左子树和右子树的高度差的绝对值不超过1,同时左右子树也都是二叉平衡树。
因为二叉平衡树对于维护平衡的规则较为严格,插入和删除时需要更多的旋转操作来维持平衡,性能比较低。
普通二叉树和红黑树的区别?
普通二叉树只要求任意节点只能最多有两个子节点,没有平衡性要求,有可能退化成线性结构;而红黑树是一种自平衡的二叉搜索树,引入了一些规则使其能够维持平衡,最差的时间复杂度为O(logn),适用于一些需要高效插入、删除以及查找的场景。
常见的树的种类?
- 二叉树
- 二叉搜索树
- 平衡二叉树
- 红黑树
- b树:一种多路搜索树,常用于数据库和文件系统中,具有高效的插入、删除和查找操作。
- b+树:是一种加强版的b+树,相比于b树而言,只在叶子节点存放数据,非叶子节点存放索引值,叶子节点之间通过双向链表连接。
Java线程
创建线程的几种方式?✅
一共有4种方式:
第一种是继承Thread类,第二种是实现Runnable接口,第三种是实现Callable接口,第四种是使用线程池的方式创建线程。通常情况下,我们项目中通常会采用线程池的方式创建线程。
那用runnable和callable接口创建的线程有什么区别?
最主要的区别就是runnable接口的run方法是没有返回值的,而callable的call方法是有返回值的,这个返回值是一个泛型,需要结合FutureTask获取异步执行的结果。
第二个区别就是,runnable接口的run方法不能向上抛异常,可以自己捕捉进行处理,callable接口的call方法是允许抛异常的。
在实际开发当中,如果需要拿到执行的结果,那么可以使用实现callable接口的方式创建线程,并调用FutureTask.get() 获取这个返回值。
start和run方法有什么区别?
start方法是用来启动线程的,它可以调用run方法,执行run方法中定义的逻辑代码。
start方法只能被调用一次,而run方法中封装了线程需要执行的代码,相当于一个普通方法,可以被调用多次。
线程的生命周期?线程的几种状态?
线程经历一个完整的生命周期通常有以下几种状态:
新建:线程被新创建出来
就绪:线程调用start方法,该线程可以运行,但是需要等待分配CPU
运行:线程获得了CPU的使用权,执行程序代码
阻塞:线程因为某种原因放弃了CPU的使用权,暂时停止运行。直到线程进入就绪状态后,才可能再次进入运行状态。
阻塞分三种情况,分别是:
等待阻塞:正在运行的线程调用了wait方法,该线程就会进入等待状态,释放掉自己持有的锁,这个线程就会进入等待池中,进入等待池的线程是无法自动唤醒的,只有等其他线程调用notify或者notifyAll方法唤醒该线程,唤醒后的线程会进入锁池中并尝试获取之前释放掉的锁,如果成功获取到则进入就绪状态,获取不到则继续等待获取其他同步锁,wait方法是object类的方法。
同步阻塞:正在运行的线程想要获取同步锁,但是这个同步锁被其他线程占用了,该线程就会被放入锁池中,当持有同步锁的线程释放掉了锁,锁池中的线程就会去竞争这个同步锁,成功获取到的线程进入就绪状态,而其他线程继续等待。
其他阻塞:正在运行的线程中执行了sleep或者join方法,该线程就会进入阻塞状态,直到sleep超时或者是调用join方法的线程执行结束,该线程进入就绪状态。sleep方法是Thread类的静态方法。
死亡状态:线程执行结束或者说因为异常退出了run方法,这个线程的生命周期也就结束了。
关于锁池和等待池:
锁池:所有需要竞争同步锁的线程都会放在锁池中,其中获取到同步锁的线程会进入到就绪队列中等待分配CPU,而其他线程需要等待。
等待池:线程中调用了wait方法后该线程就会进入到等待池中,并释放掉自己持有的锁,进入等待池的线程是无法自动唤醒的,需要让其他线程调用notify或者notifyAll方法进行唤醒,唤醒后的线程会进入锁池中并尝试获取之前释放掉的锁,如果获取成功则进入就绪队列中等待分配CPU,获取失败则继续等待,与其他线程竞争同步锁。
sleep方法和wait方法的区别?
- sleep方法是Thread的静态方法,而wait是Object类的方法。
- wait方法的调用需要先获取wait对象的锁,并且执行完之后会释放掉这个锁给其他线程使用,而sleep方法不依赖锁,如果有锁也不会释放。
- wait方法不能自动唤醒,需要让其他线程调用notify或者notifyAll方法唤醒,而sleep方法到指定的时间就会醒来。
yield和join
yield方法是让当前线程放弃cpu的使用权,该线程进入就绪状态,但是可能又立刻获取到CPU使用权,进入运行状态。
join方法是让线程进入阻塞状态,比如a线程调用b线程的join方法,那么a线程就会进入阻塞状态,直到b线程执行结束,a线程才能继续执行,通过join方法可以限定线程的执行顺序。
如何停掉一个正在运行的线程?
- 使用一个标志,在主线程或者其他线程中改变这个标志的值从而停止该线程。
- 使用interrupt方法打断线程
- 如果打断的是阻塞线程,会抛出一个InterruptedException异常。
- 如果打断的是正常运行的线程,可以在这个线程外部打断该线程,然后在这个线程中获取isInterrupted的值,如果为true,则中断该线程。
对线程安全的理解?
线程安全指的是多个线程操作一个对象和单个线程操作这个对象返回的结果是一样的,这个就是线程安全。
线程安全的基本特征:
原子性,可见性,有序性。
谈谈你对ThreadLocal的理解
ThreadLocal可以理解为线程本地变量,它会在每个线程中都创建一个副本,线程只需要访问这个副本就可以了。
ThreadLocal的主要功能有两个:一个是实现线程间资源的隔离,避免资源争用引发的线程安全问题;第二个是实现了线程内的资源共享。
实现原理:
每个线程中都存在一个ThreadLocalMap类型的threadlocals(threadlocals引用了ThreadLocalMap),它里面存储的是本线程中所有的ThreadLocal对象的弱引用以及对应的变量副本。
当执行set方法时,是以ThreadLocal作为key,以对应的变量副本作为value放入当前线程的ThreadLocalMap集合中。
get方法和remove方法和set方法类似,都是以ThreadLocal作为key,然后去ThreadLocalMap集合中操作相关联的变量副本。
使用场景:
- 线程上下文传递:可以将用户信息保存在ThreadTocal中,从而在后续的请求链路中都可以方便的获取到用户信息。
- 数据库连接管理:在使用连接池的情况下,可以将数据库连接放在ThreadLocal里面,从而让每个线程独立管理自己的数据库连接,避免线程之间的竞争和冲突,比如Mybatis中的sqlSession对象就是存放在ThreadLocal中。
- 事务管理:在一些需要手动管理事务的场景中,可以使用ThreadLocal来存储事务的上下文,每个线程可以独立操作自己的事务,保证事务的隔离性。
ThreadLocal中的内存泄露问题?
emmm,我了解一点,因为ThreadLocalMap中的key是弱引用,而value是强引用,这意味着key会被GC回收掉,而对应的value却不会被回收,这样就导致了内存泄露,如果使用的比较多的话就会导致内存溢出。
解决方法的话:每次使用ThreadLocal对象时将它定义成private static,这样的话key就是强引用了,就不会被动地被GC回收了;当使用完之后,再手动的进行remove操作。
强引用:最明显的就是new出来一个对象,强引用的对象不会被GC回收,即使内存空间不足,抛出OOM的异常,JWM也不会回收这种对象。
弱引用:如果一个对象被弱引用,不管内存空间是否充足,JVM都会回收弱引用的对象。
乐观锁会产生什么问题?
乐观锁一般使用版本号或者时间戳的方式实现,它可以保证数据被修改时不会发生冲突。和悲观锁相比,乐观锁更适用于读多写少的场景,可以提高并发性能。
出现的问题:
- 冲突检测和重试,性能开销:乐观锁更新数据之前需要先进行冲突检测,以确保数据没有被其他线程修改,如果存在冲突,那么就会进行重试,这个过程会损耗一定的性能,而且高并发场景下可能会导致大量的重试,从而影响系统的吞吐量。
- 更新的丢失:如果两个线程同时拿到数据进行修改,并尝试去更新,那么最终只能有一个线程修改成功,这样就会导致部分更新的丢失,可能造成数据的不一致。
介绍一下CAS机制
CAS的全称是Compare And Swap,是比较再交换的意思,体现的是一种乐观锁的思想,可以在无锁状态下保证线程操作共享变量的原子性。
它的工作流程是这样的:
1.当一个线程想要操作共享变量的时候,先去主内存中获取该共享变量到工作内存中,当对这个共享变量修改完之后,先去查看主内存中该共享变量的值与之前取的值是否一致,如果一致则将主内存中的共享变量进行修改;如果不一致则会通过自旋的方式尝试重新修改直到修改成功。
- 自旋的流程是这样的:该线程重新读取主内存中的共享变量到工作内存中并进行修改,然后去和主内存中的变量值作比较,如果一致则修改,不一致则重新读取。
使用自旋的方式的好处是 不用加锁,所以线程不会阻塞,效率更高。
但是也有缺点,就是 如果线程竞争比较激烈,每次替换操作都不能成功,那么效率也不会高。
CAS的缺点?
- aba问题
- 长时间自旋导致给CPU带来巨大的开销
- 只能保证一个共享变量的原子操作,不能保证多个变量的原子操作,解决办法是使用锁synchronized实现。
说一下CAS中的aba问题?
ABA问题就是指的是当线程1从共享内存中读到了共享变量A,这个时候线程2短时间内通过CAS操作将变量A修改为B,然后再修改成A,因为这个过程线程1是无法感知到的,所以线程1继续进行CAS操作仍然可以成功,但是实际上这个过程中变量被修改过了,所以会出现并发问题。
通常这种情况一般发生在数据库中,解决的话,通常采用版本号法,也就是版本号+1的方式进行解决。
java里面哪里用到版本号机制
- 数据库乐观锁:通常给数据表加一个版本号字段,并在更新时判断版本号是否一致。
- 集合类:如ConcurrentHashMap中的版本号就可以解决并发中的数据一致性问题。
- synchronized 关键字或 Lock 接口也可以使用版本号实现对共享资源的并发访问控制。
请谈谈你对volatile关键字的理解?
- 保证可见性:当一个线程修改了volatile修饰的变量的值,那么新值对其他线程是立即可见的,因为JMM会把这个新值从工作内存中强制刷新回主存。
- 禁止指令重排序:重排序是编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,用volatile修饰的共享变量会在读写共享变量时分别加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止指令重排序的作用。
JMM:JAVA MEMORY MODEL(JAVA内存模型的缩写)。
volatile并不能保证对一个变量的操作是原子性的,比如一个变量使用volatile修饰,然后在多线程下进行自增操作,因为自增操作需要读取主内存中的值,修改值,写入主内存这三个步骤,所以可能会发生写入被覆盖的情况,得不到预期值,所以volatile并不能保证原子性。
java中锁的原理
java中的锁是用于实现并发控制的工具,可以分为悲观锁和乐观锁。
悲观锁:
认为在操作过程中会发生冲突,因此在访问共享资源时,需要先获取锁并阻塞其他线程。
java中悲观锁常见实现就是synchronized关键字,它可以使用在方法和代码块中。当线程进入synchronized关键字修饰的方法和代码块中,会先尝试获取当前对象的锁,如果获取成功则继续执行,获取不到则阻塞,直到获取到锁才继续执行。
悲观锁的状态主要有无锁,偏向锁,轻量级锁,重量级锁。这些情况是根据线程的竞争情况动态调整的。
乐观锁:
认为在操作过程中不会发生冲突,因此允许多个线程同时访问共享资源,但是在更新资源时需要判断资源是否已经被其他线程修改。
java中乐观锁的常见实现就是版本号法和时间戳法,当访问共享资源时,会记录当前版本号或者时间戳,执行完操作后准备去更新资源时需要先去判断当前资源的版本号或时间戳是否与之前记录的一致,如果一致则进行修改,不一致则放弃修改或者重试。
synchronized的实现原理?
synchronized修饰代码块:synchronized在编译之后,会在代码块的前后生成两个monitorenter和monitorexit字节码指令,在执行monitorenter指令时,会尝试获取对象锁,如果这个对象没被锁定或者当前线程已经持有对象锁了,那么锁的计数器+1,相应的,在执行monitorexit指令时,锁的计数器-1,如果计数器的值为0,那么锁就会被释放。如果获取对象锁失败,那么当前线程就会阻塞,直到另一个线程释放锁为止。
synchronized修饰方法:使用ACC_synchronized标识符来实现同步,标识当前方法是一个同步方法。
synchronized锁住的是什么?
实例对象结构里有一个对象头,对象头里面有一块结构为mark word,mark word指针指向了monitor。synchronized实际上是锁住了monitor这个监视器。
synchronized的锁升级的情况?
java中的synchronized主要有三种形式,分别是偏向锁,轻量级锁以及重量级锁,分别对应的是锁只能被一个线程持有,不用线程交替持有锁和多线程竞争锁三种情况。
- 重量级锁:底层使用的是Monitor实现的,里面涉及到了用户态和内核态的切换,线程上下文的切换,成本比较高,性能比较低。
- 轻量级锁:线程加锁的时间是错开的。它修改了对象头的锁标志,相比于重量级锁性能提升了很多。每次加锁都是CAS操作从而保证原子性。
- 偏向锁:在一段很长的时间内只有一个线程持有锁,就可以使用偏向锁。在第一次获得锁时,会有一个CAS操作,当该线程再次尝试获取锁时,只需要去锁对象的对象头中判断是不是自己的线程id即可,而不是去执行开销较大的CAS操作。
一旦锁发生了竞争,都会升级为重量级锁。
可以分为四种状态:无锁,偏向锁,轻量级锁和重量级锁。
无锁:也就是没有加锁,允许多个线程同时访问共享资源。
偏向锁:当一个线程访问共享资源时,就会升级为偏向锁。
轻量级锁:当两个或者以上的线程交替获取锁但没有在对象上并发的获取锁,就会升级为轻量级锁,每次尝试获取锁都是通过CAS的自旋方式。
重量级锁:当两个或以上的线程并发的在一个对象上获取锁时,就会升级成重量级锁。
ReentrantLock的底层实现原理?
ReentrantLock是一种可重入的锁,调用lock方法获取锁,当再次调用lock方法的时候线程不会阻塞,调用unlock方法释放锁。
底层使用的是CAS + AQS 实现的。
ReentrantLock支持公平锁和非公平锁,它的构造方法可以传一个参数,如果为true,则表示公平锁,false为非公平锁,不传参也是非公平锁。公平锁的效率没有非公平锁的效率高。
synchronized 是非公平锁。
ReentrantLock 和 synchronized 都是悲观锁。
当调用lock方法加锁的时候:
如果是公平锁,那么会先去检查AQS队列中是否有线程在排队,如果有排队的线程,那么当前线程也会进行排队。
如果是非公平锁,那么先不去检查AQS队列中是否有线程在排队,而是直接去竞争锁,如果竞争不到则到AQS队列中排队。
什么是AQS?
- AQS是一种悲观锁,像ReentrantLock就是使用的AQS实现的。
- AQS内部维护了一个先进先出的队列,队列中存放的是等待获取锁的线程。
- AQS中有一个属性state,默认是0,无锁状态,当一个线程获取锁之后,state变为1,这时其他线程过来想要获取锁就会失败,放到队列中进行等待。
- 假如有多个线程同时竞争锁,AQS使用了cas操作来保证原子性。
synchronized和lock的区别是什么?
- 语法层面
synchronized 是一个关键字,是由c++实现的,当退出同步代码块的时候锁会自动释放。
lock是一个接口,是由java语言编写的,需要手动释放锁,所以说有可能导致死锁。
功能层面
synchronized 和 lock都是悲观锁,都具备基本的互斥,同步,锁重入功能。
但是lock有更多丰富的功能,比如说可打断,可超时,公平锁等。而且它还有实现类,比如ReentrantLock。
在没有竞争的时候,synchronized 做了很多优化,比如偏向锁,轻量级锁,重量级锁,性能不赖。在竞争激烈的时候,lock往往会提供更好的性能。
为什么使用线程池?(使用线程池的好处)
- 复用线程,减少线程创建和销毁带来的性能消耗。
可以将线程和任务进行解耦,避免了Thead类中线程和任务绑定到一块。
- 响应速度快,任务来了可以直接使用线程,而不需要先创建线程,再执行任务。
- 线程是稀缺资源,如果无限制的创建会耗费大量的系统资源,使用线程池可以对线程进行统一管理。
什么时候会使用线程池?
- 提高并发任务数量。
- 统一管理线程资源。
创建线程池的几种基本方式(线程池的种类、常用的线程池)
在jdk中默认提供了使用 Executors 的方式去创建线程池,常见的线程池有:
- newCachedThreadPool:创建一个可缓存的线程池,如果线程数超过处理需要,则会缓存一段时间进行回收,如果线程数不够则会新建线程。
- newFixedThreadPool:创建一个定长的线程池,可以控制线程的最大并发数量,超出的任务会在队列中等待。
- newScheduledThreadPool:创建一个定长的线程池,可以执行定时或者是周期性任务。
- newSingleThreadExecutor:创建一个只有一个线程的线程池,可以保证过来的任务按顺序执行。
但是我们一般不建议使用Executors的方式创建线程池。
为什么不建议使用 Executors的方式创建线程池?
这个问题其实在阿里巴巴的java开发手册中提到过:
- 因为 Executors中的CachedThreadPool允许创建的线程数量为Integer.MAX_VALUE,所以说可能创建大量的线程导致出现OOM异常。
- FixedThreadPool默认的工作队列的长度为Integer.MAX_VALUE,可能导致大量的任务堆积,出现OOM异常。
我们一般使用ThreadPoolExecutor的方式创建线程池,它可以自定义参数,避免出现资源耗尽的情况。
线程池的核心参数、拒绝策略、线程队列。
7个核心参数
在线程池中一共有7个核心参数:
corePoolSize 核心线程数目 :池中会保留的最多线程数。
maximumPoolSize 最大线程数目 :核心线程+救急线程的最大数目。
keepAliveTime 最长生存时间:在这个时间范围内如果没有新任务来,那么救急线程就会被回收。
unit 救急线程的生存时间单位。
workQueue 工作队列:当核心线程都不空闲时,新来的任务会放入工作队列中,工作队列满则创建救急线程执行任务。
threadFactory 线程工厂:可以对线程进行定制,比如给线程起名字,设置是否是守护线程等。
handler 拒绝策略:当所有的线程都在忙,而且工作队列也放满了,则会触发拒绝策略。
4种拒绝策略
第一种是抛异常,第二种是由调用者自己处理,第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
线程池中常见的阻塞队列
- ArrayBlockingQueue:是一种基于数组结构的有界阻塞队列,根据先进先出的原则对任务进行排序。
- LinkedBlockingQueue:是一种基于单向链表的有界阻塞队列,也是遵循先进先出的原则,默认是无界的,队列长度为Integer.MAX_VALUE,一般我们都会指定一个容量。
LinkedBlockingQueue会分别在队头和队尾加两把锁,入队和出队可以同时进行,两个操作互不影响,而ArrayBlockingQueue只有一把锁,出队和入队都靠这一把锁。所以LinkedBlockingQueue的性能是要比ArrayBlockingQueue高的。
DelayedWorkQueue:是一种具有优先级的阻塞队列,它可以保证出队的任务都是当前队列中执行时间最靠前的。
SynchronousQueue(sen ke rui nai z ):是一种不存储元素的阻塞队列,每个插入操作都必须有另外一个线程移出队列中的任务。
线程池的执行流程(执行原理)
当一个任务过来的时候,判断是否有核心线程空闲,如果有空闲的那么任务交给空闲的核心线程处理,如果没有空闲核心线程,并且核心线程数没达到要求,那么就会创建新的核心线程处理任务。
如果核心线程达到最大数量且都在忙,则判断工作队列是否满了,如果没有满,则将这个任务放到工作队列中,当核心线程执行完任务后就会去执行工作队列中的任务。
如果工作队列满了,则判断线程池中的最大线程数量是否达到,如果没有达到则创建救急线程去执行任务,同时这个救急线程配置了最大生存时间keepAliveTime,如果超过这个时间还没有任务过来交给它执行时,这个线程就会被线程池回收。
如果工作队列满了,且线程池中的最大线程数达到了,就会触发拒绝策略,根据拒绝策略去执行任务。
线程池中的线程是如何做到保活和回收的呢?
核心线程是保活,临时线程会回收。
当我们处理完一个任务后,会调用workQueue.poll()或者take()方法阻塞式地获取阻塞队列中的任务,这样就能保证核心线程的保活了。
关于回收的话,当线程处理完任务之后,会判断当前线程池中的线程数是否大于最大核心线程数,如果大于,则调用workQueue.poll()方法指定一个超时时间去阻塞式地获取阻塞队列中的任务,如果超过这个超时时间并且没有任务需要处理时,那么这个线程就会被回收。
怎么实现一个线程安全的无锁计数器?
- 使用java中的原子类AtomicInteger,调用incrementAndGet、decrementAndGet和get方法来实现计数器的功能,这些操作都是具有原子性的,不需要显式地加锁。
这种方式在并发场景下,线程竞争激烈的时候性能比较低,因为每个线程都有自己的工作内存,AtomicInteger操作完共享变量之后需要进行CAS操作将共享变量刷回共享内存,这样耗费性能,而且CAS操作也不一定成功。
- 使用java8新引入的LongAdder实现计数器的功能。
这种方式的好处就是引入了分段锁的机制,将一个计数器分成了多个小计数器,每个小计数器独立维护自己的值,当调用sum方法时,会将每个小计数器中的值进行累加得到最终结果,这种方式对性能开销较小,从而在高并发场景下具有更好的性能。
synchronized和volatile的区别?
- synchronized可以修饰方法和代码块,同时也能修饰静态方法和静态代码块,通过获取对应的锁来实现互斥访问,即同一时间只能有一个线程进入被synchronized修饰的方法和代码块;而volatile修饰变量,当一个线程修改了使用volatile修饰的变量,那么其他线程也能立即看到修改后的值。
- 被synchronized修饰的方法由于只能有一个线程去访问,所以能够保证复合操作的原子性,而volatile不能保证。
- volatile的开销要比synchronized小,因为synchronized需要进行加锁,解锁等操作,而volatile只需要进行内存屏障等操作。
CountDownLatch
允许一个或者多个线程等待其他线程执行完任务自己再继续执行。
构造方法中指定计数器的值,只能使用一次,使用完就不能再使用了。
await:阻塞当前线程,等待其他线程执行。
countDown:计数器减一,当计数器值为0时阻塞的线程再执行。
应用场景:我们想同时启动多个线程,实现最大程度的并行数,可以将CountDownLatch的计数器的值置为1,然后多个线程调用CountDownLatch.await方法,这样主线程只需要调用一次countDown方法就可以让多个线程同时执行。
Semaphore--控制访问特定资源的线程数量
Spring
Spring是什么?
- spring是一种封装javabean的轻量级框架,同时是一个容器,同时是一个生态,没有spring,springmvc、springboot这些上层框架也将不复存在。spring可以将redis,mybatis这些组件整合起来组成复杂的应用。
- spring是为了简化企业开发而生的,使得开发更加的简洁和优雅。
- 在spring中有两个重要的概念:IOC(控制反转)和AOP(面向切面)。
请谈谈你对SpringMVC的理解?✅
先说一下什么是mvc,mvc是一种设计模式,包含m-model 模型,v-view 视图,c-controller 控制器,而SpringMVC是mvc的一种开源框架,是spring推出后的一个后续产品,它提供了web应用的MVC模块,可以把SpringMVC看做是Spring的一个子模块。
IOC和AOP✅
IOC : 即控制反转,把对象的创建、初始化、销毁交给Spring容器管理,而不是由开发者控制,这样给程序员减少了一定的负担。IOC的解耦:当我们的service层中需要一个新对象时,只需要使用注解注入即可,而不需要硬编码先创建对象再放入构造函数中。
AOP : 即面向切面编程,在不改变原来的设计的基础上对其进行功能增强,符合spring的无侵入式理念,当我们写一个项目的时候有很多方法,它们除了实现核心功能之外,还需要进行一些额外的操作,比如说日志处理,事务处理以及权限控制等。我们可以将这些可复用的代码抽取出来组成一个切面,然后注入到目标对象中,实现代码的复用和解耦,并有利于未来的可扩展性和可维护性。
SpringAOP是基于动态代理的,如果一个类实现了接口,那么使用jdk进行动态代理,否则使用cglib进行动态代理。
SpringBoot默认使用cglib进行动态代理,如果无法使用cglib(目标类使用final修饰),则考虑使用 jdk进行动态代理。
什么是代理?
代理是一种设计模式,就是我们想要调用一个类的时候不直接去调用目标类,而是在中间加上一层代理类,我们调用代理类,通过代理类去调用目标类,简单来说就是由直接调用变为间接调用。
使用代理模式最大的好处就是可以在代理类调用目标类之前或者之后进行一些额外的操作,比如说可以记录日志和权限校验。在实现方式上,代理模式分为了静态代理和动态代理。
静态代理和动态代理的区别
静态代理在编译时就确定好了被代理的类,并且只能代理这一个类,如果有多个类需要代理的话,那么工作量就会很大。
动态代理是在程序运行时动态地生成代理对象,可以代理任意类型的对象,它是利用的反射机制实现的,能够更加灵活的对代理对象进行增强操作。
JDK动态代理和Cglib的对比
- jdk动态代理要求被代理对象实现接口,而cglib是继承了被代理对象。
- jdk动态代理和cglib动态代理都是在运行期生成字节码文件。
- jdk动态代理调用代理方法,是通过反射机制实现的,而cglib是通过fastclass机制实现的。
- 如果被代理对象是用final修饰的终结类,那么无法用cglib进行动态代理。
Spring中bean的生命周期?
- 先通过BeanDefintion类得到bean的所有信息,这里面包含了bean的属性、方法以及是否是单例等。如果有多个构造函数的话,就判断使用哪个构造函数并进行实例化得到一个bean对象。
- 填充bean里面的属性(循环依赖,三级缓存)。
- 回调Aware接口,如果一个bean实现了Aware接口需要重写里面的方法然后执行。
- 执行BeanPostProcesser初始化之前的方法。
- 调用init方法进行初始化操作。
- 执行BeanPostProcesser初始化之后的方法,如果一个类中使用了aop,则会进行动态代理对象的创建。
- 判断当前bean对象是否是单例的,如果是单例的则放到单例池中。
- 使用bean。
- 当spring容器关闭时则调用destory方法销毁这个bean。
Bean的生命周期-十步骤版本 1.实例化 2.依赖注入 3.BeanNameAwareBeanFactoryAare方法执行啦! 4.初始化前BeanPostProcessorbefore方法 5.InitialingBean接口的方法执行啦! 6.初始化 7.初始化前BeanPostProcessorafter方法 8.使用Bean 9.DisposableBean接口的方法执行啦! 10.销毁Bean
Aware接口的作用
当一个bean实现了Aware接口之一之后,就可以获取到Spring容器的某些资源或者信息的访问权,它允许bean与spring底层服务进行交互,而无需将框架代码硬编码到bean中,从而达到解耦的目的,提高了可维护性。
常见的Aware接口:
BeanNameAware:获取到当前bean在容器中注册的名字。
BeanFactoryAware:可以访问bean工厂,从而访问其他bean或者spring容器的配置信息。
ApplicationContextAware:可以获取spring应用上下文,从而可以访问容器中的各种资源,比如其他bean,环境变量等。
Spring中bean的作用域
- singleton:单例,也是spring中的默认作用域,意味着spring容器中针对一个beanName只存在一个实例。
- prototype:多例的,每次去spring容器中获取bean都返回一个新的实例对象。
针对spring中的web应用引入三种不同的作用域:
- request:每次http请求都会创建一个新的bean实例。
- session:同一个session共享一个bean实例,不同session使用不同的bean实例。
- globalSession:全局session共享一个bean实例。
Spring中的单例bean是线程安全的吗?
不是线程安全的,spring中并没有对单例bean做任何多线程的封装处理。对于无状态的bean,比如说dao,service,controller层的bean,虽然说它本身不是线程安全的,但是只是调用了其中的方法,不会造成线程安全问题,而对于有状态的bean,则需要开发者自己保证线程安全,最简单的做法就是改变bean的作用域,将singleton改为prototype,这样的话每次创建的都是一个新对象,能够保证线程安全。
无状态:不保存数据
有状态:保存数据
Spring如何开启事务?
- 在spring的配置文件中配置事务管理器,用于配置事务的开始,提交和回滚操作。
- 在spring的配置类上加上**
@EnableTransactionManagement**注解,从而开启spring的事务管理功能,这样spring就能扫描到带有@Transactional注解的类和方法,并给其提供事务支持。 - 在需要开启事务的类或者方法上加上
@Transactional注解。
Spring的事务是如何实现的?
spring的事务底层是基于数据库事务和AOP实现的。
- 在需要开启事务的类或者方法上加上
@Transactional注解,spring就会针对这个bean创建一个代理对象。 - 针对这个代理对象,使用事务管理器创建一个数据库连接。
- 将数据库连接的autocommit属性设置为false,禁止自动提交,这是spring事务非常重要的一步。
- 执行当前方法,方法中会执行sql语句。
- 如果当前方法没有出现异常则提交事务,否则回滚事务。
Spring中的事务传播机制?
多个事务方法相互调用时,这里就出现了事务的传播。
spring一共提供了7种事务传播行为:
- REQUIRED:这是默认的事务传播行为,如果当前有事务,则加入到该事务中;如果没有事务,则创建一个事务,确保方法只在一个事务中。
- SUPPORTS:支持方法以事务的方式运行,如果当前有事务,则加入到该事务中,如果没有事务,则以非事务的方式运行。
- MANDATORY(慢特陶瑞):方法必须在一个已有的事务中运行,否则抛出异常。
- REQUIRES_NEW:无论当前是否存在事务,都会创建一个新事务,如果当前存在事务,则挂起该事务。
- NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,则挂起该事务。
- NEVER:以非事务的方式运行,如果存在事务,则抛异常。
- NESTED:如果当前存在事务,则在嵌套事务中运行,如果当前没有事务,则新创建一个事务,这种方式适用于需要局部回滚的场景。
Spring中事务失效的场景有哪些?
同一个类中方法的调用:这里是当前对象而不是动态代理对象调用方法,所以事务会失效。解决办法是获取当前对象的代理对象,用这个代理对象去调用开启事务的方法。
开启事务的方法使用private或者final修饰,原因是使用cglib动态代理子类是动态代理对象重写不了父类的方法,所以事务会失效。
方法本身自己捕捉了异常,这样即使出现异常也不会触发事务的回滚,所以说要进行抛出,而不是自己捕捉。
造成事务回滚的异常一般是Error类异常或者是运行时异常,如果抛出的是检查型异常的话,那么不会造成事务回滚,所以我们可以将rollbackfor配置为Exception。
开启事务的bean没有交给spring容器管理。
数据库不支持事务。
Spring AOP 失效的场景有哪些?
- 当前类没有被spring容器管理
spring aop是在bean创建的初始化后阶段进行的,如果当前bean没有交给spring容器管理,那么spring aop也会失效。
- 同一个类中方法的调用
这样调用是当前对象进行调用,而不是动态代理对象进行调用,所以spring aop不会生效。
- 内部类方法的调用
该方式会直接调用内部类实例对象的方法,同时没有使用代理对象,所以aop会失效。
- 调用private 和 final 修饰的方法:这些方法是无法被重写的,所以无法使用代理对象。
- static修饰的方法:该方法是类对象所有,而不属于实例对象,所以无法使用代理对象。
什么是bean的自动装配?有哪些装配方式?
bean的自动装配指的是在mapper.xml文件中定义bean的时候指定autowired属性,或者说在定义类的时候在对应的属性上加上autowired注解。
自动装配可以作用到setter方法,构造器以及字段上。
装配方式有:
- 手动装配:在xml文件中通过ref属性指定要装配的bean。
- byType:按照类型装配,autowired就是按照类型装配。
- byName:按照名称装配,resource是按照名称装配。
- 构造器装配:使用构造器装配可以将构造方法中的参数中对应的类进行自动装配,如果找不到对应的类则会报异常。
Spring为什么不推荐字段注入?
- 使用字段注入的方式将类之间的依赖关系暴露出来,破坏了封装性,同时使得类的耦合度增加。
- 字段注入是通过反射类注入的,实现是在对象初始化完成之后,不符合类对象的创建过程,同时性能比较低。
- 推荐使用构造器注入,搭配使用lombok,这样对应的依赖是在类对象创建的时候进行初始化的,同时也避免了使用反射类,提高了性能。
@Autowired和@Resource到底有什么区别
来源不同
它们两个来源于不同的父类。@Autowired是Spring定义的注解,而@Resource是Java定义的注解
依赖查找的顺序不同
@Autowired是先根据类型查找,如果存在多个相同类型的bean再根据名称进行查找;@Resource是先根据名称去查找,如果查不到,再根据类型进行查找。
支持的参数不同
@Autowired只支持一个required参数,而@Resource可以支持很多参数,比如name和type参数等。
- 依赖注入的用法支持不同
@Autowired支持属性注入、构造方法注入和setter注入,而@Resource不支持构造方法注入,支持其他两种注入方式。
- 当我们要注入对象是mapper对象时,适用@Autowired注解编译器会报警告,但是程序可以正常执行,而使用@Resource则不会报警告。
SpringBoot自动配置的原理
- 在springboot的启动类上有一个@SpringBootApplication注解,它是一个复合注解,对@SpringBootConfiguraion、@CommponentScan和@EnableAutoConfiguraion注解进行了封装。
- 其中@SpringBootConfiguraion和@Configuraion注解一样,表明启动类是一个配置类。
- @CommponentScan是定义类的包扫描路径。
- 而@EnableAutoConfiguraion是SpringBoot实现自动装配的核心注解,它内部使用了一个@Import注解导入了对应的配置选择器,该配置选择器会加载当前项目路径下的META-INF目录下的spring.factories文件,然后找出所有的自动配置类,并根据@Conditional过滤掉不需要的自动配置类从而实现自动配置。
如果想让SpringBoot对自定义的jar包进行自动配置的话,需要怎么做?
- 创建一个自定义的jar包。
- 在jar包中提供一个自动配置类,这个类里面需要用到 @Configuration 和 @ConditionalOnClass 注解,以便根据条件来自动装配bean。
- 在resources目录下创建一个META-INF/spring.factories文件,并将自动配置类配置到文件中。
- 将jar包引入到springboot项目中。
- 启动springboot项目。
对于springboot中starter的理解?
在springboot中,starter是一种特殊的依赖,它提供了一种简化配置以及快速启动应用的方式。
一个starter包中通常包含以下组件:
自动装配类
它用来根据应用程序的类路径和配置信息,自动配置和初始化Spring应用程序所需的各种组件和功能。
依赖管理
starter中会定义一组依赖项,这些依赖项是相关组件所需要的类和框架,springboot会自动管理这些依赖项的版本和冲突问题,确保它们能够正确地集成和协同工作。
条件化装配
starter中的自动配置类通常采用的是条件化装配的方式,会根据应用程序的环境以及配置信息, 选择性地装配和初始化某些组件。
**总结 **:使用Spring Boot的starter可以极大地简化应用程序的配置和启动过程 开发人员只需要引入适当的starter依赖,Spring Boot会自动完成大部分的配置和初始化工作,使得开发人员能够更专注于业务逻辑的实现。
Spring容器的启动流程?
Spring容器启动时会先进行扫描得到所有的BeanDefinition对象,并存放在一个Map中。
然后筛选出非懒加载的单例BeanDefinition进行创建bean,对于多例bean不需要在启动时创建,而是在每次尝试获取bean时使用BeanDefinition创建。
利用BeanDefinition创建这个过程涉及到了bean的创建生命周期,包括了合并BeanDefinition,推断构造函数,实例化,属性填充,初始化前,初始化,初始化后等步骤,其中AOP就是发生在初始化后这一步骤。
单例bean创建完成之后,Spring容器会发布一个容器启动事件。
Spring容器启动结束。
SpringBoot是如何启动Tomcat的?
- 首先,SpringBoot启动时会先创建一个Spring容器。
- Spring容器创建过程中,会根据@ConditionOnClass注解判断当前classpath路径下是否有Tomcat的依赖,如果有则创建一个用来启动Tomcat容器的bean。
- Spring容器创建成功后,就会获取启动Tomcat容器的bean,并创建Tomcat对象,同时完成绑定端口等操作,然后启动Tomcat。
RESTful API
RESTful API通过URL定位资源,使用HTTP动词(GET、POST、PUT、DELETE等)操作资源,并使用标准HTTP状态码表示请求结果。它具有以下特点:
- 基于HTTP协议:RESTful API使用HTTP协议通信,可以利用HTTP提供的各种功能,如缓存、认证、安全等。
- 资源定位:RESTful API将每个资源抽象为一个URI,客户端通过访问URI来操作资源。
- 统一接口:RESTful API使用统一的接口规范,包括HTTP动词、URI命名规范和标准HTTP状态码。
- 无状态性:RESTful API是无状态的,每个请求都是独立的,服务器不会记录客户端的状态。
- 可缓存性:RESTful API利用HTTP协议的缓存机制,允许客户端缓存资源,提高性能和可伸缩性。
- 分层系统:RESTful API支持分层系统结构,客户端可以通过中间层代理访问资源。
RESTful API的优点包括简单、灵活、易于扩展和维护,适用于各种平台和编程语言。它也具有一些限制和挑战,如URI设计、HTTP动词选择、资源状态管理等方面需要特别注意。
SpringMVC处理一个请求的过程?(SpringMVC的工作流程)
- 客户端发送http请求到服务器,服务器接收到请求之后会交给DispatcherServlet处理。
- DispatcherServlet会根据请求的path找到对应的handler。
- handler其实就是一个加了@RequestMapping注解的方法,然后通过反射执行该方法。
- 在执行方法之前会先去解析方法参数,同时在请求中获取对应的数据传给对应的参数。
- 执行方法中定义的逻辑。
- 执行完方法之后会得到返回值,SpringMVC会对返回值进行解析。
- 如果方法上加了@ResponseBody注解,那么就将返回值返回给客户端,这个过程中可能需要把java对象转成json字符串。
- 如果方法上没有加@ResponseBody注解,那么就会进行视图解析,然后把解析之后的html数据返回给客户端。
过滤器在这个流程中是什么时候执行的? 🌟
过滤器是在请求达到DispatcherServlet之前,并且在DispatcherServlet处理完之后按照相反的过滤器链执行。
如果有多个过滤器,那么执行顺序是什么样的?
执行顺序是按照在web.xml文件中配置的顺序执行的;如果是使用注解的方式,可以搭配@Order注解,值小的过滤器先执行。
过滤器和拦截器的区别?🌟
运行顺序不同
过滤器是servlet容器接收到请求之后,但是在servlet被调用之前运行的;而拦截器则是在servelt调用之后,但是在响应被发送到客户端之前运行的。
配置方式不同
过滤器在web.xml中或者注解的方式进行配置,而拦截器是在spring中的配置文件中进行配置或者使用注解的方式进行配置。
- 过滤器依赖于servlet容器,而拦截器不依赖于servlet容器。
- 过滤器只能对request和response进行操作,而拦截器可以对request、response、handler、modelAndView、exception进行操作,相当于多了对springMVC生态下的组件的一个操作能力。
拦截器和AOP的区别?
作用范围不同:拦截器是针对Http请求进行拦截处理,而AOP是针对对象方法。
实现方式不同:拦截器是基于java反射机制实现的,通过实现HandlerInterceptor接口实现具体的拦截逻辑。而AOP是基于动态代理机制实现的,通过织入切面来实现功能增强。
目的不同:拦截器主要是为了实现对请求的过滤,验证以及转发等功能,对业务流程的控制有一定的作用;而AOP主要是为了实现对方法的功能增强,比如记录日志,事务管理等。
Spring中的循环依赖?
解释:两个或者以上的bean互相持有对方形成闭环,也就是A依赖B,B依赖A。
spring是如何解决的?
使用三级缓存解决:
一级缓存:单例池,用来存放已经初始化完成的对象。
二级缓存:半成品池,用来缓存实例化好,但是未初始化完成的对象。
三级缓存:对象工厂,是用来创建某个对象的。
具体解决流程:
比如现在有两个对象A和B,它俩存在循环依赖。
那么流程是这样的:
- 先实例化A对象,同时会创建factoryA存入对象工厂。
- 因为A对象初始化需要B对象,所以先去实例化B对象,同时创建factoryB放入对象工厂。
- B对象需要注入A,会通过对象工厂中的factoryA创建一个A的代理对象放入半成品池中。
- B从半成品池中获取A的代理对象后正常注入,B对象初始化完成,放入单例池中,同时对象工厂中的factoryB销毁。
- 再回到A对象的初始化,因为B对象初始化完成,所以直接从单例池中拿到B对象进行注入,A对象初始化完成,半成品池中的临时A对象会被清除,对象工厂中的factoryA也会清除。
Spring和SpringBoot的区别?
- 传统的spring更加的灵活,可以适用于多种复杂项目的需求,但是想要使用一个组件的话,需要将对应框架需要的依赖全部导入,并且需要开发者自己管理依赖的版本;而springboot提供了大量的starter组件,使用哪个组件,只需要把对应的starter组件导入即可,对应的依赖就会导入,不需要自己管理依赖版本,开发者可以更专注于业务代码的编写。
- springboot内部部署了tomcat等应用服务器,简化了应用的部署。
总而言之,springboot简化了传统的spring的前期搭建和后期维护工作,使得开发者可以更加专注于业务代码的开发。
Spring中的单例bean是单例模式吗?
单例模式指的是在一个JVM中,同一个类只能有一个对象,而在spring中,单例bean也是单例模式,但是仅限范围是bean的名字,如果注入了相同类型不同名的bean,那么同一个类就有多个对象了。
Spring开启事务实际上是创建一个数据库连接,如果是有两个事务,实际上是创建了两个连接。
SpringAOP和AspectJ的区别?
SpringAOP和AspectJ是AOP面向切面编程的两种实现方式,两者并没有特别强的关系。AspectJ是基于编译器实现的,当一个类编译成字节码文件的时候,会将定义的一些额外逻辑织入字节码文件中;SpringAOP是基于动态代理机制实现的,使用动态代理生成代理对象后,当执行某些方法时,会执行一些额外定义的切面逻辑。
BeanFactory和ApplicationContext的区别?
BeanFactory是spring中的一个核心组件,表示一个对象工厂,可以生成bean,维护bean,而ApplicationContext继承了BeanFactory,它具备BeanFactory的所有特点,除此之外,它还继承了其他接口,具有获取环境变量,国际化,事件发布等功能。
SpringBoot中定义bean的方式?
- @Bean、@Component
- @Controller、@RestController、@Service、@Repository
- @ControllerAdvice、@RestControllerAdvice,相当于对于Controller的切面。
- @Configuration
- @Import,括号内写要导入spring容器的类
- < /bean> 在spring.xml文件中定义bean,并导入这个xml文件到spring容器中。
BeanFactory和FactoryBean的区别?
相同点:beanfactory和factorybean都是用来创建对象的。
不同点:使用beanfactory需要严格遵循bean的生命周期流程,比较复杂。如果我们想要简单的自定义创建一个bean对象,同时想要这个对象交给spring容器来管理,这样就需要实现factorybean接口了。
里面有三个重要的方法:
isSingleton:是否是单例对象。
getObjectType:获取返回对象的类型
getObject:自定义创建对象的过程(可以使用new,反射,动态代理)
Spring中用到的设计模式?
- 单例模式:创建bean默认都是单例的。
- 工厂模式:使用beanFactory。
- 代理模式:动态代理。
- 原型模式:可以指定bean的作用域为prototype。
- 构造器模式:使用BeanDefinition构造器。
模板方法,策略模式,观察者模式,适配器模式,装饰者模式。
拦截器中使用到了责任链模式和装饰器模式
责任链:在请求处理前后执行自己的逻辑,然后根据链路执行下一个拦截器。
装饰器模式:同时实现 HandlerInterceptor 接口定义自己的逻辑,从而实现一些额外的功能。
SpringBoot怎么处理异常?
定义一个controller,加上@RestControllerAdvice注解用于统一处理异常。同时在类中的方法上加上@ExceptionHandler注解用于捕捉特定类型的异常,并进行处理。
SpringBoot不想加载一个bean可以怎么办?
在不想加载的bean上加一个 @Conditional 注解,value属性赋值为一个实现了 Condition 接口的类,在这个实现类中重写matches方法,并自定义加载bean的逻辑,返回true则加载bean,返回false则不加载bean。
SpringBoot读取配置文件的顺序
- 项目根目录下的config目录 2. 项目根目录 3. resources/config 4. resources/
默认都是先读取application.properties文件,再读取application.yml文件,如果有多个相同的属性,默认使用第一个配置的属性值,后面的不会覆盖前面配置的属性值。
Mybatis
mybatis的优缺点🌟
优点:
- mybatis是基于sql语句编程的,可以动态的进行sql语句的编写,同时不会对应用程序和数据库的设计产生影响,sql语句是写在xml文件中的,降低了代码的耦合性,并且可复用。
- 与JDBC相比,开发者不再需要关心连接对象的开启和关闭问题,并且消除了JDBC中大量冗余的代码。
- 能够与spring进行很好的集成。
- 支持对象和数据库ORM字段的关系映射,可以对对象的关系进行维护。
缺点:
- sql语句编写的工作量大,并且如果涉及到多表关联查询时对开发人员的功底有一定的要求、
- 移植性差,如果mysql数据库更改为oracle,需要更改大量编写好的sql语句。
mapper接口和写sql语句的xml文件是如何进行绑定的?
两者之间进行绑定是通过xml文件中mapper标签中的namespace 属性值赋值为对应的mapper接口的包名加接口名路径 进行绑定。
- 首先定义一个mapper接口。
- 接着在xml文件中mapper标签写对应的sql语句,这里的方法名和参数值要和接口中的对应,同时将namespace属性赋值为 定义mapper接口的包名 + 接口名。
- 在mybatis的配置文件中(一般是mybatis-config.xml),通过mappers标签将mapper接口和xml文件进行关联。
Mybatis执行流程?
- 先读取mybatis的核心配置文件mybatis-config.xml,里面包含了数据库连接等配置信息以及关于mapper文件的映射文件。
- 创建一个会话工厂sqlSessionFactory,这是一个单例对象,一个项目中只有一个。
- 会话工厂创建会话SqlSession,这里面包含了执行sql语句的所有方法。
- 加载Executor执行器,它是真正操作数据库的接口,同时也负责查询缓存的维护。
- 加载Executor接口中的MappedStatement参数,里面封装了mapper.xml文件中配置的映射信息。
- 输入参数映射,将java中的类型转成数据库支持的类型。
- 操作数据库。
- 操作数据库完成后,进行输出结果的映射,也就是把数据库类型转成java支持的类型。
Mybatis是否支持延迟加载?
支持延迟加载,但是默认是关闭的。
延迟加载指的是当需要用到该数据的时候才会进行加载,否则不加载。
比如一个user对象,其中有一个orderList属性需要去查询订单表,当我们配置了延迟加载之后如果不去getOrderList,那么是不会查询订单表的,只有调用getOrderList才会去查询订单表。
mybatis支持一对一关联对象和一对多关联集合对象的延迟加载。
如果想要开启延迟加载,那么我们可以在mybatis的配置文件中配置lazyLoadingEnabled为true就可以了,默认为false,也就是不开启延迟加载。
延迟加载的底层原理?
延迟加载底层是使用的cglib动态代理实现的,当启用延迟加载后,会使用cglib创建目标对象的代理对象。
当我们调用目标方法后,会进入拦截器的invoke方法,当判断目标方法值为null的话,则去数据库中查询记录,并调用set方法设置相应的属性值,然后继续查询目标方法,就能够获取值了。
mybatis的缓存机制?
一共有两级缓存。
一级缓存:存储的作用域为sqlSession,当sqlSession进行flush或者close操作,那么对应的缓存会被清空,一级缓存默认是开启的。
二级缓存:基于namespace 和 mapper 的作用域,默认是不开启的,它不依赖于sqlSession。
如果我们想要开启二级缓存,需要在mybatis的配置文件中将cacheEnabled设置为true,并且在对应的mapper文件中加上<cache/ >标签。
注意事项:
- 缓存的更新机制: 当某一个作用域 (一级缓存sqlSession,二级缓存namespace)中进行了增删改操作后,那么该作用域下所有select操作的缓存会被清除。
- 二级缓存需要缓存的数据需要实现Serializable接口。
- 只有当会话提交或者关闭后,一级缓存中的数据才会转移到二级缓存中。
说一下jdbc?
jdbc是java访问关系型数据库的标准接口。通过jdbc,java可以与各种关系型数据库进行数据交互,完成sql查询,更新数据等操作。
jdbc操作数据库的流程?
- 加载数据库驱动。
- 获取数据库连接。
- 通过connection对象创建statement或者preparedstatement对象。
- 编写sql语句。
- 使用statement或者preparedstatement对象执行查询或者更新操作,得到结果集。
- 处理结果集。
- 关闭结果集,statement对象以及数据库连接。
- 如果处理过程中出现异常,那么就需要使用try-catch块来捕获并处理异常。
Java集合
集合框架体系
集合主要分为两类:单列集合和双列集合。
单列集合 中的Collection接口 下面有两个接口,list和set。
其中list接口的实现类有 Vector,ArrayList,LinkedList。
set接口的实现类有 HashSet 和 TreeSet,HashSet有一个子类是LinkedHashSet。
双列集合中的Map接口下面有四个实现类,分别是 HashMap,TreeMap,HashTable以及LinkedHashMap,其中 LinkedHashMap 同时继承了HashMap类 ,HashTable下面有一个子类Properties类。
ArrayList的扩容机制
ArrayList底层是维护了一个Object类型的elementData的数组,当我们创建一个ArrayList集合,调用的无参构造方法时,初始时elementData中的元素个数为0,当第一次添加数据的时候,则会将elementData数组的容量扩容到10,当需要再次扩容时,则elementData数组的容量会扩容到原来的1.5倍。
如果创建ArrayList集合的时候指定了容量大小,那么初始时elementData的容量大小就是指定的容量大小,当扩容时也是扩容到原来的1.5倍。需要注意的是,每次扩容的时候都需要进行数组拷贝。
添加和获取元素时需要先判断下标是否越界。
如何实现数组和List之间的转换?
数组转List,Arrays.asList,当数组中的元素发生变化后,List集合会受影响,因为这个方法是用的List的实现类ArrayList对数组进行了包装,指向的内存地址是一样的。
List转数组,list.toArray,当list中的元素发生变化时,数组不受影响,因为这个方法是进行了数组的拷贝,内存地址不一样。
Vector和ArrayList的比较
| 底层结构 | 线程安全 | 扩容 | |
|---|---|---|---|
| Vector | 可变数组 | 安全,操作方法上加了synchronized关键字 | 调用无参构造函数,初始为10,后面扩容为原来的2倍;指定容量,直接扩容2倍 |
| ArrayList | 可变数组 | 不安全 | 上面有 |
Map的类型
常见的有三种,分别是HashMap、TreeMap和LinkedHashMap。
HashMap:基于哈希表实现,具有快速的查找和插入操作,适用于需要快速查找键值对的场景。
TreeMap:基于红黑树实现,可以对键进行排序,适用于需要对键进行排序的场景。
LinkedHashMap:基于哈希表和链表实现,保持键值对的插入顺序,适用于需要保持插入顺序的场景。
还有一种是ConcurrentHashMap,是一种线程安全的,支持高并发读写操作的哈希表。
JVM
什么是JVM?它的作用是什么?
jvm是一个工具,负责将java编译后的字节码转成计算机能够识别的机器码,达到“一次编译,处处运行”的效果。
作用:
- 负责将字节码翻译成计算机能够识别的机器码。
- 进行内存管理,jvm对内存区域进行了划分,能够更好的管理内存。
- 垃圾回收:jvm提供自动垃圾回收的功能,提高了内存空间的利用率。
- 性能监控:jvm提供了各种工具可以对java程序的运行状态,性能表现进行监控和调优。
为什么说java是一次编译处处运行?
因为java可以生成无关平台的字节码文件,它可以在任何安装了JVM的平台上运行,JVM在运行时会把字节码转成机器能够识别的机器码。
JVM由哪些部分组成,运行流程是什么?
由类加载器,运行时数据区,执行引擎以及本地库接口组成。
运行流程:
- 类加载器将java代码转成字节码。
- 运行时数据区将字节码加载到内存中,但是字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是由执行引擎执行。
- 执行引擎将字节码翻译成底层系统指令,再交给CPU去执行,此时需要调用其他语言的本地库接口来实现整个程度的功能。
JVM内存分区(运行时数据区)?
分为了堆,栈,方法区,本地方法栈以及程序计数器这几部分。
- 堆解决的是对象实例存储的问题,是垃圾回收管理的主要区域。
- 栈解决的是程序运行的问题,栈里面存放的是栈帧,每个栈帧对应一次方法的调用。栈帧里面包含以下几部分:局部变量表,操作数栈,动态链接,方法出口以及其他信息。
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量,静态变量,即时编译器编译后的代码等,jdk1.7中的永久代,jdk1.8的元空间都是方法区的一种体现。
- 本地方法栈和栈类似,不过执行的是本地方法,本地方法是由其他语言编写的,在java中使用native关键字修饰。
- 程序计数器保存着当前线程执行字节码的位置,每个线程工作时都有个独立的计数器,只为执行java方法服务,执行native方法时计数器为空,它不会出现OOM异常,也没有GC。
方法区和堆是线程共享的区域,其他的都是每个线程私有的。
类加载机制 🌟
类加载流程包括 加载,连接(包括验证,准备,解析)以及初始化三个阶段。
加载:根据类的完全限定名找到对应的字节码文件创建class对象。
验证:确保该字节码文件符合虚拟机要求,不会危害虚拟机安全。
准备:对static修饰的类变量分配内存,并设置初始值(0或者null),这里不包含final修饰的静态变量,因为final修饰的变量会在编译时分配内存。
解析:这个过程就是将常量池中的符号引用替换成直接引用,符号引用不是具体的内存地址,而直接引用是如果当前类引用了另一个对象,那么这里就是另一个对象在堆中的内存地址。
初始化:主要完成类的成员变量,静态变量的赋值以及静态代码块的执行。
类加载器是什么?常见的类加载器有哪些?
因为JVM只会加载二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让java程序能够跑起来。
常见的类加载器一共有4种:
- Bootstrap ClassLoader :是顶级类加载器,负责加载JAVA_HOME/jre/lib目录下的类库。
- ExtClassLoader :是ClassLoader的子类,负责加载JAVA_HOME/jre/lib/ext目录下的类库。
- AppClassLoader:是ClassLoader的子类,负责加载classpath下的类,也就是我们自定义编写的类。
- 自定义类加载器,需要继承ClassLoader实现自定义类加载规则,但是为了符合双亲委派模型,一般都是继承AppClassLoader类。
解释一下双亲委派模型?有什么好处?
双亲委派指的是子类加载器加载一个类的时候,先不自己加载,而是向上委托父类加载器加载,直到顶层的启动类加载器,如果父类加载器加载成功则返回,加载不成功则子类加载器再尝试加载。
好处:
- 避免了类的重复加载,当父类的类加载器已经加载好后,自己就不用加载了,保证了类的唯一性。
- 避免java的核心API被篡改。
破坏双亲委派模型?
类继承classloader,并重写loadclass方法,指定加载该类需要用到的类加载器。
介绍一下java中的堆
java中的堆是线程共享的区域,在堆中分为了年轻代和老年代。
年轻代:
- 年轻代被分为了三个区域,eden区和两个大小严格相同的survivor区,某一个时刻只有一个survivor区是被使用的,另一个是用作垃圾回收时复制对象的。
- 当eden区快满的时候,会将使用的survivor区和eden区中存活的对象复制到空闲的survivor中,清空eden区和使用的survivor区。
- 如果survivor区中的对象经过了几次垃圾回收之后仍然存活,则会被晋升到老年代中。
老年代中存放的是生命周期长的对象,一般是一些老的对象。
年轻代采用的是复制算法,老年代采用的是标记整理算法。
作为成员变量的基本类型随着对象的创建一起会被初始化到堆中。
GC怎么判断对象可以被回收?
GC判断对象是否能回收主要是基于两个方法:
- 引用计数法:每个对象都有一个引用计数属性,如果对象新增一个引用则计数+1,如果释放掉这个引用则计数-1,如果计数为0就可以进行回收了。
java中没有使用引用计数法,因为java中存在循环依赖的问题,这种情况对应的引用计数永远不为0。
循环依赖:两个或者以上的bean互相持有对方形成闭环,也就是A依赖B,B依赖A。解决办法是使用@Lasy注解实现懒加载,什么时候用到再加载。
- 可达性分析:从GC roots开始向下搜索,搜索过的路径称为一个引用链,如果某一个对象到这个GC roots没有任何的引用链,则表明这个对象就可以被回收了。
常见的GC roots对象有:
虚拟机栈中引用的对象
方法区中常量引用的对象
JVM垃圾回收算法有哪些?
常见的垃圾回收算法一共有四种:标记清除算法,标记整理算法和复制算法和分代收集。
标记清除算法:分为两个步骤:标记和清除。先根据可达性分析法标记存活的对象,然后将未标记的对象清除。
优点:标记和清除的速度快。
缺点:内存碎片化严重。这样可能导致存储不了比较大的对象和数组,所以这种算法使用的比较少。
标记整理算法:和标记清除算法差不多,也是先通过可达性分析法标记存活的对象,然后清除未标记的对象。不同的是它会对存活的对象进行整理,将存活的对象移动到连续的内存空间中。
优点:解决了标记清除算法的内存碎片化问题。
缺点:多了一步移动对象的步骤,效率比较低。
适用于老年代垃圾回收器。
- 复制算法:将内存空间分成了两块大小相等的区域,也是先通过可达性分析法判断标记哪些对象存活,然后把存活的对象复制到另一块内存区域中并进行整理,原来的内存区域清空。
优点:垃圾比较多的情况下,效率比较高。
缺点:将内存空间分成了两份,内存利用率低。
适用于年轻代垃圾回收器。
JVM有哪些垃圾回收器
常见的垃圾回收器有串行垃圾回收器,并行垃圾回收器,GMS垃圾回收器以及G1垃圾回收器。
串行垃圾回收器:同一时刻只有一个垃圾回收线程进行垃圾回收,java中的所有线程都要暂停,等待垃圾回收完成。
并行垃圾回收器:同一时刻有多个垃圾回收线程并行进行垃圾回收,java中的所有线程都要暂停,等待垃圾回收完成。
GMS垃圾回收器:是一种并发的,基于标记清除算法的垃圾回收器,主要是针对老年代垃圾进行回收,特点是停顿时间短,进行垃圾回收的时候应用能够正常运行。
G1垃圾回收器
是应用于新生代和老年代的垃圾回收器,是jdk9之后默认使用垃圾回收器。
它被划分为多个区域,每个区域都可以充当eden,survivor,old,humongous(黑哦忙格斯)。其中humongous是专为大对象准备的。
采用的是复制算法。
它的响应时间和吞吐量都非常优秀。
垃圾回收主要分为三个阶段:新生代回收,并发标记以及混合收集。
新生代回收阶段: 刚开始内存区域都是空闲的,先挑出几个区域作为eden区存放新创建的对象,当eden区快放满的时候,会挑出一个空闲区域作为survivor区,利用复制算法将eden区中存活的对象复制到survivor区中,eden区会被回收. 随着时间的流逝,eden区又快放满了,这时就会把eden区存活的对象和之前survivor区存活的对象通过复制算法,复制到新的survivor区中,之前survivor区中较老的对象会复制到old区中。 并发标记阶段:当old区占用内存超过阈值的时候,就会触发并发标记,标记old区、eden区和survivor区中存活的对象,当标记完成以后进入混合收集阶段。 混合收集阶段:这个阶段并不是对所有的old区进行垃圾回收,而是挑出一些存活对象比较少的old区,和eden区,survivor区同时进行垃圾回收,垃圾回收结束后进入新一轮的新生代回收,并发标记和混合收集。如果并发失败(回收速度赶不上创建新对象的速度),会触发fullgc。
垃圾回收为什么只作用于堆?
因为堆中创建的对象的生命周期是不固定的,它们在任何时候都能被引用,也能在任何时候都变成垃圾,所以需要垃圾回收机制来回收不再使用的对象,释放它们所占的内存空间,以便给其他的对象分配内存。
而栈中存放的局部变量和引用会随着方法调用的结束自动释放,所以不需要显式的垃圾回收机制回收这些内存。
Minor GC、Major GC、Full GC?
minor gc 是发生在新生代中的垃圾回收,暂停时间短。
major gc 是发生在老年代中的垃圾回收,当老年代空间不足时,先会触发minor gc,当minor gc后空间还不足,则会触发major gc,major gc比较慢,暂停时间长。
full gc 是新生代+老年代完整的垃圾回收,暂停时间长,应尽量避免。
什么时候会触发GC?
- 当应用程序需要分配堆内存但是空间不足时,会触发垃圾回收。
- 调用System.gc(),这个指令并不会立即进行垃圾回收,而是向虚拟机发送一个建议性的请求,虚拟机根据情况会选择是否立即响应该请求。
- 年轻代空间不足触发minor gc。
- 老年代空间不足会先触发minor gc,如果还不足触发major gc,如果还不足则触发full gc。
- 长时间停顿:当应用程序执行时间较长时,这个阶段没有进行垃圾回收,那么虚拟机可能会为了避免堆内存空间耗尽触发垃圾回收。
- 系统空闲时:当系统空闲时,虚拟机可能利用这个时间触发垃圾回收,以充分利用系统资源。
什么时候会触发fullgc?
直接调用System.gc。
当老年代空间不足以存放更多的对象时,就会触发fullgc。
- 系统一次性加载了过多的数据到内存中,导致大对象进入了老年代中。
- 发生了内存泄漏:当一个对象用完后,仍然有其他的对象引用它,那么它就无法被回收,先触发fullgc,最后导致OOM。
- 系统频繁的创建一些生命周期长的对象,导致这些对象会进入老年代中,最终导致fullgc。
- jvm参数设置的内存空间过小。
fullgc对程度的影响?
fullgc是指对整个堆进行扫描,回收掉无用的对象。
- 程序暂停时间长:进行fullgc时,所有的用户进程都会阻塞等待垃圾回收完毕,这个暂停时间可能有几百毫秒或者数秒,这样会使得程序的性能下降,同时给用户带来不好的体验。
- 占用系统资源:因为fullgc需要堆整个堆进行扫描并进行垃圾回收,所以需要消耗大量的系统资源。
- 系统吞吐量下降:fullgc导致程序暂停,影响了系统的吞吐量。
fullgc怎么处理?
- 使用监控工具,如JVisualVM查看JVM的内存使用情况和垃圾回收情况,确定具体原因并着手解决。
- 调整JVM参数,如增加堆内存大小,调整新生代和老年代的比例,调整对象晋升到老年代的年龄阈值等。
- 检查代码中是否有大量创建新的对象的操作,以及是否存在内存泄漏等问题,可以通过优化代码结构,减少新对象的创建,及时释放掉不再使用的对象等方式来优化代码。
被static修饰的变量会被垃圾回收吗?
不会被回收,被static修饰的变量和类的生命周期一样,即使该变量没有被引用也不会被销毁,只有类被卸载后才会回收。
java中的四种引用?
1.强引用:
指的是一个对象被一个引用变量引用,那么它始终是可达的,即使
出现内存溢出,该对象也不会被垃圾回收器回收,所以强引用也是造成内存泄漏的主要原因之一。
2.软引用:
对于软引用的对象,在内存空间充足的情况下是不会被回收的,当内存空间不足它会被垃圾回收器回收。软引用通常用在对内存空间敏感的程序中,作为缓存使用。
3.弱引用:
对于只有弱引用的对象,不管内存空间是否充足,只要垃圾回收一运行,那么这些对象就会被回收,它可以解决内存泄漏的问题。
4.虚引用:
它不能单独使用,必须结合引用队列使用。它的主要作用是跟踪对象被垃圾回收的状态。
如果发现内存溢出,应该怎么排查🌟
- 查看关键报错信息,如java.lang.OutOfMemoryError。
- 使用内存映射分析工具对Dump出来的堆储存快照进行分析,分析清楚是内存溢出还是泄漏。
- 如果是内存溢出,则进一步通过工具查看泄漏对象到GC roots的引用链,修复应用程序中的内存泄漏。
- 如果不存在泄漏,则先检查代码中是否有死循环,递归等,再考虑增加堆内存的大小。
MySql
介绍一下mysql?
mysql是一种开源免费的关系型数据库,是一种用来存放数据的容器。它具备以下特点:
- 关系型数据库:数据以表格的方式进行存储,表格分为了行和列,便于存储和管理结构化数据。
- 跨平台性:mysql可以在多个操作系统上运行。
- 开源免费
- 使用标准的sql语言,使用户可以方便的对数据进行增删改查等操作,并且单词相对简单,上手快。
- 事务支持:有四大事务特性,可以保证数据的一致性和完整性。
- 高性能和可扩展性:mysql可以处理大量的并发请求,同时也支持分库分表,主从复制来提高数据库的负载能力。
- 安全性:用户需要输入用户名和密码才能操作数据库,并且也可以对具体的用户设置访问权限。
mysql缓存
mysql5.7是内部支持的,而8.0之后就废弃了查询缓存。
查询缓存是在第一次执行sql查询语句后,将对应的结果存储在内存中,后续再执行相同的sql语句就可以直接从内存中获取结果,速度更快。
但是mysql的查询缓存有一些缺点:
- 使用缓存占用一定的内存空间。
- 只有相同的sql才会去查询缓存。
- 一旦进行数据更新后,相应的缓存就会失效,所以在更新较频繁的场景下,缓存命中率低。
sql语句隐式类型转换?
如果在sql语句中,发生了不同数据类型的运算或者比较时,可能就会进行隐式类型转换。这个操作是数据库自动完成的,不需要显式的进行类型转换。
比如整数和小数进行计算,数据库会根据小数的精度将整数转成小数进行运算;当字符类型和数值类型进行比较和运算时,数据库会将字符串转成数据类型进行运算。
需要注意的是,这种情况可能导致计算结果出现异常,所以应该尽量避免使用隐式转换,而使用显式的类型转换从而确保结果的准确性。
数据库主键如何选择?自增还是UUID?
自增:
优点:长度较小,占用内存空间小;自增在mysql的b+树中可以按顺序存储。
缺点:分库分表情况下,自增会导致主键重复,不利于记录的区分。
UUID:
优点:可以保证分库分表情况下主键的唯一性;本地可以直接生成,不需要依赖网络。
缺点:mysql需要将UUID转成二进制形式进行顺序存储,由于是随机生成的,所以会造成随机IO;长度比较长,需要占用更大的内存空间。
综上:优先选择自增的主键,如果在分库分表的情况下,建议使用雪花算法生成唯一主键。
MySql常见数据类型?
- 整数类型
tinyint 1字节 smallint 2字节 mediumint 3字节 int 4字节 bigint 8字节
- 浮点数类型
float 4字节,单精度浮点数 double 8字节,双精度浮点数
- 定点数类型
decimal 用于精度计算,需要指定精度和小数位数
- 日期和时间类型
date 日期 time 时间 datetime 日期时间 timestamp 时间戳
- 字符串类型
char 固定长度字符串 varchar 可变长度字符串 text 可变长度字符串,用于存放大文本内容
- 其他类型
blog 二进制大对象,用于存放大量的二进制数据 enum 枚举类型,用于指定列时只能从指定的值中进行选择
MySQL添加一个新字段的底层逻辑
- 给表的元数据添加字段相关信息。
- 为新字段分配存储空间。
- 如果表中有数据,需要对数据进行调整以适应新数据,比如表中记录新字段的值为NULL。
sql语句的执行顺序?
from 在哪个表中查询数据。
where 后面跟筛选条件。
group by 根据指定的列进行分组
having 类似于where,不过是对分组后的结果进行筛选。
select 后面跟指定查询的列。
order by 以哪个列为依据进行排序
limit 进行分页
内联,左联和右联的区别?
内联返回join的两个表中匹配的行。
左联返回join左表中所有的行,以及右表对应的行,如果右表没有对应的记录,则对应的值为NULL。
右联返回join右表中所有的行,以及左表对应的行,如果左表没有对应的记录,则对应的值为NULL。
数据库的三大范式
第一范式:数据库中的字段应具备原子性,不能够再被拆分,比如一个userinfo字段为zhangsan12345,应该拆分成username和phone两个字段。
第二范式:必须在满足第一范式的前提下,非主键列必须完全依赖于主键,不能只依赖主键的一部分。
第三范式:必须在满足第二范式的前提下,非主键列必须直接依赖于主键,不能存在传递依赖(非主键列直接依赖于其他非主键列,而其他非主键列直接依赖于主键)。
一般情况下,可以不遵守第三范式,允许出现一定的传递依赖,造成一定的数据冗余,但是这样做可以减少表关联查询的频率,提高查询效率。
什么是聚簇索引和非聚簇索引?有什么区别?
首先我们要明白一点就是聚簇索引又叫聚集索引,非聚簇索引又叫二级索引,当问到这个的时候不要懵。
聚簇索引通常使用的是b+树的结构,叶子节点保存了整行数据以及对应的主键值,并且叶子节点之间是顺序相连的,有利于进行范围查询。聚簇索引有且只有一个,一般情况下主键会作为聚簇索引。
非聚簇索引也多采用b+树,但是它的叶子节点没有整行数据,是保存了索引列的值以及对应的主键值。非聚簇索引可以有多个,一般情况下我们给字段创建的索引都是非聚簇索引。
innodb主键采用的是聚簇索引,MyISAM不管是主键索引,还是二级索引,采用的都是非聚簇索引。(这个别说,自己知道就行,要不容易给自己挖坑)
它们之间的主要区别是b+树的叶子节点是否保存了整行数据。
InnoDB和MyISAM的区别
事务和外键
innodb支持事务和外键,可以保证事务操作的安全性和完整性,适合更新操作比较多的场景。
myisam不支持事务和外键,比较擅长高速存储和索引,适合大量查询操作的场景。
锁
innodb支持行级锁,可以锁住某条记录;而myisam仅支持表锁,锁住整张表。
索引
innodb主键使用的是聚簇索引,索引和记录放在一块存储;myisam主键使用的是非聚簇索引,索引和记录分开存储。
并发处理能力
innodb的读写阻塞与隔离级别相关,可以采用多版本并发控制,来支持高并发。
myisam使用表锁,会导致写操作效率低,读操作之间不阻塞,读写阻塞。
Innodb是如何实现事务的?
Innodb是通过Buffer Pool,Log Buffer,Redo Log 和Undo Log实现事务的,以一个update语句举例。
- 当innodb收到一个update语句后,会先找到该数据所在的数据页,并将该页缓存在buffer pool中。
- 执行update语句,修改buffer pool中的数据。
- 针对update语句生成一个redo log对象,并存入log buffer中。
- 针对update语句生成undo log日志,用于事务回滚。
- 将修改信息写入binlog中,然后事务提交。
- 如果事务成功提交,那么就把redo log对象持久化,后续还有其他机制将buffer pool中的数据持久化到磁盘中。
- 如果事务回滚,则利用undo log日志进行回滚。
一个数据页的大小为16kb。
Buffer pool默认大小为128M。
Log Buffer的作用?
默认16kb。
log buffer:日志缓冲区,用来缓存要写到磁盘上log文件的数据,它会定期将内容刷新到磁盘的log文件中。
作用:用来优化每次更新操作都要把数据写入redo log而产生的IO操作过多的问题。
为什么innodb用的最多?
因为innodb相较于其他存储引擎有很多优点:
- 支持事务处理,具备ACID的特性。
- 支持行级锁,可以提供更高的并发性能。
- 支持外键,可以保证关系型数据库中的数据的参照完整性。
- innodb引擎提供了良好的崩溃修复机制。
- innodb的扩展性比较好,它可以进行分库分表。
对索引的理解
索引是帮助mysql高效获取数据的一种数据结构,它的主要作用是用来提高数据检索的效率,降低数据库的IO成本,同时通过索引列对数据进行排序,可以降低数据排序的成本,减少CPU的消耗。
强制使用索引的命令
select * from table_name froce index index_name
索引的基本原理?
索引的作用就是为了快速获取指定的数据。如果没有了索引,一般需要进行全表扫描。
它的原理就是把无序的数据变为有序的查询。
- 把创建了索引的列的内容进行排序。
- 对排序结果生成倒排表。
- 在倒排表的内容上接上数据地址链。
- 在查询的时候,先查到倒排表的内容,然后根据数据地址链找到具体的数据。
索引的底层数据结构
mysql默认的存储引擎innodb采用的是B+树来存储索引。
那么为什么采用b+树而不采用二叉树,红黑树或者b树呢?
如果采用二叉树的话,比如采用二叉搜索树,如果这个树是相对平衡的话,那么这个查找的时间复杂度是O(logn),如果数据是有序的,那么这个二叉搜索树就会退化成链表,查找的时间复杂度为O(n),查找速率太慢,也就是说使用二叉搜索树这个查找的效率不稳定。
如果使用红黑树的话,数据规模比较小,就可以避免不平衡的问题,但是如果数据规模过于庞大,因为每个节点只有两个分叉,那么这个红黑树的深度就会很深,查找的效率也会变慢。
b+树相对于b树的优点:
- b+树只在叶子节点存放数据,所以同样大小的磁盘页,b+树可以容纳更多的节点元素,在相同数据量的情况下,b+树相对于b树更加的矮胖,因此查询时IO次数更少。
- b+树的查询效率是稳定的,每次查找都需要到叶子节点,而b树是只要查找到元素即可,所以要查找的元素可能在根节点附近,也可能在叶子节点,查找效率不稳定。
- b+树的扫库和扫表能力更强,因为叶子节点之间使用指针连接,所以进行范围查询效率更高,而b+树需要把整棵树扫描一遍。
综上来看,使用b+树存储索引最优。
Hash索引和B+树索引的区别?
- hash索引采用的是hash表的结构,它将索引键的值通过hash函数映射到一个固定大小的桶中,然后在桶内进行查找。b+树采用的是多路平衡树的结构,它将索引键的值顺序存储到树的节点中,并通过节点之间的指针进行查找。
- hash索引在等值查询上具有良好的性能,但是对于排序操作和范围查询性能较低。b+树索引对于排序操作和范围查询更加高效,因为它的索引节点是顺序存储的,有利于进行范围查询。
- hash索引对于磁盘的利用效率不高,因为桶之间的数据分布是随机的,查询时可能导致磁盘的随机访问。而b+树的索引节点是有序存储的,有利于磁盘的顺序访问,减少了磁盘的IO操作。
- hash索引在插入和删除操作上相对比较简单,只需要通过hash函数确定桶的位置然后进行插入和删除操作即可。而b+树进行插入和删除操作时需要维护树的平衡性,可能会涉及到节点的拆分和合并,相对来说更加复杂。
索引的分类
按照字段的特性可以分为:主键索引,前缀索引,联合索引,普通索引。
按照数据结构可以分为:b+树索引和hash索引。
按照物理存储分类:聚簇索引和非聚簇索引。
索引的创建原则有哪些?(什么情况下创建索引,建立索引需要注意什么)
创建索引一般是在数据量庞大的情况下创建,一般来说,如果单表的数据量超过10万条,那么就考虑创建索引了。
创建索引的常见规则有:
添加索引的字段一般是查询比较频繁的字段,比如说作为查询条件,排序,分组的字段这些。
一般我们创建索引都是创建联合索引,因为联合索引的话,走覆盖索引的情况比较多,这样就避免了回表查询,提高了效率。
尽量使用区分度比较高的列(性别,状态字段不适合添加索引,区分度太低)作为索引,尽量使用唯一索引,区分度越高,使用索引的效率越高。
要控制索引的数量,并不是创建的越多越好,因为增删改操作都需要维护索引的内容。
索引的缺点?
一个是增加了存储空间,因为索引的存储需要一定的空间;第二个是会降低写操作的性能,因为增删改操作的时候需要维护整个索引的内容。
Mysql联合索引(a,b,c)相当于创建了a b c三个索引对吗?
对,这相当于创建了三个独立索引a,b,c。使用联合索引可以支持多列条件的查询。
innodb中主键索引和其他索引的区别?
- 主键索引是作为唯一标识每一行的数据,而其他索引是用来加速查询操作的。
- 主键索引是聚簇索引,数据和索引分开存储,而其他索引是非聚簇索引,数据和索引是放在一块存储的。
- 一个数据表中只能有一个主键索引,但是可以有多个其他索引。
- 主键索引要求每一行数据都必须要有唯一的主键值,而其他索引可以有重复的索引值。
怎么判断添加的索引是否生效?🌟
可以通过mysql的explain自动执行计划进行判断:
type字段:显示连接使用了哪种类型,如果为all,则未使用索引,如果是ref,range,eq_ref这些则使用了索引。
key:显示实际使用的索引名称,如果为NULL,则没有使用索引。
索引什么情况下会失效?
- 创建联合索引(a,b,c),查询条件未使用a,但是使用到了b和c;范围查询列后面的索引会失效。
- 使用like查询,%加最左面。
- 索引列使用计算或者函数,因为索引不支持计算和函数操作。
- 索引列进行了类型转换,比如一个varchar类型的值,查询时用了 a = 1转成数字类型。
- 使用 is null,is not null,or ,!= 会导致索引失效。
- 范围查询数据量过多会导致索引失效。
什么是最左匹配原则?
如果查询语句中使用到了联合索引中最左边的字段,那么这条查询就会利用这个索引进行查询。
例如我们建了一个联合索引(a,b,c),查询条件使用a/ab/abc都会走索引,而bc不会走索引。
当使用了范围查询,那么范围查询后面的字段不会用到索引。
假如查询条件为 where a = 1 and b > 2 and c = 3,那么字段a和b会用到索引,c不会用到索引。
因为在b+树的索引结构中,a字段的值是有序的,在a字段的值有序的前提下b字段的值是有序的,而b字段使用到了范围查询,所以这个范围内b字段的值是无序的,这就导致c字段的值是无序的,所以c字段用不了索引。
还有一种情况就是如果有个字段加了索引,但是使用了模糊查询,在最左面加上%后,也会违反最左匹配原则,因为无法得知查找的具体范围。
MySql什么情况下适合走索引,什么情况下不适合?
适合走索引的情况:
查询条件用到了索引列;进行排序,分组,聚合,表关联操作。
不适合走索引的情况:
数据量小,如果再使用索引可能有降低性能。
对数据表频繁的进行增删改操作,因为索引使用b+树进行存储的,进行这些操作需要维护树的节点。
查询条件没有用到索引列。
char和varchar的区别?
- char是用固定长度的方式进行存储,如果存入的长度小于指定的长度,那么就会使用空格进行填充;而varchar是以可变长度的方式进行存储,存多长就是多长。
- char类型由于是固定长度存储,所以mysql在读取数据时可以直接根据偏移量进行读取,而varchar由于是长度不固定的,所以就需要先读取偏移量和长度信息,再读取数据,性能上慢一点。
如果是存储固定长度的数据,建议使用char类型,比如邮政编码,手机号等,而存储不固定长度的数据,则使用varchar类型,比如邮箱等。
在MySql中,如何定位慢查询?
mysql中提供了慢日志的功能,它可以通过设置sql执行超过多长时间就会记录到一个日志文件中。mysql默认是不开启慢日志的功能的,我们在调试阶段可以在mysql的配置文件中开启慢日志的功能,并且设置sql执行时间为2s,这样的话就可以在这个日志文件中找到执行比较慢的sql了。生产环境下是不推荐开启慢日志的功能的,因为它会损耗一定的性能。
如果一条sql语句执行很慢,那么应该怎么进行排查呢?🌟
如果一条语句执行的很慢,那么我们可以通过mysql自带的分析工具explain去查看这条sql的执行情况。
第一个可以通过key和ken_len判断是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况。如果未添加索引则可以添加索引,如果未命中索引,则需要检查sql语句的编写。
第二个可以通过type查看sql是否有进一步优化的空间,如果存在了全索引扫描或者全表扫描,则需要对这条sql语句进行优化。
第三个可以通过extra判断是否有回表的情况,如果有则可以通过添加索引或者修改返回值来解决这个问题。
事务的特性是什么?以及实现方式?
事务的特性是ACID,分别指的是原子性、一致性、隔离性、持久性。
A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败。
在转账的过程中,数据要一致,A扣除了500,B必须增加500。
在转账的过程中,隔离性体现在A向B转账,不能受其他事务影响。
在转账的过程中,持久性体现在事务提交后,要把数据持久化。
实现方式:
- 基于日志的方式:事务的操作被记录在一个日志中,当事务提交时,将日志写入磁盘,以保证持久性。如果系统故障导致事务未提交,可以通过日志进行回滚和恢复。
- 锁机制:采用锁机制来实现事务的隔离性,通过加锁来控制并发访问,避免出现数据不一致的情况。常见的锁机制包括共享锁和排他锁,通过在读操作和写操作中加锁来保证事务的隔离性。
什么是Sql注入?如何预防?
#{ }和${ }的区别?
#{ }是预编译处理,而${ }是字符串替换。
mybatis在处理#{ }时,会将它替换成?,它是一个占位符,然后调用PreparedStatement的set方法进行赋值。
mybatis在处理${ }时,会将${ }替换成变量的值。
sql注入存在一定的安全隐患,比如我们在mybatis中查询id为1的数据,但是由于马虎导致用$代替了# 号,那么当我们传参的时候写成了 1 or 1=1 的话,这样就会查出数据表中的所有数据而不是id为1的这一条数据。
那怎么预防呢?
第一点就是用# 代替 $ ;
第二点是前端传过来的参数后端要进行校验。
sql优化?
避免使用select * ,而是使用具体的字段代替,使用select * 不会走覆盖索引,会有大量的回表操作,性能比较低。
尽量使用union all代替union,因为使用union需要对结果进行遍历比较,去掉重复的结果,所以更耗时,而union all可以出现重复的结果,不需要进行遍历。
小表驱动大表:以小表的结果集驱动大表的结果集。
批量操作:每次获取数据库连接都会耗费一定的性能,所以我们可以使用批量操作,但是批量操作需要把握一个度,否则会导致数据库响应过慢,如果数据很多,可以分多批次进行处理。
多用limit:这样查询的结果就会小很多。
使用连接查询代替子查询:使用子查询需要创建临时表,耗费一定的性能。
join表不宜过多。
尽量使用inner join代替 left join,因为使用inner join,数据库会默认以小表为驱动,而使用left join是以左边的表为驱动。
对查询较频繁的字段添加索引,这样可以更快的进行查询。
进行分组前应尽量缩小数据范围。
sql优化,判断是否走了索引,可以使用explain执行计划查看。
什么是索引下推?
索引下推是mysql5.6引入的查询优化方案,它可以减少回表的次数,提高查询效率。
比如说有一个user表,里面包含 id,name,age等字段,然后创建一个联合索引( name,age),现在我们要查询姓名以张开头并且年龄等于10岁的记录。
如果没有开启索引下推,查询过程是这样的:先从二级索引中查询所有姓张的数据行得到符合条件的主键索引id,然后根据id去聚簇索引中找到匹配的数据行,再在mysql的server层使用age = 10 这个条件进行过滤。
开启了索引下推会将过滤的场景下推到存储引擎层面上,查询过程是先根据(name,age)这个索引找到匹配的数据行,然后再根据id进行回表查询,减少了回表的次数。
当explain执行计划中extra列中出现 using index condition ,表明部分字段被下推到存储引擎进行过滤。
什么是回表?
先介绍一下什么是聚簇索引和非聚簇索引。
而回表是和聚簇索引和非聚簇索引有关联的,回表指的是先从非聚簇索引中找到对应的主键值,然后根据这个主键值从聚簇索引中找到对应的整行数据,这个过程就称为回表。
什么是覆盖索引?
覆盖索引指的是select查询语句使用了索引,在返回的列中,必须在索引中能够全部找到。
如果我们使用id查询的话,走的就是覆盖索引,只要进行一次索引扫描就能够直接返回数据,性能比较高。
如果我们走二级索引进行查询,如果返回的列中没有创建索引,那么就会触发回表查询,这样话性能比较低,所以尽量避免使用select * ,并且查询的字段尽量都包含索引。
MySql超大分页怎么处理?
超大分页一般就是在数据量比较大的时候使用分页查询,因为分页查询需要对数据进行排序,这样的话性能就很低,我们可以考虑使用覆盖索引 + 子查询 来解决。
我们先分页查询id,并且对id进行排序,得到一个id集合,因为查询id是走的覆盖索引,查询效率比较高,然后我们再用这个id集合去关联原来的数据表做联合查询即可。
select * from user where id >= (select id from user limit 10000,1) limit 1000;
mysql中锁的类型有哪些?
按照锁的粒度分类:
表锁:锁住的是整张表,锁的粒度最大,并发度低。
行锁:锁的是某行数据,锁的粒度最小,并发度高。
间隙锁:锁住的是某个区间。
行锁根据属性可以分为:(使用行锁的前提:使用InnoDB引擎,开启事务)
共享锁:也叫读锁,一个事务给某行记录加了读锁,其他事务只能给这条记录加读锁,不能加写锁,能够解决不可重复读的问题。
排他锁:也叫写锁,一个事务给某行记录加了写锁,其他事务对这条记录不能加读锁,也不能加写锁,能够解决脏读的问题。
InnoDB默认会给insert,update,delete语句加排他锁,如果想要给查询语句加上锁可以使用如下语句:
lock in shar mode(加共享锁)
for update(加排他锁)
还能分为:
乐观锁,悲观锁。
InnoDB中的行锁什么情况下会升级成表锁?
因为InnoDB中的行锁是给索引加锁,所以如果没有使用索引、索引失效或者索引字段的重复率过高(重复率过高会放弃使用索引)就会升级成表锁。
MySQL中的死锁是怎么产生的?
- 多个事务对依赖的资源进行操作,导致资源无法被释放,其他事务得不到。
- 多个事务竞争有限的资源,比如锁。
怎么处理MySQL中的死锁?
- 通过查看MySQL日志确定产生死锁的事务及详细信息。
- 手动停止一个事务,以解除死锁状态。
- 合理安排事务的执行顺序,尽量避免并发冲突。
- 给锁设置超时时间,避免一直阻塞式的获取锁。
隔离性的四个隔离级别?
读未提交:会读取事务未提交的数据,产生脏读的问题。
读已提交:会读取事务已提交的数据,解决脏读的问题,但是解决不了不可重复读的问题。
可重复读:这是mysql的默认隔离级别,可以解决脏读和不可重复读的问题,但是解决不了幻读的问题。(是由MVCC实现)
串行化:可以解决幻读,不可重复读和幻读的问题,但是它是让事务串行执行,性能太低(对读取的数据进行加锁,会导致大量超时和锁竞争的问题)。
并发事务带来的问题?(不考虑隔离性带来的问题)
脏读:当一个事务对数据进行了修改,但是这个修改还没有提交到数据库中,这时另一个事务到数据库中读取了还未完成修改的数据,这种情况称为脏读。
不可重复读:指的是一个事务中需要对一个数据进行多次读取,在读取的空隙来了一个事务对这条数据进行了修改,这样就会导致原来的事务多次读取数据不一致的情况,这种情况称为不可重复读。
幻读:和不可重复读类似,但是这种情况多发生在新增操作上,当一个事务对数据进行了范围查询得到几行记录后,另一个事务到数据库中新增了几条数据,这样当第一个事务再次查询的时候发现多了几行数据,就好像发生了幻觉一样,这种情况称为幻读。
可重复读可以解决幻读问题吗?
事务1多次查询中如果没有更新语句,那么得到的结果是一样的,这里依靠的是MVCC加版本链。
如果在事务1读取数据的时候加了for update 间隙锁,那么事务2进行更新就会被阻塞,这样就能够解决幻读问题。
但是如果事务1查询的时候没有加锁,事务2更新记录之后,事务1更新记录,然后再查询就会出现幻读问题。
所以说能否解决幻读是看建立在什么条件之上的,按理说可以解决,但是如果幻读包含了所有的写操作,那么就没有解决幻读问题。
如果不用分库分表,在海量存储数据和高并发请求的场景下应该如何减轻数据库压力以及如何优化查询性能
- 使用redis缓存常用的数据。
- 使用消息队列将一部分写操作转成异步的方式。
- 为经常查询的字段添加合适的索引。
- 优化sql语句。
mysql三大日志
1. binlog:(在事务提交前写入;是MySQL服务层实现的,所有引擎都可以用,记录的是逻辑日志)
它是一种二进制日志,里面记录了对mysql数据库执行数据变更的所有操作,并且记录了发生时间,执行时长,操作数据等其他信息。但是它不记录select,show这种不修改数据的sql语句。binlog主要用于数据库恢复和主从复制以及审计操作。如果mysql数据库意外发生故障,可以通过biglog日志完成数据库中的数据恢复。
三种模式:
- statement:该模式下记录的是数据库执行的sql语句,日志量小,可以减少磁盘的IO,可以提升存储和恢复的速度;但是在某些情况下会出现主从不一致的问题,比如now()函数。
- row:该模式下记录的是每一行数据修改的细节,在进行批量操作时,会产生大量的日志;但是在主从复制和数据恢复时不会出现数据不一致的问题。
- mixed:该模式是前两种模式的结合,它会根据执行的sql语句,在statement模式和row模式中选择一种记录到binlog中,默认使用的是statement模式。
2.redo log(是innodb引擎特有的,记录的是物理日志)
也叫重做日志,记录的是事务提交后数据页的物理修改,可以保证事务的持久性。
redo log 日志中记录了事务提交后对数据库的所有修改信息。
当事务提交后, buffer pool 中的数据页发生变化,这时还没有刷新到磁盘中的数据页称为脏页,脏页如果可以正常刷新到磁盘中,那么没redo log 什么事,如果服务宕机,脏页刷新失败,则会通过redo log 恢复数据。
3.undo log
也叫回滚日志,里面记录的是数据被修改前的信息(逻辑日志),比如delete一条记录,那么undo log中会记录一条对应的insert记录。当事务发生回滚时,可以通过逆操作恢复原来的数据。该日志可以保证事务的原子性和一致性。
能不能只用binlog 而不使用 redo log?
当mysql服务崩溃需要恢复数据的时候,binlog不能够很好的辨识需要恢复哪些数据。
我们需要恢复未写入磁盘但是写入biglog日志中的记录,虽然binlog中记录的是全量数据,但是当一条记录写入磁盘,另一条记录未写入磁盘时,binlog没有标识去判断哪些已经写入磁盘哪些没有,所以不管是两条记录都恢复到内存中还是都不恢复,结果都是错误的。
而redolog会将写入磁盘中的数据在redolog中抹除,这样数据库重启之后,只需要将redo log中的数据恢复到内存中就可以了。
mysql场景题
- 具体用户记录唯一的表中,初始化时用户却产生多条记录,怎么删除多余的(只保留任意一条)
解决方法:我们可以根据记录的创建时间来保留最早的一条记录。
DELETE FROM table_name
WHERE id NOT IN (
SELECT id FROM (
SELECT id
FROM table_name
WHERE name = 'zhangsan'
ORDER BY create_time
LIMIT 1
) AS t
);这里将子查询的结果放到临时表中是因为在子查询中order by 和 limit 使用 可能会先执行 limit,再执行order by ,这样就得不到预期结果了。
说一下MVCC?
mvcc称为多版本并发控制,是一种用于解决读写冲突的无锁并发控制,它会为每个事务分配一个单向增长的时间戳,并且为每一次修改记录一个版本,版本和事务时间戳相关联。读操作只读该事务开启之前数据库的快照。这样读操作不会阻塞写操作,写操作在不阻塞读操作的同时,也避免了脏读和不可重复读的问题,提高了数据库并发读写的性能。
实现原理依靠的是什么?
记录中的三个隐藏字段,undo log日志以及read view。
MVCC不能解决幻读,需要结合锁来解决幻读,或者更改隔离级别为串行化。
count(列名)、count(1)和count(*)的区别
count(1)和count(*)会返回总记录数,忽略值为null的情况;而count(列名)会返回列值不为null的总记录数,不会忽略值为null的情况。
执行效率上:count(列名) < count(1) = count(*)
因为count(1)和count(*)会直接返回总行数,而count(列名)还需要判断值是否为null,并进行累加,所以效率比较低。
Redis
为什么使用Redis?
主要有两个方面:
从高并发上来讲:访问缓存与直接访问数据库相比,缓存能够承受的请求量远远大于数据库,所以我们一般会将数据库中经常访问的数据放入缓存中,这样就可以先访问缓存而不是直接访问数据库了。
从高性能上来讲:访问缓存也就是访问内存,内存的响应速度是很快的,所以使用redis这种缓存中间件可以加快数据的访问和读取速度,提高系统的性能。
那为什么使用Redis而不使用java自带的缓存,例如map?
缓存分为本地缓存和分布式缓存。
像map这种的就是本地缓存,本地缓存的特点是轻量且快速,但是它是保存在jvm中的,会随着jvm的销毁而结束。如果存在多个节点的话,每个jvm中都需要保存一份缓存,缓存不具有一致性。
而redis是分布式缓存,多个节点共享一份缓存数据,数据具有一致性。
Redis突然变慢,有哪些原因?
存在bigkey:如果redis中存放了bigkey,那么淘汰删除bigkey释放内存这个操作耗时比较久。所以应该尽量避免存储bigkey。
缓存雪崩:如果缓存中有大量的key同时过期,那么主线程会不停扫描过期的key,然后交给子线程执行del操作,这个也是非常耗时的。
网络拥堵:影响redis的性能的原因除了内存之外,网络IO也是主要原因,如果网络慢那么redis的效率就会变慢。
频繁的短连接:如果频繁的使用短连接,那么redis就会耗费大量时间在连接的建立和销毁上。解决方案是尽量使用长连接操作redis。
Redis中的bigkey
1.什么是bigkey?
bigkey指的是key对应的value很大。
一般以下情况被称为bigkey:
- string类型的值大于10kb。
- list、hash、set、zset对应的元素个数超过5000个。
2.bigkey会造成什么问题?
- 操作bigkey会导致主线程阻塞,客户端长时间得不到响应。
- 获取bigkey会消耗大量网络资源,造成网络阻塞。
- 如果使用分片集群,那么存储bigkey的插槽内存占用较大,造成内存分布不均。
3.如何找到bigkey?
可以使用redis-cli --bigkeys遍历所有的key,返回每种数据类型top1的bigkey,这种方式排查bigkey有问题,一是排名第一的不一定是bigkey,二是如果排名第一的是bigkey,那么第二名有可能也是bigkey,这样排查不出来。
自己手动编程使用scan扫描redis中的所有key,通过计算元素大小以及元素长度来判定是否是bigkey。
可以采用阿里云的网络监控工具,监控进出redis的网络数据,超出预警值时会报警。
4.如何删除bigkey?
在redis3.0及以下版本
如果是集合类型,就遍历BigKey的元素,先逐个删除子元素,最后删除Bigkey。
在redis4.0以后
使用unlink异步删除BigKey。
Redis的keys命令有什么问题?
因为redis是单线程执行命令,所以执行keys命令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复正常。
可以使用scan命令,scan命令使用渐进式遍历的方式可以解决keys导致线程阻塞的问题,每次遍历的时间复杂度为O(1),要想真正实现keys的命令,需要使用多次scan命令。
但是scan命令同样存在缺点,就是如果在scan的过程中key发生了变化,那么就可能导致新增的key没有被遍历到,或者遍历到了重复键的问题,也就是说scan命令并不能保证完整的遍历出所有的key。
常见数据类型以及应用场景?🌟
- string:缓存对象(set),分布式锁(setnx),常规计数(incr,decr)。
- list:实现消息队列(rpush和lpop)(但是有两个问题:1.生产者需要自行实现全局唯一id,2.不能以消费者组的形式消费数据)。
- hash:缓存对象以及记录购物车(以用户作为key,商品名作为field,商品数量作为value)。
- set:实现聚合计算(并集,交集,差集)。比如点赞,共同关注,可能认识的人,抽奖活动等。
- zset:排序场景,比如实现排行耪(以时间或者热度作为score)。
后续版本推出的4种数据类型:
- bitmap:二值状态的统计,比如签到,判断用户登录状态。
- hyperloglog:海量数据基数统计的场景,比如百万级网页UV计数。
- geo:存储地理位置信息的场景,比如打车,附近商铺。
- stream:消息队列,相比于list类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息id,支持以消费者组的形式消费数据。
Redis中String类型的底层原理?
我这里是基于redis3.2之前的版本说的:
我们知道redis是基于c语言进行编写的,而string类型在c语言中是用了一个sds抽象的数据类型进行实现的。
sds里面有三个属性,分别是 int len (字符数组的长度)、 int free(字节数组中可用的长度)、char buf[] (存放字符的数组)。我们存放的字符串是在这个字符数组中,以 '\0'收尾,这个'\0' 是不计入长度的。
然后我们说一下sds这样设计的好处:
1. 使用len用来记录以存放的字符的长度,保证了计算字符串长度这个操作的时间复杂度降为O(1),如果不加的话需要遍历字节数组,时间复杂度为O(n)。
1. free记录了可用字符数组的长度,这里面就涉及到扩容了。
我简单说一下扩容。初始存放字符"abc",那么len =3, free = 0。当我们继续向字节数组中添加“def”的时候,这时候空间不够了,那么就需要动态分配内存空间,扩容后 len = 6,free= 6 (至于为什么等于6呢,这里设计的时候是以string类型存放数据 1mb进行划分,如果小于1mb,那么再次扩容后 len = free,这样就避免了频繁进行扩容;如果大于1mb,那么free = 1mb)。
还有一点,就是涉及到了惰性删除,当我们删除“abc”中的“bc”后,len变为 1,而free变为2,原来的内存空间并不会立马释放,这样也起到了避免频繁扩容的效果。
这样我们就大概了解了free的作用了,就是记录了可用字节的空间,从而避免了频繁的扩容。
以上就是我对string底层实现的理解了,当然redis3.2之后对于string类型进行了改进,但是大体上还是一样的。
zset的底层实现原理?
zset底层是基于压缩列表和跳表实现的。
压缩列表本质上就是一个数组,只不过它增加了列表的长度,尾部偏移量,列表元素个数以及列表结束标志。这使得我们在查询列表首尾元素时间复杂度为O(1),查询其他位置为O(n)。
那么什么时候采用压缩列表呢?
当zset存储元素个数小于128个 或者 存储所有元素的长度小于64字节 这两种情况使用压缩列表。
跳表是什么?
跳表是一种在链表的基础上增加了多级索引的数据类型。它可以通过多级索引进行转跳,实现快速查找元素的效果。
跳表查找元素的时间复杂度是多少?
O(logn)。同样增删元素时间复杂度为O(logn)。
为什么使用跳表,而不是使用二叉搜索树或者红黑树?
1. zset有一个重要特性就是范围查询,使用跳表可以利用多级索引快速查找起点,然后向后遍历就可以了;而二叉搜索树或者红黑树进行范围查询效率就没有这么高。
1. 跳表更容易实现,而二叉搜索树和红黑树需要额外维护节点。
跳表如果插入重复数据会怎么样?
会更新该元素的分数,并根据分数调整该元素的排序位置。
Redis是单线程吗?✅
redis是单线程的,这里一般指的是 接收客户端请求 -> 解析请求 -> 进行数据读写操作 -> 发送数据给客户端 的这个过程是由一个线程完成的。
但是redis程序并不是单线程的,redis在启动的时候,是会启动后台线程的。
- 在redis2.6版本,会启动两个后台线程,分别处理关闭文件,AOF刷盘这两个任务。
- redis4.0之后新增了一个后台线程,用来异步释放redis内存。
比如执行unlink key / flushdb async(额森可) / flushall async 等命令,会将这些删除操作交给后台线程来执行。比如我们删除一个大key的时候,不要使用del命令,这样会导致主线程阻塞,推荐使用unlink命令异步删除大key。
redis之所以为 关闭文件、AOF刷盘、释放内存 这些任务交给单独的线程来处理,是因为这些操作是很耗时的,如果将这些任务交给主线程来处理,那么会造成主线程阻塞,无法处理后续请求。
Redis是单线程的为什么还这么快?
- redis是基于内存对数据进行操作的,数据存储在内存中,读写速度快。
- redis使用单线程避免了多线程之间的竞争,省去了线程上下文切换的开销,而且还不会出现死锁问题。
- redis使用了I/O多路复用模型来处理大量的客户端Socket请求。
那么解释一下IO多路复用?
IO多路复用就是利用单个线程同时监听多个Socket,并在某个Socket可读可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的IO多路复用都是采用的epoll模式实现的,它会在通知用户进程某个Socket就绪的同时,把就绪的Socket写入用户空间,不需要挨个遍历来判断Socket是否就绪,提升了性能。
- 高效的数据结构:redis的每种数据类型底层都做了一定的优化,目的就是为了追求更快的速度。
BIO、NIO以及AIO
BIO:同步阻塞IO,线程的读取和写入必须在一个阻塞的线程中执行。类似于有多个水壶需要烧水,只有当一个水壶烧开水了,其他水壶才能烧水,期间其他水壶一直在等着。
NIO:同步非阻塞IO,以烧开水举例,就是使用一个线程不断轮询查看水壶的状态是否发生变化,根据水壶的状态进行下一步操作。
AIO:异步非阻塞IO,以烧开水为例,它不需要轮询查看水壶的状态,而是给每个水壶设置一个开关,水烧开了那么开关状态就会变化,从而通知线程去执行对应的操作。
BIO和NIO的优劣:
BIO的优点:实现较为简单,适用于连接数较少并且连接时间长的场景;缺点:每个连接都需要单独的线程处理,对资源的消耗过大,同时在网络通信或者文件处理中可能会发生阻塞,影响系统的吞吐量。
NIO的优点:可以使用一个线程处理多个连接,系统资源消耗少,同时增大了系统的吞吐量;缺点:实现较为复杂,需要处理事件的就绪状态。
select、poll、epoll
这些都是操作系统提供的IO多路复用模型。
select模型,使用的是数组来存储Socket套接字,容量是固定的,需要通过轮询判断是否发生了IO事件。
poll模型,使用的是链表来存储Socket套接字,容量是不固定的,同样需要通过轮询判断是否发生了IO事件。
epoll模型,和poll是完全不同的,它是一种事件通知模型,当发生了IO事件时,应用程序才会进行IO操作,不需要poll模型那样主动去轮询。
阻塞IO和非阻塞IO的区别,使用场景?
阻塞IO:执行IO操作时进程会被阻塞,直到操作完成或者出错。
非阻塞IO:执行IO操作时进程不会阻塞,但是需要通过轮询来获取IO操作的结果。
使用场景:
阻塞IO适用于对实时性要求不高,且IO操作耗时较短的场景。
非阻塞IO适用于对实时性要求较高,并且需要同时处理多个IO操作的场景。
Redis为什么使用单线程?
- 这个问题我看过redis官方给出的回答,它的回答的大致意思就是redis单线程无法使用服务器的多核CPU,但这并不是制约redis性能的瓶颈,更多的是取决于内存大小以及网络IO的限制。所以说redis使用单线程并没有什么问题,如果想要使用服务器的多核CPU,那么可以在一台服务器上启动多个节点或者使用分片集群的方式。
- 使用单线程,那么可维护性就变高了。如果使用多线程,那么会增加系统的复杂度,同时会带来线程上下文切换,加锁,解锁,死锁等性能问题。
Redis6.0之后为什么引入多线程?
网络I/O是制约redis性能的瓶颈,而随着硬件性能的提升,网络I/O的制约越发明显,同时为了适应服务器的多核CPU,redis6.0就引入了多线程处理网络I/O,但是redis处理命令仍然是单线程的。
Redis存在线程安全问题吗?
在redis服务端是单线程执行命令,所以不存在线程安全的问题,而redis6.0引入的多线程,只是使用多线程去处理网络IO,执行命令还是单线程的。
在redis客户端来说的话,如果有多个redis客户端同时执行多个指令的时候,这种情况下无法保证这些指令操作的原子性。
解决方法是尽可能使用redis里面的原子指令,或者对多个访问资源的客户端进行加锁,或者使用lua脚本来实现多个指令的操作。
Redis中的数据过期删除策略?
- 惰性删除:当给key设置过期时间后不去管它,当需要该key时,再去检验是否过期,如果过期则删除。
优点:当需要该key时再去检查,使用CPU资源较少。
缺点:如果一个key过期了,不去访问则一直保存在内存中,占用内存空间。
- 定期删除:每隔一段时间就选取一定数量的key进行检查,删除里面过期的key。
优点:定期删除过期的key释放内存空间,对内存空间比较友好。
缺点:定期时间不太好掌控,如果执行的太频繁,对CPU资源使用过多,如果执行的不频繁,那么就和惰性删除一样,过期的key无法得到释放。
所以Redis选择惰性删除 + 定期删除 配合使用,以求在使用CPU资源和内存空间上取得平衡。
Redis内存淘汰策略有哪些?
- 不进行数据淘汰(noeviction 闹欸维克神)
当redis内存不足时,不淘汰任何数据,而是不再提供服务,直接返回错误。
- 进行数据淘汰,分为在 设置了过期时间的数据中 进行淘汰 和 在所有数据范围内进行淘汰。
- 过期时间
- volatile-random:随机淘汰设置了过期时间的任意键值。
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru:淘汰在所有设置了过期时间的键值中,最久未使用的键值。
- volatile-lfu:淘汰在所有设置了过期时间的键值中,最少使用的键值。
- 所有数据
- allkeys-random:随机淘汰任意键值。
- allkeys-lru:淘汰最久未使用的键值。
- allkeys-lfu:淘汰最少使用的键值。
使用allkeys-lru的淘汰策略,保存热点数据。
Redis的两种持久化方式
RDB:它是一个快照文件,会把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,可以读取RDB的快照文件来恢复数据。
AOF:追加文件,当redis操作写命令的时候,都会存储在aof文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
RDB和AOF的优缺点
RDB:
- 优点:
- 文件体积小:记录的是redis内存中的数据。
- 故障恢复速度快:只需要执行一遍rdb文件就能够恢复数据。
- 适合备份:因为rdb文件是redis某个时间点的快照,所以适合进行备份和数据迁移。
- 缺点:
- 可读性差:rdb文件中记录的是二进制数据,不容易理解,所以进行故障排查和恢复不如aof。
- 数据容易丢失:因为rdb是在某个时间点执行快照,所以两次快照之间的数据容易丢失。而且如果没有执行最后一次快照,redis实例宕机,那么最后的数据也会丢失。
AOF:
- 优点:
- 可读性好:aof文件以文本格式记录了redis的写命令,容易理解,利于进行故障排查和恢复。
- 数据不容易丢失:aof默认是每隔一秒记录一次写命令,所以最多允许一秒的数据丢失,数据完整性高,适用于对数据完整性要求较高的场景。
- 缺点:
- 文件体积大:记录的是redis中的写命令。
- 故障恢复速度慢:需要依次执行记录的写命令。
AOF执行的流程
首先开启aof,并配置文件名和刷盘策略。
先把写操作执行到redis内存中,如果没问题再写入内存中的aof缓存区中。
根据配置的刷盘策略,redis会根据条件将aof缓冲区中的命令追加到磁盘中的aof文件中。
为了避免aof文件过大,redis提供了aof重写机制,通过重写去除冗余的命令,减小aof文件的体积。
当redis实例启动的时候,会重新执行一遍aof文件中的命令从而恢复数据。
为了保持aof文件的较小体积,redis会定期的重写aof文件。
Redis如何实现延迟队列?
延迟队列就是一个任务向后推迟一段时间再做。常见使用场景:
- 购物平台下单,一段时间未支付订单自动取消。
- 打车软件打车,一段时间没有司机接单则订单取消。
在redis中可以使用zset来实现延迟队列的功能,zset中有一个score属性用来记录延迟的时间,
然后通过zrangebyscore查询所有符合条件的任务去执行。
Redis事务支持回滚吗?
答案是不支持回滚,当redis开启事务并提交时,正确的指令可以被执行,而错误的指令不能被执行,所以redis并不一定保证原子性(事务中的命令要不全部成功,要不全部失败)。
为什么Redis不支持事务回滚?
这里我看过官方文档给出的回答,大致意思就是:
- redis的事务执行的时候,发生的错误通常都是开发者自己写错了,而在真正的生产环境下很少出现,所以没有必要为redis提供事务回滚功能。
- 事务回滚这种复杂的功能与redis追求的简单高效的设计宗旨不符。
Redis是怎么实现分布式锁的?
可以使用set命令中的nx属性实现,并且使用ex属性为key设置过期时间,从而保证服务宕机之后锁正常释放不会产生死锁。
Redis在集群情况下如何保证分布式锁的可靠性?
集群情况下出现的问题:
当主节点获取锁成功后,还未同步到其他节点,这时主节点宕机。此时新的主节点仍然可以获取锁,这就出现了一把锁被多个节点持有的问题。
解决:可以使用Redisson提供的 红锁 解决这个问题。
红锁是基于多个redis节点的分布式锁,这些节点都是主节点,彼此互相独立。它的基本思路就是客户端依次向多个独立的redis节点申请加锁,如果加锁的节点超过半数并且总耗时没有超过锁的有效时间,那么就认为客户端成功地获得分布式锁,否则加锁失败。加锁失败会依次向redis节点申请释放锁,过程与单节点释放锁一致。
红锁存在的问题:
时钟偏移:红锁算法依赖于各个节点之间的时钟同步,如果节点之间的时钟存在较大的偏移,可能会导致锁的获取和释放出现问题。
网络延迟和分区:如果有网络延迟和分区的情况下,可能导致节点之间无法正常通信,节点的多数性无法满足,从而使得锁无法正常获取和释放。
Redis如何实现限流?
redis可以使用令牌桶算法或者漏桶算法来实现限流。
令牌桶算法:使用zset存储令牌,其中score表示令牌的到达时间,成员表示一个令牌,并配合lua脚本定时检查zset中的令牌数量进行限流。
漏桶算法:使用string类型来存储漏桶中的水量,请求过来之后根据请求量更新漏桶中的水量,超出容量的请求会被丢弃。
Redis作为缓存,mysql的数据如何与redis进行同步?(双写一致性)
有三种方案:
先删缓存,再更新数据库。
存在的问题是更新数据库耗时较长,这时另外一个线程去查缓存,缓存中查不到,则查询数据库并将旧数据更新到缓存中,这样后续请求读取到的都是旧数据。
先更新数据库,再删缓存。
存在的问题是更新数据库和删除缓存这段时间内读取到的都是旧数据,不过等数据库更新完成,就会读取到新数据,影响相对较小。
异步方式同步数据。
使用阿里的canal组件进行数据同步,这种方式不需要更改业务代码,需要部署一个canal服务。 canal服务将自己伪装成一个mysql的从节点,当mysql数据更新以后,canal会读取binlog日志中的数据,然后再通过canal的客户端获取到数据,再根据这个数据更新redis缓存即可。
那为什么不使用延时双删来保证强一致性呢?
如果是延时双删的话,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,但是这个具体延迟多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以不采用它。
如果先更数据库,再删缓存,redis缓存删除失败怎么办?
- 可以将这两个操作放在一个事务中进行处理,如果redis删除失败可以通过回滚事务来保证数据的一致性。
- 如果redis删除缓存失败,可以实现一个简单的重试机制,多次去尝试删除缓存,并设置重试次数和重试间隔,确保能够成功删除缓存。
- 使用日志记录redis的操作,如果删除失败,可以利用日志进行排查和手动处理。
- 设置一个定时任务,周期性地检查mysql和redis中的数据是否一致,如果不一致则进行修复工作。
布隆过滤器
布隆过滤器可以解决缓存穿透,当一个请求进入redis中,先去查布隆过滤器中有没有,如果有则放行去查redis缓存,没有则将该请求进行拦截。
布隆过滤器底层是使用的一个bit数组(位图 , 10个int类型 , 一共占320位bit),初始全为0,当一个key来了之后经过三次hash运算,模于数组长度得到对应的下标,将数组中原来的0改为1,这样三个位置就能够确定一个key是否存在。查找的过程也是一样的。
当然也是有缺点的,就是布隆过滤器可能存在一定程度的误判,因为有可能某一个key经过三次hash运算模于数组长度得到对应三个位置的元素全为1,但是redis中并没有该key。不过误判率很小,一般可以容忍。解决方法就是增加bit数组的长度或者hash运算次数多一点,这个根据业务来定。
假阳性:布隆过滤器判断一个元素存在于集合中,但是这个元素实际上不存在,这就发生了假阳性。
假阴性:布隆过滤器判断一个元素不存在于集合中,但是这个元素实际上存在,这就发生了假阴性。
考虑增加bit数组的长度或者hash运算次数多一点的方法减少假阳性的概率。
其他应用场景:
拦截器和黑名单:在网络安全领域,布隆过滤器可以快速判断某个IP地址或者url是否在黑名单中,从而及时进行拦截。
去重:可以使用布隆过滤器进行去重操作,避免处理重复数据。
如何通过redis踢出直播间的人 并且10分钟内这个人不能再回直播间?
当管理员想要踢出某个人时,可以将用户id作为key,当前时间戳作为value存入redis中,并设置过期时间为10分钟,当用户想要进入直播间的时候,先去redis中查询当前用户id时候存在且未过期则拒绝进入直播间;如果不存在记录则可以进入直播间。
Redis事务
redis事务是通过MULTI,EXEC,DISCARD和WATCH四个原语实现的。
Redis事务不能保证原子性,事务中的其中一条指令执行失败,其他指令仍然能正常执行,不会回滚。
- MULTI是开启一个事务,然后让要执行的redis指令放入一个队列中,当EXEC命令被调用时,队列中的命令才会执行。
- EXEC表示执行事务块中的所有指令,并按顺序返回命令的返回值。
- DISCARD命令表示清空事务队列中的命令,放弃执行事务,同时客户端退出事务状态。
- WATCH命令可以为redis事务提供CAS行为,可以监控一个或者多个键,当监控的键发生修改或者删除,那么之后的事务就不会执行,监控会一直持续到EXEC命令。
Redis的三种集群
主从复制
可以布置一主多从或者多主多从,但是一般都会布置一主多从,主节点负责写数据,从节点负责读数据,主节点写入的数据会同步到从节点上。这样提高了读写效率,但是主节点宕机,那么数据就无法同步,需要自己手动将一个从节点提升为主节点。
哨兵模式
通过心跳监测,哨兵发现主节点客观下线之后,会通过选举的方式将一个从节点提升为主节点,这样就解决了主从复制的痛点,所以一般我们将项目中会使用主从复制加哨兵模式搭建集群。
分片集群
可以解决海量数据存储的问题,集群中有多个主节点,并且还可以为每个主节点配置多个从节点,这样就极大的提高了并发能力,每个主节点都会存放不同的内容,key通过CRC16算法存储到指定的插槽上,但是会存在存放bigkey导致的数据倾斜问题。(16384个插槽)
主从同步的流程
- 第一次建立连接 ----- 全量同步
从节点请求与主节点建立连接,会发送自己的 replication id 和 offset 偏移量,主节点通过判断replication id 是否与自己的一致从而确定是否是第一次建立连接,如果不同那么就会将自己的 replication id 和 offset 发送给从节点 从而保持一致。
同时主节点会执行 bgsave 生成 rdb文件发送给从节点进行数据同步,从节点会先把数据清空,然后执行 rdb文件从而保持数据的一致。
如果rdb 文件生成和执行期间,依然有请求到达了主节点,那么主节点会以命令的方式记录到缓冲区,缓冲区就是一个日志文件,然后再把日志文件发送给从节点,这样就能保证主从节点的数据完全一致了。后期再同步数据的时候都是依赖于这个日志文件,这个就是全量同步。
- 从节点重启 ------ 增量同步
当从节点服务重启之后,数据就不一致了,那么从节点请求主节点同步数据,携带自己的 replication id 和 offset , 发现 replication id 是一样的,那么就会从命令日志中获取到offset之后的数据发送给从节点进行数据同步。
场景题:几万人同时访问淘宝页面,如果redis承载不足,超额并发,有什么解决方案?
- 搭建主从加哨兵集群,提高redis服务的高可用性。
- 前端使用静态页面,提前加载好静态资源。
- 使用多级缓存,比如还可以使用本地缓存,同时及时更新本地缓存中的数据。
- 降级策略:当redis服务不可用的时候,那么就可以返回默认数据或者缓存前的数据。
- 使用sentinel进行流量控制。
MQ
为什么要使用MQ?
核心:解耦,异步,削峰。
解耦:比如A系统想要发消息给BCD系统,如果B系统不想收到消息怎么办,如果增加了E系统想要获取消息怎么办。如何更改的话耗费时间太多,系统之间的耦合度太高,如果我们将A系统生成的消息放入MQ中,哪个系统想要就过来取,这样A系统就不需要关注哪个系统需要消息了,提高了系统的容错性和可扩展性。
异步提速:用户下单成功直接响应,将具体的操作放到MQ中交由其他系统消费,提高了响应速度。
削峰填谷:比如说系统一次性来个5000个请求,我们把这些请求放到MQ中,控制MQ每秒消费1000个请求,这就完成了“削峰”,延长处理请求的时间,而这些时间在平常请求量比较低,就完成了“填谷”。使用MQ可以控制并发量。
使用MQ的缺点?
- 系统可用性降低:引入MQ外部依赖,系统的稳定性变差,如果MQ宕机,会影响整个业务流程。
- 系统复杂性提高:没有使用MQ之前,系统之间进行通信都是同步远程调用,引入MQ变为了异步调用。
- 一致性问题:A系统发送消息到MQ中,B,C,D系统去MQ中消费消息,如果B,C系统处理成功,而D系统处理失败,就会出现最终数据不一致的问题。
Kafka、RabbitMQ、RocketMQ 都有什么区别?
RabbitMQ是基于AMQP协议,Erlang语言开发的,而Kafka用的是Scala(s 改 乐)和Java开发的,而RocketMQ是用Java开发的。
Kafka和RocketMQ的吞吐量都能达到十万级,而RabbitMQ比它俩小一个量级,可以达到万级。
Kafka和RocketMQ的延迟是毫秒级,而RabbitMQ的延迟是微秒级。
RabbitMQ的社区活跃度最高。
RabbitMQ支持消息持久化。
RabbitMQ和RocketMQ支持死信队列,延迟队列以及优先级队列,而Kafka这些功能都不支持。
消息获取:RabbitMQ以推模式为主,支持拉模式;Kafka采用的拉模式;Rocket是推、拉两种模式结合。
如果是大数据处理或者日志收集,优先采用Kafka,而业务处理可以选择RabbitMQ。
RabbitMQ的工作模式?(待)
Kafka的组件
- broker(代理):集群中每个节点称为broker,它可以接收生产者发送的消息并将消息存储在磁盘上,同时处理消费者的拉取消息请求。
- topic(主题):在kafka中是一个逻辑概念,kafka可以根据topic将消息进行分类,生产者发送消息指定topic,然后被订阅该topic的消费者进行消费,topic在每个broker上有不同的分区,每个分区保存一部分数据。
- partition(分区):分区存储topic的部分数据,解决了统一存储文件过大的问题,并且提高了读写效率(可以在不同的分区进行读写)。
- producer:生产者负责生产消息。
- consumer:消费者负责消费消息。
- zookeeper:负责管理和协调broker、生产者以及消费者。
MQ是如何保证高可用的?
RabbitMQ是最具代表性的,因为它是基于主从做高可用的。它有三种模式,分别是单机模式,普通集群模式以及镜像集群模式。
单机模式:本地启动一个实例,一般是自己练习用的,实际生产不会使用。
普通集群模式:在多个节点布置多个RabbitMQ实例,每个机器都启动一个RabbitMQ实例,而创建的queue只存在一个实例上,但是其他实例会同步queue的元数据(元数据指的是queue的一些配置信息,通过元数据可以找到queue所在的实例)。当消费数据的时候,会从queue所在的实例中拉取消息,该方案主要是用来提高吞吐量的,利用多个实例服务某个queue的读写操作。
镜像集群模式:这种模式才是RabbitMQ所谓的高可用模式。与普通集群不一样的是,其他实例不仅有queue的元数据,还有该queue的消息,也就是其他实例保存了queue的完整镜像。然后每次写消息到queue的时候,都会将消息同步到其他实例的queue上。这样即使一个节点宕机,消费者仍然可以在其他节点上获取数据。坏处就是性能消耗大,消息需要同步到所有机器上。
Kafka的高可用:
一个Kafka集群有多个broker,每个broker相当于一个实例,当创建一个topic的时候,topic中的数据可以存放到不同的分区,这些分区在不同的broker中,每个分区存放一部分数据,这就是天然的分布式消息队列。Kafka还有一个副本机制,每个分区中的数据会同步到其他broker上,形成自己的多个副本,所有的副本会选举出一个leader来,其他的副本称为follower,读取和消费数据在leader上,然后leader再把消息同步到其他follower上,如果某个broker宕机了,会在其他的follower中选取一个作为leader,然后在新leader上读写数据。
MQ如何保证消息不丢失?
RabbitMQ:
生产者方面:由于网络波动可能导致连接MQ失败,所以我们可以开启重连机制,还有就是可以开启确认机制,当生产者发送消息到MQ中会有一个回执,如果发送成功是ack,否则是nack,如果是nack的话我们可以尝试重发消息。通过以上手段,我们可以基本保证生产者发送消息的可靠性了,但是这样做会增加系统和资源的开销,所以一般不开启生产者确认机制,除非对消息的可靠性有较高的要求。
MQ方面:因为RabbitMQ支持消息持久化,所以可以开启持久化功能。
消费者方面:可以开启消费者确认机制,设置为auto,当消费者处理完消息之后,会向MQ发送一个回执,如果返回nack我们可以尝试重发消息,并为了避免消息不断重复入队导致MQ压力增大,可以设置一定的重试次数,可以设置为3次,如果超过3次消息还是处理失败,那么就将失败的消息放到异常交换机中,交给人工处理。
Kafka:
生产者方面:可以采用异步回调的方式发送消息,如果消息发送失败,我们可以获取到失败的消息信息,并考虑重试或者记录好日志,后续再进行补偿。
MQ方面:为了保证消息不在broker丢失,可以利用副本机制,当生产者发送消息的时候,有一个参数acks,我们将它设置为all,只有当leader和所有的follower都确认之后才算消息发送成功,这样就可以最大程度的保证消息不在broker丢失。
消费者方面:kafka消费消息是按照offset偏移量消费的,消费者默认是按期提交已经消费的偏移量,如果出现重平衡的情况,可能会导致重复消费或者消息丢失,所以我们一般会禁用掉这个功能,改为手动提交,当消费者消费成功之后再报告给broker消费的位置,这样就能避免消息丢失和重复消费了。
MQ重复消费问题是怎么出现的?
- 网络问题:由于网络波动导致生产者无法得到消息队列的响应触发重试机制,这样就会导致消息重复发送给消息队列。
- 消费者处理逻辑:
消费者处理完消息之后是自动提交,但是这个时候网断了,MQ无法得知消息已经被处理,所以消息会重新放入队列中发送给消费者。
手动提交,如果处理完消息,没有手动提交消息消费者宕机了,也就出现重复消费的问题。
RabbitMQ解决重复消费问题
业务幂等性:
指的是同一个业务,执行一次或者多次对业务的状态的影响是一致的。
常见的幂等性业务:查询业务,删除业务。
非幂等性业务:用户下单业务,用户退款业务。
RabbitMQ实现业务幂等性:
方案一:给每条消息都设置一个唯一ID,利用id来判断是否是重复消息。
给每条都生成一个唯一id,当消费者处理消息成功后将这个唯一id保存到数据库中,如果再次收到相同消息需要去数据库中判断该id是否存在,如果存在则认为是重复消息放弃处理。
但是这种方式需要在数据表中加入一个唯一id,对业务具有侵入性,并且需要在数据库中进行读写操作,对性能会产生影响,所以这种方案不推荐使用。
方案二:结合业务逻辑,基于业务本身作判断。
比如说一个订单业务,我们要在支付成功后修改订单状态为已支付。当消费者获取到订单消息,需要更新订单支付状态时,先到数据库中查询该订单是否已经支付,如果未支付则更改订单状态,其他情况不做处理。
如何保证消息的顺序性?
RabbitMQ:一个queue对应一个消费者。
Kafka:生产者发送消息的时候指定分区号,或者根据业务使用相同的key,因为kafka是按照key的hashcode值选择分区的,hash值如果一样,那么分区也一样。
RabbitMQ中的死信交换机?
如果消息超时未消费就会变成死信,队列可以绑定一个死信交换机,然后这个交换机可以绑定其他队列,当我们发送消息的时候可以根据业务需求指定超时时间,这样就能完成延迟队列的功能了。
RabbitMQ还有一种方式可以实现延迟队列,那就是在MQ中安装一个死信插件,在声明交换机的时候指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就可以了。
假如有100万条消息堆积在MQ,如何解决?
如果生产者发送消息的速度大于消费者处理的速度,那么就会产生消息堆积,如果队列中满了,那么之后发送的消息就会变成死信,可能会被丢弃,这就是消息堆积问题。
解决办法:
- 可以增加消费者消费同一个队列中的消息。
- 可以在消费者端开启线程池取队列中的消息。
- 使用惰性队列扩大队列容积,因为使用惰性队列会直接将消息存储在磁盘中,当用到的时候再从磁盘中读取到内存中,所以它可以支持数百万条消息的存储。但是它也有缺点,就是磁盘IO操作较多,影响性能。
被丢弃的数据可以自己写个程序查出来,然后重新灌到MQ中,将丢失的数据补回来。
微服务
什么是CAP理论?
CAP理论是分布式领域的一个重要理论,C表示强一致性,A表示可用性,P表示分区容错性。
CAP理论指的是在当前的硬件条件下,一个分布式系统必须要保证分区容错性,而在这个前提下,分布式系统要么保证CP,要么保证AP,无法同时保证CAP。
分区容错性表示,一个系统虽然是分布式的,但是对外看上去是一个整体,不能因为分布式系统中的某个结点挂了,或者网络出现故障而导致系统对外出现异常。所以分布式系统是一定要保证分区容错性的。
强一致性表示,分布式系统中的各个结点应该能够及时地同步数据,并且同步数据的过程中是不对外提供服务的,不然就会造成数据的不一致,所以强一致性和可用性是不能同时保证的。
可用性表示,一个分布式系统要保证对外可用。
RabbitMQ被归类为CP系统,因为它更注重于强一致性和分区容错性。
什么是BASE理论?
由于不能同时出现CAP,所以出现了BASE理论:
BA:表示基本可用,允许一定程度的不可用,比如系统故障导致请求时间变长,或者系统故障导致部分非核心功能不可用,都是允许的。
S:表示分布式系统可以处在一种中间状态,比如正在同步数据。
E:表示最终一致性,不要求分布式系统的数据实时保证一致,允许一段时间后再达到一致,在达到一致的过程中,系统是可用的。
负载均衡算法有哪些?
轮询法:将请求轮流平均的分配到后端服务器上。
加权轮询:根据系统配置以及负载能力给后端服务器分配权重,权重大的接收的请求多,权重小的接收的请求少,这样可以减轻系统的负载。
ip_hash:将客户端的同一个IP通过hash算法定位到后端的同一台服务器上。
url_hash:将同一个url的请求分配到同一台后端服务器上,这样当再次访问的时候,资源就可以在缓存中获取。
最少连接数:因为后台系统的配置不相同,处理请求的速度也不同,这种策略是根据后端服务器当前的连接数量,动态选择当前连接数最少的机器处理请求,尽可能地提高后端服务器的利用效率。
随机法:通过系统的随机算法,根据后端服务器的列表大小值随机选取其中的一台服务器进行访问。随着客户端调用服务器的次数增多,其实际效果越来越接近于轮询的效果。
常见的支持系统高并发高可用的方案有哪些?
- 负载均衡:将请求按照某种负载均衡策略转发到不同的服务器上。
- 使用redis缓存减少数据库压力。
- 数据库读写分离:写操作由写库处理,读操作由读库处理。
- 异步处理:使用消息队列或者其他异步任务将某些耗时的操作转为异步处理,从而减少同步请求的阻塞。
- 分布式架构:将一个系统拆分成多个独立的节点,从而提升系统的扩展性和可维护性。
- 故障转移和容灾:使用高可用的架构,比如主从复制、集群化部署以及跨机房部署等,从而保证一个节点出现故障可以快速地切换到备用节点。
- 水平扩展:增加服务器节点,分担请求的压力。
- 限流和熔断:通过设置限流和熔断规则,避免高并发请求对系统造成过多的压力。
- 监控和预警:通过部署监控系统,对系统的各项指标进行监控,同时设置预警机制,发生问题及时预警。
保证订单状态和返还积分的最终一致性
分布式事务保证最终一致性的方案:目前使用的比较多的就是使用本地消息表搭配mq实现。
当我们创建订单时,同时在本地消息表中记录订单相关信息。(这里使用编程式事务,确保创建订单和增加本地消息记录同时成功或者失败),然后通过一个定时任务去查询本地消息表中符合条件的订单发送到mq中交由积分系统处理。如果mq发送消息成功,则更新对应的本地消息表中的记录同时更新订单状态;如果发送失败,则由下一次定时任务扫描出符合条件的记录发送到mq中。
有一个场景会出现并发问题,就是有一个支付单,这个支付单会有到期的自动关闭,以及用户也可以主动取消订单进行关闭。这两个操作,有的时候就会出现并发的问题,导致重复关单,重复发消息给下游,导致下游处理失败。为了解决这个并发的问题,你在项目该如何来解决。
解决:将修改订单状态的操作放入消息队列中,可以避免消息被重复消费;执行修改订单状态时,尝试去获取redisson的分布式锁,保证同时只能由一个线程去处理消息队列中的任务;同时去数据库中进行更新操作时,也要注意保证业务幂等性,如果订单状态为未支付,则根据是否是超时还是主动取消去修改订单状态。
使用中间件的好处?
提高应用程序的性能
使用缓存中间件redis可以快速的获取目标数据;使用消息中间件可以将一些耗时久的操作转成异步处理,提高程序的响应速度。
简化开发
中间件提供了一些通用的服务和功能,可以使开发人员无需从头开始编写所有的代码,节省了开发人员的时间和精力,提高了开发速度。
- 提高可靠性
中间件提供了各种机制来保证应用程序的可靠性和稳定性,比如使用seata可以管理分布式事务,从而确保数据的一致性。
服务雪崩、服务熔断、服务降级
服务雪崩:A服务调用B服务,B服务调用C服务,这是大量请求达到A服务,A服务能承受住这些请求,但是C服务承受不住导致请求大量堆积,服务不可用,这样就导致B服务请求大量堆积服务不可用,最终导致A服务不可用。解决方式是服务熔断和降级。
服务熔断:当下游请求暂时不可用时,上游服务为了自身不受影响,会不再调用下游服务,直接返回一个结果,从而减轻服务的压力,直到下游服务恢复为止。
服务降级:当发现系统压力过载时,可以通过关闭某个服务,或者限流某个服务来减轻系统压力。
Linux常用命令
ls:查看当前目录下的内容
cd:切换工作目录
clear:清屏
pwd:显示当前工作目录
mkdir:常见目录,配合-p可创建多级目录
touch:创建一个新的空文件
cp:将一个文件或者目录拷贝到另一个文件或者目录中
mv:移动或者重命名文件或者目录
rm -rf:递归删除目录和文件
cat:查看或者合并文件
cat error.log 查看error.log文件
cat text1.txt > text3.txt 先清空test3.txt再把test1.txt test2.txt里面的内容追加到test3里面
cat text1.txt >> text3.txt 把test1.txt里面的内容追加到test3里面
head/tail -f error.log:查看文件的前后几行 {} -行数 文件
find:搜索文件
grep:对文件中的文本内容进行搜索
tar -zcvf/-zxvf:压缩/解压文件
more:分屏显示,当文件内容过多时,可以使用more命令每次只显示一页,按空格显示下一页,按q键退出
linux系统命令:
cal:查看当前日历
date:显示或设置时间
ps -aux | grep java:显示正在运行的进程的信息
top:动态显示正在运行的进程
kill -9 pid:终止进程
df:显示文件系统的整体磁盘使用情况
du:显示指定目录或者文件所占用的磁盘空间大小,默认是显示当前目录或者文件的
ifconfig(7.0以下版本)/ip addr(7.0及以上版本):查看或者配置网卡信息
ping:测试远程主机连通性
java jar包名(不带扩展名)/javac jar包全名称 :运行java程序
防火墙管理:
6.x版本
service iptables status【start、stop】 查看防火墙状态/ 开启、关闭防火墙
7.x版本
firewall-cmd --state 查看防火墙状态
ststemctl stop/start firewalld.service 关闭/开启防火墙
linux网络命令
wget 只下载
yum -y install vim 下载并安装vim编辑器
JWT✅
jwt分为三部分:分别是头部,载荷以及签名。
头部保存令牌类型以及使用的加密算法。
载荷里面是需要传递的数据,包括用户权限,用户信息等,以键值对的方式存在。
签名:负责校验数据的完整性和准确性,它是由头部,载荷以及一个密钥加密而成的,以防止信息被篡改。
Git
常用命令
初始化一个新的Git仓库:
git init克隆远程仓库到本地:
git clone 远程仓库url地址添加文件到暂存区:
git add 文件名提交更改到本地仓库:
git commit -m 描述信息查看当前仓库的状态:
git status查看文件变动的具体内容:
git diff从远程仓库拉取最新版本:
git pull将本地仓库的修改推动到远程仓库:
git push创建一个新的分支:
git branch 分支名称 / git branch 查看当前分支,以及列出所有分支
切换到指定分支:
git checkout 分支名称合并指定分支到当前分支:
git merge 分支名称查看提交历史:
git log
Git如果发生冲突怎么办?
当我们修改文件之后想要提交到远程仓库,这时候就可能发生冲突。我们首先要定位到发生冲突的文件,并手动解决冲突,然后再提交到远程仓库中。
场景题
前端传10G的大文件,应该怎么上传?
将大文件进行分割,分多个文件进行上传,以减少单个文件传输的压力。比如前端可以进行分片上传,后端可以将这些分片的文件进行合并。
压缩上传,在传数据之前对文件进行压缩,后端接收到文件之后再进行解压操作。
其他
一个软件完整的开发流程
总共有5个阶段:
- 需求分析:在这个阶段,开发团队与客户一起讨论和确定软件的功能和需求。这包括采集用户需求,确定系统的功能以及制定项目计划等。
- 设计阶段:在这个阶段,开发团队根据需求分析的结果设计软件的架构和模块。这包括确定系统的组成部分、定义数据结构和算法、设计用户界面等。
- 编码阶段:在这个阶段,开发团队根据设计文档开始编写代码。这包括实现各个模块、进行代码审查等。
- 测试阶段:在这个阶段,开发团队对软件进行各种测试,以确保它的功能和性能符合预期。这包括单元测试、集成测试、系统测试、性能测试等。
- 部署和维护:在这个阶段,软件被部署到生产环境中,并开始正式使用。开发团队还会继续监控和维护软件,修复bug、添加新功能等。
编写接口时,如何考虑接口的性能问题?
- 减少网络延迟:网络延迟是影响接口性能的重要因素。
- 合理设计接口参数:避免使用冗余的参数,尽量做到简洁。
- 使用分页查询:对于返回大量数据的接口,可以使用分页查询的方式,避免一次性返回全部数据。
- 使用异步处理:对于一些耗时的操作,可以使用异步处理,将任务放到队列里,由后台线程进行处理。
- 缓存常用的数据:对于一些查询频繁的数据,可以放到redis缓存中,从而提高接口的响应速度。
- 使用连接池:如果接口需要连接数据库或者其他服务,可以使用连接池,避免频繁的创建和销毁连接。
- 采用负载均衡:对于一些高并发的接口,可以使用负载均衡将请求转发到多台服务器上。
- 监控和优化:对于一些经常被访问的接口,可以对其进行监控,发现影响性能的瓶颈,从而对其进行优化。
正向代理和反向代理的区别?
正向代理代理的是客户端向服务端发送请求,服务端不知道是哪个客户端发送的请求。正向代理用于加速访问(代理服务器同时兼容联通和电信,这样联通卡访问电信网络速度变快了),缓存数据(如果之前被访问过可以直接从本地缓存中读取)。
反向代理代理的是服务端,客服端不知道是哪个服务端接收到的请求。作用有保护服务器的安全以及实现负载均衡。
灰度发布
当出现了新功能之后,先让一部分用户体验,这部分用户体验完没有问题,就会让所有的用户使用新功能,王者的体验服就是这样的。
uuid相较于雪花算法作为主键有什么缺点
- uuid是无意义的,当出现故障后进行故障修复不太方便;而雪花算法生成的id是有意义的,它是由时间戳 + 数据中心标识 + 机器标识 + 序列号 组成,当发生故障后,就可以根据这些信息找到对应记录进行故障修复。
- uuid是无序的,而雪花算法生成的id是有序的,它可以支持更多的场景,比如范围查询,而uuid不支持范围查询。
- 雪花算法生成的id可以保证在不同进程中的不重复,同一进程的id保持自增。
Vue的生命周期
vue实例从创建到销毁的过程就是生命周期,一共有八个部分,分别是创建前后,挂载前后,更新前后以及销毁前后。
beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
created:实例已经创建完成,属性和方法也已经准备好,但是还未挂载到DOM上。
beforeMount:在挂载之前被调用。
mounted:实例被完全挂载到DOM后调用,此时可以进行DOM操作。
beforeupdate:数据更新,但DOM还未更新。
updated:数据更新导致的DOM元素重新渲染完成之后调用。
beforeDestroy:实例被销毁之前调用。
destroyed:实例被销毁之后调用。
created和mounted的区别?
- 时机不同:created是实例创建完成之后,属性和方法已经初始化完成,但是组件的模板还没有渲染到DOM上;而mounted是实例已经完全挂载到DOM上之后,此时可以对DOM元素进行操作。
- 可执行的操作不用:created主要进行一些数据相关的操作,而mounted除了可以对数据进行操作外,还可以直接与页面上的DOM元素进行交互,比如获取元素的尺寸,位置等。
黑马点评项目
优惠券表:
商铺id
标题
使用规则
支付金额
抵扣金额
券的种类:是普通券还是秒杀券
状态:上架、下架和过期
创建时间
更新时间
秒杀券表:
关联的优惠券id
生效时间
失效时间
库存
创建时间
更新时间
抖音探店项目的简单介绍:
抖音作为目前短视频的龙头,里面集成了一系列功能,现在探店风潮的兴起,使得我们也会观看一些探店博主发布的作品,同时也会在平台上抢购优惠券。我作为一个大三学生,对这部分比较感兴趣,所以针对这部分功能进行了模拟,目前已经实现了大部分功能。
我实现的该项目主要就是用户可以登录系统查找附近的店铺,然后去实地消费之后可以针对该店铺发布探店笔记,从而给他人提供参考。同时店铺可以根据自身经营情况去有选择性的提供普通优惠券和秒杀优惠券,用户可以在规定时间内去抢购优惠券,同时在生效时间内去消费。
该项目主要是针对一些常用redis场景进行了模拟,同时解决常见缓存问题和与数据库的数据一致性问题。
秒杀是怎么实现的?
先去判断秒杀券是否在规定的时间范围内,再去判断库存有没有,如果有的话,则尝试去获取分布式锁,锁的粒度是用户id,先判断该用户id是否在订单表中存在,如果存在则抛异常,没有则去尝试扣减库存,这里使用乐观锁,当去更新库存的时候判断库存是否大于0,如果大于0则成功扣减(redis单线程加lua脚本保证原子性),否则直接返回库存不足。扣减并生成订单之后释放分布式锁。
12306系统
会员表:手机号
乘客表:会员id(关联会员表,谁添加的乘客)、姓名、身份证、旅客类型
车票表:会员id,乘客id,乘客姓名(冗余字段,打印车票的时候好打印)、日期、车次、厢序、行号、列号、出发站、出发时间、终点站、到达时间、座位类型(一等座,二等座)。
车次表:车次编号、车次类型(火车、高铁)、出发站、出发站拼音、出发时间、终点站、终点站拼音、到达时间
余票表:日期,车次编号,出发站,出发站拼音,出发时间,出发站序,到达站,到达站拼音,到达时间,到站站序、 一等座(二等座,硬卧,软卧)余票数量,一等座(二等座,硬卧,软卧)票价 *额外字段:*新增时间,修改时间
简单介绍:
火车售票是一个经典的持续高并发场景,我作为学生,希望能够在解决高并发高可用方面有一些经验,所以借鉴了12306这个项目,我们知道在春运或者国庆假期这种人流量巨大的情况下,12306需要承受巨大的流量,它之所以不被压垮,是因为它在前端,后端以及数据库等方面都做了一些相应的措施。
我模仿12306系统,根据角色将系统划分成管理端和会员端,管理端负责日常车次的维护以及车票的维护工作,会员端主要功能有登录注册,管理自身信息,管理乘客信息,购票以及自己已购买车票的查询。我针对购票这个核心业务做了令牌大闸,用令牌数模拟余票数量,同时使用异步处理,分布式锁,限流等对该功能的性能进行了优化,使得该业务的吞吐量得到了显著的提升。
Spring Security
这个框架有两个强大的功能,就是认证和授权。
认证和授权流程:
前端传入用户名和密码,然后UsernamePasswordAuthenticationFilter(奥森 tei ken shen ) 将用户名和密码作为构造参数封装成一个Authentication对象,接着会调用AuthenticationManager.authenticate方法进行认证,认证是去调用userDetailsService中的loadUserByUsername方法去查询用户信息,如果查不到,就抛该用户未注册的异常,查到了则根据用户类型分配对应的角色和权限,角色的话前面需要加上ROLE_前缀,将用户信息和权限信息封装成UserDetails对象返回。
通过PasswordEncoder校验UserDetails中的密码和Authentication中的密码是否一致,如果一致则将UserDetails封装到Authentication对象中。
返回Authentication对象,然后把自定义一个AuthenticationTokenFilter,把他放在UsernamePasswordAuthenticationFilter的前面,目的就是为了直接在redis缓存中查看是否有对应的token,如果有,则在redis(存入UserDetails对象,这个对象有用户信息和权限)中查出用户信息和权限封装成Authentication对象存入SecurityContextHolder上下文中,没有则认证失败,需要重新登录。
后续在对应的方法上加上 @PreAuthorize注解就可以限制用户是否有权限去操作这个方法了。
这里查用户和权限也可以使用 RBAC权限模型,通过在数据库中添加对应的资源和角色表进行联查赋予对应的权限。
同时,SecurityConfig也可以统一限制指定权限的用户访问某些方法。