java基础
基本数据类型
数据类型 | 关键字 | 字节 | 取值范围 | 默认 | 包装类 |
---|---|---|---|---|---|
整数 | byte | 1 | -2^7 – 2^7-1 (-128 – 127) | 0 | Byte |
short | 2 | -2^15 – 2^15-1 ( -32768 – 32767) | 0 | Short | |
int(默认) | 4 | -2^31 – 2^31-1 | 0 | Integer | |
long | 8 | -2^63 – 2^63-1 | 0L | Long | |
浮点 | float | 4 | 负数:-3.402823E+38到-1.401298E-45 正数: 1.401298E-45到3.402823E+38 | 0.0f | Float |
double (默认) | 8 | 负数:-1.797693E+308到-4.9000000E-324 正数:4.9000000E-324 到1.797693E+308 | 0.0d | Double | |
字符 | char | 2 | \u0000 – \uffff (0-65535) | \u0000’ (空格) | Character |
布尔 | boolean | 1 | true,false | FALSE | Boolean |
数据范围小的可以直接赋值给数据范围大的,数据范围大的不可以直接赋值给数据范围小的,需要进行类型转换
- 定义long类型的变量时,需要在整数的后面加L(大小写均可,建议大写)。因为整数默认是int类型,整数太大可能超出int范围。
- 定义float类型的变量时,需要在小数的后面加F(大小写均可,建议大写)。因为浮点数的默认类型是double, double的取值范围是大于float的,类型不兼容。
e+38表示是乘以10的38次方,同样,e-45表示乘以10的负45次方。
== 和 equals的区别:
== 应用在普通数据类型时,比较值是否相等,应用在引用数据类型时,比较地址是否相等
equals 应用在引用数据类型,未重写时, 比较两个对象地址是否相等(不是内容)。重写equals方法时,就按照重写的方式比较两个对象是否相等,重写equals方法同时必须还要重写hashCode方法,为什么呢?
- hashCode是用于查找位置使用的,而equals是用于比较两个对象的是否相等的
- hashCode() 的作用是获取哈希码,也称为散列码,它返回的就是根据对象的内存地址换算出的一个值;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象(比如hashSet)在哈希表中的索引位置。
如果两个对象相等,则 hashcode 一定也 是相同的。但是hashcode 相同,类却不一定相同,所以重写 equals 时必须重写 hashCode
就好比说,我在衣柜里找两件相同的衣服,第一步我肯定是通过hash值确定在衣柜的那个盒子里,第二部才是使用equal比较衣服一样吗
内部类分类
-
成员内部类
- 静态成员内部类
- 非静态成员内部类
-
局部内部类
-
匿名内部类
- 前提:存在一个类或者接口,这里的类可以是具体类也可以是抽象类
- 本质:是一个继承了该类或者实现了该接口的 **子类匿名对象,**不是创建了接口的对象
- 格式:
new 接口名(){ 重写接口方法 }
- 高级用法:Lambda表达式=》jdk8新语法,简化匿名内部类的写法,注意:只能简化函数式接口的匿名内部类的写法形式
- 什么是函数式接口?必须是接口,并且只有一个抽象方法的形式,同时会在接口上加上一个@FunctionalInterface注解,标记该接口必须满足函数式接口
- 格式:
接口名 aa = (参数列表) -> { }
请简述一下面向对象(OOP)的特征
**封装:**类中将资源(全局变量、方法)私有化,使用private关键字实现,隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。提高数据的安全性,提高代码整洁度,提高代码的复用性,降低了程序之间的耦合性
**继承:**发生在两个类中的关系,一个类继承另一个类中具有访问权限的属性(成员变量)和行为(方法),使用extends关键字实现,只能单继承,提高了代码的复用性,并且可以实现子类对父类进行扩展和修改(方法的重写)
**多态:**多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法。这样当同一个行为(方法调用)执行时可以根据不同对象,当有父类引用指向子类对象(上转型对象), 此为多态 。(继承,重写,父类引用指向子类对象)
方法重载/方法重写的规则?
**重载:**发生在一个类中,方法名相同,参数列表不同(类型,个数,顺序) ,与访问修饰符,返回值无关
**重写:**发生在继承类中,方法名相同,参数列表和返回值 相同
- 构造方法不能被重写;声明为 final 的方法不能被重写;声明为 static 的方法不存在重写(重写和多态联合才有意义);访问权限不能比父类更低;重写之后的方法不能抛出更宽泛的异常
Java 创建对象的几种方式?
new 创建新对象;通过反射机制;采用 clone 机制;通过序列化机制
什么叫抽象?
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两 方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
接口组成
1、 静态常量:public static final
2、 抽象方法:public abstract
3、 默认方法(Java8):default
4、 静态方法(Java8):static
5、 私有方法(Java9):private或者private static
String str=new String(“a”+“b”)创建了几个对象
四个
- "a"在字符串常量池创建,这是第一个对象
- "b"在字符串常量池创建,这是第二个对象
- “a”+"b"组合"ab"在字符串常量池创建,这是第三个对象
- new String(),在堆中分配内存,里面存着这字符串"ab"在字符串常量池中的地址,这是第四个对象。str 存储在栈中,里面存储着指向堆中new String()的地址
String、StringBuilder、StringBuffer 区别?
String 字符串常量 不可变 线程安全 使用字符串拼接时是不同的 2 个空间
StringBuffer 字符串变量 可变 线程安全 字符串拼接直接在字符串后追加
StringBuilder 字符串变量 可变 非线程安全 字符串拼接直接在字符串后追加
执行效率: StringBuilder > StringBuffer > String.
java.lang.String 是一个常量,是不可变的,所以对于每一次+=赋值都会创建一个新的对象, StringBuffer 和 StringBuilder 都是可变的,当进行字符串拼接时采用 append 方法,在原来的基础上进行追加,所以性能比 String 要高,又因为 StringBuffer 是线程安全的而 StringBuilder 是线程非安全的,所以 StringBuilder 的效率高于StringBuffer.
对于大数据量的字符串的拼接,采用 StringBuffer,StringBuilder.
Object 中有哪些公共方法?
-
clone:是一个native方法,native方法的效率一般来说都是远高于Java中的非native方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常
-
equals:Object中与==是一样的,子类一般需要重写该方法
-
hashCode();返回对象的哈希代码值
-
getClass(); 返回此对象的运行时类。
-
toString(); 返回对象的字符串表示形式。
-
wait():使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。一直等待,直到获得锁或者被中断
-
wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。 调用该方法后当前线程进入睡眠状态,直到以下事件发生:
- 其他线程调用了该对象的notify方法
- 其他线程调用了该对象的notifyAll方法
- 其他线程调用了interrupt中断该线程
- 时间间隔到了 ,此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常
-
notify(); 唤醒正在等待此对象监视器的单个线程。如果有线程正在等待这个对象,那么会选择唤醒其中一个线程。选择是任意的,并由实施者自行决定。线程通过调用其中一个等待方法来等待对象的监视器。
-
notifyAll(); 唤醒正在等待此对象监视器的所有线程。线程通过调用其中一个等待方法来等待对象的监视器。
什么是Java中的异常?
异常就是有异于常态,和正常情况不一样,有错误出现。在java中,阻止当前方法或作用域的情况,称之为异常。异常可能来自不同类型的情况,例如用户输入的错误数据,硬件故障,网络连接故障等。
每当执行java语句时发生任何错误,都会创建一个异常对象,然后JRE会尝试查找异常处理程序来处理异常。如果找到合适的异常处理程序,则将异常对象传递给处理程序代码以处理异常,称为捕获异常。如果未找到处理程序,则应用程序将异常抛出到运行时环境,JRE将终止该程序。
Java异常处理框架仅用于处理运行时错误,编译时错误不由异常处理框架处理。
解释Java异常层次结构?
Java异常是分层的,继承用于对不同类型的异常进行分类。如下
Java中所有异常,都继承自java.lang.Throwable类。
异常主要分为:错误、编译时异常(受控异常)、运行时异常(非受控异常)
- Error:系统级别问题,是程序中无法处理的错误
- Virtual MachineError(虚拟机运行错误)
- NoClassDefFoundError(类定义错误)
- OutOfMemoryError是java.lang.VirtualMachineError的子类,当JVM用完堆内存时,它会抛出它。我们可以通过提供更多内存来通过java选项运行java应用程序来修复此错误。
- Exception:程序本身可以捕获并且处理的异常
- 编译时异常:编译期间出现的异常,必须处理异常,否则编译不通过
- IOException :操作输入流和输出流时可能出现的异常;IOexception 表示发生了某种 I / O 异常的信号。此类是由失败或中断的 I / O 操作产生的一般异常类
- ClassNotFoundException 找不到类异常
- SQLException 提供有关数据库访问错误或其他错误的信息的异常。
- FileNotFoundException 当试图打开指定路径名表示的文件失败时,抛出此异常。
- EOFException 当输入过程中意外到达文件或流的末尾时,抛出此异常。
- 运行时异常:运行期间可能出现的异常,不要求处理异常
- NullPointerException 空指针异常
- ArithmeticException 数学操作异常
- NumberFormatException 数字转换异常
- IndexOutOfBoundsException 数组索引越界异常
- ClassCastException(类型转换异常类)
- IllegalArgumentException(非法参数异常)
- 编译时异常:编译期间出现的异常,必须处理异常,否则编译不通过
将三种异常类型再次划分,将派生于Error或者RuntimeException的异常称为unchecked异常 (非受检异常),所有其他的异常(CheckedException)成为checked异常(受检异常)。
Java中的异常处理关键字是什么?异常处理
java异常处理中使用了
- throw:throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常。
- throws:throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。throws 表示出现异常的一种可能性,并不一定会发生这种异常。
- try-catch:我们在代码中使用try-catch块进行异常处理。try是块的开始,catch是在try块的末尾处理异常。我们可以使用try有多个catch块,try-catch块也可以嵌套。catch块需要一个应该是Exception类型的参数。
- finally:finally块是可选的,只能用于try-catch块。由于异常会暂停执行过程,因此我们可能会打开一些不会关闭的资源,因此我们可以使用finally块。finally块总是被执行,无论是否发生异常
格式:try-catch-finally、throw、throws、try-with-resources(io中使用)
try-catch-finally-return执行顺序
执行,并且finally的执行早于try里面的return
- 不管是否有异常产生,finally块中代码都会执行;
- 当try和catch中有return语句时,finally块仍然会执行;
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
谈谈final,finally,finalize的区别
-
被 final 修饰的类不可以被继承,被 final 修饰的方法不可以被重写,被 final 修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变.被 final 修饰的方法,JVM 会尝试将其内联,以提高运行效率,被 final 修饰的常量,在编译阶段会存入常量池中.
-
finally是异常处理语句结构的一部分,表示总是执行,通常最终也会用来关闭资源。
-
finalize是Object类的一个方法,当垃圾回收器 要回收对象所占内存之前,会调用 被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等
finalize()的调用具有不确定行,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)。
Java异常类的重要方法是什么?
异常及其所有子类不提供任何特定方法,并且所有方法都在基类Throwable中定义。
- String getMessage() - 此方法返回消息String of Throwable,并且可以在通过构造函数创建异常时提供消息。
- String getLocalizedMessage() - 提供此方法,以便子类可以覆盖它以向调用程序提供特定于语言环境的消息。此方法getMessage()的可抛出类实现只是使用方法来返回异常消息。
- synchronized Throwable getCause() - 此方法返回异常的原因或null id,原因未知。
- String toString() - 此方法以String格式返回有关Throwable的信息,返回的String包含Throwable类和本地化消息的名称。
- void printStackTrace() - 此方法将堆栈跟踪信息打印到标准错误流,此方法已重载,我们可以将PrintStream或PrintWriter作为参数传递,以将堆栈跟踪信息写入文件或流。=>打印的数据和正常报错信息一样
如何在Java中编写自定义异常?
我们可以扩展Exception类或其任何子类来创建我们的自定义异常类。自定义异常类可以拥有自己的变量和方法,我们可以使用它们将错误代码或其他与异常相关的信息传递给异常处理程序。
//1,定义异常类
// 2,继承RuntimeException或其他异常类
class MyException extends RuntimeException{
// 3,空参构造
public MyException(){}
// 4,有参构造
public MyException(String message){
//super(message);
System.out.println(message + "=======");
}
}
集合体系图
List , Set 和 map 比较,各自的子类比较
List 底层为数组结构,有序的可重复集合,元素可以重复因为每个元素有自己的角标(索引),可以在任意位置增加删除元素,用 Iterator 实现单向遍历,也可用ListIterator 实现双向遍历。
- ArrayList:底层的数据结构是数组结构,特点是:查询很快,增 删 稍微慢点,线程不同步 。
- LinkedList:底层使用的是双向链表数据结构,特点是:增 删很快,查询慢。
- Vector:底层是数组数据结构,线程同步,被 ArrayList 代替了,现在用的只有他的枚举。
Set:底层是红黑树,无序的不包含重复元素的集合(用对象的 equals() 方法来区分元素是否重复),set 中最多包含一个 null 元素,只能用 Iterator 实现单项遍历,线程不同步 。
- HashSet:底层是哈希表数据结构。HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的值已经存在时返回 false。根据 hashCode 和 equals 方法来确定元素的唯一性
- TreeSet:底层的数据结构是二叉树,可以对 Set 集合中的元素进行排序(自然循序),也可以自己写个类实现 Comparable 或者 Comparator 接口,定义自己的比较器,将其作为参数传递给 TreeSet 的构造函数。
Map:JDK1.7及之前:数组+链表,JDK1.8:数组+链表+红黑树(当链表长度到 8 时,转化为红黑树)。这个集合是存储键值对的, 而且要确保键的唯一性
- HashTable:底层是哈希表(散列值)数据结构,不可以存入 null 键和 null 值,该集合线程是同步的,效率比较低。出现于 JDK1.0 。
- HashMap:底层是哈希表数据结构,可以存入 null 键和 null 值,线程不同步,效率较高,代替了 HashTable,出现于 JDK 1.2 。
- TreeMap:底层是二叉树数据结构,不可以存入 null 键但是可以存入null 值,线程不同步,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。
TreeMap 可以保证顺序,HashMap 不保证顺序,即为无序的,Map 中可以将 Key 和 Value 单独抽取出来,其中 KeySet()方法可以将所有的 keys 抽取成一个 Set,而 Values()方法可以将 map 中所有的 values 抽取成一个集合。
HashSet如何检查重复
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用**equals()**方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
Collection 和 Collections 的区别?
Collection 是集合类的上级接口,继承与他的接口主要有 Set 和 List.
Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
Comparable 和 Comparator 接口的区别?
Comparable和Comparator接口都是实现集合中元素的比较、排序的,众所周知,诸如Integer,double等基本数据类型,java可以对他们进行比较,而对于类的比较,需要人工定义比较用到的字段比较逻辑。可以把Comparable理解为内部比较器,而Comparator是外部比较器。
- Comparable 是一个对象本身就已经支持自比较所需要实现的接口,如String、Integer自己就实现了Comparable接口,可完成比较大小操作
- Comparable 接口只包含一个 compareTo()方法。这个方法可以个给两个对象排序。具体来说, 它返回负数,0,正数来表明输入对象小于,等于,大于已经存在的对象。
- Comparator 接口通过 compare()排序
IO流的分类
字节流(byte) | 字符流(char) | |||
---|---|---|---|---|
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
抽象基类 | InputStream | OutputStream | Reader | Writer |
操作文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
操作数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象操作流 | ObjectInputStream | ObjectOutputStream | ||
打印流 | PrintStream | PrintWriter |
另外,字符缓冲流还有两个独特的方法:
- BufferedWriter类newLine() :写入一个行分隔符。这个方法会自动适配所在系统的行分隔符。
- BufferedReader类readLine() :读取一个文本行。
注意:输出流必须刷新才能写入,当然也可以直接调用
close()
,他里面包含了flush
,但是使用close()
关闭流后就不能在使用了如上,常用方法
- 输入流都有read方法
- 输出流都有write,flush方法
什么是缓冲区?有什么作用?
缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性。对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。
字节流和字符流的区别?
字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直 接操作文本本身的,而**字符流的操作会先经过缓冲区(**内存)然后通过缓冲区再操作文件以字节为单位输入输出数据,字节流按照 8 位传输
以字符为单位输入输出数据,字符流按照 16 位传输
如何实现对象克隆?
有两种方式:
- 实现 Cloneable 接口并重写clone()方法;=>浅克隆
- 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
- private static final long serialVersionUID = 6957528274628957691L; => 来验证的版本一致的
什么是java序列化,如何实现java序列化?
对象序列化:将对象以二进制的形式保存到硬盘上;
反序列化:将二进制文件转化为对象读取.
序列化的实现:将需要被序列化的类实现 Serialize 接口,没有需要实现的方法,此接口只是为了标注对象可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,再使用 ObjectOutputStream 对象的 write(Object obj)方法就可以将参数 obj 的对象写出
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
进程和线程的区别
进程是操作系统资源分配的基本单元。线程作为调度和分配的基本单位,它被包含在进程之中,是进程中的实际运作 单位
- 进程:是正在运行的程序
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
一个程序运行后至少有一个进程,一个进程可以包含多个线程,但是至少有一个线程
进程间不能共享数据段地址,但是同进程的线程之间可以
举例说明,ctrl+alt+delete打开任务管理器,运行的每个软件都是一个进程,软件里中的例如360能够同时执行几个任务
如何在Java中实现线程?实现多线程有几种方式?
方式 | 优点 | 缺点 |
---|---|---|
继承Thread接口 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
实现Runnable接口 | 扩展性强,可以继续继承和实现其他的类 | 不能返回线程执行的结果 |
实现Callable接口配合FutureTask | 扩展性强,可以继续继承和实现其他的类。可以得到线程执行的结果 | 编程相对复杂 |
Callable 和 Runnable 的区别是什么?
两者都能用来编写多线程,但实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果.Callable 通常需要和Future/FutureTask 结合使用,
什么是 FutureTask?
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算,用于获取异步计算结果.。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完 成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包 装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。
Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。
线程状态/线程生命周期
线程的几种状态?
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取 CPU 的使用权。即在就绪状态的进程除 CPU 之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
-
等待阻塞:运行的线程执行 wait()方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify() 或 notifyAll()方法才能被唤醒,
-
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM会把该线程放入“锁池”中。
-
其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时, 线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
什么是线程安全?什么是线程同步?线程同步的实现方式
线程安全:多个线程同时操作一个共享资源的时候可能会出现业务安全问题,称为线程安全问题
线程同步:为了解决线程安全的问题。线程同步的核心思想:加锁,把共享资源上锁,每次只能让一个线程进入访问,访问完毕之后解锁,然后其他线程才能进来,实现方式如下
-
同步代码块
synchronized(任意对象) { 多条语句操作共享数据的代码 }
-
同步方法
- 普通同步方法:
修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体;}
=> 锁对象是this
- 静态同步方法:
修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体;}
=> 锁对象是类名.class
- 普通同步方法:
-
同步锁(ReentrantLock锁;互斥锁):虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
常用方法:
-
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 void lock() 获得锁 void unlock() 释放锁
-
-
synchronized 和 ReentrantLock的区别?
- Lock是一个接口,而Synchronized是关键字。
- Synchronized会自动释放锁,而Lock必须手动释放锁。
- Lock可以让等待锁的线程响应中断,而Synchronized不会,线程会一直等待下去。
- Synchronized能锁住类、方法和代码块,而Lock是块范围内的
- 通过Lock可以知道线程有没有拿到锁,而Synchronized不能。
主要相同点:Lock 能完成 synchronized 所实现的所有功能
主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。
线程同步的方法?
wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒 某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是 让它们竞争。
死锁是什么,产生死锁的条件?
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
什么情况下会产生死锁
- 互斥条件:任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。而只能由获得该资源的进程资源释放。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生
在实际使用中, 是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。, 例如:线程在获得了锁 A 并且没有释放的情况下去申请锁 B,这时,另一个线程已经获得了锁 B,在释放锁 B 之前又要先获得锁 A,因此闭环发生,陷入死锁循环。
所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的 所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用 其它对象的延时方法和同步方法。
怎样尽量避免死锁的出现?
- 设置获取锁的超时时间,至少能保证最差情况下,可以退出程序,不至于一直等待导致死锁;
- 设置按照同一顺序访问资源,类似于串行执行;
- 避免事务中的用户交叉;
- 保持事务简短并在一个批处理中;
- 使用低隔离级别;
- 使用绑定链接。
volatile 的理解?
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
-
可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这 新值对其他线程来说是立即可见的。
- 使用 volatile 关键字会强制将修改的值立即写入主存;
- 使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU 的 L1 或者 L2 缓存中对应的缓存行无效);
- 由于线程 1 的工作内存中缓存变量stop 的缓存行无效,所以线程 1 再次读取变量 stop的值时会去主存读取。
-
禁止进行指令重排序。
使用 volatile 关键字的场景?
synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile 关键字在某些情况下性能要优于 synchronized,但是要注意 volatile 关键字是无法替代synchronized 关键字的,因为volatile 关键字无法保证操作的原子性。通常来说,使用volatile 必须具备以下 2 个条件:
1) 对变量的写操作不依赖于当前值
2) 该变量没有包含在具有其他变量的不变式中
说一下线程之间是如何通信的?
线程之间的通信有两种方式:共享内存和消息传递。
共享内存:在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
- 线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
- 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
消息传递:在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。
线程通信-线程 B 怎么知道线程 A 修改了变量?
synchronized 修饰修改变量的方法
volatile 修饰变量;
wait/notify while 轮询
Java 中如何停止一个线程?
Java 提供了很丰富的 API 但没有为停止线程提供 API。JDK 1.0 本来有一些像 stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的 JDK 版本中他们被弃用了, 之后 Java API 的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当 run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,所以最好定义一个变量判断关闭就行
Java 中 notify 和 notifyAll 有什么区别?
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等 待的时候它才有用武之地。而 notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
wait 和 sleep 的区别?
sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程, 但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。
wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
常见线程场景
场景 | 方法名 | 说明 |
---|---|---|
线程休眠 | public static void sleep() | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
线程优先级 | final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 | |
守护线程 | void setDaemon(boolean on) | 守护线程是指为其他线程服务的线程。 与守护线程相对应就是用户线程,用户线程可以理解为我们平常创建的普通线程,而守护线程守护的就是用户线程。当用户线程全部执行完毕,守护线程会跟着结束。 也就是说守护线程必须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,那么守护线程自然会退出。 |
礼让线程/出让线程 | yield() | 线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。 |
插队线程/插入线程 | join() | 线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程的所有任务。 |
生产者消费者-等待唤醒机制 | void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 | |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
守护线程是什么?它和非守护线程的区别?
JAVA中的线程主要分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。JAVA语言中无论是线程还是线程池,默认都是用户线程,因此用户线程也被称为普通线程。守护线程也被称之为后台线程、服务线程或精灵线程,守护线程是为用户线程服务的,当线程中的用户线程都执行结束后,守护线程也会跟随结束。守护线程具有自动结束生命周期的特性,而非守护线程则不具备该特性。
守护线程:当主线程执行结束后,所有在运行的子线程都直接结束,不管子任务是否完成。线程可以通过 setDaemon(true) 方法将线程类型更改为守护线程
场景:程序运行完毕,jvm 会等待非守护线程完成后关闭,但是 jvm 不会等待守护线程.守护线程最典型的例子就是 GC 线程.
线程阻塞有哪些原因?
1、sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试, 直到条件满足为止
2、suspend() 和 resume() 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地, suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
3、yield() 使当前线程放弃当前已经分得的 CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程
4、wait() 和 notify() 两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式, 一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用.
什么是乐观锁和悲观锁?
1) 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁 认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作 尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2) 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观 锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
什么是反射?反射的作用?
反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为 Java 反射机制。
获取 class 的三种方式?
类名.class属性;对象名.getClass()方法;Class.forName(全类名)方法
Java 的内存划分?
-
程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计 数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。(唯一不会抛出 OutOfMemoryError)
-
本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。
-
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。
前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈 帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的 新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对Java 栈的操作只有两个,就是对栈帧的压栈和出栈。
栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出 的定义等。
-
堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。( 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配)
理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行 进一步的细分,最有名的就是新生代、老年代的划分。
-
方法区(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。 Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。
-
运行时常量池(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息 就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。
什么是 Java 虚拟机?为什么 Java 被称作是无关平台的编程语言?
Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。 Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
双亲委派模型?
双亲委派模型工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不 到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
呢么为什么需要双亲委派模型?
防止内存中出现多份同样的字节码
Java 中类的生命周期是什么?
-
加载,查找并加载类的二进制数据,在 Java 堆中也创建一个 java.lang.Class 类的对象
-
连接,连接又包含三块内容:验证、准备、初始化。
1)验证,文件格式、元数据、字节码、符号引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3) 解析,把类中的符号引用转换为直接引用
-
初始化,为类的静态变量赋予正确的初始值
-
使用,new 出对象程序中使用
-
卸载,执行垃圾回收
说一下你熟悉的设计模式?
单例模式:保证被创建一次,节省系统开销。
工厂模式(简单工厂、抽象工厂):解耦代码。
- 工厂方法:用来生产同一等级结构中的固定产品,支持增加任意产品。
- 简单工厂:用来生产同一等级结构中的任意产品,对于增加新的产品,无能为力。
- 抽象工厂:用来生产不同产品族的全部产品,对于增加新的产品,无能为力;支持增加产品 族。
观察者模式:定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有的 依赖者都会收到通知并自动更新。
外观模式:提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层的接 口,让子系统更容易使用。
模版方法模式:定义了一个算法的骨架,而将一些步骤延迟到子类中,模版方法使得子类可 以在不改变算法结构的情况下,重新定义算法的步骤。
状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
UML 是什么?
统一建模语言(Unified Modeling Language,UML)是用来设计软件蓝图的可视化建模语言, 1997 年被国际对象管理组织(OMG)采纳为面向对象的建模语言的国际标准。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
统一建模语言能为软件开发的所有阶段提供模型化和可视化支持。而且融入了软件工程领域 的新思想、新方法和新技术,使软件设计人员沟通更简明,进一步缩短了设计时间,减少开 发成本。它的应用领域很宽,不仅适合于一般系统的开发,而且适合于并行与分布式系统的 建模。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
数据库相关
数据库的分类
关系型数据库:使用二维表格来存储数据;
- Oracle:美国Oracle(甲骨文)公司,用于电信、金融行业,下载免费,服务收费
- DB2:美国IBM,主要用于金融领域
- Sql server:微软,只能用于微软平台
- Mysql:瑞典NySQL AB开发,属于oracle旗下产品,分为社区版和收费版,使用广泛
非关系型数据库:多数使用哈希表,表中以键值(key-value)的方式实现特定的键和一个指针指向的特定数据。
- ElasticSearch:分布式全文检索引擎
- Redis(Remote Dictionary Server ),即远程字典服务,用于做缓存
非关系型数据库和关系型数据库区别,优势比较
非关系型数据库的优势:
-
性能:NOSQL 是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过 SQL 层的解析,所以性能非常高。
-
可扩展性:同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。
关系型数据库的优势:
- 复杂查询:可以用 SQL 语句方便的在一个表以及多个表之间做非常复杂的数据查询。
- 事务支持:使得对于安全性能很高的数据访问要求得以实现。
SQL分类
DDL(Data definition language,数据定义语言):操作数据库和数据表结构,关键字:create、drop、alter
DML(Data Manipulation language,数据操作语言):对数据表中的的内容进行增删改,关键字insert、delete、update
DQL(Data query language,数据查询语言):对数据表中的数据就行查询,关键字:select, where 等
TPL(Transaction process language,事务处理语言):对事务进行处理
DCL(Data control language,数据控制语言):定义数据库访问权限相关(创建用户、用户授权);关键字:GRANT, REVOKE 等
MySql 的存储引擎有哪些,区别是什么?MySQL 的 MyISAM 与 InnoDB 两种存储引擎在,事务、锁级别,各自的适用场景?
MySQL 常见的存储引擎为 InnoDB、MyISAM 。
事务处理上方面:InnoDB 支持事务安全,MyISAM 和 MEMORY 两个不支持。
-
MyISAM:强调的是性能,每次查询具有原子性,其执行数度比 InnoDB 类型更快,但是不提供事务支持。
-
InnoDB:提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback) 和崩溃修复能力(crash recovery capabilities) 的事务安全(transaction-safe (ACID compliant))型表。对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
锁级别
-
MyISAM:只支持表级锁,用户在操作 MyISAM 表时,select,update,delete,insert 语句都会给表自动加锁,如果加锁以后的表满足 insert 并发的情况下,可以在表的尾部插入新的数据。
-
InnoDB:支持事务和行级锁,是 innodb 的最大特色。行锁大幅度提高了多用户并发操作的新能。但是 InnoDB 的行锁,只是在 WHERE 的主键是有效的,非主键的 WHERE 都会锁全表的。
⭐SQL 优化的具体操作
- 尽量避免使用
select *
返回无用的字段会降低查询效率。优化方式:使用具体的字段代替·*
,只返回使用到的字段。 - 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
- 尽量避免使用 in 和 not in,会导致数据库引擎放弃索引进行全表扫描。 优化方式:如果是连续数值,可以用 between 代替。如果是子查询,可以用 exists 代替。
- 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。 优化方式:尽量在字段后面使用模糊查询。
- 尽量避免进行 null 值的判断,会导致数据库引擎放弃索引进行全表扫描。优化方式:可以给字段添加默认值 0,对 0 值进行判断。
- 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
⭐事务的四个特性(简称ACID)
什么是事务?: 多条sql语句,要么全部成功,要么全部失败。
- 原子性(Atomicity):事务中所有操作作为一个整体,是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败,如果操作失败则不能对数据库有任何影响。。
- 一致性(Consistency):事务执行后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
- 隔离性(Isolation):隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
- 持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。
⭐事务的并发问题?事务并发读问题?
- 脏读
- 在事务A执行过程中,事务A对数据资源进行了修改,事务B读取了事务A修改后的数据;由于某些原因,事务A并没有完成提交,发生了回滚操作,则事务B读取的数据就是脏数据。
- 这种读取到另一个事务未提交的数据的现象就是脏读。
- 不可重复读
- 事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致;
- 这种在同一个事务中,前后两次读取的内容不一致的现象就是不可重复读。
- 幻读:幻读解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的 状态(一致性)。
- 事务A查询出一些数据,事务B插入或者删除一些数据;这时候真实的数据已经发生变化,但是事务A查询不到这种变化,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
- 这种在同一个事务中,前后两次读取的数量不同的现象就是幻读
⭐如何解决事务并发读问题?说说事务的隔离级别?
四大隔离级别: 为了解决事务并发读问题,主流的关系型数据库都会提供四种事务的隔离级别。事务隔离级别从低到高分别是:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)【默认】、串行化(SERIALIZABLE)
- 读未提交(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
- 读已提交(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
- 可重复读(Repeated Read):在同一个事务内的查询都是事务开始时刻一致的,Mysql的InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读(多个事务同时修改同一条记录,事务之间不知道彼此存在,当事务提交之后,后面的事务修改的数据将会覆盖前事务,前一个事务就像发生幻觉一样)
- 可串行化(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 |
读已提交(READ COMMITTED) | 不可能 | 可能 | 可能 |
可重复读(REPEATABLE READ)(默认) | 不可能 | 不可能 | 可能 |
串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
什么是内连接、外连接、交叉连结、笛卡尔积等?
内连接(NNER JOIN ON): 只连接匹配的行
左外连接: 包含左边表的全部行(不管右边的表中是否存在与它们匹配的行),以及右边表中全部匹配的行
右外连接: 包含右边表的全部行(不管左边的表中是否存在与它们匹配的行),以及左边表中全部匹配的行
全外连接(UNION): 包含左、右两个表的全部行,不管另外一边的表中是否存在与它们匹配的行。
交叉连接(CROSS JOIN): 生成笛卡尔积-它不使用任何匹配或者选取条件,而是直接将一个数据源中的每个行与另一个数据源的每个行都一一匹配
count(*)、count(1)、count(column)的区别
count(*)对行的数目进行计算,包含 NULL
count(1)这个用法和 count(*)的结果是一样的。现代 SQL 优化器通常会将 COUNT(1)
和 COUNT(*)
优化为相同的执行计划,因此执行速度非常快。
count(column)对特定的列的值具有的行数进行计算,不包含 NULL 值。
MySQL 中 varchar 与 char 的区别?varchar(30) 中的 30代表的涵义?
- varchar 与 char 的区别,char 是一种固定长度的类型,varchar 则是一种可变长度的类型。
- 对效率要求高用 char,对空间使用要求高用 varchar。
int(11) 中的 11 代表什么涵义?
int(11) 中的 11,不影响字段存储的范围,只影响展示效果。
数据库的五大范式、三大范式
列不可再分
一个表中只能保存一种数据
确保每列都和主键列直接相关,而不是间接相关
- (确保每列保持原子性)所有字段值都是不可分解的原子值。=>列不可再分
- 第一范式是最基本的范式。合理遵循需要根据系统的实际需求来定。
- 比如某些数据库系统中需要用到“地址” 这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访 问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细 地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。这样设计才算 满足了数据库的第一范式
- (确保表中的每列都和主键相关)在一个数据库表中,**一个表中只能保存一种数据,**不可以把多种数据保存在同一张数据库表中。
- 第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和 主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号 作为数据库表的联合主键。
- (确保每列都和主键列直接相关,而不是间接相关) 数据表中的每一列数据都和主键直接相关,而不能间接相关。=》表的非主属性不能依赖与
其他表的非主属性 外键约束 且三大范式是一级一级依赖的,第二范式建立在第一范式上,第三范式
建立第一第二范式上- 比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关 系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。
- BCNF:符合 3NF,并且,主属性不依赖于主属性。
- 若关系模式属于第二范式,且每个属性都不传递依赖于键码,则 R 属于 BC 范式。
- 通常 BC 范式的条件有多种等价的表述:每个非平凡依赖的左边必须包含键码;每个决定因素必须包含键码。
- BC 范式既检查非主属性,又检查主属性。当只检查非主属性时,就成了第三范式。满足BC范式的关系都必然满足第三范式。
- 还可以这么说:若一个关系达到了第三范式,并且它只有一个候选码,或者它的每个候选码 都是单属性,则该关系自然达到 BC 范式。
- 一般,一个数据库设计符合 3NF 或 BCNF 就可以了。
- 要求把同一表内的多对多关系删除。
- 从最终结构重新建立原始结构。
什么是视图
视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。
大表如何优化?
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
-
限定数据的范围
- 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
-
读/写分离
- 经典的数据库拆分方案,主库负责写,从库负责读;
-
垂直分区
-
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
-
垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
-
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
-
-
水平分区
-
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。
-
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
-
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
-
水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。下面补充一下数据库分片的两种常见方案:
- 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
- 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现
在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
- 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现
- 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
-
详细内容可以参考: MySQL大表优化方案: https://segmentfault.com/a/1190000006158186
分库分表之后,id 主键如何处理?分布式ID生成有几种方案?
因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id来支持。
分布式ID的特性
- 唯一性:确保生成的ID是全网唯一的。
- 有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
- 高可用性:确保任何时候都能正确的生成ID。
- 带时间:ID里面包含时间,一眼扫过去就知道哪天的交易
生成全局 id 有下面这几种方式:
- UUID:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
- 数据库自增 id (如 MySQL 的 auto_increment): 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
- 利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
- Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
- 美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。
前端基础
undefined 和 null 区别
6种数据类型,5 种简单数据类型:Undefined(未定义)、Null(空值)、Boolean、Number 和 String。还有一种复杂数据类型—Object
Null/Undefined 类型是一个只有一个值的数据类型,即特殊的值 null/undefined。
有个要说明的是:undefined 是派生自 null 的,因此 undefined 和 null 两个值的比较是相等的,所以,未初始化的变量和赋值为 null 的变量会相等。这时,可以采用 typeof 变量的类型进行比较。
JavaScript组成部分
ECMAScript:规定了JS的核心语法,是一种在国际认可的标准的脚本语言规范。如语法, 数据类型,关键字,保留字,运算符,对象等。
DOM:文档对象模型(Document Object Model),DOM赋予了JavaScript操作 HTML 的能力,即Document操作;
BOM:浏览器对象模型(Browser Object Model),BOM赋予了JavaScript操作浏览器的能力,即Window操作。
cookie、localStorage和sessionStorage三者的区别
1、cookie的有效期是可以设置的,默认的情况下是关闭浏览器后失效
2、sessionStorage的有效期是仅保持在当前页面,关闭当前会话页或者浏览器后就会失效
3、localStorage的有效期是在不进行手动删除的情况下是一直有效的
什么是Vue
Vue.js是一个构建数据驱动的 web 界面的渐进式框架。Vue.js 的目标是通过尽可能简单的 API实现响应的数据绑定和组合的视图组件。核心是一双向数据绑定
vue生命周期
生命周期(钩子函数):从Vue实例创建、运行、到销毁期间,总是伴随着各种各样的事件,这些事件,统称为生命周期!其中整个流程分三个阶段,创建(1-5)、运行(6)、销毁(7-8);步骤如下:
-
创建一个vue实例对象
-
初始化 一些默认的生命周期函数和默认的事件 => beforeCreate()
这时候,data和methods中的数据都没初始化
-
初始化 数据 => created()
data和methods中的数据都被初始化好了
-
编译模板 => beforeMount()
即
<div id="app">{{msg}} </div>
=> 在内存中生成一个编译好的最终模板字符串 -> 把这个模板字符串渲染为内存中dom注意:只是在内存中渲染好了模板,并没有把模板挂载到页面上去,此时 页面还是旧的, 简单的说 结果就是在内存中渲染了一个
<div id="app">ok</div>
的dom元素,但是页面上还是<div id="app">{{msg}} </div>
-
将编译好的模板真实提换到页面中去 => mounted()
即 将内存中渲染好的dom元素即
< div id="app">ok< /div>
已经 提换了页面上的< div id="app">{{msg}} < /div>
-
当数据改变时 即完成data(model层) ->view(视图层)的更新
-
先在内存中渲染一份最新的dom树 => beforeUpdate()
页面上的数据还是旧的,但是data中的数据都是最新的,页面和最新的数据尚未保存同步
-
将最新的dom树重新渲染到真实的页面上去 => updated()
页面上的数据和data中的数据都是最新的,页面和最新的数据保存同步
-
-
销毁之前,实例上的data和所有methods,以及过滤器、指令。。。都处于可用状态,还未真正销毁 => beforeDestroy()
-
销毁,实例上的data和所有methods,以及过滤器、指令。。。都处于不可用状态,还未真正销毁 => destroyed()
为什么data是一个函数
组件的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一分新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果
常见指令
v-clock_解决屏幕闪烁
v-text_v-html_v-pre_渲染文本内容
- v-text:不解析html,解决屏幕闪烁
- v-html:解析html
- v-pre:显示原始信息,
v-bind/:_属性绑定
v-on/@_事件绑定
v-model_表单双向数据绑定
v-for_遍历 <p v-for="(item, i) in list1">
v-if_v-show_显示隐藏
- v-show隐藏则是为该元素添加css–display:none,dom元素依旧还在。
- v-if显示隐藏是将dom元素整个添加或删除
常见事件修饰符
.stop:阻止冒泡
.prevent:阻止默认行为
.capture:实现捕获触发机制
.self:实现只有点击当前元素(不是子元素)的时候,才能触发事件处理函数 与stop不同的是,self只只会阻止自身冒泡行为,并不会真正阻止冒泡
按键修饰符 @keyup.键盘码
<input v-on:keyup.enter="submit" value="回车触发">
vue 组件通信
-
父传递子
- 父:自定义属性名 + 数据(要传递)=>
<component :demoData="msg"></component>
- 子:props ["父组件上的自定义属性名“] =>
props ["demoData“]
=>这时候相当于子组件定义了一个demoData的变量,值是msg
- 父:自定义属性名 + 数据(要传递)=>
-
子传递父
-
在父组件中注册子组件并在子组件标签上绑定自定义事件的监听。
-
子:this. e m i t ( ‘自定义事件名称’ , 数据 ) 子组件标签上绑定 @ 自定义事件名称 = ’回调函数’ = 》 ‘ t h i s . emit(‘自定义事件名称’, 数据) 子组件标签上绑定@自定义事件名称=’回调函数’ =》 ` this. emit(‘自定义事件名称’,数据)子组件标签上绑定@自定义事件名称=’回调函数’=》‘this.emit(‘func’, this.sonmsg.name)`
-
父:methods: {自定义事件() {//逻辑处理} } =>
-
<com2 @func="show"></com2> methods: { show(data) { console.log('调用了父组件身上的 show 方法: --- ' + data) console.log(data); this.datamsgFormSon = data } },
-
-
什么是 webservice?
从表面上看**,WebService 就是一个应用程序向外界暴露出一个能通过 Web 进行调用的 AP**I,
也就是说能用编程的方法通过 Web 来调用这个应用程序。我们把调用这个 WebService 的应用程序叫做客户端,而把提供这个 WebService 的应用程序叫做服务端。
网络
网络编程三要素
网络编程概述:在通信协议下,不同计算机上运行的程序,可以进行数据传输。在java中java.net提供了相关接口
- IP地址:设备在网络中的地址,是唯一的标识,作用就是找到是哪台电脑
- 端口:应用程序在设备中的唯一的标识。作用就是找到这台电脑运行的那个程序。用两个字节表示的整数,它的取值范围是0 ~ 65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
- 协议:数据在网络中传输的规则,常见的协议UDP、TCP、http、https、ftp。它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。如果没有协议就无法确定返回的数据是不是自己想要的
OSI 的七层模型都有哪些?说说TCP/IP四层网络模型
OSI(Open System Interconnection)开放系统互连参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系。
OSI模型分为七层,自下而上为 物理层(Physical Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表达层(Presentation Layer)、应用层(Application Layer)。
http协议是什么?一次完整的 Http 请求是怎样的?
http协议:HTTP(hypertext transport protocol),即超文本传输协议。是一个简单的请求-响应协议,这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。
http请求流程:浏览器发起请求-> 解析域名得到ip->进行TCP连接 ->浏览器发送HTTP请求->服务器端响应HTTP请求->渲染数据-> 关闭TCP连接或保持 。
- 浏览器发起请求
- 浏览器根据域名解析IP地址
- 浏览器根据访问的域名找到其IP地址;
- DNS(Domain Name Server,域名服务器)是进行域名(domain name)和与之相对应的IP地址 (IP address)转换的服务器。DNS中保存了一张域名(domain name)和与之相对应的IP地址 (IP address)的表,以解析消息的域名
- 浏览器与WEB服务器建立一个TCP连接=》 TCP的3次握手。
- 浏览器发送HTTP请求(一个HTTP请求报文由请求行(request line)、请求头(headers)、空行(blank line)和请求数据/请求体(request body)4个部分组成。)
- 请求行分为三个部分:请求方法、请求地址URL和HTTP协议版本,它们之间用空格分割。例如,GET /index.html HTTP/1.1。
- 请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。
- 请求体:可选部分,比如GET请求就没有请求数据,post就有。
- 服务器端响应HTTP请求(HTTP响应报文由状态行(status line)、响应头(headers)、空行(blank line)和响应数据(response body)4个部分组成。)
- 状态行:由3部分组成,分别为:协议版本,状态码,状态码描述。
- 响应头:与请求头部类似,为响应报文添加了一些附加信息
- 响应数据:用于存放需要返回给客户端的数据信息。
- 浏览器得到服务器的数据,对页面进行渲染呈现给用户
- 浏览器拿到HTML文件后,开始解析HTML代码,遇到静态资源时,就向服务器端去请求下载。
- 服务器关闭TCP连接(四次挥手)
HTTP状态码分类,和常用状态码
HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:
分类 | 分类描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
什么是 Http 协议无状态协议?怎么解决 Http 协议无状态协议?
无状态协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息 也就是说,当客户端一次 HTTP 请求完成以后,客户端再发送一次 HTTP 请求,HTTP 并不知道当前客户端是一个”老用户“。
可以使用 Cookie 来解决无状态的问题,Cookie 就相当于一个通行证,第一次访问的时候给客户端发送一个 Cookie,当客户端再次来的时候,拿着 Cookie(通行证),那么服务器就知道这个是”老用户“。
http 中重定向和请求转发的区别?
本质区别:转发是服务器行为,重定向是客户端行为。
重定向特点:两次请求,浏览器地址发生变化,可以访问自己 web 之外的资源,传输的数据会丢失。
请求转发特点:一次强求,浏览器地址不变,访问的是自己本身的 web 资源,传输的数据不会丢失。
Http 与 Https 的区别
- HTTP 的 URL 以 http:// 开头,而 HTTPS 的 URL 以 https:// 开头
- HTTP 是不安全的,而 HTTPS 是安全的
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
- HTTP 标准端口是 80 ,而 HTTPS 的标准端口是 443
- 在 OSI 网络模型中,HTTP 工作于应用层,而 HTTPS 的安全传输机制工作在传输层
- HTTP 无需证书,而 HTTPS 需要 CA 机构 wosign 的颁发的 SSL 证书
- http的连接很简单,是无状态的。Https协议是由SSL+Http协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
Tcp 和 Udp 的区别?
- TCP 保证数据正确性,UDP 可能丢包
- TCP 保证数据顺序,UDP 不保证。
BS 与 CS 的联系与区别?
C/S和B/S是软件发展过程中出现的两种软件架构方式。
-
CS:(Client/Server,客户端/服务器模式):
桌面级应用 响应速度快,安全性强,个性化能力强
例如QQ;需要编写服务器端程序,以及客户端程序,例如我们安装的就是QQ的客户端程序;
缺点:软件更新时需要同时更新客户端和服务器端两端,比较麻烦;
优点:安全性比较好。
-
BS:(Browser/Server,浏览器/服务器模式),
web应用 可以实现跨平台,客户端零维护,但是个性化能力低,响应速度较慢。
优点:只需要编写服务器端程序,不需要安装专门的客户端软件,只需要浏览器就行;
说说HTTP、TCP、Socket 的关系是什么?
TCP/IP是传输层的一种协议,指的是一系列协议。
HTTP本身就是一个协议,是应用层一种协议,需要先建立TCP连接才能为web应用程序提供数据传输服务
Socket 是 TCP/IP 网络的 API ,其实就是一个门面模式.是面向编程者通过它来向网络发送报文
servlet
什么是 Servlet?
Servlet是指直接或者间接实现了这个Servlet接口的类
Servlet 主要用于接受请求,处理请求,完成响应
Servlet体系结构图?实现Servlet的三种方式?
实现Servlet接口
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
继承GenericServlet类
public class CServlet extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("CServlet....");
}
}
继承HttpServlet类
public class DServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("doGet.......");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("doPost........");
}
}
tomcat 容器是如何创建 servlet 类实例?用到了什么原理?
浏览器向服务器请求时,服务器不会直接执行我们的类,而是到web.xml里寻找路径名
第一步,浏览器输入访问路径后,携带了请求行,头,体
第二步,根据访问路径找到已注册的servlet名称
第三步,根据映射找到对应的servlet名
第四步,根据根据servlet名找到我们全限定类名,既我们自己写的类,服务器找到全限定类名后,通过反射创建对象,
在 servlet 注册时加上<load-on-startup>1</load-on-startup>
如果为正数,则在一开始就实例化,如果不写或为负数,则第一次请求实例化。
Servlet的运行过程 / 生命周期
Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:
- Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
- 由web服务器(tomcat)装载并创建该Servlet的一个实例对象(通过xml或者注释中的全限定名 通过反射创建对象)。同时也创建了servletConfig,里面存放了一些初始化信息(注意服务器只会创建一次servlet对象,所以servletConfig也只有一个)
- 调用Servlet实例对象的init()方法。
- 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
- WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
request 作用?
- 获取请求头: request.getMethod()
- 获取请求参数 request.getParameter(“username”)
- 获取当前 Web 应用的虚拟路径request.getContextPath
- 转发 request.getRequestDispatcher(路径).forward(request,response);
如何防止表单重复提交?
针对于重复提交的整体解决方案:
-
用 redirect(重定向)来解决重复提交的问题
-
点击一次之后,按钮失效
-
通过 loading(Loading 原理是在点击提交时,生成 Loading 样式,在提交完成之后隐藏该样式)
-
自定义重复提交过滤器
转发(forward)和重定向(redirect)的区别?
-
浏览器 URL 地址:
- Forward 是服务器内部的重定向,服务器内部请求某个 servlet,然后获取响应的内容,浏览器的 URL 地址是不会变化的;
- Redirect 是客户端请求服务器,然后服务器给客户端返回了一个 302 状态码和新的 location,客户端重新发起 HTTP 请求,服务器给客户端响应 location 对应的 URL 地址,浏览器的 URL 地址发生了变化。
- 数据的共享:
- Forward 是服务器内部的重定向,request 在整个重定向过程中是不变的,request 中的信息在 servlet 间是共享的。
- Redirect 发起了两次 HTTP 请求分别使用不同的request。
- 请求的次数:
- Forward 只有一次请求;Redirect 有两次请求。
get 请求中文乱码?
乱码的根本原因:浏览器的编码方式 UTF-8 和 服务器的解码方式 ISO-8895-1 不一样
Tomcat8.0及以上版本不会乱码,Tomcat7及以下版本会乱码.解决方法:
-
第一种方式 使用 URLEncoder 和 URLDecoder 两个类 编解码。先以 iso-8895-1 进行编码,然后再以 utf-8 进行解码
-
第二种方式 使用 String 类的方法进行编解码 new String(request.getParameter(“name”).getBytes(“ISO-8859-1”), “UTF-8”);
-
第三种方式 更改 server.xml 配置文件。
GET 请求是在URL 地址栏中传递请求参数的,它会被 Tomcat 服务器自动解码,而 Tomcat服务器默认的字符集也是 ISO-8859-1,所以我们需要修改 Tomcat 服务器的字符集为 UTF-8。
post 请求中文乱码问题?
-
post 请求方式乱码的原因是:因为 post 是以二进制流的形式发送到的服务器。服务器收到数据后。默认以 iso-8859-1 进行编码。
-
post 请求乱码解决,只需要在获取请求参数之前调用 request.setCharacterEncoding(“UTF-8”); 方法设置字符集 即可。
响应乱码?
1、原因: 由服务器编码,默认使用 ISO-8859-1 进行编码由浏览器解码,默认使用 GBK 进行解码
2、解决方案
-
设置响应头: response.setHeader(“Content-Type”,“text/html;charset=utf-8”);
-
设置响应的内容类型:response.setContentType(“text/html;charset=utf-8”); // 使用的时候设置这个就行;包含response.setCharacterEncoding(“UTF-8”);和response.setCharacterEncoding(“UTF-8”);
通过这种方式可以在响应头中告诉浏览器响应体的编码方式是 UTF-8;同时服务器也会采用该字符集进行编码,但需要注意的是,两种方法一定要在 response.getWriter()之前进行。
session 和 cookie 的区别?
session 是存储在服务器端,cookie 是存储在客户端的,所以安全来讲 session 的安全性要比cookie 高,然后我们获取 session 里的信息是通过存放在会话 cookie 里的 sessionid 获取的。
cookie分为两大类分为 会话 cookie 和 持久化 cookie
- 会话 cookie 存放在客户端浏览器的内存中,所以说他的生命周期和浏览器是一致的,浏览器关了会话cookie 也就消失了
- 持久化 cookie 存放在客户端硬盘中,而持久化 cookie 的生命周期就是我们在设置 cookie 时候设置的那个保存时间,然后我们考虑一问题当浏览器关闭时session 会不会丢失,
从上面叙述分析 session 的信息是通过会话 cookie 的 sessionid 获取的, 当浏览器关闭的时候会话 cookie 消失所以我们的 sessionid 也就消失了,但是 session 的信息还存在服务器端,这时我们只是查不到所谓的 session 但它并不是不存在。那么,session 在什么情况下丢失,就是在服务器关闭的时候,或者是 session 过期(默认时间是 30 分钟),再 或 者 调 用 了 invalidate() 的 或 者 是 我 们 想 要 session 中 的 某 一 条 数 据 消 失 调 用session.removeAttribute()方法,然后 session 在什么时候被创建呢,确切的说是通过调用getsession()来创建,这就是 session 与 cookie 的区别.
Cookie 对象的缺陷?
-
Cookie 是明文的,不安全
-
不同的浏览器对 Cookie 对象的数量和大小有限制
-
Cookie 对象携带过多费流量
-
Cookie 对象中的 value 值只能是字符串,不能放对象网络中传递数据只能是字符串
Session 的运行机制?session原理?
Session底层依赖Cookie
- 用户第一次请求服务器时,服务器端会生成一个sessionid,该对象有一个全球唯一的 ID
- 服务器端将生成的sessionid返回给客户端
- 客户端收到sessionid会将它保存在cookie中,当客户端再次访问服务端时会带上这个sessionid
- 当服务端再次接收到来自客户端的请求时,会先去检查是否存在sessionid,不存在就新建一个sessionid重复1,2的流程,如果存在就去遍历服务端的session文件,找到与这个sessionid相对应的文件,文件中的键值便是sessionid,值为当前用户的一些信息
- 此后的请求都会交换这个 Session ID,进行有状态的会话。
jsp
jsp 和 servlet 的区别、共同点、各自应用的范围?
JSP (JavaServlet Page)其实就是servlet。是从Servlet继承而来。只不过它在Servlet当中又添加/修改了一些方法,作了新的封装。。JSP 编译后是“类 servlet”。
Servlet 和 JSP 最主要的不同点在于,Servlet 的应用逻辑是在 Java 文件中,并且完全从表示层中的HTML 里分离开来。而 JSP 的情况是 Java 和 HTML 可以组合成一个扩展名为.jsp 的文件。JSP 侧重于视图,Servlet 主要用于控制逻辑。在 struts 框架中,JSP 位于 MVC 设计模式的视图层, 而 Servlet 位于控制层.
jsp三大指令,七大动作
三大指令:
-
page指令 :设定整个JSP网页的静态属性。
语法:<%@ page 标签元素=“值”%>,比如 <%@ page language=“java”%>标签元素:language、import、contentType、session、errorPage、isErrorPage等等。 -
include指令 :用来向当前页面插入一个静态文件的内容。这个文件可以是JSP、HTML、文本或是Java程序。
语法:<%@ include file=“filename” %> 比如 <%@ include file=“111.txt” %>标签元素:file -
taglib指令 :使用标签库定义新的自定义标签,在JSP页面中启用定制行为。当页面引用了用户自定义标签时,taglib指令用于引用自定义标签库,并指定标签的前缀。
语法:<%@ taglib uri=“URIToTagLibrary” prefix=“tagPrefix” %>如<%@ taglib uri=“http://www.jspcentral.com/tags” prefix=“JAXP” %>标签元素:uri、Prefixpage指令元素的属性
jSP 7 个 动作指令如下 :
- jsp : forward: 执行页面转向,将请求的处理转发到下一个页面。
- jsp : param: 用于传递参数,必须与其他支持参数曲标签一起使用。
- jsp : include: 用于动态引入一个 JSP 页面。
- jsp : plugin: 用于下载 JavaBean 或 Applet 到客户端执行。
- jsp : useBean: 用来创建和查找bean 对象;
- jsp : setProperty: 修改 JavaBean 实例的属性值。
- jsp : getProperty: 获取 JavaBean 实例的属性值。
jsp 有哪些内置对象?作用分别是什么?JSP 有 9 个内置对象:
JSP 有 9 个内置对象:
- page:JSP 页面本身(相当于 Java 程序中的 this);
- request:封装客户端的请求,其中包含来自 GET 或 POST 请求的参数(即HttpServletRequest类的对象);
- session:封装用户会话的对象;
- application:封装服务器运行环境的对象(即ServletContext类的对象);
- response:封装服务器对客户端的响应(即HttpServletResponse类的对象);
- pageContext:通过该对象可以获取其他对象;
- exception:封装页面抛出异常的对象。
- out:输出服务器响应的输出流对象(等同与response.getWriter());
- config:Web 应用的配置对象;
写出熟悉的 JSTL 标签
< c:if >、< c:choose >、< c: when >、< c: otherwise >、< c:forEach >、< c:set >。
页面间对象传递的方法?
cookie、request、session、application
jsp 的四种范围?
JSP有4种范围,分别是page、request、session和application,都能用setAttribute(“”, “”)添加变量,getAttribute(“”)获取变量,对于page,得用pageContext。作用范围:
-
page 在同一个页面有效;
-
request 在同一次请求(请求页面)间有效,一般请求完毕则失效,但若是通过forward的方式跳转,则forward页面依旧能拿到request的值。但如果是通过redirect的方式,则相当于重新发送一次页面请求,request中的值失效;
注: <jsp:forward page= “相对路径”/> 作用是将请求转到另一个页面,表现为跳转,实际上也分享了request的值 -
session 在一个会话的生命周期内有效,简单来说就是与服务器连接的时间内,注意的是所有页面共享,如果期间断线,便会失效;
-
application 作用域最大,直到服务器停止才会失效,所有页面共享。但使用时不宜定义太多,不然或造成服务器负担加重;
作用域由大到小:application> session> request> page
值得注意的是一个request可以包含多个page(include, forward, filter)
JSP 和Servlet中的请求转发分别如何实现?
JSP 中的请求转发可利用forward 动作实现:<jsp:forward />;
Serlvet 中实现请求转发的方式为:getServletContext().getRequestDispatcher(path).forward(req,res)。
filter
Filter概念
与Servlet类似,用来拦截请求,不同的是 当且仅当满足条件的时候才会放行,不是用来处理请求的;
执行地位在Servlet之前,客户端发送请求时,会先经过Filter,再到达目标Servlet中;响应时,会根据执行流程再次反向执行Filter。可以解决多个Servlet共性代码的冗余问题(例如:乱码处理、登录验证)
Filter 的工作原理?
Filter 接口中有一个 doFilter 方法,当我们编写好 Filter,并配置对哪个 web 资源进行拦截后,WEB 服务器每次在调用 web 资源的 service 方法之前,都会先调用一下 filter 的 doFilter 方法,因此,在该方法内编写代码可达到如下目的:
-
调用目标资源之前,让一段代码执行。
-
是否调用目标资源(即是否让用户访问 web 资源)。
-
调用目标资源之后,让一段代码执行。
web 服务器在调用 doFilter 方法时,会传递一个 filterChain 对象进来,filterChain 对象是filter 接口中最重要的一个对象,它也提供了一个doFilter 方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则 web 服务器就会调用 web 资源的 service 方法,即 web 资源就会被访问,否则 web 资源不会被访问
Filter 链是什么?
在一个 web 应用中,可以开发编写多个 Filter,这些 Filter 组合起来称之为一个 Filter 链。web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter,当第一个 Filter 的 doFilter 方法被调用时,web 服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法。在 doFilter方法中,开发人员如果调用了FilterChain 对象的doFilter 方法,则web 服务器会检查FilterChain对象中是否还有 filter,如果有,则调用第 2 个 filter, 如果没有,则调用目标资源。
Listener
Listener概念
监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。监听器其实就是一个实现特定接口的普通Java类,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法立即被执行。
Servlet Filter Listener 启动顺序?
启动的顺序为 listener->Filter->servlet.
简单记为:理(Listener)发(Filter)师(servlet).
执行的顺序不会因为三个标签在配置文件中的先后顺序而改变。
监听器类型?
按监听的对象划分:servlet2.4 规范定义的事件有三种:
- 用于监听应用程序环境对象(ServletContext)的事件监听器
- 用于监听用户会话对象(HttpSession)的事件监听器
- 用于监听请求消息对象(ServletRequest)的事件监听器按监听的事件类项划分
linux
网络类型
- 仅主机模式
- 虚拟机只能和主机通信,不能上网,相当于用一根线将两台电脑链接起来,没有连路由器。在真机中仅主机模式模拟网卡对应的物理网卡是VMnet1。
- 桥接
- 虚拟机和主机在局域网中地位一样,就相当于两台电脑同时连接了Wifi,但是不好的地方就是,如果WiFi断开了,呢吗虚拟机和主机无法通信
- nat模式
- 通过vmnet8做中转和主机连接。让 VM 虚拟机的网络服务发挥路由器的作用,使得通过虚拟机软件模拟的主机可以通过物理主机访问外网,在真机中NAT 虚拟机网卡对应的物理网卡是 VMnet8。相当于自己的电脑成了网关,虚拟机成了子主机
linux的目录结构
- root:该目录为系统管理员HOME目录
- bin:这个目录下放着经常使用的命令
- boot:这里存放的是启动Linux时的一些核心文件
- etc:存放系统管理所需要的配置文件和子目录
- home:普通用户的HOME目录
- usr:unix shared resources(共享资源) ;默认安装软件的目录,类似Windows中的Program Files目录
- opt:optional(可选) ;是主机额外安装软件拜访的目录
- 须知:/usr/local 是 /usr 下的一个用户级的程序目录,用户自己安装的软件一般选择安装到这个目录下
怎么查看当前进程?怎么执行退出?怎么查看当前路径?
查看当前进程: ps
- ps -l 列出与本次登录有关的进程信息;
- ps -aux 查询内存中进程信息;
- ps -aux | grep * 查询*进程的详细信息;
- top 查看内存中进程的动态信息;
- kill -9 pid 杀死进程。
执行退出: exit
查看当前路径: pwd
查看文件有哪些命令
vi 文件名 #编辑方式查看,可修改
cat 文件名 #显示全部文件内容
more 文件名 #分页显示文件内容
less 文件名 #与 more 相似,更好的是可以往前翻页
tail 文件名 #仅查看尾部,还可以指定行数
head 文件名 #仅查看头部,还可以指定行数
你平时是怎么查看日志的?
Linux查看日志的命令有多种: tail、cat、tac、head、echo等,本文只介绍几种常用的方法。
VIm=》Linux系统默认的编辑器;类似于Windows下的记事本、notepad++。
-
三种操作模式切换
-
三种操作模式
- 命令模式:默认处于命令模式,vi将输入的字符作为命令对待,并对每个命令做出回应,但不显示这些字符;
- 插入模式:在输入模式下,Vim 可以对文件执行写操作,类似于在 Windows 系统的文档中输入内容。
- 末行模式:编辑模式用于对文件中的指定内容执行保存、查找或替换等操作。所有以冒号":"开始的命令将使vi处于最后一行模式,光标移动到屏幕最底一行,输入的命令将在该行显示。
Git篇
什么是Git?
-
分布式版本控制系统(Version Control System):是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制系统不仅可以应用于软件源代码的文本文件,而且可以对任何类型的文件进行版本控制。常见的版本控制系统有:cvs、svn、git
-
Git使用 C 语言编写。
-
三种分区
- **工作区:**就是你在电脑里能看到的目录(代码编辑区)。
- **暂存区:**一般存放在 “.git目录下” 下的index文件中(过渡层,避免误操作)。
- **版本库/本地仓库:**工作区有一个隐藏目录.git (本地仓库,专门控制版本)。
-
三种状态
- 已修改(modified):已修改表示修改了文件,但还没保存到数据库。
- 已暂存(staged):已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
- 已提交(committed):已提交表示数据已经安全的保存到本地数据库。
-
git工作流程
- 在工作目录修改文件;暂存文件,将文件快照放到暂存区域;提交更新到本地库。暂存区保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。
git与svn的区别及优缺点
svn: 集中式的版本控制系统,并且提交修改也需要依赖于中央服务器。
- **好处:**每个人都可以一定程度上看到项目中的其他人正在做些什么。而管理员也可以轻松掌控每个开发者的权限。
- 缺点:因此网络挂了之后所有人都无法使用
git pull 和 git fetch 有什么区别?
都是获取最新代码的,区别在于git pull会自动合并到本地代码,而 git fetch 不会
git pull = git fetch + git merge
什么是 git stash?
首先应该解释 git stash 的必要性。
通常情况下,当你一直在处理项目的某一部分时,如果你想要在某个时候切换分支去处理其他事情,事情会处于混乱的状态。问题是,你不想把完成了一半的工作的提交,以便你以后就可以回到当前的工作。解决这个问题的答案是 git stash。
再解释什么是git stash。
stash 会将你的工作目录,即修改后的跟踪文件和暂存的更改保存在一堆未完成的更改中,你可以随时重新应用这些更改。
你使用过git stash命令吗?你一般什么情况下会使用它?
命令git stash是把工作区修改的内容存储在栈区。 以下几种情况会使用到它:
- 解决冲突文件时,会先执行git stash,然后解决冲突;
- 遇到紧急开发任务但目前任务不能提交时,会先执行git stash,然后进行紧急任务的开发,然后通过git stash pop取出栈区的内容继续开发;
- 切换分支时,当前工作空间内容不能提交时,会先执行git stash再进行分支切换;
描述一下你所使用的分支策略?
这个问题被要求用Git来测试你的分支经验,告诉他们你在以前的工作中如何使用分支以及它的用途是什么,你可以参考以下提到的要点:
- dev:开发环境
- sit:测试环境
- uat:顾问测试环境
- production:生产环境
- feature-jira-jira数字:功能分支,要实现的功能
hotfix-jira号
:紧急bug修改分支
如果本次提交误操作,如何撤销?
git reset --hard [commit]
--mixed 或不带选项(默认):移动 HEAD 指针并重置索引,不会修改工作区,撤销了提交和暂存的更改,但保留了工作区的修改。
--soft: 只移动HEAD指针,暂存区和工作目录中的更改都会保留在工作目录中,以便再次提交。
--hard: 移动 HEAD 指针并重置索引和工作区,彻底删除了提交以及暂存区和工作区的修改,慎用,因为会导致工作区的内容丢失。
maven
什么是maven?
项目构建和依赖管理工具
Maven能为我们解决什么问题?
- ①添加第三方jar包
- 按照最原始的做法,我们是手动复制jar包到项目WEB-INF/lib下,每个项目都会有一份,造成大量重复文件。而Maven将jar包放在本地仓库中统一管理,需要jar包只需要用坐标的方式引用即可。
- ②jar包之间的依赖关系
- jar包之间往往不是独立的,很多jar需要在其他jar包的支持下才能够正常工作,称为jar包之间的依赖关系。如果我们手动去导入,要知道jar包之间的依赖关系并一一导入是及其麻烦而且容易出错的。如果使用Maven,它能够将当前jar包所依赖的其他所有jar包全部导入。
- ③获取第三方jar包
- 开发过程中我们需要用到很多jar包,每个jar包在官网获取的方式不尽相同,给工作带来了额外困难。但是使用Maven可以以坐标的方式依赖一个jar包,Maven从中央仓库进行下载,并同时下载这个jar包依赖的其他jar包。
- ④将项目拆分为多个工程模块
- 项目的规模越来越大,已经不可能通过package结构来划分模块,必须将项目拆分为多个工程协同开发。
什么是Maven的坐标?项目定位?
Maven其中一个核心的作用就是管理项目的依赖,引入我们所需的各种jar包等。为了能自动化的解析任何一个Java构件,Maven必须将这些Jar包或者其他资源进行唯一标识,这是管理项目的依赖的基础,也就是我们要说的坐标。包括我们自己开发的项目,也是要通过坐标进行唯一标识的,这样才能才其它项目中进行依赖引用。
maven的坐标通过groupId,artifactId,version唯一标志一个构件。(GAV)
- groupId通常为公司或组织名字
- artifactId通常为项目名称
- version为版本号。
多模块使用
parent、modules
在多模块项目中使用,暂时可以不用了解
讲一下maven的生命周期
Maven的 生命周期:从我们的项目构建,一直到项目发布的这个过程
清理(mvn clean
),编译(mvn compile
),测试,报告,打包(mvn package
根据项目生成的jar),安装(mvn install
在本地Repository中安装jar),部署。
说说maven的依赖原则
- 最短路径原则(依赖传递的路径越短越优先)
- pom文件申明顺序优先(路径长度一样,则先申明的优先)
- 覆写原则(当前pom文件里申明的直接覆盖父工程传过来的)
说说插件的解析机制
在用到插件的时候会先从本地仓库查找插件,如果本地仓库没有则从远程仓库查找插件并下载到本地仓库。
mybatis
JDBC 编程有哪些步骤?
- 加载四大参数
- 加载驱动
- 建立连接:Connection
- 操作数据库Statement 和 PreparedStatement方式 ,为了防止sql注入我们一般使用PreparedStatement方式
- 处理和显示结果。
- 释放资源。
// 1、加载四大参数
String driverName = "com.mysql.jdbc.Driver"; //驱动名
String url = "jdbc:mysql://localhost:3306/mydbjdbc?useSSL=false"; //连接数据库的url
String username = "root"; //用户名
String password = "root"; //密码
//2、加载驱动
Class.forName(driverName);
//3、建立连接
Connection connection = DriverManager.getConnection(url, username, password);
/**
* 4、操作数据库 常规方式和 PreparedStatement方式 ,为了防止sql注入我们一般使用PreparedStatement方式
*/
statement2 = connection.prepareStatement("INSERT INTO `user` (`username`, `age`) VALUES (?, ?),(?, ?)"); // 获取发送sql的对象
statement2.setString(1, "张三"); // 设置第一个参数的值为“张三” 就是把第一个“?”的值替换成“张三”
statement2.setInt(2, 20);
statement2.setString(3, "李四");
statement2.setInt(4, 40);
updateNumber = statement2.executeUpdate(); // 发送SQL语句
// 4.处理和显示结果。
System.out.println("成功插入" + updateNumber + "条数据");
// 6、释放资源 遵循先开后关原则,释放所使用到的资源对象。
if (statement2 != null) {
statement2.close();
}
connection.close();
MyBatis 的优点?
MyBatis是一个优秀的基于java的持久层框架,能够代替JDBC,简化JDBC开发。
它内部封装了JDBC,使开发者只需要关注SQL语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
MyBatis通过XML或注解的方式将要执行的各种statement配置起来,并通过JAVA对象和statement中SQL的动态参数进行映射生成最终执行的SQL语句。
最后MyBatis框架执行SQL并将结果映射为Java对象并返回。采用ORM思想解决了实体和数据库映射的问题,对JDBC进行了封装,屏蔽了JDBC API底层访问细节,使我们不用与JDBC API打交道,就可以完成对数据库的持久化操作
MyBatis 框架的缺点?
(1) SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。
(2) SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}和${}的区别是什么?
#{}是预编译处理,${}是字符串替换。
- Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;使用#{}可以有效的防止SQL注入,提高系统安全性
- Mybatis在处理 时,就是把 {}时,就是把 时,就是把{}替换成变量的值。
Mybatis 是如何 sql 执行结果封装为目标对象?都有哪些映射形式?Mybatis是如何将sql执行结果封装为目标对象并返回的?当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
映射形式:
-
当列名和封装查询结果的类的属性名一一对应时:这时MyBatis 有自动映射功能,将查询的记录封装到resultType 指定的类的对象中去
-
<mapper namespace="com.hanT.dao.UserDao"> <!--id对应接口中的方法名,resultType为返回结果的类型,要用类的全限定名--> <select id="getUserList" resultType="com.hanT.pojo.User"> select * from mybatis.user </select> </mapper>
-
-
当列名和封装查询结果的类的属性名不对应时:当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
-
使用resultMap 标签,在标签中配置属性名和列名的映射关系
-
<resultMap type="cn.com.mybatis.pojo.User" id="UserResult"> <result property="username" column="name"/> </resultMap> <select id="findUserById" parameterType="java.lang.Long" resultMap="UserResult"> select id,name,email from t_user where id=#{id} </select>
-
-
除了使用resultMap 标签之外, 第二种是使用sql 列的别名功能,将列的别名书写为对象属性名,也可以做到映射关系
-
Mybatis是如何将sql执行结果封装为目标对象并返回的
- 有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用**分页插件(pagehelper)**来完成物理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
MyBatis实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询(一对一(association)/一对多(collection)),联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。
<!-- 第二种方式,一般使用这种方式 -->
<resultMap id="orderMap" type="SysOrder">
<id column="id" property="id" />
<result column="ordertime" property="ordertime" />
<result column="total" property="total" />
<!--
association:用于建立一对一的关系
javaType:指定属性的类型,不区分大小写
-->
<association property="sysUser" javaType="sysuser">
<id column="uid" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
</association>
</resultMap>
<!--
resultMap:用于指定要使用的resultMap
-->
<select id="findAll" resultMap="orderMap">
SELECT o.*,
u.id uid,
u.username username,
u.password password
FROM `sys_order` o,
user u
WHERE o.uid = u.id;
</select>
嵌套查询
<resultMap id="orderMap" type="SysOrder"><!-- type区分大小写 -->
<id column="id" property="id" />
<result column="ordertime" property="ordertime" />
<result column="total" property="total" />
<!--
association:用于建立一对一的关系
javaType:指定属性的类型
column: 数据库中的列名,或者是列的别名,被设置为对应嵌套Select语句的参数
select:用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
注意这里select中的写法,SysUserMapper.xml中相应ID处有对应的SQL语句
-->
<association property="sysUser" column="uid" javaType="sysUser" select="com.demo.mapper.SysUserMapper.findById" />
</resultMap>
说说Mybatis的缓存机制?
当开启缓存后,数据的查询执行的流程为:二级缓存 -> 一级缓存 -> 数据库
一级缓存
-
mybatis的的一级缓存是SqlSession(sqlSession用来操作数据库的)级别的缓存
-
同一个SqlSession的发起多次同构查询,会将数据保存在一级缓存中。当SqlSession提交、关闭以及其他的更新数据库的操作发生后,一级缓存就会清空。
-
无需任何配置,默认开启一级缓存。
-
MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。
二级缓存
- 二级缓存是SqlSessionFactory级别的缓存,同一个SqlSessionFactory产生的SqlSession都共享一个二级缓存
- SqlSessionFactory级别的缓存,同一个SqlSessionFactory构建的SqlSession发起的多次同构查询,会将数据保存在二级缓存中。注意:在sqlSession.commit()或者sqlSession.close()之后生效。
MyBatis 中见过什么设计模式?
5)
MyBatis 中比如 UserMapper.java 是接口,为什么没有实现类还能调用?
使用JDK动态代理+MapperProxy。本质上调用的是MapperProxy的invoke方法。
Spring
什么是spring?
Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。常见的配置方式有三种:基于XML的配置、基于注解的配置、基于Java的配置。
主要由以下几个模块组成:
Spring Core:核心类库,提供IOC服务;
Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
Spring AOP:AOP服务;
Spring DAO:对JDBC的抽象,简化了数据访问异常的处理;
Spring ORM:对现有的ORM框架的支持;
Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传;
Spring MVC:提供面向Web应用的Model-View-Controller实现。
Spring核心?SpringIOC 是什么?什么是控制反转(IOC),什么是依赖注入(DI)?
IOC(控制反转)与DI(依赖注入)
- IOC不是什么技术,它是一种降低对象耦合关系的一种设计思想。
- DI(依赖注入) :是ioc实现方式, 指容器负责创建和维护对象之间的依赖关系,而不是通过对象本身负责自己的创建和解决自己的依赖。在当前类需要用到其他类的对象,由Spring为我们提供,我们只需要在配置中说明。而DI依赖的是java中的反射。
aop与动态代理
- AOP(技术):是一种编程思想,即面向切面编程。
- 动态代理是AOP的实现方式。动态代理是一种在不修改源码的情况下对方法进行增强的技术。
- 动态代理有两种实现方式,即 JDK动态代理和Cglib动态代理
Spring IOC 注入的几种方式?
- 构造器注入
- set 方法注入
- 注解
Spring AOP(面向切面)编程的原理?
-
AOP 面向切面编程,它是一种思想。它就是针对业务处理过程中的切面进行提取,以达到优化代码的目的,减少重复代码的目的。 就比如,在编写业务逻辑代码的时候,我们习惯性的都要写:日志记录,事物控制,以及权限控制等,每一个子模块都要写这些代码,代 码明显存在重复。这时候,我们运用面向切面的编程思想,采用横切技术,将代码中重复的 部分,不影响主业务逻辑的部分抽取出来,放在某个地方进行集中式的管理,调用。 形成日志切面,事物控制切面,权限控制切面。 这样,我们就只需要关系业务的逻辑处理,即提高了工作的效率,又使得代码变的简洁优雅。这就是面向切面的编程思想,它是面向对象 编程思想的一种扩展。
-
AOP 的使用场景: 缓存、权限管理、内容传递、错误处理、懒加载、记录跟踪、优化、校准、调试、持久化、资源池、同步管理、事物控制等。
-
AOP 的相关概念:
- Target(目标对象):被增强的对象;
- Proxy (代理):一个类被增强后,就产生一个结果代理类(代理=目标+增强);
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点;
- Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义;
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知;Spring切面可以应用五种类型的通知:
- before:前置通知,在一个方法执行前被调用。
- after: 在方法执行之后调用的通知,无论方法执行是否成功。
- after-returning: 仅当方法成功完成后执行的通知。
- after-throwing: 在方法抛出异常退出时执行的通知。
- around: 在方法执行之前和之后调用的通知。
- Aspect(切面):是切入点和通知的结合;
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
-
Spring AOP 的编程原理?
代理机制
JDK 的动态代理:只能用于实现了接口的类产生代理。
Cglib 代理:针对没有实现接口的类产生代理,应用的是底层的字节码增强技术,生成当前类的子类对象。
解释一下代理模式?
-
代理模式: 代理模式就是本该我做的事,我不做,我交给代理人去完成。就比如,我生产了一些产品,我自己不卖,我委托代理商帮我卖,让代理商和顾客打交道,我自己负责主 要产品的生产就可以了。 代理模式的使用,需要有本类,和代理类,本类和代理类共同实现统一的接口。然后在 main 中调用就可以了。本类中的业务逻辑一般是不会变动的,在我们需要的时候可以不断的添加代理对象,或者修改代理类来实现业务的变更。
-
代理模式可以分为:
-
静态代理
优点:可以做到在不修改目标对象功能的前提下,对目标功能扩展
缺点:因为本来和代理类要实现统一的接口,所以会产生很多的代理类,类太多,一旦接口增加方法,目标对象和代理对象都要维护。
-
动态代理
- JDK 代理/接口代理) 代理对象,不需要实现接口,代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象,需要我们指定代理对象/目标对象实现的接口的类型。
- Cglib 代理 特点: 在内存中构建一个子类对象,从而实现对目标对象功能的扩展。
-
-
使用场景: 修改代码的时候。不用随便去修改别人已经写好的代码,如果需要修改的话, 可以通过代理的方式来扩展该方法。 隐藏某个类的时候,可以为其提供代理类 当我们要扩展某个类功能的时候,可以使用代理类 当一个类需要对不同的调用者提供不同的调用权限的时候,可以使用代理类来实现。 减少本类代码量的时候。 需要提升处理速度的时候。就比如我们在访问某个大型系统的时候,一次生成实例会耗费大量的时间,我们可以采用代理模式,当用来需要的时候才生成实例,这样就能提高访问的速度。
什么是 Spring bean?bean与ioc容器的关系?
被 Spring IoC 管理的对象,就是 Bean。
Spring容器是Spring的核心,一切Spring bean都存储在Spring容器内,并由其通过IoC技术管理。Spring容器也就是一个bean工厂(BeanFactory)。应用中bean的实例化,获取,销毁等都是由这个bean工厂管理的。
org.springframework.context.ApplicationContext接口用于完成容器的配置,初始化,管理bean。一个Spring容器就是某个实现了ApplicationContext接口的类的实例。也就是说,从代码层面,Spring容器其实就是一个ApplicationContext。
解释 Spring 支持的几种 bean 的作用域?
当通过 Spring 容器创建一个 Bean 实例的时候,不仅可以完成 bean 实例的实力化,还可以为 bean 指定作用域。Spring bean 元素的支持以下 5 种作用域:
-
Singleton:单例模式,在整个 spring IOC 容器中,使用 singleton 定义的 bean 将只有一个实例。
-
Prototype:多例模式,每次通过容器中的 getBean 方法获取 prototype 定义的 beans 时,都会产生一个新的 bean 的实例。
-
Request:对于每次 Http 请求,使用 request 定义的 bean 都会产生一个新的实例,只有在web 应用时候,该作用域才会有效。
-
Session:对于每次 Http Session,使用 session 定义的 Bean 都将产生一个新的实例。
-
Globalsession:每个全局的 Http Sesisonn,使用 session 定义的本都将产生一个新的实例
Spring bean 的生命周期?
首先说一下Servlet的生命周期:实例化、依赖注入、初始化、使用Bean、销毁Bean
- Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
- Bean实例化后对将Bean的引入和值注入到Bean的属性中
- 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
- 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
- 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
- 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
- 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
- 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
- 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
Spring 中 beanFactory 和 ApplicationContext 的联系和区别?
BeanFactory 是 spring 中较为原始的 Factory,无法支持 spring 的许多插件,如 AOP 功能、Web 应用等。
ApplicationContext 接口是通过 BeanFactory 接口派生而来的,除了具备 BeanFactory 接口的功能外,还具备资源访问、事件传播、国际化消息访问等功能。
总体区别如下:
- 使用 ApplicationContext,配置 bean 默认配置是 singleton,无论是否使用,都会被实例化。优点是预先加载,缺点是浪费内存;
- 使用 BeanFactory 实例化对象时,配置的 bean 等到使用的时候才会被实例化。优点是节约内存,缺点是速度比较慢,多用于移动设备的开发;
- 没有特殊要求的情况下,应该使用 ApplicationContext 完成,ApplicationContext 可以实现BeanFactory 所有可实现的功能,还具备其他更多的功能。
Spring 的重要注解?
注解 | 说明 |
---|---|
@Component | 使用在类上用于实例化Bean |
@Controller | 使用在Web层类上用于实例化Bean(SpringMVC中会讲到) |
@Service | 使用在Service层类上用于实例化Bean |
@Repository | 使用在Dao层类上用于实例化Bean |
@Autowired | 使用在字段上用于根据类型依赖注入 |
@Qualifier | 结合@Autowired一起使用用于根据名称进行依赖注入 |
@Resource | 相当于@Autowired+@Qualifier。默认按照名称装配,当找不到与名称匹配的 bean 才会按照类型装配 |
@Value | 注入普通属性 |
@Scope | 标注Bean的作用范围 |
@PostConstruct | 使用在方法上标注该方法是Bean的初始化方法 |
@PreDestroy | 使用在方法上标注该方法是Bean的销毁方法 |
@Transactional | 在需要进行事务控制的类或是方法上开启事务 |
新注解 | 说明 |
@Configuration | 用于指定当前类是一个 Spring配置类,当创建容器时会从该类上加载注解 |
@ComponentScan | 用于指定Spring在初始化容器时要扫描的包。作用和在Spring的xml配置文件中的<context:component-scan base-package=“com.qfedu” />一样。 |
@Bean | 使用在方法上,将该方法的返回值存储到Spring容器中,通常用于管理第三方类(不是自己定义的类)的bean |
@PropertySource | 用于加载.properties文件中的配置 |
@Import | 用于导入其他配置类 |
@MapperScan | 扫描特定包下的mapper |
@EnableTransactionManagement | 配置事务的注解驱动 |
什么是 spring 自动装配?
就是将一个 Bean 注入到其它的 Bean 的 Property 中,默认情况下,容器不会自动装配,需要我们手动设定。Spring 可以通过向 Bean Factory 中注入的方式来搞定 bean 之间的依赖关系,达到自动装配的目的。
自动装配有哪些方式?依赖注入的方式
基于xml文件的自动装配:byType(根据类型,这个过程需要setter注入来完成,因此必须存在setter方法),byName(根据名称), constructor(根据构造函数)
基于注解的自动装配:@Autowired,@Resource,@Value
列举 spring 支持的事务管理类型?Spring 事务实现方式?
Spring 支持两种类型的事务管理:
- 编程式事务:手动开启、提交、回滚事务。就是自己写Try catch语句调用commit\rollback等函数来实现那段业务数据改变的事务性。
- 声明式事务:通过AOP(面向切面)方式在方法前使用编程式事务的方法开启事务,在方法后提交或回滚。用配置文件的方法或注解方法(如:@Transactional)控制事务。
事务注解的本质是什么?
@Transactional 这个注解仅仅是一些(和事务相关的)元数据,在运行时被事务基础设施读取消费,并使用这些元数据来配置bean的事务行为。 大致来说具有两方面功能,一是表明该方法要参与事务,二是配置相关属性来定制事务的参与方式和运行行为
@Transactional注解既可以标注在类上,也可以标注在方法上。当在类上时,默认应用到类里的所有方法。如果此时方法上也标注了,则方法上的优先级高。 另外注意方法一定要是public的。
声明式事务主要是得益于Spring AOP。使用一个事务拦截器,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。
Spring 的事务传播行为?Spring事务定义了7种传播机制:
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 默认的Spring事物传播级别,如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
Spring框架中都用到了哪些设计模式?
这是一道相对有难度的题目,你不仅要回设计模式,还要知道每个设计模式在Spring中是如何使用的。
- 简单工厂模式:Spring 中的 BeanFactory 就是简单工厂模式的体现。根据传入一个唯一的标识来获得 Bean 对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。
- 工厂模式:Spring 中的 FactoryBean 就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean() 调用获得该 bean 时,会自动调用该 bean 的 getObject() 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect()方法的返回值。
- 单例模式:在 spring 中用到的单例模式有: scope=“singleton” ,注册式单例模式,bean 存放于Map 中。bean name 当做 key,bean 当做 value。
- 原型模式:在 spring 中用到的原型模式有: scope=“prototype” ,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。
- 迭代器模式:在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。
- 代理模式:Spring 中经典的 AOP,在 AOP 中被使用最多。就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。
- 适配器模式:Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
- 观察者模式:Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承EventListener。
- 模板模式:Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。
责任链模式:DispatcherServlet 中的 doDispatch() 方法中获取与请求匹配的处理器HandlerExecutionChain,this.getHandler() 方法的处理使用到了责任链模式。
注意:这里只是列举了部分设计模式,其实里面用到了还有享元模式、建造者模式等。可选择性的回答,主要是怕你回答了迭代器模式,然后继续问你,结果你一问三不知,那就尴了尬了。
Spring 框架中的单例 Bean 是线程安全的么?
不是线程安全的
Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究
SpringMVC
什么是MVC模式
MVC:MVC是一种设计模式
SpringMVC 工作流程?
理解springMVC中的几个组件:
- 前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。
- 处理器映射器(HandlerMapping):根据URL去查找处理器。
- 处理器(Handler):需要程序员去写代码处理逻辑的。
- 处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理器,类比笔记本的适配器(适配器模式的应用)。
- 视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页面。
流程
- 用户发送请求,请求被spring前端控制器DispatcherServlet捕获
- DispatcherServlet对url解析,根据url调用HandlerMapper
- HandlerMapper根据xml配置、注解等进行查找具体的Handler(即处理器)对象以及Handler对象对应的拦截器(如果有则生成),最后以HandlerExceptionChain(处理器执行链)对象返回
- DispatcherServlet调用HandlerAdapter(处理器适配器)处理相应的Handler
- HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器,就是我们书写的对外接口);
- Controller执行完成返回ModelAndView;
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet;
- DispatcherServlet将ModelAndView传给ViewReslover视图解析器;
- ViewReslover解析后返回具体View;
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中);
- DispatcherServlet响应用户。
- 现在浏览器就能正常访问了
介绍一下 WebApplicationContext?
WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定与哪个 servlet 关联的能力方面有所不同。
- ApplicationContext 接口是通过 BeanFactory 接口派生而来的,除了具备 BeanFactory 接口的功能外,还具备资源访问、事件传播、国际化消息访问等功能。
- BeanFactory 可以理解为一个Spring容器
SpringMVC常用的注解有哪些?
- @Controller:这个注解的作用和@Component作用是一样的,用来配置bean让Spring容器进行管理。配置了@Controller注解之后,需要在SpringMVC配置文件中配置包扫描:
<context:component-scan />
- @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
- @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
- @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
- @RequestHeader、
- @CookieValue
- @PathVariable(“username”)配合@GetMapping(“/test7/{username}”)
springBoot
为什么要用SpringBoot
Spring Boot 优点非常多,如:
- 一、独立运行
- Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
- 二、简化配置
- spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
- 三、自动配置
- Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
- 四、无代码生成和XML配置
- Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
- 五、应用监控
- Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})。
- @ComponentScan:Spring组件扫描。
运行Spring Boot有哪几种方式?
1)打包用命令或者放到容器中运行
2)用 Maven/Gradle 插件运行
3)直接执行 main 方法运行
如何理解 Spring Boot 中的 Starters?
Starters是什么
Starters可以理解为启动器,一个starter就是一个开发场景的支持( 依赖 + 配置)。
Starters命名:
- 官方的:spring-boot-starter-*
- 第三个的或者自定义的::xxx-spring-boot-starter
Spring Boot中的监视器是什么?
Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态。
如何使用Spring Boot/Spring MVC实现异常处理?
Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice类,来处理控制器类抛出的所有异常。
@ControllerAdvice
public class WebExceptionHandler {
/**
* 全局异常处理,处理Exception
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleRuntimeException(Exception e) {
System.out.println("MyException----全局异常处理..." + e.getMessage());
return "{\"msg\":\"出错了\"}";
}
}
springboot常用的starter有哪些
spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持
spring-boot-starter-data-jpa 数据库支持
spring-boot-starter-data-redis redis数据库支持
spring-boot-starter-data-solr solr支持
mybatis-spring-boot-starter 第三方的mybatis集成starter
SpringBoot 实现热部署有哪几种方式?
主要有两种方式:
依赖:spring-boot-devtools
或者直接使用jrebel
如何理解 Spring Boot 配置加载顺序?
优先级:bootstrap.properties > bootstrap.yml > application.properties > application.yml
在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties文件;
2)YAML文件;
3)系统环境变量;
4)命令行参数;
等等……
Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
pring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
-
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
-
bootstrap 配置文件有以下几个应用场景。
- 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
- 一些固定的不能被覆盖的属性;
- 一些加密/解密的场景;
mybatisPlus
mybatisPlus功能
概念:MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis常用方法
public interface UserMapper extends BaseMapper<User> {
}
// 插入一条记录
int insert(T entity);
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 whereEntity 条件,更新记录
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page,
@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
条件构造器=》QueryWrapper
-
allEq(Map<R, V> params)
-
基本比较操作
-
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("password", "123456").ge("age", 20).in("name", "朱雀", "玄武", "饕鬄"); // - eq:等于 = - ne:不等于 <> - gt:大于 > - ge:大于等于 >= - lt:小于 < - le:小于等于 <= - between:BETWEEN 值1 AND 值2 - notBetween:NOT BETWEEN 值1 AND 值2 - in:字段 IN (value.get(0), value.get(1), ...) - notIn:字段 NOT IN (v0, v1, ...)
-
-
排序=>
orderBy(true, true, "id", "name")`--->`order by id ASC,name ASC
/orderByAsc("id", "name")
—>order by id ASC,name ASC
-
select("id", "name", "age")
=> 在MP查询中,默认查询所有的字段,如果有需要也可以通过select方法进行指定字段。
常用注解
-
当表名跟实体类类名不一致时,要使用@TableName注解进行映射=》
@TableName(value = "tb_employee")
-
当表中的列与实体类属性不一致时,使用TableField指定数据库中的列名=>
@TableField(value = "emp_name")
-
ID生成策略=>
@TableId(type = IdType.ID_WORKER)
-
@Getter public enum IdType { /** * 数据库ID自增 */ AUTO(0), /** * 该类型为未设置主键类型 */ NONE(1), /** * 用户输入ID * <p>该类型可以通过自己注册自动填充插件进行填充</p> */ INPUT(2), /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */ /** * 全局唯一ID (idWorker) */ ID_WORKER(3), /** * 全局唯一ID (UUID) */ UUID(4), /** * 字符串全局唯一ID (idWorker 的字符串表示) */ ID_WORKER_STR(5); private final int key; IdType(int key) { this.key = key; } }
-
Swagger常见注解
Swagger的所有注解定义在
io.swagger.annotations
包下
Swagger注解 | 说明 |
---|---|
@Api(tags = “xxx模块说明”) | 作用在控制器类上 |
@ApiOperation(“xxx接口说明”) | 作用在接口方法上 |
@ApiModel(“xxxPOJO说明”) | 作用在模型类上:如VO、BO |
@ApiModelProperty(value = “xxx属性说明”,hidden = true) | 作用在类方法和属性上,hidden设置为true可以隐藏该属性 |
@ApiImplicitParams()和@ApiParam(“xxx参数说明”) | 作用在参数、方法和字段上,类似@ApiModelProperty |
java与json互相转化
-
在日常实践中通常会对JSON数据和Java对象进行相互转换,转换需要用到JSON解析器,常见的解析器如下:
- Jsonlib(JSON官方)
- Gson(Google)
- Jackson(Spring官方)
- Fastjson(Alibaba)
作为 Spring MVC / SpringBoot 技术栈 开发的话,用默认的
Jackson
是最好的!数据量少时:Gson快
数据量多时:FastJson快
数据量无论多少:Jackson最稳定
Fastjson使用
//java对象(包括list、map)转json字符串
String pStr1 = JSON.toJSONString(user);
System.out.println("bean=》json:"+pStr1);
//json字符串转java对象:
User bean= JSONObject.parseObject("{\"username\":\"张三\"}", User.class);
System.out.println(bean);
日志
- 日志:有异常则就需要日志记录,这也是一种规范
- 规范:日志规范就是一些接口,常见的规范,apache的JCL(Jakarta Commons Logging),slf4j (Simple Logging Facade for Java)
- 具体实现技术:Log4j、JUI、LogBack、log4j2
- 发展历程:
- Log4j:Apache的一个开源项目,解决传统日志
System.out.println()
无法定制化,且日志粒度不够细。 - JUI(java util logging): Java原生日志框架,亲儿子,仿照Log4J
- JCL(Jakarta Commons Logging):制定日志标准(就像jdbc一样),使其可以选择合适的日志实现类
- Slf4j(simple Logging Facade for java):原Log4J的作者,它觉得Apache Commons Logging不够优秀,所以他想搞一套更优雅的方案,于是slf4j日志体系诞生了,slf4j实际上就是一个日志门面接口,它的作用类似于Commons Loggins。 并且他还为slf4j提供了一个日志的实现-logback。
- LogBack: 由Log4j之父做的另一个开源项目
- log4j2: 因为slf4j以及它的实现logback出来以后,很快就赶超了原本apache的log4j体系,所以apache在2012年重写了log4j, 成立了新的项目Log4j2 ,几乎涵盖了logback的全部新特性
- Log4j:Apache的一个开源项目,解决传统日志
- 日志级别:用来控制系统中哪些日志级可以输出、输出不低于设定级别的日志信息。优先级从低到高依次为:ALL < TRACE < DEBUG(logback默认) < INFO < WARN < ERROR(log4j默认) < FATAL < OFF,加粗的标识常见
- all【所有】:所有日志级别,包括定制级别。
- trace【跟踪/追踪】: 一般用来追踪详细的程序运行流,比如程序的运行过程中,运行到了哪一个方法,进入了哪一条分支。通过trace程序的运行流程,可以判断程序是否按照期望的逻辑在运行。
- debug【调试】: 这类日志往往用在判断是否有出现bug的场景,且往往记录了代码运行的详细信息,比如方法调用传入的参数信息。
- info【信息】:用来记录程序运行的一些关键信息,它不像trace那样记录程序运行的整个流程,也不像debug那样为了解决问题而记录详细的信息。info记录的是整个系统的运行信息,比如系统运行到了哪一个阶段,到达了哪一个状态。
- warn【警告】:用来记录一些警告信息。警告信息表示,程序进入了一个特殊的状态,在该状态下程序可以继续运行,但是不建议让程序进入该状态,因为该状态可能导致结果出现问题。
- error【错误】:用来记录运行时的错误信息,表示程序运行过程中出现了需要被解决的问题,往往是一些异常。使用error日志的时候,一般会将详细的异常出现的原因记录
- fatal【致命】:指明非常严重的可能会导致应用终止执行错误事件。
- off【关闭】:最高级别,不打印日志。
- 打印格式注意
- 日志要打印出方法的入参、出参
- 建议使用参数占位{},而不是用+拼接。
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例:logger.error(各类参数或者对象 toString + “_” + e.getMessage(), e); - 禁止在线上环境开启 debug,因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。
springcloud
什么是SpringCloud
Spring Cloud 是一套完整的微服务解决方案,基于 Spring Boot 框架,准确的说,它不是一个框架,而是一系列框架的集合体,它将市面上较好的微服务框架集成进来,从而简化了开发者的代码量。
SpringBoot和SpringCloud的区别?
SpringBoot专注于快速方便的开发单个个体微服务。
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系.
SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
什么是微服务?架构演进?
单体应用架构—>垂直应用架构—>分布式架构—>SOA架构—>微服务架构
-
单体架构:所有模块都在一块,完成所有功能
- 缺点:系统的启动、重启时间周期过长;任何一个模块的错误均可能造成整个系统的宕机;
-
垂直架构:垂直架构是指将单体架构中的多个模块拆分为多个独立的项目。形成多个独立的单体架构。
- 优点:一个系统发生了故障,不影响其他系统的运行情况,提高了整体的容错率。
- 缺点:
- 1:拆分后的各系统之间相对比较独立,无法进行互相调用。
- 2:各系统难免存在重叠的业务,会存在重复开发的业务,后期维护比较困难。
-
分布式架构:是指在垂直架构的基础上,将公共业务模块抽取出来,作为独立的服务供其他调用者消费,以实现服务的共享和重用。底层通过RPC(远程过程调用实现)
- RPC: Remote Procedure Call远程过程调用。有非常多的协议和技术来都实现了RPC的过程。比如: HTTP REST风格,Java RMI规范、WebService SOAP协议Hession等等。
- 优点
- 1:将重复的业务代码抽象出来,形成公共的访问服务,提高了代码的复用性。
- 2:可以有针对性的对系统和服务进行性能优化,以提升整体的访问性能。
- 缺点
- 系统之间的耦合度变高,调用关系变得复杂,难以维护。
-
SOA: (Service- Oriented Architecture,面向服务的架构):是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和契约联系起来。
- ESB: (Enterparise Servce Bus):企业服务总线,服务中介。主要是提供了一一个服务于服务之间的交互。ESB包含的功能如:负载均衡,流量控制,加密处理,服务的监控,异常处理,监控告急等等。
- 优点:使用注册中心解决了各个服务之间的服务依赖和调用关系的自动注册与发现。
- 缺点:服务之间的依赖与调用关系复杂,测试部署的困难比较大。
-
微服务架构:微服务架构是在SOA上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
springCloud常用组件的用途及其学习流程
技术搭配方案如下
- nacos :注册中心(服务的注册与发现)
- nacos :配置中心(动态配置管理-微服务配置文件的中心化管理,同时配置信息的动态刷新)
- Ribbon: 负载均衡(客户端负载均衡器,解决微服务集群负载均衡的问题)
- Openfeign:声明式HTTP客户端(解决微服务之间远程调用问题)
- Sentinel:服务容错(限流、熔断、降级 - 微服务流量防卫兵,以流量为入口,保护微服务,防止服务雪崩)
- gateway:微服务网关(webflux编码模式-服务集群的入口,路由转发以及负载均衡(全局认证、流控))
- sleuth:链路追踪(可以快速的进行链路的错误和耗时排查)
- seata:分布式事务解决方案
以上是SpringCloud Alibaba的组件 - 就nacos、sentinel、seata
什么是服务熔断?什么是服务降级?
熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。
服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。
nacos领域模型/实现多环境部署
-
- nacos的服务由三元组唯一确定 (namespace、group、servicename)
- nacos的配置由三元组唯一确定 (namespace、group、dataId)
- 不同的namespace是相互隔离的,相同namespace但是不同的group也是相互隔离的
- 默认的namespace是public ,不能删除
- 默认的group是DEFAULT-GROUP
- 使用领域模型实现多环境的服务隔离(多环境公用一个nacos)
负载平衡(Load Balance,简称 LB)的意义什么?
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
什么是Ribbon?
ribbon是一个负载均衡客户端,可以很好的控制http和tcp的一些行为。feign默认集成了ribbon。
Ribbon快速入门
springcloud alibaba 对Ribbon做了兼容,所以我们不需要在导入依赖
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
Ribbon负载均衡原理
ribbon饥饿加载和懒加载
- Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
- 而饥饿加载则会在项目启动时创建,降低第一次访问的耗时
Ribbon负载均衡策略切换(IRule)
- Ribbon核心组件IRule:定义负载均衡规则,根据特定算法从服务列表中选取一个需要访问的服务;其中IRule是一个接口,有七个自带的落地实现类,可以实现不同的负载均衡算法规则,一般就使用下面三个
名称 | 功能 |
---|---|
RoundRobinRule | 轮询 |
RandomRule | 随机 |
ZoneAvoidanceRule(默认) | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
远程服务调用-OpenFeign
解决服务之间的远程调用,使用OpenFeign取代Resttemplate
Feign vs OpenFeign
- Feign 内置了Ribbon,封装了Ribbon和RestTemplate, 用来做客户端负载均衡调用服务注册中心的服务。Feign本身并不支持Spring MVC的注解,它有一套自己的注解。为了更方便的使用Spring Cloud孵化了OpenFeign。并且支持了Spring MVC的注解,如@RequestMapping,@PathVariable等等。
什么是 feigin ?它的优点是什么?
Feign 是Spring Cloud Netflix组件中的一量级Restful的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架**,封装了Ribbon和RestTemplate,** 实现了WebService的面向接口编程,进一步降低了项目的耦合度
Feign默认集成了Ribbon,Nacos也很好的兼容了Feign,默认实现了负载均衡的效果。
1.feign采用的是基于接口的注解
2.feign整合了ribbon,具有负载均衡的能力
3.整合了Hystrix,具有熔断的能力
Feign/OpenFeign使用:
- 添加pom依赖。
- 启动类添加@EnableFeignClients
- 定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务
Feign vs OpenFeign
Feign 内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务。
Feign 支持的注解和用法参考官方文档:https://github.com/OpenFeign/feign官方文档,使用 Feign 的注解定义接口,然后调用这个接口,就可以调用服务注册中心的服务。
Feign本身并不支持Spring MVC的注解,它有一套自己的注解,为了更方便的使用Spring Cloud孵化了OpenFeign。并且支持了Spring MVC的注解,如@RequestMapping,@PathVariable等等。
OpenFeign的@FeignClient可以解析Spring MVC的@RequestMapping注解下的接口,并通过动态代理方式产生实现类,实现类中做负载均衡调用服务。
服务容错-Sentinel
概述:Sentinel 是阿里巴巴开源的一个轻量级流量控制框架,主要用于保护系统稳定性和流畅性。它提供了多种流量控制策略,包括QPS限流、并发数限流、线程池限流等,并且支持集群限流。此外,Sentinel还提供了熔断降级、系统负载均衡等功能。Sentinel通过流量控制(flow control)以及熔断降级来保护系统资源
雪崩问题以及雪崩问题的四种解决思路?
雪崩问题:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
- 1,服务提供者不可用(硬件故障,程序bug,缓存击穿,用户大量请求)
- 2,重试加大流量(用户重试,代码逻辑重试)
- 3,服务调用者不可用(同步等待造成的资源耗尽)
雪崩问题的四种解决思路
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
- 这只能缓解雪崩问题,而不能彻底根除,因为比如设置等待1s就释放1个连接,但一秒钟之内进来了2个请求,终有一天,资源也可能被耗尽。
- 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
- 不至于所有资源被耗尽(不至于整条船全进水了)。如此就解决了雪崩问题,但造成了一点资源浪费,明知A到C不通了,但还是在消耗资源去请求。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会
熔断
该业务,拦截
访问该业务的一切请求。 - 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障
微服务网关-gateway
在springcloud中网关的实现方式有两种:
- zuul:zuul是基于servlet的实现,属于阻塞式编程
- gateway:geteway基于spring5中提供的webflux,属于响应式编程的实现,具有更好的性能
- 底层都是servlet,两者均是web网关
Sentinel规则持久化
- 原始模式:
- 如果不做任何修改,Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中:这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
- Pull模式
- FileRefreshableDataSource 定时从指定文件中读取规则JSON文件【图中的本地文件】,如果发现文件发生变化,就更新规则缓存。
- Push模式
- 生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经 Sentinel 数据源推送至配置中心。
微服务网关-gateway:网关的作用?
- 身份认证和权限校验
- 服务路由、负载均衡
- 请求限流
网关的技术实现
在springcloud中网关的实现方式有两种:
- zuul:zuul是基于servlet的实现,属于阻塞式编程
- gateway:geteway基于spring5中提供的webflux,属于响应式编程的实现,具有更好的性能
相同点:
- 底层都是servlet
- 两者均是web网关
gateway核心概念
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
- Predicate(断言、谓词):开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
- Path
- After
- …
- Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
gateway的工作流程
-
- 1:客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由
- 2:将其发送到 Gateway Web Handler。
- 3:Handler 再通过指 定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
Sleuth与Zipkin的关系?
- sleuth :链路追踪器
- zipkin:链路分析器。可以理解为可视化界面,配合Sleuth可以清晰定位请求流程。
分布式事务解决方案-seata使用
- 创建undo_log表
- pom依赖
- 相关配置
- @Globaltransational
seata分布式事务的工作流程/原理/机制
- 模式角色:
- Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
- Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
- Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
- 分布式事务的工作流程
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
redis
什么是 Redis?
Redis 是完全开源的,C语言编写,基于内存,是一个高性能的 Nosql(非关系型) 数据库。
Redis 是一个开源(BSD 许可)、基于内存、支持多种数据结构的存储系统,可以作为数据库、缓存和消息中间件。
它支持的数据结构string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)等,除此之外还支持 bitmaps、hyperloglogs 和地理空间(geospatial )索引半径查询等功能。
它内置了复制(Replication)、LUA 脚本(Lua scripting)、LRU 驱动事件(LRU eviction)、事务(Transactions)和不同级别的磁盘持久化(persistence)功能,并通过 Redis 哨兵(哨兵)和集群(Cluster)保证缓存的高可用性(High availability)
redis的数据类型,以及每种数据类型的使用场景
一共五种
- (一)String 这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
- (二)hash 这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
- (三)list 使用 List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
- (四)set: 因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。 另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
- (五)sorted set:多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP(N) 操作。Sorted Set 可以用来做延时任务。
为什么要用缓存?Redis:使用场景
使用缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,带来更好的性能,带来更高的并发量。 Redis 的读写性能比 Mysql 好的多,我们就可以把 Mysql 中的热点数据缓存到 Redis 中,提升读取性能,同时也减轻了 Mysql 的读取压力。
Redis:使用场景
-
1:查询缓存
-
2:分布式锁
-
setnx
-
RLock lock = redisson.getLock("myLock"); try { //尝试加锁,最多等待10秒,上锁以后10秒自动解锁 if (lock.tryLock()) { try { //处理 logger.info("tryLock thread---{}, lock:{}", Thread.currentThread().getId(), lock); } catch (Exception e) { } finally { //解锁 lock.unlock(); } } } catch (InterruptedException e) { //处理 //保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应、 }
-
-
3:秒杀活动数据存储(将Redis当做database来存储商品信息以及用户的下单信息)
-
4:注册、登录 使用Redis存储验证码(Redis的key可以设置过期时间)
说一下 Redis 有什么优点和缺点
优点
- 速度快:因为数据存在内存中,类似于 HashMap , HashMap 的优势就是查找和操作的时间复杂度都是O (1) 。
- 支持丰富的数据结构:支持string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)五种基础的数据结构。
- 持久化存储:Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的万一 Redis 挂掉,数据会消失掉
- 高可用:内置 Redis Sentinel ,提供高可用方案,实现主从故障自动转移。 内置 Redis Cluster ,提供集群方案,实现基于槽的分片方案,从而支持更大的 Redis 规模。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务
- 丰富的特性:Key过期、计数、分布式锁、消息队列等。
缺点
- 由于 Redis 是内存数据库,所以,单台机器,存储的数据量,跟机器本身的内存大小。虽然Redis 本身有 Key 过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
- 如果进行完整重同步,由于需要生成 RDB 文件,并进行传输,会占用主机的 CPU ,并会消耗现网的带宽。不过 Redis 2.8 版本,已经有部分重同步的功能,但是还是有可能有完整重同步的。比如,新上线的备机。
- 修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中, Redis不能提供服务。
Redis 为什么是单线程的?Redis 为什么设计成单线程的?
多线程处理会涉及到锁,并且多线程处理会涉及到线程切换而消耗 CPU。官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
为什么 Redis 单线程模型效率也能那么高?单线程的redis为什么这么快?
- C语言实现,效率高
- 纯内存操作
- 基于非阻塞的IO复用模型机制
- 单线程的话就能避免多线程的频繁上下文切换问题
- 丰富的数据结构(全称采用hash结构,读取速度非常快,对数据存储进行了一些优化,比如亚索表,跳表等)
Redis 持久化方式有哪些?以及有什么区别?
Redis 提供两种持久化机制 RDB 和 AOF 机制:
-
RDB (redis data base)持久化方式(默认):在指定的时间间隔内将内存中的数据集(二进制序列化)快照写入一个临时文件( dump.rdb),通过配置文件中的save参数来定义快照的周期( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)。
- 实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
- 数据恢复:用这个临时文件替换上次持久化的文件,达到数据恢复。
- 优点:
- 只有一个文件 dump.rdb ,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 相对于数据集大时,比 AOF 的启动使用速度更快。
- 缺点:
- 数据安全性低。 RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
-
AOF(Append only file)持久化方式:将每个写(没有读)操作追加到aof文件的末尾。
- 数据恢复:根据日志完整走一遍
- 优点:
- (1)数据安全,不太可能丢失数据, AOF 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 AOF 文件中一次。
- (2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
- (3) AOF 机制的 rewrite 模式。 AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall )
- 缺点:
- (1) AOF 文件比 RDB 文件大,且写入性能比RDB低。
- (2)数据集大的时候,比 RDB 启动效率低。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
持久化有两种,那应该怎么选择呢?
- 不要仅仅使用 RDB ,因为那样会导致你丢失很多数据。
- 也不要仅仅使用 AOF ,因为那样有两个问题
- 第一,你通过 AOF 做冷备没有 RDB 做冷备的恢复速度更快;
- 第二, RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug 。
- Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
- 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。
说说你对Redis事务的理解
什么是 Redis 事务?原理是什么?
-
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。
-
它的原理是先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。
-
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 Redis会将一个事务中的所有命令序列化,然后按顺序执行。
- MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
- EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
- WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
Redis 事务的注意点有哪些?
- Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
- Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
Redis 事务为什么不支持回滚?
- Redis 的事务不支持回滚,但是执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
Redis 缓存刷新策略有哪些?
Redis 报内存不足怎么处理?
Redis 内存不足可以这样处理:
- 配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存;
- 使用 Redis 集群模式,提高存储量。
- 设置缓存淘汰策略,提高内存的使用效率;
redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
- 定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
- 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。 在redis.conf中有一行配置maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据,新写入操作会报错 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
缓存击穿,缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
-
缓存击穿:
某一个
热点key,在失效的一瞬间
,持续的高并发访问击破缓存
直接访问数据库,导致数据库造的周期性压力(极端情况下导致数据库宕机)- 加锁(分布式锁),互斥锁
- 分散key的过期时间或者设置永不过期;
-
缓存雪崩:缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到Redis内的热点数据失效导致大量并发查询穿过redis直接击打到底层数据库,而缓存雪崩是指Redis中大量的key几乎同时过期,然后大量并发查询穿过redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。
- 分散key的过期时间或者设置永不过期;这一点和缓存击穿中的方案一样;
- 集群
-
缓存穿透:指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
- 缓存空值:直接将不合法的数据也存到redis中-存在大量的垃圾数据
- 布隆过滤(redisson):先把不合法的数据存到布隆过滤器,然后直接过滤不合法的数据
-
缓存预热:缓存预热如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。
- 1、直接写个缓存刷新页面,上线时手工操作下;
- 2、数据量不大,可以在项目启动的时候自动进行加载;
- 3、定时刷新缓存;
-
缓存更新/缓存同步(缓存和数据库数据同步问题):缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。
- 先删除缓存,再更新数据库:理论上可能会出现一小段时间数据不一致,不过这种概率也比较低,大部分的业务也不会有太大的问题。=》一般使用延迟双删(修改数据库之后再次删除缓存)
- 先更新数据库,在删除缓存:这种方法在并发下最容易出现长时间的脏数据,不可取
- 有个问题,删除缓存失败=>canal(阿里框架,感知到数据库的变化,监听mysql的binlog日志。相当于主从)+mp
-
缓存降级:Redis服务不可用,或者网络抖动,导致服务不稳定。但是仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
- 使用Sentinel熔断降级策略进行服务熔断降级
- 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
redis集群如何部署
redis有三种集群模式:主从模式、哨兵模式、Cluster模式。
- 一开始使用的主从模式做集群,但是master宕机需要手动配置slave转为master
- 所以后来提出来哨兵模式,这样master宕机就自动将slave转为master。但是也有一个问题,就是不能动态扩展
- 所以后来提出来Cluster集群
Redis集群:一主二从三哨兵
-
-
主(master):读写操作
-
从(slave):只读
-
哨兵:是整个集群的入口。为什么三个,因为一旦出现问题,要从“从”中选择一个新的为“主”
- 监控(Monitoring):Sentinel 会不断地定期检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automaticfailover): 当主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作,选举新主
为什么 使用 Redis 而不是用 Memcache 呢?
这时候肯定想到的就是做一个 Memcache 与 Redis 区别。
-
Redis 和 Memcache 都是将数据存放在内存中,都是内存数据库。不过 Memcache 还可用于缓存其他东西,例如图片、视频等等。
-
Memcache 仅支持key-value结构的数据类型,Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,hash等数据结构的存储。
-
虚拟内存– Redis 当物理内存用完时,可以将一些很久没用到的value 交换到磁盘
-
分布式–设定 Memcache 集群,利用 magent 做一主多从; Redis 可以做一主多从。都可以一主一从
-
存储数据安全– Memcache 挂掉后,数据没了; Redis 可以定期保存到磁盘(持久化)
-
Memcache 的单个value最大 1m , Redis 的单个value最大 512m 。
-
灾难恢复– Memcache 挂掉后,数据不可恢复; Redis 数据丢失后可以通过 aof 恢复
-
Redis 原生就支持集群模式, Redis3.0 版本中,官方便能支持Cluster模式了, Memcached 没有原生的集群模式,需要依赖客户端来实现,然后往集群中分片写入数据。
-
Memcached 网络IO模型是多线程,非阻塞IO复用的网络模型,原型上接近于 nignx 。而 Redis使用单线程的IO复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现类epoll,kqueue 和 select ,更接近于Apache早期的模式。
ElasticSearch篇
什么是ElasticSearch?
Elasticsearch是一个基于Lucene的分布式多用户全文搜索引擎,提供了RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎
ElasticSearch中的集群、节点、分片、索引、文档、类型是什么?
-
集群**(Cluster)是一个或多个节点(服务器)的集合**,它们共同保存您的整个数据,并提供跨所有节点的联合索引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设置为按名称加入群集,则该节点只能是群集的一部分。
-
节点**(Node)是属于集群一部分的单个服务器**。它存储数据并参与群集索引和搜索功能。
-
分片(shard)是指分布在不同节点的数据,当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的。
- 当你分片设置为5,数据量为30G时,es会自动帮我们把数据均衡地分配到5个分片上,即每个分片大概有6G数据,当你查询数据时,ES会把查询发送给每个相关的分片,并将结果组合在一起。
-
副本(Replia)为提高查询吞吐量或实现高可用性,可以使用分片副本。副本是分片的副本。,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。
- 副本,就是对分布在5个分片的数据进行复制。因为分片是把数据进行分割而已,数据依然只有一份,这样的目的是保障查询的高效性,副本则是多复制几份分片的数据,这样的目的是保障数据的高可靠性,防止数据丢失。
注意:索引建立后,分片个数是不可以更改的
RDBS | ES(NOSQL) |
---|---|
数据库(database) | index(索引库) |
表(table) | 类型(type)(ES6.0之后被废弃,es7中完全删除) |
表结构(schema)字段和字段类型 | 映射(mapping) |
行(row) | 文档(document)以json方式存储数据 |
列(column) | 域(field) |
索引(b+tree) | 倒排索引(根据关键词找文档列表) |
SQL | 查询DSL |
SELECT * FROM table | GET http://… |
UPDATE table SET | PUT http://… |
DELETE | DELETE http://… |
能说说ElasticSearch 写索引的逻辑吗?
ElasticSearch 是集群的 = 主分片 + 副本分片。
写索引只能写主分片,然后主分片同步到副本分片上。但主分片不是固定的,可能网络原因,之前还是 Node1 是主分片,后来就变成了 Node2 经过选举成了主分片了。
客户端如何知道哪个是主分片呢? 看下面过程。
- 客户端向某个节点 NodeX 发送写请求
- NodeX 通过文档信息,请求会转发到主分片的节点上
- 主分片处理完,通知到副本分片同步数据,向 Nodex 发送成功信息。
- Nodex 将处理结果返回给客户端。
熟悉ElasticSearch 集群中搜索数据的过程吗?
- 客户端向集群发送请求,集群随机选择一个 NodeX 处理这次请求。
- Nodex 先计算文档在哪个主分片上,比如是主分片 A,它有三个副本 A1,A2,A3。那么请求会轮询三个副本中的一个完成请求。
- 如果无法确认分片,比如检索的不是一个文档,就遍历所有分片。补充一点,一个节点的存储量是有限的,于是有了分片的概念。但是分片可能有丢失,于是有了副本的概念。比如:ES 集群有 3 个分片,分片 A、分片 B、分片 C,那么分片 A + 分片 B + 分片 C = 所有数据,每个分片只有大概 1/3。分片 A 又有副本 A1 A2 A3,数据都是一样的。
谈谈分词与倒排索引的原理
首先说分词是给检索用的。
-
英文:一个单词一个词,很简单。I am a student,词与词之间空格分隔。
-
中文:我是学生,就不能一个字一个字地分,我-是-学生。这是好分的。还有歧义的,使用户放心,使用-户,使-用户。人很容易看出,机器就难多了。所以市面上有各种各样的分词器,一个强调的效率一个强调的准确率。
倒排索引:倒排针对的是正排。(es默认分词器(standard))
- 正排就是我记得我电脑有个文档,讲了 ES 的常见问题总结。那么我就找到文档,从上往下翻页,找到 ES 的部分。通过文档找文档内容。
- 倒排:一个 txt 文件 ES 的常见问题 -> D:/分布式问题总结.doc。所以倒排就是文档内容找文档。当然内容不是全部的,否则也不需要找文档了,内容就是几个分词而已。这里的 txt 就是搜索引擎。
说说分段存储的思想
Lucene 是著名的搜索开源软件,ElasticSearch 和 Solr 底层用的都是它。
分段存储是 Lucene 的思想。
早期,都是一个整个文档建立一个大的倒排索引。简单,快速,但是问题随之而来。文档有个很小的改动,整个索引需要重新建立,速度慢,成本高,为了提高速度,定期更新那么时效性就差。
现在一个索引文件,拆分为多个子文件,每个子文件是段。修改的数据不影响的段不必做处理。
谈谈你对段合并的策略思想的认识
分段的思想大大的提高了维护索引的效率。但是随之就有了新的问题。
每次新增数据就会新增加一个段,时间久了,一个文档对应的段非常多。段多了,也就影响检索性能了。
检索过程:
- 查询所有段中满足条件的数据
- 对每个段的结果集合并
所以,定期的对段进行合理是很必要的。真是天下大势,分久必合合久必分。
策略:将段按大小排列分组,大到一定程度的不参与合并。小的组内合并。整体维持在一个合理的大小范围。当然这个大到底应该是多少,是用户可配置的。这也符合设计的思想。
了解文本相似度 TF-IDF(TF-IDF = TF / IDF)吗
简单地说,就是你检索一个词,匹配出来的文章,网页太多了。比如 1000 个,这些内容再该怎么呈现,哪些在前面哪些在后面。这需要也有个对匹配度的评分。
TF-IDF 就是干这个的。
- TF = Term Frequency 词频,一个词在这个文档中出现的频率。值越大,说明这文档越匹配,正向指标。
- IDF = Inverse Document Frequency 反向文档频率,简单点说就是一个词在所有文档中都出现,那么这个词不重要。比如“的、了、我、好”这些词所有文档都出现,对检索毫无帮助。反向指标。
熟悉ElasticSearch 性能优化
- 批量提交
- 背景是大量的写操作,每次提交都是一次网络开销。网络永久是优化要考虑的重点。
- 优化硬盘
- 索引文件需要落地硬盘,段的思想又带来了更多的小文件,磁盘 IO 是 ES 的性能瓶颈。一个固态硬盘比普通硬盘好太多。
- 减少副本数量
- 副本可以保证集群的可用性,但是严重影响了 写索引的效率。写索引时不只完成写入索引,还要完成索引到副本的同步。ES 不是存储引擎,不要考虑数据丢失,性能更重要。 如果是批量导入,建议就关闭副本。
ElasticSearch 查询优化手段有哪些?
设计阶段调优
(1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引;
(2)使用别名进行索引管理;
(3)每天凌晨定时对索引做 force_merge 操作,以释放空间;
(4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存储;
(5)采取 curator 进行索引的生命周期管理;
(6)仅针对需要分词的字段,合理的设置分词器;
(7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。………
写入调优
(1)写入前副本数设置为 0;
(2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制;
(3)写入过程中:采取 bulk 批量写入;
(4)写入后恢复副本数和刷新间隔;
(5)尽量使用自动生成的 id。
查询调优
(1)禁用 wildcard;
(2)禁用批量 terms(成百上千的场景);
(3)充分利用倒排索引机制,能 keyword 类型尽量 keyword;
(4)数据量大时候,可以先基于时间敲定索引再检索;
(5)设置合理的路由机制。
其他调优
部署调优,业务调优等。
上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。
elasticsearch 索引数据多了怎么办,如何调优,部署?
面试官:想了解大数据量的运维能力。
解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。
如何调优:
动态索引层面
- 基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。
存储层面
- 冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。
部署层面
- 一旦之前没有规划,这里就属于应急策略。结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。
MQ
为什么要使用MQ
核心:解耦,异步,削峰
- 解耦:
- A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
- 就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
- 异步:
- A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 +450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。
- 削峰:减少高峰时期对服务器压力。
MQ有什么优缺点?
优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。
缺点有以下几个:
- 系统可用性降低 系统引入的外部依赖越多,越容易挂掉。万一 MQ 挂了,MQ 一挂,整套系统崩溃,你不就完了?
- 系统复杂度提高 硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?问题一大堆。
- 一致性问题 A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
相关概念
- 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。
- 主题(Topic):每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
- 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
- 生产者组(Producer Group):同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
- 消费者组(Consumer Group):同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别?
对于吞吐量来说kafka和RocketMQ支撑高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。对于延迟量来说RabbitMQ是最低的。
-
1.从社区活跃度
- 按照目前网络上的资料,RabbitMQ 、activeM 、ZeroMQ 三者中,综合来看,RabbitMQ 是首选。
-
2.持久化消息比较
- ActiveMq 和RabbitMq 都支持。持久化消息主要是指我们机器在不可抗力因素等情况下挂掉了,消息不会丢失的机制。
-
3.综合技术实现
- 可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统等等。
- RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。
-
4.高并发
- 毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。
-
5.比较关注的比较, RabbitMQ 和 Kafka
- RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上, RabbitMq 胜于 Kafka (理论上)。
- 另外,Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个日志(消息)系统一个重要组件,针对性很强,所以 如果业务方面还是建议选择 RabbitMq 。
- 还有就是,Kafka 的性能(吞吐量、TPS )比RabbitMq 要高出来很多。
RabbitMQ 如何保证高可用的?
RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
-
单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式
-
普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。
-
镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
如何保证消息的可靠传输?如果消息丢了怎么办?
数据的丢失问题,可能出现在生产者、MQ、消费者中
-
生产者丢失:生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
- 此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。吞吐量会下来**,因为太耗性能**。
- 所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个ack消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。所以一般在生产者这块避免数据丢失,都是用confirm机制的。
-
MQ中丢失:就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。设置持久化有两个步骤:创建 queue 的时候将其设置为持久化,这样就可以保证RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。注意,哪怕是你给RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
-
消费端丢失:你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。这个时候得用 RabbitMQ 提供的ack机制,简单来说,就是你关闭 RabbitMQ 的自动ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
如何保证消息的顺序性
先看看顺序会错乱的场景:RabbitMQ:一个 queue,多个 consumer,这不明显乱了;
解决
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
消息积压处理办法:临时紧急扩容:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。 新建一个topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资
源扩大 10 倍,以正常的 10 倍速度来消费数据。 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。 MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有
类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
让你来设计一个消息队列,你会怎么设计
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。
怎么保证消息消费的时候0丢失?
答:Rocketmq默认就有重试机制,如果第一次消费的时候,broker收到的回应是(ConsumeConcurrentlyStatus.RECONSUME_LATER),那么这条消息不会丢失,会进入重试队列,重试的次数与重试的时间可以配置,如果重试的次数超过配置的次数,那么这条消息没有丢失,但是放入死信队列
RocketMQ是全局有序的吗?如果不是怎么做到全局有序?
答:默认一个topic有4个queue,只能做到每个queue的局部有序,不能做到全局有序,如果要做到全局有序,可以将消息发送到一个指定的queue里面。
怎么解决消费幂等问题?
什么是消息的消费幂等
同一个消息,同样一个处理逻辑被成功地处理多次
答:处理办法,幂等令牌,唯一性处理