未完待续 !!!
本篇为本人找实习所准备的面试题,如有问题,请帮提出,谢谢…
相似点:
(1)接口和抽象类都不能被实例化
(2)实现接口或继承抽象类的普通子类都必须实现这些抽象方法
不同点:
(1)抽象类可以包含普通方法和代码块,接口里只能包含抽象方法,静态方法和默认方法,
(2)抽象类可以有构造方法,而接口没有
(3)抽象类中的成员变量可以是各种类型的,接口的成员变量只能是 public static final 类型的,并且必须赋值
重载发生在同一个类中,方法名相同、参数列表、返回类型、权限修饰符可以不同
重写发生在子类中,方法名相、参数列表、返回类型都相同,权限修饰符要大于父类方法,声明异常范围要小于父类方法,但是final和private修饰的方法不可重写
==比较基本类型,比较的是值,==比较引用类型,比较的是内存地址
equlas是Object类的方法,本质上与==一样,但是有些类重写了equals方法,比如String的equals被重写后,比较的是字符值,另外重写了equlas后,也必须重写hashcode()方法
(1)使用try、catch、finaly捕获异常,finaly中的代码一定会执行,捕获异常后程序会继续执行
(2)使用throws声明该方法可能会抛出的异常类型,出现异常后,程序终止
1.HashMap在Jdk1.8以后是基于数组+链表+红黑树来实现的,特点是,key不能重复,可以为null,线程不安全
2.HashMap的扩容机制:
HashMap的默认容量为16,默认的负载因子为0.75,当HashMap中元素个数超过容量乘以负载因子的个数时,就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达64且链表长度大于8时,链表转为红黑树
3.HashMap存取原理:
(1)计算key的hash值,然后进行二次hash,根据二次hash结果找到对应的索引位置
(2)如果这个位置有值,先进行equals比较,若结果为true则取代该元素,若结果为false,就使用高低位平移法将节点插入链表(JDK8以前使用头插法,但是头插法在并发扩容时可能会造成环形链表或数据丢失,而高低位平移发会发生数据覆盖的情况)
(1)使用ConcurrentHashMap
(2)使用HashTable
(3)Collections.synchronizedHashMap()方法
JDK1.7:使用分段锁,将一个Map分为了16个段,每个段都是一个小的hashmap,每次操作只对其中一个段加锁
JDK1.8:采用CAS+Synchronized保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全
(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低
(2)HashTable的Key不允许为null
(3)HashTable只对key进行一次hash,HashMap进行了两次Hash
(4)HashTable底层使用的数组加链表
ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;因为数组在内存中是连续的地址,所以ArrayList查找数据更快,由于扩容机制添加数据效率更低
LinkedList的底层使用链表,在内存中是离散的,没有扩容机制;LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高
(1)使用collentions.synchronizedList()方法为ArrayList加锁
(2)使用Vector,Vector底层与Arraylist相同,但是每个方法都由synchronized修饰,速度很慢
(3)使用juc下的CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为list创建一个副本,期间其它线程读取的都是原本list,写操作都在副本中进行,写入完成后,再将指针指向副本。
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。
StringBuffer可变并且线程安全
StringBuiler可变但线程不安全。
操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer。
hashCode()和equals()都是Obkect类的方法,hashCode()默认是通过地址来计算hash码,但是可能被重写过用内容来计算hash码,equals()默认通过地址判断两个对象是否相等,但是可能被重写用内容来比较两个对象
所以两个对象相等,他们的hashCode和equals一定相等,但是hashCode相等的两个对象未必相等
如果重写equals()必须重写hashCode(),比如在HashMap中,key如果是String类型,String如果只重写了equals()而没有重写hashcode()的话,则两个equals()比较为true的key,因为hashcode不同导致两个key没有出现在一个索引上,就会出现map中存在两个相同的key
面向对象有封装、继承、多态性的特性,所以相比面向过程易维护、易复用、易扩展,但是因为类调用时要实例化,所以开销大性能比面向过程低
浅拷贝:浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝:深拷贝会创造一个一摸一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对对象。
多态的实现要有继承、重写,父类引用指向子类对象。它的好处是可以消除类型之间的耦合关系,增加类的可扩充性和灵活性。
反射是通过获取类的class对象,然后动态的获取到这个类的内部结构,动态的去操作类的属性和方法。
应用场景有:要操作权限不够的类属性和方法时、实现自定义注解时、动态加载第三方jar包时、按需加载类,节省编译和初始化时间;
获取class对象的方法有:class.forName(类路径),类.class(),对象的getClass()
(1)new关键字 (2)Class.newInstance (3)Constructor.newInstance
(4)Clone方法 (5)反序列化
所以在开发里面用个约定俗成, 重写了equals一般会重写hashcode。
16个线程
执行并发写操作,及任意数量线程的读操作。基于 Segment
,包含多个 HashEntry。Node(首结点)
(实现 Map.Entryfinal
用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型。java中的final类有java.lang.String、java.lang.Math、java.util.Scanner、java.net.URL、java.lang.reflect.Parameter、java.time.Year等。
finally
finally是异常处理语句结构的一部分,表示总是执行。
finally中的return会覆盖try和catch中的return值。
finally语句在return语句执行之后return返回之前执行的。
finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变,也可能不变。
try或catch代码块中(return/throw)的返回值保留,再来执行finally代码块中的语句,等到finally代码块执行完毕之后,在把之前保留的返回值给返回出去。
finally代码块执行不到的情况:
在执行到try catch finally前就抛出了异常
try中有 System.exit(0) 代码,会退出JVM
线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),或宕机
finalize
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法最多只被调用一次,但JVM不保证此方法总被调用,
三次握手:
三次握手的目的是建立可靠的通信信道,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的,可以从以下两个方面考虑(我们这里假设客户端是首先发起连接请求):
所以三次握手就能确认双发收发功能都正常,缺一不可
接收端传回发送端所发送的 ACK 是为了告诉客户端,我接收到的信息确实就是你所发送的信号了,这表明从客户端到服务端的通信是正常的。而回传 SYN也就是同步序列编号则是为了建立并确认从服务端到客户端的通信
四次挥手:
标志位FIN=1,序列号seq=u
。此时客户端只能接收数据,不能向服务端发送数据。标志位ACK=1,序列号seq=v,确认号ack=u+1
。此时客户端到服务端的连接已经释放掉,但服务端到客户端的单向连接还能正常传输数据,即使客户端无法收到。标志位FIN=1,标志位ACK=1,序列号seq=w,确认号ack=u+1
。标志位ACK=1,序列号seq=u+1,确认号ack=w+1
。FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK
和FIN
一般都会分开发送,从而比三次握手导致多了一次。
MSL的意思是报文的最长寿命。要确保服务器是否已经收到了我们的ACK报文,过了一个MSL后如果没有收到的话,服务器会重新发FIN报文给客端,此时又花了一个MSL,客户端再次收到FIN报文之后,就知道之前的ACK报文丢失了,然后再次发送ACK报文,一来一去正好2个MSL。
若客户端发送确认释放包后直接关闭,而服务端因为某种原因没有收到客户端的确认释放包,就会一直发送确认请求,而客户端永远不会再响应该请求。
流量控制是TCP 提供的可以让「发送方」根据「接收方」的实际接收能力控制发送数据量的⼀种机制。因为如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费
拥塞控制和流量控制不同,拥塞控制是一个全局性的过程,而流量控制指点对点通信量的控制。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致于过载。
TCP 流量控制,主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。如果TCP发送方收到接收方的零窗口通知后,会启动持续计时器。计时器超时后向接收方发送零窗口探测报文,如果响应仍为0,就重新计时,不为0就打破死锁
慢开始:从小到大主键发送窗口,每收到一个确认报文窗口大小指数增长
拥塞避免:当窗口大小到达一定阈值时,转为拥塞避免,每收到一个确认报文窗口大小+1。若此时网络超时,就把阈值调小一半,重新慢开始
快重传:要求接收方收到请求后要立即回复
快恢复:发送方连续收到多个确认时,就把拥塞避免阈值减小,然后直接开始拥塞避免
UDP
TCP
作用:GET方法的含义是请求从服务器获取资源。而POST方法则是相反操作,它向URI指定的资源提交数据,数据就放在报文的 body里。
参数位置:GET的参数放在URL中,POST的参数存储在实体主体中,并且GET方法提交的请求的URL中的数据最多是2048字节,POST请求没有大小限制
安全性:在 HTTP 协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。GET方法是安全的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的。POST因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的
幂等性:所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。结合刚才说的,GET方法是具有幂等性的。而POST方法不具有幂等性
HTTP 1.0
中,默认使用的是短连接,也就是说每次请求都要重新建立一次连接。HTTP 1.1
起,默认使用长连接 ,默认开启 Connection: keep-alive。HTTP 1.1
中新增了 24 个错误状态响应码。HTTP 1.1
引入了更多的缓存控制策略HTTP 1.0
中会存在浪费带宽的现象,主要是因为不支持断点续传功能,客户端只是需要某个对象的一部分,服务端却将整个对象都传了过来。在HTTP 1.1
中请求头引入了range头域,它支持只请求资源的某个部分,返回的状态码为206。在 HTTP/1.0 中默认使用短连接。也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如 JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 HTTP 会话。
而从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。使用长连接的 HTTP 协议,会在响应头加入:Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。
Keep-Alive
不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。
HTTP最凸出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。
缺点:
URI是统一资源标志符,可以唯一标识一个资源。
URL是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
1xx:请求正在处理
2xx:请求成功处理
3xx:请求重定向 301:永久重定向 302:临时重定向 304:使用本地缓存
4xx:客户端错误 400:请求格式错误 403:没有访问权限 415:请求体过大
5xx:服务端错误
cookie和session都是用来跟踪浏览器用户身份的会话方式
区别:
cookie数据存放在客户的浏览器上,session数据放在服务器缓存中
session比cookie更加安全,因为攻击者可以分析存放在本地的cookie进行cookie欺骗
session会比较占用服务器性能,当访问增多时应用cookie
cookie有大小限制,很多浏览器都限制一个站点最多保存20个cookie
可以通过对URL进行重写,把sessionID附加在URL路径的后面
线程私有区:
(1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧
(2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
(3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
线程共享区:
(4)堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代,内存比例为1:2,新生代的Eden区内存不够时时发生MinorGC,老年代内存不够时发生FullGC
(5)方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用永久代实现,JDK1.8后用元空间实现,元空间使用的是本地内存,而非在JVM内存结构中
堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
(1)标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
(2)复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代
(3)标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
(1)引用计数法:已淘汰,为每个对象添加引用计数器,引用为0时判定可以回收,会有两个对象相互引用无法回收的问题
(2)可达性分析法:从GCRoot开始往下搜索,搜索过的路径称为引用链,若一个对象t没有任何的GCRoo引用链,则判定可以回收
GCRoot有:虚拟机栈中引用的对象,方法区中静态变量引用的对象,本地方法栈中引用的对象
CMS:以最小的停顿时间为目标、只运行在老年代的垃圾回收器,使用标记-清除算法,可以并发收集。
G1 :JDK1.9以后的默认垃圾回收器,注重响应速度,支持并发,采用标记整理+复制算法回收内存,使用可达性分析法来判断对象是否可以被回收。
类加载器:
从父类加载器到子类加载器分别为:
BootStrapClassLoader 加载路径为:JAVA_HOME/jre/lib
ExtensionClassLoader 加载路径为:JAVA_HOME/jre/lib/ext
ApplicationClassLoader 加载路径为:classpath
还有一个自定义类加载器
双亲委派机制:
如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传说到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(它的搜索返回中没有找到所需的类)时,子类加载器才会尝试自己去加载
强引用:new的对象。哪怕内存溢出也不会回收
软引用:只有内存不足时才会回收
弱引用:每次垃圾回收都会回收
虚引用:必须配合引用队列使用,一般用于追踪垃圾回收动作
(1)加载 :把字节码通过二进制的方式转化到方法区中的运行数据区
(2)连接:
验证:验证字节码文件的正确性。
准备:正式为类变量在方法区中分配内存,并设置初始值,final类型的变量在编译时已经赋值了
解析:将常量池中的符号引用(如类的全限定名)解析为直接引用(类在实际内存中的地址)
(3)初始化 :执行类构造器(不是常规的构造方法),为静态变量赋初值并初始化静态代码块。
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
(1)检查类是否已被加载,没有加载就先加载类
(2)为对象在堆中分配内存,使用CAS方式分配,防止在为A分配内存时,执行当前地址的指针还没有来得及修改,对象B就拿来分配内存。
(3)初始化,将对象中的属性都分配0值或null
(4)设置对象头
(5)为属性赋值和执行构造方法
对象头中有两部分,一部分是MarkWork,存储对象运行时的数据,如对象的hashcode、GC分代年龄、GC标记、锁的状态、获取到锁的线程ID等;另外一部分是表明对象所属类,如果是数组,还有一个部分存放数组长度
-Xmx[]:堆空间最大内存
-Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的
-Xmn[]:新生代的最大内存
-xx:[survivorRatio=3]:eden区与from+to区的比例为3:1,默认为4:1
-xx[use 垃圾回收器名称]:指定垃圾回收器
-xss:设置单个线程栈大小
一般设堆空间为最大可用物理地址的百分之80
GC的目的实现内存的自动释放,使用可达性分析法判断对象是否可回收,采用了分代回收思想,
将堆分为新生代、老年代,新生代中采用复制算法,老年代采用整理算法,当新生代内存不足时会发生minorGC,老年代不足时会发送fullGC
JDK1.7
JDK1.8
-Xms256m: 设置堆的初始化大小为256m
-Xmx: 设置堆的最大大小
-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
设置年轻代中Eden区和两个Survivor区的大小比例。默认比例为8:1:1
-XX:newSize 设置年轻代的初始大小
年轻代和老年代默认比例为1:2。可以通过调整二者空间大小
-XX:MaxNewSize 设置年轻代的最大大小
初始大小和最大大小两个值通常相同
-Xss 对每个线程stack大小的调整,-Xss128k
设置线程栈的大小,默认为1M。用于存放栈帧、调用参数、局部变量等
-Xmn设置年轻代的大小
jps,(Java Process Status)
输出JVM中运行的进程状态信息(现在一般使用jconsole)
jstack 查看java进程内线程的堆栈信息
jmap 用于生成堆转存快照
jstat 显示垃圾回收信息、类加载信息、新生代统计信息等
jconsole 用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
VisualVM: 故障处理工具。
它又叫做堆外内存,不受 JVM 内存回收管理,是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理
在JVM中共有四大部分,分别是
它们的运行流程是:
虚拟机栈是描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢。保存执行方法时的局部变量、动态连接信息、方法返回地址信息等等。方法开始执行的时候会进栈,方法执行完会出栈【相当于清空了数据】,所以这块区域不需要进行 GC。
第一、可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
第二、为了安全,防止核心类被篡改
在java8时,堆被分为了两份:新生代和老年代,它们默认空间占用比例是1:2
对于新生代,内部又被分为了三个区域。Eden区,From区,To区默认空间占用比例是8:1:1
具体的工作机制是有些情况:
当然也有特殊情况,如果进入Eden区的是一个大对象,在触发YoungGC的时候,会直接存放到老年代
当老年代满了之后,触发FullGC。FullGC同时回收新生代和老年代,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起。 我们在程序中要尽量避免FullGC的出现。
新生代主要用来存放新生的对象。
老年代主要存放应用中生命周期长的内存对象。
永久代指的是永久保存区域。主要存放Class和Meta(元数据)的信息。在Java8中,永久代已经被移除,取而代之的是一个称之为元空间的区域。元空间和永久代类似,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。
我们当时的项目是SpringBoot项目,可以在项目启动的时候,java -jar中加入参数就行了
第一,可以通过jmap指定打印他的内存快照 dump文件
第二,可以通过jdk自带工具VisualVM去分析 dump文件
第三,通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
第四,找到对应的代码,通过阅读上下文的情况,进行修复即可
第一,可以使用使用top命令查看占用cpu的情况
第二,通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id
第三,可以通过ps 查看当前进程中的线程信息,看看哪个线程的cpu占用较高
第四,可以jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号
1、进程是资源分配的最小单位,线程是资源调度的最小单位。
2、线程是在进程下运行的。一个进程可以包含多个线程。
3、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间。而线程是共享进程中的数据的,使用相同的地址空间。
4、同一进程下不同线程间数据容易共享,不同进程间数据很难共享。
5、线程之间没有单独的地址空间,一个线程死掉,整个进程也死掉。而一个进程死掉并不会对另外一个进程造成影响。
InnoDB有三大特性,分别是事务、外键、行级锁,这些都是MyIsAm不支持的,
另外InnoDB是聚簇索引,MyIAm是非聚簇索引,
InnoDB不支持全文索引,MyIAm支持
InnoDB支持自增和MVCC模式的读写,MyIAm不支持
MyIsAM的访问速度一般InnoDB快,差异在于innodb的mvcc、行锁会比较消耗性能,还可能有回表的过程(先去辅助索引中查询数据,找到数据对应的key之后,再通过key回表到聚簇索引树查找数据)
原子性:一个事务内的操作统一成功或失败
一致性:事务前后的数据总量不变
隔离性:事务与事务之间相互不影响
持久性:事务一旦提交发生的改变不可逆
原子性:由undolog日志保证,他记录了需要回滚的日志信息,回滚时撤销已执行的sql
一致性:由其他三大特性共同保证,是事务的目的
隔离性:由MVCC保证
持久性:由redolog日志和内存保证,MySQL修改数据时内存和redolog会记录操作,宕机时可恢复
在高并发情况下,并发事务会产生脏读、不可重复读、幻读问题,这时需要用隔离级别来控制
读未提交: 允许一个事务读取另一个事务已提交的数据,可能出现不可重复读,幻读。
读提交: 只允许事务读取另一个事务没有提交的数据可能出现不可重复读,幻读。
可重复读: 确保同一字段多次读取结果一致,可能出现欢幻读。
可串行化: 所有事务逐次执行,没有并发问日
Inno DB 默认隔离级别为可重复读级别,分为快照度和当前读,并且通过间隙锁解决了幻读问题。
*快照读读取的是当前数据的可见版本,可能是会过期数据,不加锁的select就是快照都
*当前读读取的是数据的最新版本,并且当前读返回的记录都会上锁,保证其他事务不会并发修改这条记录。如update、insert、delete、select for undate(排他锁)、select lockin share mode(共享锁) 都是当前读
MVCC是多版本并发控制,为每次事务生成一个新版本数据,每个事务都由自己的版本,从而不加锁就决绝读写冲突,这种读叫做快照读。只在读已提交和可重复读中生效。
实现原理由四个东西保证,他们是
undolog日志:记录了数据历史版本
readView:事务进行快照读时动态生成产生的视图,记录了当前系统中活跃的事务id,控制哪个历史版本对当前事务可见
隐藏字段DB_TRC_ID: 最近修改记录的事务ID
隐藏字段DB_Roll_PTR: 回滚指针,配合undolog指向数据的上一个版本
主键索引:一张表只能有一个主键索引,主键索引列不能有空值和重复值
唯一索引:唯一索引不能有相同值,但允许为空
普通索引:允许出现重复值
组合索引:对多个字段建立一个联合索引,减少索引开销,遵循最左匹配原则
全文索引:myisam引擎支持,通过建立倒排索引提升检索效率,广泛用于搜索引擎
聚簇索引:
非聚簇索引(辅助索引)
1. 非聚簇索引的叶子节点存放的是数据行地址,先根据索引找到数据地址,再根据地址去找数据
他们都是b+数结构
开启慢查询日志( SET GLOBAL long_query_time=阈值;超过阈值的sql就会记录到慢查询日志当中),或查看执行计划(explain+SQL)。慢查询优化如下:
(1)分析sql语句,是否加载了不需要的数据列
(2)分析sql执行计划,字段有没有索引,索引是否失效,是否用对索引
(3) 使用复杂查询时,尽量使用关联查询来代替子查询,并且最好使用内连接
(4)orderby查找时使用索引进行排序,否则的话需要进行回表,然后在排序缓冲区中进行排序。
(5)groupby查询时,同样要建立联合索引,避免使用到临时表
(6)分页查询时,如果偏移量太大,比如要查询一百万条数据后的十条记录,可以使用主键+子查询的方式,避免进行全表扫描
(7)使用count函数时直接使用count的话count(*)的效率最高,也可以额外创建一张表去统计不同表中的数据行数,但维护麻烦
count(*)或count(唯一索引)或count(数字):表中总记录数,count(字段)不会统计null
(8) 在写update语句时,where条件要添加使用索引,否则会锁会从行锁升级为表锁
(9)表中数据是否太大,是不是要分库分表
用外连接的话连接顺序是固定死的,比如left join,他必须先对左表进行全表扫描,然后一条条到右表去匹配;而内连接的话MySQL会自己根据查询优化器去判断用哪个表做驱动。
子查询的话同样也会对驱动表进行全表扫描,所以尽量用小表做驱动表。
(1)客户端向 MySQL 服务器发送一条查询请求
(2)服务器首先检查查询缓存,如果命中缓存,则返回存储在缓存中的结果。否则进入下一阶段
(3)服务器进行 SQL 解析、预处理、再由优化器生成对应的执行计划
(4)MySQL 根据执行计划,调用存储引擎的 API 来执行查询
(5)将结果返回给客户端,同时缓存查询结果
注意:只有在8.0之前才有查询缓存,8.0之后查询缓存被去掉了
我们想看一个sql的执行计划使用的语句是explain+SQL,表中的字段包括:
id:select查询的优先级,id越大优先级越高,子查询的id一般会更大
select_type:查询的类型,是普通查询还是联合查询还是子查询,常见类型有simple(不包含子查询),primary(标记复杂查询中最外层的查询),union(标记primart只后子查询)
table:者一行的数据是数哪张表的
type:扫描类型,效率从底到高为
ALL(全表扫描)>index(全索引扫描,我们的需要的数据在索引中可以获取)>range(范围索引扫描)>ref(使用非唯一索引列进行了关联查询)> eq_ref (使用唯一索引进行关联查询)>const(查询结果只有一行,体现在使用唯一索引进行单表查询)>system(表中只有一行数据)
possible_keys(可能的):当前查询语句可能用到的索引,可能为null(如果用了索引但是为null有可能是表数据太少innodb认为全表扫描更快)
key:实际使用到的索引
ref(编号):显示索引的哪一行被使用了
rows:估算大概多少行数据被查找了
excess(额外的):MySQL如何查询额外信息,常见的有:
Using filesort:排序时使用外部排序而不是索引排序,需要回表查询数据,然后在排序缓冲区中进行排序
backward index scan:排序时使用了索引排序,但如果是按照降序排序的话就会使用反向扫描索引
Using index:表示使用覆盖索引(覆盖索引:如果key中显示有索引,select后面查询的字段都可以从通过这个索引获取)
Using temporary:查询时要建立一个临时表存放数据
(1)where条件中有or,除非所有查询条件都有索引,否则失效
(2)like查询用%开头,索引失效
(3)索引列参与计算,索引失效
(4)违背最左匹配原则,索引失效
(5)索引字段发生类型转换,索引失效
(6)MySQL觉得全表扫描更快时(数据少),索引失效
二叉树:索引字段有序,极端情况会变成链表形式
AVL数:树的高度不可控
B数:控制了树的高度,但是索引值和data都分布在每个具体的节点当中,若要进行范围查询,要进行多次回溯,IO开销大
B+树:非叶子节点只存储索引值,叶子节点再存储索引+具体数据,从小到大用链表连接在一起,范围查询可直接遍历不需要回溯3
基于粒度:
*表级锁:对整张表加锁,粒度大并发小
*行级锁:对行加锁,粒度小并发大
*间隙锁:间隙锁,锁住表的一个区间,间隙锁之间不会冲突只在可重复读下才生效,解决了幻读
基于属性:
*共享锁:又称读锁,一个事务为表加了读锁,其它事务只能加读锁,不能加写锁
*排他锁:又称写锁,一个事务加写锁之后,其他事务不能再加任何锁,避免脏读问题
内连接取量表交集部分,左连接取左表全部右表匹部分,右连接取右表全部坐表匹部分
from、 on 、join 、where 、group by、having、select、order by、limit
(1)抽取实体,如用户信息,商品信息,评论
(2)分析其中属性,如用户信息:姓名、性别…
(3)分析表与表之间的关联关系
然后可以参考三大范式进行设计,设计主键时,主键要尽量小并且定义为自增和不可修改。
where是约束声明,having是过滤声明,where早于having执行,并且where不可以使用聚合函数,having可以
第一范式:每个列原子性,都不可再分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
char是不可变的,最大长度为255,varchar是可变的字符串,最大长度为2^16
事务1已经获取数据A的写锁,想要去获取数据B的写锁,然后事务2获取了B的写锁,想要去获取A的写锁,相互等待形成死锁。
MySQL解决死锁的机制有两个:1.等待, 直到超时 2.发起死锁检测,主动回滚一条事务
死锁检测的原理是构建一个以事务为顶点、 锁为边的有向图, 判断有向图是否存在环, 存在即有死锁。
我们平时尽量减少事务操作的资源和隔离级别
innodb 引擎:
MySQL8.0前,下次自增会取表中最大 id + 1。原理是最大id会记录在内存中,重启之后会重新读取表中最大的id
MySQL8.0后,仍从删除数据 id 后算起。原理是它将最大id记录在redolog里了
myisam:
自增的 id 都从删除数据 id 后算起。原理是它将最大id记录到数据文件里了
(1)一次sql插入多条数据,可以减少写redolog日志和binlog日志的io次数(sql是有长度限制的,但可以调整)
(2)保证数据按照索引进行有序插入
(3)可以分表后多线程插入
以上的随便列几个
主键约束(PRIMARY KEY,非空和唯一的结合),确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。
唯一约束(UNIQUE),保证某列的每行必须有唯一的值。
非空约束(NOT NULL),指示某列不能存储 NULL 值。
默认约束(DEFAULT),规定没有给列赋值时的默认值。
检查约束(CHECK),保证列中的值符合指定的条件。
外键约束(FOREIGN KEY),保证一个表中的数据匹配另一个表中的值的参照完整性。
自连接,外连接(左外连接、右外连接、全连接),交叉连接。
多表关系
多表查询
区别主要有以下几个方面
1、最大长度:char最大长度是255字符,varchar最大长度是65535个字节。
2、定长:char是定长的,不足的部分用隐藏空格填充,varchar是不定长的。
3、空间使用:char会浪费空间,varchar会更加节省空间。
4、查找效率:char查找效率会很高,varchar查找效率会更低。
varchar需要计算内容占用的长度,而char不会,所以char的效率稍高一些
在项目中的使用,这两种方式都会用到,比如像一些枚举值可以选择使用char,像一些描述信息或名字类可以选择使用varchar
事务:由多个操作组成的一个逻辑单元,组成这个逻辑单元的多个操作要么都成功,要么都失败。
事务的四个特性ACID:
索引(index)类似一本书的目录,是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
**回表查询:**先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取数据的方式,就称之为回表查询
**回表的原因:**是 select查询要的列在二级索引的列中不存在,需要去主键索引获取,因为主键索引含有整行记录值
覆盖索引 是指 查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到
如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。
索引下推是MySQL 5.6后出的特性,符合某种条件下,把原来需要在Server端完成的条件判断转交给存储引擎去处理,这种现象称为索引下推
好处:减少回表查询次数,提高查询效率,节约io开销
条件:只适合于二级索引(非主键索引)且为多列索引(复合索引、联合索引)
索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
1.哪些数据量大的,且查询频繁的字段
2.where order_by group_by on 后面的字段
3.推荐建立联合索引
1.数据量少的,查询频率小的字段
2.null值多的字段
3.重复值多的字段
4.更改频繁的字段
1.慢查询日志记录了所有执行时间超过指定参数的所有SQL语句的日志。
2.要定位查询,需要先开启慢查询日志,通过SET GLOBAL slow_query_log = 'ON’开启
3.在设置慢查询日志的阈值,通过SET GLOBAL long_query_time=2指定阈值大小
4.通过MySQL.slow_log表查看慢查询日志
可以采用EXPLAIN查询sql执行计划。
主要可以根据以下字段,判断sql是否需要优化,特别是是否能命中索引或命中索引的情况
(1)完全基于内存操作
(2)数据结构简单,对数据操作简单
(3)redis执行命令是单线程的,避免了上下文切换带来的性能问题,也不用考虑锁的问题
(4) 采用了非阻塞的io多路复用机制,使用了单线程来处理并发的连接;内部采用的epoll+自己实现的事件分离器
其实Redis不是完全多线程的,在核心的网络模型中是多线程的用来处理并发连接,但是数据的操作都是单线程。Redis坚持单线程是因为Redis是的性能瓶颈是网络延迟而不是CPU,多线程对数据读取不会带来性能提升。
String 常用命令: set,get,decr,incr,mget等
Hash 常用命令: hget,hset,hgetall 等
List 常用命令: lpush,rpush,lpop,rpop,lrange 等
Set 常用命令: sadd,spop,smembers,sunion 等
SortSet 常用命令: zadd,zrange,zrem,zcard 等
(1)快照持久化RDB
redis的默认持久化机制,通过父进程fork一个子进程,子进程将redis的数据快照写入一个临时文件,等待持久化完毕后替换上一次的rdb文件。整个过程主进程不进行任何的io操作。持久化策略可以通过save配置单位时间内执行多少次操作触发持久化。所以RDB的优点是保证redis性能最大化,恢复速度数据较快,缺点是可能会丢失两次持久化之间的数据
(2)追加持久化AOF
以日志形式记录每一次的写入和删除操作,策略有每秒同步、每次操作同步、不同步,优点是数据完整性高,缺点是运行效率低,恢复时间长
缓存穿透是指频繁请求客户端和缓存中都不存在的数据,缓存永远不生效,请求都到达了数据库。
解决方案:
(1)在接口上做基础校验,比如id<=0就拦截
(2)缓存空对象:找不到的数据也缓存起来,并设置过期时间,可能会造成短期不一致
(3)布隆过滤器:在客户端和缓存之间添加一个过滤器,拦截掉一定不存在的数据请求
缓存击穿是只一个热点key,在某一瞬间失效,导致大量请求到达数据库
解决方案:
(1)设置热点数据永不过期
(2)给缓存重建的业务加上互斥锁,缺点是性能低
缓存雪崩是值某一时间Key同时失效或redis宕机,导致大量请求到达数据库
解决方案:
(1)搭建集群保证高可用
(2)进行数据预热,给不同的key设置随机的过期时间
(3)给缓存业务添加限流降级,通过加锁或队列控制操作redis的线程数量
(4)给业务添加多级缓存
原理是使用setnx+setex命令来实现,但是会有一系列问题:
(1)任务时常超过缓存时间,锁自动释放。可以使用Redision看门狗解决
(2)加锁和释放锁的不是同一线程。可以在Value中存入uuid,删除时进行验证。但是要注意验证锁和删除锁也不是一个原子性操作,可以用lua脚本使之成为原子性操作
(3)不可重入。可以使用Redision解决(实现机制类似AQS,计数)
(4)redis集群下主节点宕机导致锁丢失。使用红锁解决
(1)主从模式:个master节点,多个slave节点,master节点宕机slave自动变成主节点
(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点
(3)分片集群:主从模式和哨兵模式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定
主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave。
后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据
Redis缓存一致性解决方案主要思考的是删除缓存和更新数据库的先后顺序
先删除缓存后更新数据库存在的问题是可能会数据不一致,一般使用延时双删来解决,即先删除缓存,再更新数据库,休眠X秒后再次淘汰缓存。第二次删除可能导致吞吐率降低,可以考虑进行异步删除。
先更新数据库后删除缓存存在的问题是会可能会更新失败,可以采用延时删除。但由于读比写快,发生这一情况概率较小。
但是无论哪种策略,都可能存在删除失败的问题,解决方案是用中间件canal订阅binlog日志提取需要删除的key,然后另写一段非业务代码去获取key并尝试删除,若删除失败就把删除失败的key发送到消息队列,然后进行删除重试。
采用的定期过期+惰性过期
定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。
惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。
当内存不足时按设定好的策略进行淘汰,策略有(1)淘汰最久没使用的(2)淘汰一段时间内最少使用的(3)淘汰快要过期的
Spring是个轻量级的框架,通过IOC达到松耦合的目的,通过AOP可以分离应用业务逻辑和系统服务进行内聚性的开发,不过配置各种组件时比较繁琐,所以后面才出选了SpringBoot的框架。
IOC是控制反转,是一种思想,把对象的创建和调用从程序员手中交由IOC容器管理,降低对象之间的依赖关系。
创建一个bean的方式有xml方式、@Bean注解方式、@Componte方式
我们在对一个bean进行实例化后,要对他的属性进行填充,大多数我们都是使用 @Autowire直接的填充依赖注入的,他是有限按照类型进行匹配。
AOP是面向切面编程,可以将那些与业务不相关但是很多业务都要调用的代码抽取出来,思想就是不侵入原有代码的情况下对功能进行增强。
SpringAOP是基于动态代理实现的,动态代理是有两种,一种是jdk动态代理,一种是cglib动态代理;
jdk动态代理是原理是利用反射来实现的,需要调用反射包下的Proxy类的newProxyInstance方法来返回代理对象,这个方法中有三个参数,分别是用于加载代理类的类加载器,被代理类实现的接口的class数组和一个用于增强方法的InvocaHandler实现类。
cglib动态代理原理是利用asm开源包来实现的,是把被代理类的class文件加载进来,通过修改它的字节码生成子类来处理
jdk动态代理要求被代理类必须有实现的接口,生成的动态代理类会和代理类实现同样的接口,cglib则,生成的动态代理类会继承被代理类。Spring默认使用jdk动态代理,当被代理的类没有接口时就使用cglib动态代理
第一步:创建一个切面类,把它添加到ioc容器中并添加@Aspect注解
第二步: 在切面类中写一个通知方法,在方法上添加通知注解并通过切入点表达式来表示要对哪些方法进行日志打印,然后方法参数为JoinPoint
第三步:通过JoinPoint这个参数可以获取当前执行的方法名、方法参数等信息,这样就可以根据需求在方法进入或结束时打印日志
循环依赖就是在创建 A 实例的时候里面包含着 B 属性实例,所以这个时候就需要去创建 B 实例,而创 建 B 实例过程中也包含着 A 实例。 这样 A 实例还在创建的过程当中,所以就导致 A 和 B 实例都创建不出来。
Spring通过三级缓存来解决循环依赖:
一级缓存:缓存经过完整的生命周期的Bean
二级缓存 :缓存未经过完整的生命周期的Bean
三级缓存:缓存的是ObjectFactory,其中存储了一个生成代理类的拉姆达表达式
我们在创建 A 的过程中,先将 A 放入三级缓存 ,这时要创建B,B要创建A就直接去三级缓存中查找,并且判断需不需要进行 AOP 处理,如果需要就执行拉姆达表达式得到代理对象,不需要就取出原始对象。然后将取出的对象放入二级缓存中,因为这个时候 A 还未经 过完整的生命周期所以不能放入一级缓存。这个时候其他需要依赖 A 对象的直接从二级缓存中去获取即可。当B创建完成,A 继续执行生命周期,当A完成了属性的注入后,就可以放入一级缓存了
(1)Singleton:一个IOC容器只有一个
(2)Prototype:每次调用getBean()都会生成一个新的对象
(3)request:每个http请求都会创建一个自己的bean
(4)session:同一个session共享一个实例
(5)application:整个serverContext只有一个bean
(6)webSocket:一个websocket只有一个bean
1.当程序加载运行时会根据Spring中配置文件找到bean配置的属性和方法,并通过java反射机制创建实例化bean对象。
Bean实现了BeanNameAware接口,执行了setBeanName方法,实现注入对象。
2.实现了BeanFactoryAware工厂接口,执行了setBeanFactory方法。
3.实现了ApplicationContext接口类,执行了setApplicationContest方法。
4.实现了BeanPostProcessor接口类,执行postProcessBeforeInitialization方法
5.实现了InitiliazingBean 执行afterPropertiesSet方法,并加载配置文件定义了init-method 则执行对应初始化方法BeanPostProcessor 执行postProcessorfterInitilization方法,完成 Bean的初始化,使得bean可以使用。
6.实现了DisposabileBean接口,加载配置文件中的destroy-method方法销毁bean对象实例。
Spring事务有编程式和声明式,我们一般使用声明式,在某个方法上增加@Transactional注解,这个方法中的sql会统一成功或失败。
原理是:
当一个方法加上@Transactional注解,Spring会基于这个类生成一个代理对象并将这个代理对象作为bean,当使用这个bean中的方法时,如果存在@Transactional注解,就会将事务自动提交设为false,然后执行方法,执行过程没有异常则提交,有异常则回滚、
(1)事务方法所在的类没有加载到容器中
(2)事务方法不是public类型
(3)同一类中,一个没有添加事务的方法调用另外以一个添加事务的方法,事务不生效
(4)Spring事务默认只回滚运行时异常,可以用rollbackfor属性设置
(5)业务自己捕获了异常,事务会认为程序正常秩序
default:默认级别,使用数据库自定义的隔离级别
其它四种隔离级别与MySQL一样
(1)支持当前事务,如果不存在,则新启一个事务
(2)支持当前事务,如果不存在,则抛出异常
(3)支持当前事务,如果不存在,则以非事务方式执行
(4)不支持当前事务,创建一个新事物
(5)不支持当前事务,如果已存在事务就抛异常
(6)不支持当前事务,始终以非事务方式执行
BeanFactory用了工厂模式,AOP用了动态代理模式,RestTemplate用来模板方法模式,SpringMVC中handlerAdaper用来适配器模式,Spring里的监听器用了观察者模式
Spring的默认bean作用域是单例的,单例的bean不是线程安全的,但是开发中大部分的bean都是无状态的,不具备存储功能,比如controller、service、dao,他们不需要保证线程安全。
如果要保证线程安全,可以将bean的作用域改为prototype,比如像Model View。
另外还可以采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量,每个线程只操作自己的副本变量。
@Component,@Service,@Repository,@Controller 用于服务类
@Autowired 通过类型来实现自动注入bean
@Qualifier 配合@Autowired实现根据name注入bean。
@Scope 用于配置 Spring bean 的范围
@Configuration,@ComponentScan,@Bean 用于基于 java 的配置
@Aspect,@Before,@After,@Around,@Pointcut 用于切面编程
1、no:这是 Spring 框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在 bean 定义中用标签明确的设置依赖关系 。
2、byName:该选项可以根据bean名称设置依赖关系 。 当向一个bean中自动装配一个属性时,容器将根据bean的名称自动在在配置文件中查询一个匹配的bean。 如果找到的话,就装配这个属性,如果没找到的话就报错 。
3、byType:该选项可以根据 bean 类型设置依赖关系 。 当向一个 bean 中自动装配一个属性时,容器将根据 bean 的类型自动在在配置文件中查询一个匹配的 bean。 如果找到的话,就装配这个属性,如果没找到的话就报错 。
4、constructor :构造器的自动装配和byType模式类似,但是仅仅适用于与有构造器相同参数的bean,如果在容器中没有找到与构造器参数类型一致的bean ,那么将会抛出异常 。
5、default:该模式自动探测使用构造器自动装配或者byType自动装配 。 首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在bean内部没有找到相应的 构造器或者是无参构造器,容器就会自动选择 byTpe 的自动装配方式 。
有4种依赖注入方式:set注入、构造注入、静态工厂、实例工厂
有2 种实现方式:注解(如@Autowired,@Resource,@Required)和配置文件(xml形式)
1、一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。
2、二级缓存:二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。
默认情况下二级缓存并没有开启,要想使用二级缓存,可以使用
标签开启
3、对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。
1、Mybatis的接口方法参数需要定义为集合类型List
2、在映射文件中通过forEach标签遍历集合,获取每一个元素作为insert语句的参数值
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
默认情况下延迟加载是关闭的。
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态 拼接sql的功能,Mybatis提供了9种动态sql标签 trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此 来完成动态sql的功能。
第一种是 : 编写多表关联查询的SQL语句 , 使用ResultMap建立结果集映射 , 在ResultMap中建立多表结果集映射的标签有association
和collection
第二种是 : 将多表查询分解为多个单表查询, 使用ResultMap表的子标签association
和collection
标签的select
属性指定另外一条SQL的定义去执行, 然后执行结果会被自动封装
第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
第2种: 通过
来映射字段名和实体类属性名的一一对应的关系。
第3种,开启Mybatis驼峰命名自动匹配映射
使用insert标签中的useGeneratedKeys和keyProperty 属性。
useGeneratedKeys:是够获取自动增长的主键值。true表示获取。
keyProperty:指定将获取到的主键值封装到哪儿个属性里
拦截器是基于Java的反射机制的,而过滤器是基于函数回调。
拦截器不依赖于servlet容器,过滤器依赖于servlet容器。
拦截器只能对部分请求起作用,而过滤器则可以对几乎所有的请求起作用。
拦截器可以访问spring容器上下文里的对象,而过滤器不能访问。
拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
想要定义一个全局异常处理类的话,我们需要在这个类上添加@ControllerAdvice注解,然后定义一些用于捕捉不同异常类型的方法,在这些方法上添加@ExceptionHandler(value = 异常类型.class)和@ResponseBody注解,方法参数是HttpServletRequest和异常类型,然后将异常消息进行处理。
如果我们需要自定义异常的话,就写一个自定义异常类,该类需要继承一个异常接口,类属性包括final类型的连续id、错误码、错误信息,再根据需求写构造方法;
SpringMVC工作过程围绕着前端控制器DispatchServerlet,几个重要组件有HandleMapping(处理器映射器)、HandleAdapter(处理器适配器)、ViewReslover(试图解析器)
工作流程:
(1)DispatchServerlet接收用户请求将请求发送给HandleMapping
(2)HandleMapping根据请求url找到具体的handle和拦截器,返回给DispatchServerlet
(3)DispatchServerlet调用HandleAdapter,HandleAdapter执行具体的controller,并将controller返回的ModelAndView返回给DispatchServler
(4)DispatchServerlet将ModelAndView传给ViewReslover,ViewReslover解析后返回具体view
(5)DispatchServerlet根据view进行视图渲染,返回给用户
在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
其中@EnableAutoConfiguration
是实现自动化配置的核心注解。
该注解通过@Import
注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。
在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。
一般条件判断会有像@ConditionalOnClass
这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
@RestController :修饰类,该控制器会返回Json数据
@RequestMapping(“/path”) :修饰类,该控制器的请求路径
@Autowired : 修饰属性,按照类型进行依赖注入
@PathVariable : 修饰参数,将路径值映射到参数上
@ResponseBody :修饰方法,该方法会返回Json数据
@RequestBody(需要使用Post提交方式) :修饰参数,将Json数据封装到对应参数中
@Controller@Service@Compont: 将类注册到ioc容器
@Transaction:开启事务
Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :
提供多套配置文件,如:applcation.properties、application-dev.properties、application-test.properties、application-prod.properties,然后在applcation.properties文件中指定当前的环境spring.profiles.active=test,这时候读取的就是application-test.properties文件。
微服务
将单体服务拆分成一组小型服务。拆分完成之后,每个小型服务都运行在独立的进程中。服务与服务之间采用轻量级的通信机制来进行沟通(Spring Cloud 中是基于Http请求)
每一个服务都按照具体的业务进行构建,如电商系统中,订单服务,会员服务,支付服务等。这些拆分出来的服务都是独立的应用服务,可以独立的部署到上产环境中。相互之间不会受影响。所以一个微服务项目就可以根据业务场景进行开发。这在单体类项目中是无法实现的。
优点:松耦合,聚焦单一业务功能,无关开发语言,团队规模降低。在开发中,不需要了解多有业务,只专注于当前功能,便利集中,功能小而精。微服务一个功能受损,对其他功能影响并不是太大,可以快速定位问题。微服务只专注于当前业务逻辑代码,不会和 html、css 或其他界面进行混合。可以灵活搭配技术,独立性比较好。
缺点:随着服务数量增加,管理复杂,部署复杂,服务器需要增多,服务通信和调用压力增大,运维工程师压力增大,人力资源增多,系统依赖增强,数据一致性,性能监控难度增大。
早期我们一般认为的Spring Cloud五大组件是
随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
解决服务之间的通信、容灾、负载平衡、冗余问题,能方便服务集中管理,常用组件有注册中心、配置中心、远程调用。服务熔断、网关
C:一致性,这里指的强一致性,也就是数据更新完,访问任何节点看到的数据完全一致
A:可用性,就是任何没有发生故障的服务必须在规定时间内返回合正确结果
P:容灾性,当网络不稳定时节点之间无法通信,造成分区,这时要保证系统可以继续正常服务。提高容灾性的办法就是把数据分配到每一个节点当中,所以P是分布式系统必须实现的,然后需要在C和A中取舍
当网络发生故障时,如果要保障数据一致性,那么节点相互间就只能阻塞等待数据真正同步时再返回,就违背可用性了。如果要保证可用性,节点要在有限时间内将结果返回,无法等待其它节点的更新消息,此时返回的数据可能就不是最新数据,就违背了一致性了
各种注册中心组件的原理和流程其实大体上类似,核心的功能就一下几个 :
使用的话, 首先需要部署注册中心服务 , 然后在我们自己的微服务中引入注册中心依赖, 然后再配置文件中配置注册中心地址 就可以了
① Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
② 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
③ Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
④ Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;
总结:
服务调用过程中的负载均衡一般使用SpringCloud的Ribbon 组件实现 , Feign的底层已经自动集成了Ribbon , 使用起来非常简单
客户端调用的话一般会通过网关, 通过网关实现请求的路由和负载均衡
这个不太了解,默认负载均衡策略是轮询,要想自定义负载均衡,可以通过定义IRule实现负载均衡规则
我们项目中涉及到服务调用得地方都会定义降级, 一般降级逻辑就是返回默认值 , 降级的实现也非常简单 , 就是创建一个类实现FallbackFactory接口 , 然后再对应的Feign客户端接口上面 , 通过@FeignClient指定降级类
Spring Cloud Gateway:是Spring Cloud中所提供的一个服务网关组件,是整个微服务的统一入口,在服务网关中可以实现请求路由、统一的日志记录,流量监控、权限校验等一系列的相关功能!
项目应用:权限的校验
具体实现思路:使用Spring Cloud Gateway中的全局过滤器拦截请求(GlobalFilter、Order),从请求头中获取token,然后解析token。如果可以进行正常解析,此时进行放行;如果解析不到直接返回。
大部分的固定的配置文件都放在服务本地 , 一些根据环境不同可能会变化的部分, 放到Nacos中
Naocs中主要存放的是各个微服务共享的配置,需要随着需求动态变更的配置。
计数器算法:使用redis的setnx和过期机制实现
漏桶算法:一般使用消息队列来实现,系统以恒定速度处理队列中的请求,当队列满的时候开始拒绝请求。
令牌桶算法:计数器算法和漏桶算法都无法解决突然的大并发,令牌桶算法是预先往桶中放入一定数量token,然后用恒定速度放入token直到桶满为止,所有请求都必须拿到token才能访问系统
SprngCloud中用Hystrix组件来进行降级、熔断、限流
熔断是对于消费者来讲,当对提供者请求时间过久时为了不影响性能就对链接进行熔断,
限流是对于提供者来讲,为了防止某个消费者流量太大,导致其它更重要的消费者请求无法及时处理。限流可用通过拒绝服务、服务降级、消息队列延时处理、限流算法来实现
我们项目中使用Hystrix实现的断路器 ,默认是关闭的,如果需要开启需要在引导类上添加注解:
@EnableCircuitBreaker
断路器状态机包括三个状态:
有很多优点:
也有缺点:
1、客户端请求第三方授权
2、用户(资源拥有者)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请 令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
微服务与微服务之间实现认证,只需要将用户传递的令牌传递给其他微服务即可。如果微服务之间相互调用采用的是Feign模式,可以创建一个拦截器,每次执行请求之间,将令牌添加到头文件中即可传递给其他微服务。
加密JWT:使用加密算法将JWT中的敏感数据加密,以保护其不被篡改和窃取。
使用HTTPS:使用HTTPS协议传输JWT,以保证传输过程中的数据不被中间人窃取和篡改。
设置短有效期:设置JWT的有效期较短,例如几分钟或几小时,这样即使JWT被盗用,也能在较短时间内失效,从而减小风险。
撤销JWT:当用户注销或修改密码时,应该撤销JWT,这样即使有人盗用了JWT,也无法继续使用。
限制JWT的使用范围:在JWT中添加一些限制条件,例如限制JWT只能在特定的IP地址或设备上使用,以避免被盗用。
限制JWT的访问范围:限制JWT只能访问需要访问的资源,以避免恶意用户使用JWT访问其他不应该访问的资源。
每次写的消息都会分配一个唯一的id,如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。
如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,我们可以重试。
保证rabbitmq不丢消息
开启rabbitmq的持久化(持久化queue和message)
保证消费端不丢消息
依靠ack机制,简单来说,就是你关闭rabbitmq自动ack,通过一个api来手动ack就行,就是每次你自己代码里确保处理完的时候,在程序里ack一下。
RabbitMQ消费顺序乱了是因为消费者集群拿到消息后对消息处理速度不同导致的,比如可能将增删改变成了增改删。
解决方法:为RabbitMQ创建多个Queue,每个消费者只监听其中一个Queue,同一类型的消息都放在一个queue中,同一个 queue 的消息是一定会保证有序的。
服务器安装(windows、linux、docker),开始可以先不考虑集群
引入spring-boot-starter-amqp
application.yml里面完成配置
决定好的exchange、queue、routingkey
合适的地方发消息
合适的地方编写监听器接收消息
搭建集群
高版本的rabbitmq配合低版本的erlang,会出现资源占用后不被正常释放的问题
哪些情况会导致消息错乱?
如何保证消息的顺序性呢?两种方式
什么是幂等性?
如何保证消费的幂等性?需要根据具体业务
为什么出现消息堆积?
核心处理思路还是提高消费者的消费速率,保证消费者不出现消费故障。
哪些方式可以避免消息堆积呢?
如果发现了大量消息堆积如何解决呢?
RabbitMQ是一个由erlang语言编写的、开源的、在AMQP基础上完整的、可复用的企业消息系统。
RabbitMQ包括五种队列模式,简单队列、工作队列、发布/订阅、路由、主题、rpc等。项目中用路由和主题模式比较多
有消息确认机制
有持久化机制,提供交换机和队列的持久化
应用解耦
通过消息中间件,让系统和系统之间耦合度降低,系统B出现问题,不会导致依赖它的系统A出现问题。如,支付服务和订单服务。
异步处理
通过消息中间件,让两个操作由串行变为并行,提高吞吐量。如,发送短信和发送邮箱
流量削锋
通过消息中间件,让需要处理的请求,先进入消息队列缓冲,然后在进行消费。比如,秒杀业务中,由于瞬时访问量过大,可以用消息队列做一次缓存
延迟队列存储的是延迟消息,延迟消息是指消息被发送以后,并不想让消费者立刻拿到消息,而是等待待定时间后,消费者拿到消息进行消息
在AMQP协议和RabbitMQ中并没有直接支持延迟队列,但是可以通过DLX和TTL模拟延迟队列。
有什么业务场景?
那什么消息是死信(dead message)呢?
DLX(Dead Letter Exchanges)死信队列,本身也是一个普通的消息队列,在创建队列的时候,通过设置一些关键参数x-dead-letter-exchange,可以将一个普通的消息队列设置为死信队列
当这个队列中有死信时, RabbitMQ就会自动将这个消息重新发布到设置的Exchange上去, 进而被路由到另一个队列
默认情况下,如果消费者程序出现异常情况, Rabbitmq 会自动实现补偿机制 也就是重试机制,会一直重试到不抛出异常为准
默认是5s重试一次,重试策略可以修改
可以设置应答模式,默认是自动应答,可以采取手动应答
AMQP是什么?
AMQP模型怎样的?
AMQP工作过程
关于AMQP交换机
交换机有5种类型
为什么需要消息确认机制?
哪些情况可能导致消息失败?
做微信支付的时候用到了,支付微服务发送消息,订单微服务处理消息
具体使用逻辑
这里就实现了支付微服务和订单微服务的解耦
这里也实现了支付功能和订单状态修改的异步处理
1.简单模式
应用场景:短信验证码,聊天
具体场景:有一个oa系统,用户通过接收手机验证码进行注册,页面上点击获取验证码后,将验证码放到消息队列,然后短信服务从队列中获取到验证码,并发送给用户
2.工作模式
应用场景:抢红包,大型系统的资源调度
具体场景:有一个电商平台,有两个订单服务,用户下单的时候,任意一个订单服务消费用户的下单请求生成订单即可。不用两个订单服务同时消费用户的下单请求
3.发布/订阅模式
应用场景:邮件群发,广告
具体场景:有一个商城,我们新添加一个商品后,可能同时需要去更新缓存和数据库
4.路由模式
应用场景:根据生产者的要求发送给特定的一个或者一批队列
具体场景:有一个商城,新添加了一个商品,实时性不是很高,只需要添加到数据库即可,不用刷新缓存
5.主题模式
应用场景:根据生产者的要求发送给通配符匹配的队列
具体场景:有一个商城,新添加了一个商品,实时性不是很高,只需要添加到数据库即可,数据库包含了主数据库mysql1和从数据库mysql2的内容,不用刷新缓存
主题模式的路由匹配支持通配符模糊匹配,而路由模式仅支持完全匹配
ifconfig:查看网络接口详情
ping:查看与某主机是否能联通
ps -ef|grep 进程名称:查看进程号
lost -i 端口 :查看端口占用情况
top:查看系统负载情况,包括系统时间、系统所有进程状态、cpu情况
free:查看内存占用情况
kill:正常杀死进程,发出的信号可能会被阻塞
kill -9:强制杀死进程,发送的是exit命令,不会被阻塞
反向代理是用来代理服务器接收请求的,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。
负载均衡算法有:轮询(默认)、带权轮询、ip_hash(按ip哈希结果分配,能解决session共享问题)、url_hash(按访问的URL的哈希结果分配)、fair(根据服务端响应时间分配,响应时间短优先)
用户点击下单按钮时,进行三次判断:先判断请求路径是否合法,因为做了动态URL;再判断用户是否已经下单过,就是看redis缓存中有没有用户下单信息;最后判断库存,这里进行了redis库存预减,由于判断库存和预减库存不是原子性操作,所以用lua脚本来执行这一段代码。然后从这里开始使用分布式锁,锁id为用户id+商品id,防止一个用户发送多次请求让redis多次预减。
Redis扣减成功后,进行异步下单,直接将正在处理返回给前端,将用户id和商品Id发送RabbitMQ中,负责下单的业务会从消息队列中拿出消息,去执行以下操作:
1.减库存,减库存时用where 库存>0防止超卖
2.订单表中生成记录,订单表中的用户id和商品id添加了联合唯一索引防止超卖
减库存和增加订单放在一个事务内保证一致性
3.将用户id和订单id缓存到redis中用来最初对用户重复下单的判断
4.释放分布式锁,根据value去判断锁是不是当前线程的,判断和删除锁不是原子性操作,所以封装到了lua脚本中
(1)页面动静分离,静态页面缓存到redis
(2)分布式锁拦截不同用户的重复
(3)限流算法
(4)验证码限流
(5)rabbitMq流量削峰
(6)接口隐藏