文章目录
- 一、基础
- 二、框架
- 三、微服务
- 四、数据库
- 五、项目研发
- 杭州面试题总结:
- 1、描述一下HashMap的实现原理?
- 2、Synchronized和volatile的区别是什么?
- 3、Try-catch-finally,如果 catch 中return了,finally 还会执行吗?如果执行,实在return之前还是之后执行?
- 4、对比下ArrayList,LinkedLit,Vector的插入效率并简述,ArrayList 插入数据一定 很慢吗?
- 5、http 响应码301和302代表的什么?有什么区别?
- 6、用Java写一个快速排序
- 7、Tcp为什么要三次握手,两次不行吗?为什么?
- 8、Spring 支持几种bean的作用域
- 9、Spring Boot有哪几种读取配置的方式
- 10、写出几种流量控制,它的实现方式是什么?
- 11、RabbitMQ中vhost的作用是什么?
- 12、RabbitMQ怎么避免消息丢失?|
- 13、数据库的索引规约你了解多少
- 14、Redis怎样实现分布式锁
- 16、线程的生命周期如何?有使用过线程池吗,其中的参数有何意义,请手写一个并说明?
- 17、RabbitMQ怎么加速消息的处理
- 18、MavenJar 包冲突怎么解决
- 19、多线程的应用
- 20、Redis有哪些应用
- 21、熟悉的设计模式, 你知道什么是策略者模式吗?
- 22、MyBatis里的dao层的方法用重载吗?
- 23、SpringCloud里的Nacos,各服务之间怎样相互调用的.
- 25、HashSet 如何保证他唯一
- 26、HashMap1.8为啥引入红黑树
- 27、concurrentHashMap如何保证线程安全
- 28、CAS是什么,会产生什么问题,ABA问题怎么解决
- 29、如何避免MQ重复消费问题
- 30、mysql innodb索引 加锁 行锁
- 35、如何实现线程的顺序执行
一、基础
1.1JVM
1、JVM五大内存区域
1、程序计数器(PC),一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;
2、java虚拟机栈,线程私有,每个方法在执行的同时都会创建一个栈帧,每个栈帧对应一个被调用的方法,栈帧中用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从开始执行到结束都对应一个栈帧在虚拟机栈中入栈和出栈的过程;
3、堆,它是java内存管理的核心区域,用来放置java对象实例,几乎创建的java对象实例都是被直接分配到堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx"之类参数就是用来指定最大堆空间等指标;
4、方法区,这也是所有线程共享的一块内存区域,用于存储所谓的元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等;运行时常量池,这也是方法区的一部分,用于存放编译期间生成的各种字面量和符号引用;
5、本地方法栈,和java虚拟机栈相似,支持对本地方法的调用,也是每个线程都会创建一个。
2、新生代和老年代
java堆 == 老年代 + 新生代
新生代 = Eden + s0 + s1;新生代几乎是所有Java对象出生的地方,Java对象申请的内存和存放都是在这个地方。
老年代几乎都是从survivor中熬过来的,不会轻易“死掉”,因此major GC不会像minor GC那样频繁
当Eden区的空间满了,java虚拟机会触发一次Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到Survivor区
大对象(需要大量连续内存空间的java对象,如长字符串)直接进入老年代;
如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制,则被晋升到老年代,即长期存活的对象进入老年代;
老年代满了而无法容纳更多的对象,Minor GC之后通常会进行Full GC,Full GC清理整个内存堆,包括年轻代和老年代;
Major GC发生在老年代的GC,清理老年区,经常会伴随至少一次Minor Gc,比Minor GC慢10倍以上。
3、加载类的过程
1、加载:主要是将.class文件中的二进制字节流读入到JVM中
2、验证:连接阶段的第一步,为了确保.class文件的信息符合当前虚拟机要求,并且不会危害虚拟机的自身安全。
3、准备:是正式为静态变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配
4、解析:是虚拟机将符号引用替换为直接引用的过程
5、初始化:是类加载过程的最后一步,是一个执行类构造器()方法的过程,就是给static变量赋予用户指定的值以及执行静态代码块
6、使用
7、卸载
4、OOM
OOM(OutOfMemoryError):JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error
原因:
1、原生内存不足(操作系统不允许申请更大的内存)
2、永生代或元空间不足
3、JVM执行GC耗时太久按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
情况:
java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况
java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。
5、JVM调优
1、设定堆内存大小
-Xmx:堆内存最大限制
2、设定新生代大小,新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio:新生代和老年代占比
-XX:SurvivorRatio:Eden区空间和Survivor区的占比
3、设定垃圾回收器:
年轻代:-XX:+USeParNewGC
老年代:-XX:+UseConcMarkSweepGC
1.2 GC
1、可达性分析
用来判断一个对象是否应该被回收
通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,基本就成为可回收对象了。
2、java中的引用
1、强引用
如果一个对象具有强引用,他就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出OutOfMemoryError错误,使程序异常终止。如果想中断强引用和某个对象的之间的关联,可以显式的将引用赋值为null,这样JVM在合适的时间就会回收该对象;
2、软引用
在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存空间不足时,软引用才会被垃圾回收器回收;
3、弱引用
具有弱引用的对象拥有的生命周期更短暂。因为当JVM进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象;
4、虚引用
顾名思义就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否加入了虚引用,来了解被引用的对象是否要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
3、GC回收算法
GC最基础的算法有三种:
标记-清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法
标记-清除算法:
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法:
“复制”的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存使用完了,就将还存活的对象复制到另一块上面,然后再把已经使用完了的内存空间一次清理掉。
标记-压缩算法:
标记过程仍然和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:
把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法
4、GC回收器
Serial收集器:
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器:
ParNew收集器其实就是Serial收集器的多线程版本
Parallel收集器:
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量
Parallel Old收集器:
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器:
是一种以获取最短回收停顿时间为目标的收集器
G1收集器:
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
5、Full GC和Major Gc、Minor GC
Minor GC:
从年轻代空间(包括Eden和Survivor区域)回收内存
Major GC
是清理老年代
Full GC:
是清理整个堆空间(年轻代和老年代)
Minor GC触发条件:当Eden区满时,触发Minor GC
Full GC触发条件:
1、调用System.gc时,系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC后进入老年代的平均大小大于老年代的可用空间
5、由Eden区,from Space区向To Space区复制时,对象大小大于To Space可存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
6、防止Full GC
Full GC:
是清理整个堆空间(年轻代和老年代)
1、新生代设置过小:
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入老年代,占据了老年代剩余空间,诱发Full GC
2、新生代设置过大:
一是新生代设置过大会导致老年代过小,二是新生代GC耗时大幅度增加
3、Survivor设置过小:
导致对象从Eden直接进入老年代
4、Survivor设置过大:
导致Eden过小,增加了GC频率
一般来说新生代占整个堆1/3比较合适
GC策略的设置方式:
1、吞吐量优先:-XX:GCTimeRatio=n来设置
2、暂停时间优先:-XX:MaxGcPauseRatio=n来设置
7、GC调优
1、设定堆内存大小:
-Xmx:堆内存最大限制
2、设定新生代大小,新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio:新生代和老年代占比
-XX:SurvivorRatio:Eden 区和Survivor区空间占比
3、设定垃圾回收器:
年轻代用:-XX:+UseParNewGC
老年代用:-XX:+UseConcMarkSweepGC
1.3集合
1、ArrayList源码分析,初始容量,扩容原理
(1)ArrayList是一种可变长的集合类,基于定长数组实现,使用默认构造方法初始化出来的容量是10 (1.7之后都是延迟初始化,即第一次调用add方法添加元素的时候才将elementData容量初始化为10)。
(2)ArrayList 允许空值和重复元素,当往ArrayList中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。ArrayList扩容的长度是原长度的1.5倍
(3)由于ArrayList底层基于数组实现,所以其可以保证在O(1)复杂度下完成随机查找操作。
(4)ArrayList是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
(5)顺序添加很方便
(6)删除和插入需要复制数组,性能差(可以使用LinkindList)
2、HashMap源码分析 1.7和1.8 扩容,冲突, 长度大于8红黑二叉树为什么不是6或者7?
1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
2、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
- 如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
- 还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值
3、ConCurrentHashMap(JUC)源码分析 1.7和1.8
- 在 Java 5.0 提供了
java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,
用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中
的 Collection 实现等;ConcurrentHashMap是如下实现:
1,jdk1.7的实现:ConcurrentHashMap是采用Segment分段锁的方式,他并没有对整个数据结构进行锁定,而是局部锁定。
2,jdk1.8的实现:采用一种乐观锁CAS算法来实现同步问题,但是底层还是“数组+链表->红黑树”的实现;其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurentHashMap只是增了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中
synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考
1.JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
2.JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
3.JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
4.JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
1.因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
2.JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
3.在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈但是也是一个依据CAS:
1、CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换
2、CAS指令执行时,当且仅当内存值与预期值A相等时,才可以把内存值修改为B,否则就什么都不做
3、整个比较并替换的操作是一个原子操作,通过硬件指令支持 获取数据时不加锁,通过volatile实现。
4、HashTable和HashMap的区别
1、HashTable 线程安全,HashMap非线程安全
2、Hashtable不允许null 值(key 和 value 都不可以),HashMap允许 null 值(key和value都可以)。
3、两者的遍历方式大同小异,Hashtable仅仅比HashMap多一个elements方法。
5、HashSet实现过程,重写hashcode和equals
HashSet的实现原理总结如下:
①是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
②当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。
③HashSet的其他操作都是基于HashMap的。
6、TreeSet实现原理,红黑二叉树,比较器接口
1)TreeSet集合,元素不允许重复且有序(自然顺序) 2)TreeSet采用树结构存储数据,存入元素时需要和树中元素进行对比,需要指定比较策略。 3)可以通过Comparable(外部比较器)和Comparator(内部比较器)来指定比较策略,实现了Comparable的系统类可以顺利存入TreeSet。自定义类可以实现Comparable接口来指定比较策略。 4)可创建Comparator接口实现类来指定比较策略,并通过TreeSet构造方法参数传入。这种方式尤其对系统类非常适用。
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色
- 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
- 对于每个节点,从该点至
null
(树尾端)的任何路径,都含有相同个数的黑色节点。在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的约束条件。
1.4线程
1、线程创建方式
1、继承Thread类
1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2】创建Thread子类的实例,也就是创建了线程对象
3】启动线程,即调用线程的start()方法
2、实现Runnable接口
1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3】第三部依然是通过调用线程对象的start()方法来启动线程
3、实现Callable接口
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
4、使用线程池的方式
2、线程生命周期
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池"中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPu,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池"中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O 请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者Io 处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程 这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice)后就处于 RUNNING(运⾏) 状态。
过程:
当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠其他 线程的通知才能够返回到运⾏状态,⽽ TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加 了超时限制,⽐如通过 sleep(long millis) ⽅法或 wait(long millis) ⽅法可以将 Java 线程置 于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调⽤同步 ⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞) 状态。线程在执⾏ Runnable 的 run() ⽅法之后将会进⼊到 TERMINATED(终⽌) 状态
3、线程的交互(join yeild sleep wait notify notifyall)
1、wait():
使一个线程处于等待状态,并且释放所持有的对象的 lock。
2、sleep():
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
3、notify():
唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
4、notityAlI():
唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
5、join():
t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
6、yeild():
让当前运行线程回到可运行状态,以允许具有相同优先级的其 他线程获得运行机会
4、线程安全 Synchronized Lock
概念:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
实现线程安全有几种方式:
1、使用同步代码块Synchronized
2、使用同步方法
synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,。可重入,修饰(class、obj、代码块)。
**可重入:**一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用3、使用ReentrantLock
ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。
ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
5、死锁 会写出死锁的代码
概念:
多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释 放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。 ,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线 程就会互相等待⽽进⼊死锁状态。
排查死锁:
1、使用jps排查死锁,jdk提供的一个工具,可以查看正在运行的java进程
2、使用jstack排查死锁,jdk提供的一个工具,可以查看java进程中线程堆栈信息
3、使用jsconsole排查死锁,jdk提供的一个可视化工具,方便排查程序的一些问题:程序内存溢出,死锁等
4、使用VisualVM排查死锁,jdk提供的一个非常强大的排查java程序问题的一个工具,可以监控程序的性能,查看jvm配置信息,堆快照,线程堆栈信息。产生死锁的条件(四个):
1.资源互斥/资源不共享
2.占有和等待/请求并保持
3.资源不可剥夺
4.环路等待
防止死锁的方法:
破坏死锁产生的四个必要条件之一就行,但是开销太大,目前是避免死锁而不是防止死锁
避免死锁:
1、判断系统安全状态法
2、银行家算法
死锁的解除:
1、资源剥夺法:挂起某些死锁进程,并抢占他们的资源,将这些资源分配给其他的死锁进程
2、撤销进程法:强制撤销部分,甚至全部死锁进程,并剥夺这些进程资源
3、进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退是自愿释放资源而不是剥夺。public class DeadLock{
static Object o1 = new Object();
static Object o2 = new Object();public static void main(String[] args) { new Thread(new Runnable() { public void run() { synchronized (o1) { System.out.println("线程1锁o1"); try { Thread.sleep(1000);//让当前线程睡眠,保证让另一线程得到o2,防止这个线程启动一下连续获得o1和o2两个对象的锁。 synchronized (o2) { System.out.println("线程1锁o2"); } } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { public void run() { synchronized (o2) { System.out.println("线程2锁o2"); synchronized (o1) { System.out.println("线程2锁o1"); } } } }).start(); } }
6、线程池的七个参数的含义
1、corePoolSize,线程池里最小线程数
2、maximumPoolSize、线程池里最大线程数量,超过最大线程时候会使用RejectedExecutionHandle
3、keepAliveTime,线程最大的存活时间4、unit 空闲线程存活时间单位
5、workerQueue,缓存异步任务的队列
6、threadFactory,用来构造线程池里的worker线程7、handler 拒绝策略
7、阻塞队列
三种阻塞队列:
BlockingQueue workQueue = null;
workQueue = new ArrayBlockingQueue<>(5);//基于数组的先进先出队列,有界
workQueue = new LinkedBlockingQueue<>();//基于链表的先进先出队列,无界
workQueue = new SynchronousQueue<>();//无缓冲的等待队列,无界
8、拒接
四种拒接策略
等待队列已经排满了,再也塞不下新任务,同时线程池中线程也已经达到maximumPoolSize数量,无法继续为新任务服务,这个时候就需要使用拒绝策略来处理。
RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常,直接抛出RejectedExecutionException异常阻止系统正常运行。
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常,直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删除之后再尝试加入队列,抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务,调用者运行"一种调节机制,该策略既不会丢弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。
9、锁的分类:公平锁和非公平锁,乐观锁和悲观锁,轻量级锁和重量级锁
公平锁:是指多个线程按照申请锁的顺序来获取锁
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。非公平锁优点在于吞吐量比公平锁大
乐观锁:认为对于一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据,乐观锁认为不加锁的并发操作是没有事情的。
乐观锁适合读操作非常多的场景
悲观锁:认为对于同一个数据的并发操作,一定是会发生改变的,哪怕没有修改,也会认为修改,因此对于同一个数据的并发操作,悲观锁采用加锁的形式悲观锁适合写操作非常多的场景。
独享锁:指该锁一次只能被一个线程所持有
共享锁:是指该锁可被多个线程锁持有
偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁
重量级锁:是指锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。
10、锁的优化
锁优化的思路和方法有以下几种:
减少锁持有的时间:减小锁的持有时间是为了降低锁的冲突的可能性,提高体系的并发能力 ,只在必要时进行同步加锁操作,只在必须加锁的代码段加锁
减小锁粒度:JDK 自带的工具类 ConcurrentHashMap 就是一个典型的实现场景,它对锁的拆分方式大大提高了它的吞吐量,ConcurrentHashMap 将自身分成若干个段,每一段都是一个子 HashMap。当需要新增一个的时候,并不是对整个对象进行加锁,而是先根据 hashcode 计算该数据应该被加入到哪个段中,然后对该段加锁,默认情况下 ConcurrentHashMap 有16个段,因此运气足够好的时候可以接受 16 个线程同时插入,大大提高了吞吐量。
锁粗化:锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
锁分离:最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥。即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。根据实际的操作来选择加上不同的锁也是提升性能的重要方式之一 ,读写分离锁替代独占锁,重入锁和内部锁,自旋锁
锁销除:锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
无锁:无锁是非阻塞的锁,CAS 算法和 ThreadLocal 是实现无锁的两种方式,前者以额外空间实现无锁,后者以额外时间去实现无锁,他们都是非阻塞的。
11、锁的过程
锁的升级过程:
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
无锁:
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功
偏向锁:
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。 当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。 偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
轻量级锁:
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 轻量级锁的获取主要由两种情况: ① 当关闭偏向锁功能时; ② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
重量级锁:
如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
12、ThreadLocal实现过程
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等
ThreadLocal类中提供了几个方法:
1.public T get() { }
2.public void set(T value) { }
3.public void remove() { }
4.protected T initialValue(){ }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
get之前必须先set
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找
13、Volatile变量内存可见性
保证变量的可见性,不能保证变量的符合操作原子性。
实现内存可见:
深入的说:通过加入内存屏障和禁止重排序优化实现。对其变量执行写操作时,会在写操作后加入一条store屏障指令;对其进行读操作时,会在读操作前加入一条load屏障指令。
通俗的说:volatile变量在每次被线程访问时,都强迫从主线程中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中,这样任何时刻,不同的线程总能看到该变量最新的值。线程写volatile变量的过程:
1、改变线程工作内存中volatile变量副本的值;
2、将改变后的副本的值从工作内存刷新到主内存。
线程读volatile变量的过程:
4、从主内存中读取volatile变量的最新值到线程的工作内存中;
5、从工作内存中读取volatile变量的副本。
14、AQS AbstractQueueSynchronizer 锁生效的核心抽象类
首先AQS(AbstractQueueSynchronizer)是个抽象类,本身不能实例化,需要使用者根据实际情况去继承它。
AQS的主要功能是提供线程同步,通俗来说就是可以控制多个线程,让它们阻塞或者唤醒。通过AQS,我们能够实现像重入锁那样单个线程访问临界区代码,让其他线程等待;也可以实现像读写锁那样控制多个读线程同时访问资源,AQS主要实现的功能是让一个或多个线程能正确获取锁(或资源)。例如同一时间只有一个线程获得锁,其他线程都等待该线程释放锁。 。
AQS中首先有一个Node内部类,用来封装线程,并且通过一个head节点和tail节点来形成Node链表,实现同步线程队列。还有一个整型变量state,用来表示锁的状态(这里说的不单单是锁,也可以用来表示其他其他互斥资源),进入AQS通过判断state的值来进行相应的操作(获取锁或者进入队列等待)。还有多个关键方法(acquire等),这些方法暴露出去,实现加锁,解锁的功能。还有一个ConditionObject(下文用Condition代替)对象,这个对象是用来实现类似wait/nodify机制的。但是通过AQS设计的锁更加高端,可以创建多个Condition对象,实现多组等待唤醒控制,而传统的synchronized只能通过wait/notify方法实现一组。
15、Synchronization实现原理(monitor)
JVM规范中描述:每个对象有一个监视器锁(monitor)。
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,
这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。
但是监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
因此,这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。
1.5杂项
git分支合并
冲突解决
1、反射
反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为Java反射机制。
反射作用:
1)在运行时判断任意一个对象所属的类
2)在运行时构造任意一个类的对象
3)在运行时判断任意一个类所具有的成员变量和方法
4)在运行时调用任意一个对象的方法
2、注解,自定义注解
注解(元数据)为我们在代码中添加信息提供一种形式化的方法,我们可以在某个时刻非常方便的使用这些数据。通俗一点,就是为这个方法增加的说明或功能
注解其实就是一种标记,可以在程序代码中的关键节点(类、方法、变量、参数、包)上打上这些标记,然后程序在编译时或运行时可以检测到这些标记从而执行一些特殊操作
Java目前内置了三种注解@Override、@Deprecated、@SuppressWarnnings
@Override:用于标识方法,标识该方法属于重写父类的方法
@Deprecated:用于标识方法或类,标识该类或方法已过时,建议不要使用
@SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告
元注解就是指注解的注解,java5中有:
1、@Retention:定义注解的保留策略,生命周期
2、@Target:定义注解的作用目标
3、@Documented:说明该注解将包含在javaDoc中
4、@Inherited:说明子类可以继承父类中的该注解;
java8加了俩:
1、@Repeatable:对同一注解多次使用
2、@Native:表示这个变量可以被本地代码引用
3、BIO,NIO,AIO
NIO读写数据是由应用进程进行的,AIO读写数据是由操作系统完成的,当操作完成读写,主动通知应用进程。NIO基于epoll等模型实现,但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而AIO(异步I/O)则由操作系统(无需自己)负责进行读写,异步/O的实现会负责把数据从内核拷贝到用户空间。
同步:
当一个同步调用发出后,调用者要一直等待返回信息通知后,才能进行后续操作
异步:
当一个异步调用发出后,调用者在没有得到结果前,就可以进行后续操作
阻塞:
阻塞指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能执行其他业务
非阻塞:
指在不能立即得到结果之前,该函数不会阻塞当前线程,而是立即返回
BIO:
同步并阻塞,服务器实现模式是一个连接一个线程,这样的模式很明显的一个缺陷就是由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出,这种情况虽然可以通过线程池机制改善,但不能从本质上消除弊端
NIO:
同步非阻塞,在JDK1.4以前,java的IO模型一直是BIO,在JDK1.4后,引进新的IO模型NIO。而服务器的实现模式是多个请求一个线程,即请求会注册到多路复用器selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理
AIO:
JDK1.7后发布了NIO2.0,这是真正意义上的异步非阻塞,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)
应用场景:并发连接数不多时采用BIO,因为他编程和调试非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是使用成熟的网络通信框架Netty
4、网络编程(Socket)
网络编程:
模拟计算机通信中运输层之间的对等通信,忽略下层的打包和拆包,直接在运输层建立一条虚连接进行数据传输。主要是指网络通信,实现计算机之间的对话和文件传输等,如,QQ、P2P点对点通信等;而web主要就是B/S结构的应用,通俗一点说就是开发网站、网页,如,QQ空间、百度。
Socket:
又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议 ,socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能
5、TCP和UDP
TCP:(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,UDP(用户数据包协议)是同一层内另一个重要的传输协议。
1) TCP提供面向连接的传输,通信前要先建立连接(三次握手机制);
2) TCP提供可靠的传输(有序,无差错,不丢失,不重复);
3) TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组;
4) TCP提供拥塞控制和流量控制机制;
UDP:(User Datagram Protocol,用户数据报协议)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,IETF RFC 768是UDP的正式规范。UDP在IP报文的协议号是17。
UDP提供无连接的传输,通信前不需要建立连接。
UDP提供不可靠的传输。
UDP是面向数据报的传输,没有分组开销。
UDP不提供拥塞控制和流量控制机制。
6、TCP的三次握手和四次挥手
第一次握手:建立连接时,客户端发送syn包 (syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN ( ack=x+1),同时自己也发送一个sYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECv状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1) ,此包发送完毕,客户端和服务器进入ESTABLISHED (TCP连接成功〉状态,完成三次握手。
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1((终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT 状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2** MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCp连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
7、TCP粘包和拆包
TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
粘包和拆包原因
(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。
拆包粘包的解决策略:
- 消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。
- 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
- 更复杂的应用层协议。
8、OOP的理解
OOP,Object Oriented Programming,原来就是面向对象的编程啊,还有OOD(面向对象的设计),OOA(面向对象的分析)
1、自己买材料,肉,鱼香肉丝调料,蒜苔,胡萝卜等等然后切菜切肉,开炒,盛到盘子里。
2、去饭店,张开嘴:老板!来一份鱼香肉丝!
看出来区别了吗?这就是1是面向过程,2是面向对象。
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们我们使用的就是面向对象了。
面向对象的三大特性:
1、封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
2、继承
提高代码复用性;继承是多态的前提。
3、多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性面向对象的五大原则:
1、单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。
2、开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
3、里式替换原则LSP(the Liskov Substitution Principle LSP)
子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
4、依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的是抽象的中国人,而不是你是xx村的。
5、接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
1.6JDK的新特性
1、JDK8的新特性
Java8在Java历史上是一个开创新的版本,下面JDK8中5个主要的特性:
1、Lambda表达式
2、允许像对象一样传递匿名函数Stream API,充分利用现代多核CPU,可以写出很简洁的代码Date 与 Time APl
2、有一个稳定、简单的日期和时间库可供你使用扩展方法
3、接口中可以有静态、默认方法。
4、重复注解,你可以将相同的注解在同一类型上使用多次。
2、JDK9的新特性
待更新
3、JDK10的新特性
待更新
4、JDK11的新特性
待更新
二、框架
2.1Spring
1、创建对象的方式
1、无参构造器调用:最基本的对象创建方式,只需要有一个无参构造函数和字段的set方法。本质上就是使用无参构造器创建对象,然后使用set方法为字段赋值。
2、静态工厂创建对象:不使用Spring为我们创建对象,而是采用静态工厂的方式创建对象,由Spring调用静态工厂的静态方法创建对象,再放入Spring容器中。
3、实例工厂创建对象:实例工程方式需要先由Spring创建工厂实例,然后再调用工厂方法创建对象,放入Spring容器中。
2、属性注入的方式
set方式注入
构造器注入
注解方式注入
3、IOC和DI
IOC:就是对象之间的依赖关系由容器来创建,对象之间的关系本来是由我们开发者自己创建和维护的,在我们使用Spring框架后,对象之间的关系由容器来创建和维护,将开发者做的事让容器做,这就是控制反转。BeanFactory接口是Spring loc容器的核心接口。
DI:我们在使用Spring容器的时候,容器通过调用set方法或者是构造器来建立对象之间的依赖关系。
控制反转是目标,依赖注入是我们实现控制反转的一种手段。
4、IOC的创建过程(Spring Bean的生命周期)
1、实例化Bean对象
2、设置对象属性,使用依赖注入填充所有属性(构造器注入,set注入,注解方式注入)
3、检查是否实现Aware接口,并设置相关依赖;如果实现BeanNameAware接口,则工厂如果传递bean的ID来调用setBeanName();如果实现BeanFactoryAware接口,工厂通过传递自身的实例来调用setBeanFactory
4、执行BeanPostProcessors前置处理,调用preProcessBeforeInitialization()方法
5、 检查是否是InitializingBean以决定是否调用afterPropertiesSet方法
6、检查是否配置有自定义的init-method方法
7、执行BeanPostProcessor后置处理,调用postProcessAfterInitialization()方法
8、注册必要的Destruction相关回调接口
9、使用
10、是否实现DisposableBean接口,如果有,则执行相应的方法
11、是否配置有自定义的destroy方法,如果有则执行销毁
5、AOP(动态代理)
1、AOP面向切面编程,它是一种思想。它就是针对业务处理过程中的切面进行提取,以达到优化代码的目的,减少重复代码的目的。就比如,在编写业务逻辑代码的时候,我们习惯性的都要写:日志记录,事物控制,以及权限控制等,每一个子模块都要写这些代码,代码明显存在重复。这时候,我们运用面向切面的编程思想,采用横切技术,将代码中重复的部分,不影响主业务逻辑的部分抽取出来,放在某个地方进行集中式的管理,调用。形成日志切面,事物控制切面,权限控制切面。这样,我们就只需要关系业务的逻辑处理,即提高了工作的效率,又使得代码变的简洁优雅。这就是面向切面的编程思想,它是面向对象编程思想的一种扩展。
2、AOP的使用场景:缓存、权限管理、内容传递、错误处理、懒加载、记录跟踪、优化、校准、调试、持久化、资源池、同步管理、事物控制等。AOP的相关概念:切面(Aspect)连接点(JoinPoint)通知(Advice)切入点( Pointcut)代理(Proxy):织入(WeaVing)
3、Spring AOP的编程原理?代理机制JDK的动态代理:只能用于实现了接口的类产生代理。Cglib 代理:针对没有实现接口的类产生代理,应用的是底层的字节码增强技术,生成当前类的子类对象。
6、Spring的三级缓存(循环依赖)
循环依赖:
循环依赖的本因。当spring启动在解析配置创建bean的过程中。首先在初始化A的时候发现需要引用B,然后去初始化B的时候又发现引用了C,然后又去初始化C却发现一个操蛋的结果,C引用了A。它又去初始化A一次循环无穷尽,如你们这该死的变态三角关系一样。
Spring解决循环依赖的方法就是如题所述的三级缓存、预曝光。
三级缓存:一级缓存 singletonObjects 缓存加载完成的bean。
二级缓存 earlySingletonObjects 缓存从三级缓存中获取到的bean,此时里面的bean没有加载完毕。
三级缓存 singletonFactories 。缓存一个objectFactory工厂。
a实例化,放入三级工厂缓存,设置属性b,b实例化放入三级缓存。b设置属性a,从三级工厂缓存中获取代理后的对象a,同时,代理后的a放入二级缓存,然后设置属性c,c实例化放入三级缓存,设置属性a,此时从二级缓存中获取到的代理后的a跟b中的a是一个对象,属性a设置成功。c初始化,然后执行后置处理器。进行aop的增强。增强后将代理的c放入到一级缓存,同时删除三级缓存中的c。c加载完成,b得到c,b设置c成功。b初始化,然后执行后置处理器,进行aop增强,将增强后的代理对象b放入到一级缓存。删除三级缓存中的b。此时 a拿到b,设置属性b成功,开始初始化,初始化后执行后置处理器。在aop的后置处理器中有一个以beanName为key,经过aop增强的代理对象为value的map earlyProxyReferences。如果这个beanName已经被代理后就不在代理,这个时候执行后置处理器后,a还是未经代理的对象a。此时a再通过getSingleton 重新从缓存中获取一下a。 Object earlySingletonReference = getSingleton(beanName, false);false 表示不从三级缓存中取,只从一级,二级缓存中获取。这个时候能拿到二级缓存中的a。二级缓存中的a也是经过代理后的a。然后将代理后的a放入到一级缓存中。a加载完毕。
7、scope的值
maven的依赖范围:
compile
默认就是compile,什么都不配置也就是意味着compile。compile表示被依赖项目需要参与当前项目的编译,当然后续的测试,运行周期也参与其中,是一个比较强的依赖。打包的时候通常需要包含进去。
test
scope为test表示依赖项目仅仅参与测试相关的工作,包括测试代码的编译,执行。比较典型的如junit。
runntime
runntime表示被依赖项目无需参与项目的编译,不过后期的测试和运行周期需要其参与。与compile相比,跳过编译而已,说实话在终端的项目(非开源,企业内部系统)中,和compile区别不是很大。比较常见的如JSR×××的实现,对应的API jar是compile的,具体实现是runtime的,compile只需要知道接口就足够了。oracle jdbc驱动架包就是一个很好的例子,一般scope为runntime。另外runntime的依赖通常和optional搭配使用,optional为true。我可以用A实现,也可以用B实现。
provided
provided意味着打包的时候可以不用包进去,别的设施(Web Container)会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。相当于compile,但是在打包阶段做了exclude的动作。
system
从参与度来说,也provided相同,不过被依赖项不会从maven仓库抓,而是从本地文件系统拿,一定需要配合systemPath属性使用。
8、Spring Bean的初始化顺序:Order DependOn
1、@DependOn:
@DependsOn
注解可以用来控制bean的创建顺序,该注解用于声明当前bean依赖于另外一个bean。所依赖的bean会被容器确保在当前bean实例化之前被实例化。 直接或者间接标注在带有
@Component
注解的类上面; 直接或者间接标注在带有
@Bean
注解的方法上面; 使用
@DependsOn
注解到类层面仅仅在使用 component-scanning 方式时才有效,如果带有@DependsOn
注解的类通过XML方式使用,该注解会被忽略,<bean depends-on="..."/>
这种方式会生效。2、@Order:@Retention(value=RUNTIME)@Target(value={TYPE,METHOD,FIELD})@Documented public @interface Order ,这个标记包含一个value属性,类型是整型,如:1,2 等等。值越小拥有越高的优先级。
默认的属性是Ordered.LOWEST_PRECEDENCE,
代表的是最低优先级。
通过代码可以发现最大值和最小值的定义就是Inger的最大值和最小值。
9、@AutoWire和@Resource
Autowire默认按照类型装配,默认情况下它要求依赖对象必须存在如果允许为null,可以设置它required属性为false,如果我们想使用按照名称装配,可以结合@Qualifier注解一起使用;
Resource默认按照名称装配,当找不到与名称匹配的bean才会按照类型装配,可以通过name 属性指定,如果没有指定name属性,当注解标注在字段上,即默认取字段的名称作为bean名称寻找依赖对象,当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找依赖对象
2.2SpringMVC
1、SpringMVC的运行流程(实现原理)
1.用户发送请求至前端控制器DispatcherServlet
2.前端控制器DispatcherServlet收到请求调用处理器映射器HandlerMapping
3.处理器映射器HandlerMapping根据请求url找到具体的处理器,生成处理器对象以及处理器拦截器(如果有则生成)一并返回给前端控制器DispatcherServlet
4.前端控制器DispatcherServlet通过处理器适配器HandlerAdapter调用处理器
5.执行处理器(Controller,也叫后端控制器)
6.Controller执行完成返回ModelAndView
7.处理器适配器HandlerAdapter将controller执行结果ModelAndView返回给前端控制器DispatcherServlet
8.前端控制器DispatcherServlet将ModelAndView传给视图解析器ViewReslover
9.视图解析器ViewReslover解析后返回具体view
10.前端控制器DispatcherServlet对view进行渲染视图
11.前端控制器DispatcherServlet响应用户
2、Restful @PathVariable
restful可以规范资源获取的url路径,允许将参数通过url拼接传到服务端,对于不同的操作,要指定不同的http方法(post、get、put、delete)。
springMVC支持restful风格的请求,SpringMVC可以使用@RequestMapping注解的路径设置,结合@PathVariable注解的参数指定,来实现RESTful风格的请求
在@RequestMapping注解的请求路径中添加了一个动态数据“{id}”,它的作用是解析前台的请求路径,将动态数据所在的位置解析为名为 id 的请求参数。而在Controller的参数中,使用@PathVariable注解,在其中指定请求参数的key名称,并映射在后面定义的形参上,这里定义userId形参来接收名为id的请求参数。方法体中其余的操作就是正常的业务逻辑,最后使用@ResponseBody注解加上之前配置的类型转换器,返回客户端JSON类型的用户信息。总的来说,利用SpringMVC实现RESTful风格主要就是在于请求路径和请求参数的映射,以及RequestMethod的指定。
3、#RespoBody @RequestBody @RequestParam
@RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径;用于方法上,表示在类的父路径下追加方法上注解中的地址将会访问到该方法
@Responsebody 注解表示该方法的返回的结果直接写入 HTTP 响应正文(ResponseBody)中,一般在异步获取数据时使用,通常是在使用 @RequestMapping 后,返回值通常解析为跳转路径,加上 该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。 @Responsebody 后返回结果不会被解析为跳转路径,而是直接写入HTTP 响应正文中。 返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用;
@RequestBody 注解则是将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。 1) 该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上; 2) 再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上。 A) GET、POST方式提时, 根据request header Content-Type的值来判断:
2.3MyBatis
1、MyBatis动态SQL
在mapper配置文件中,有时需要根据查询条件选择不同的SQL语句,或者将一些使用频率高的SQL语句单独配置,在需要使用的地方引用。Mybatis的一个特性:动态SQL,来解决这个问题。
mybatis动态sql语句是基于OGNL表达式的,主要有以下几类:
- if 语句 (简单的条件判断)
- choose (when,otherwize) ,相当于java 语言中的 switch ,与 jstl 中的choose 很类似
- trim (对包含的内容加上 prefix,或者 suffix 等,前缀,后缀)
- where (主要是用来简化sql语句中where条件判断的,能智能的处理 and or ,不必担心多余导致语法错误)、
- set (主要用于更新时)
- foreach (在实现 mybatis in 语句查询时特别有用)
2、MyBatis${}和#{}区别
mybatis的#是预编译处理,$是字符串替换。
Mybatis在处理#Q}时,会将sql中的}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理 时 , 就 是 把 时,就是把 时,就是把替换成变量的值。
使用#可以有效的防止 SQL注入,提高系统安全性。
3、MyBatis批处理
mybatis 在处理数据的时候有两种方式,
第一种是使用foreach标签,
另一种是使用allowMultiQueries=true来完成。
4、MyBatis实现过程(实现原理)
1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NqOrP7G8-1648621377793)(C:\Users\33751\Desktop\assets\1606781572967.png)]
2.4 SpringBoot
1、自动装配原理(实现过程)
springboot自动装配主要是添加注解@SpringBootApplication ,使用@EnableAutoConfiguration注解开启自动装配功能,@EnableAutoConfiguration作用就是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。这些功能配置类要生效的话,会去classpath中找是否有该类的依赖类(也就是pom.xml必须有对应功能的jar包才行)并且配置类里面注入了默认属性值类,功能类可以引用并赋默认值。生成功能类的原则是自定义优先,没有自定义时才会使用自动装配类。
2、SpringBoot多环境配置
一、创建不同环境的配置文件,比如公共的变量配置在application.properties文件 ,开发环境application-dev.properties,测试环境application-test.properties ,生产环境application-prod.properties ,使用不同的环境只需要在application.properties文件中添加指定环境的变量spring.profiles.active=prod
二、把不同的环境配置文件放入不同文件夹中,开发环境是dev目录,测试环境是test目录,生产环境是prod目录。每个目录下面都有application.properties文件。
需要配置pom.xml文件中的profiles.active进行配置文件目录的名字如test,然后配置打包时的包名如test-spring-boot,同时修改配置,并且设置打包时不包含dev,test,prod目录。
3、打包方式
1、jar方式打包:
pom配置packaging为jar,打包完成后有org目录下是springboot的类,META-INF目录下是jar的元信息的描述,BOOT-INF包含了jar启动要用的lib和classes
通过maven插件spring-boot-maven-plugin,在进行打包时,会动态生成jar的启动类org.springframework.boot.loader.JarLauncher,借助该类对springboot应用程序进行启动。
优点
本地无需搭建web容器,方便开发和调试。
因为自带web容器,可以避免由于web容器的差异造成不同环境结果不一致问题。
一个jar包就是全部,方便应用扩展。
借助容器化,可以进行大规模的部署。
缺点
应用过于独立,难以统一管理。
数据源无法通过界面进行管理。
应用体积过大。
修改web容器相关配置较为困难,需要借助代码实现。
2、war包启动
把项目打包成war包,可以放到独立的web容器中。将pom配置packaging为war。
以war包方式运行,通过maven插件spring-boot-maven-plugin进行相关配置后,最终生成一个可运行在tomcat,weblogic等java web容器中的war包。
优点
可以借助web容器管理界面对应用进行管理。
可以管理JNDI数据源。
web容器配置较为灵活,配置和程序分离。
应用体积较小,甚至可以借助web容器的包管理功能(比如weblogic Library)进一步减小应用大小。
缺点
本地需要搭建web容器,对本地环境要求更高点,学习成本也响应更高。
调试较为困难,需要借助web容器。
无法兼容所有web容器(比如spring boot2.x无法运行在weblogic 11g上)。
2.5 RabbitMQ
1、消息模式P2P worker Pub/sub(exchange4种)
简单队列:
一个生产者,一个默认的交换机,一个队列,一个消费者
work工作队列:
一个生产者,一个默认的交换机,一个队列,两个消费者,默认采用公平分配
Publish/Subscribe发布订阅模式:
一个生产者,一个交换机,两个队列,两个消费者该模式需要借助交换机,生产者把消息发送到交换机,在通过交换机到达队列
有四种交换机(exchange):
direct/topic/headers/fanout:默认交换机是direct,发布订阅使用fanout
direct:直连交换机,处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “abc”,则只有被标记为“abc”的消息才被转发,不会转发abc.def,也不会转发dog.ghi,只会转发abc。
topic: 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“”匹配不多不少一个词。因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.” 只会匹配到“abc.def”。
headers:不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic 的路由键都需要要字符串形式的。
fanout: 不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。
Routing路由模式:
一个生产者,一个交换机,两个队列,两个消费者生产者将消息发送到direct交换机(路由模式需要借助直连交换机实现),在绑定队列和交换机的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费消息。也就是让消费者有选择性的接收消息。
Topic主题模式:
一个生产者,一个交换机,两个队列,两个消费者。又称通配符模式(可以理解为模糊匹配,路由模式相当于精确匹配)
使用直连交换机可以改善我们的系统,但是它仍有局限性,它不能实现多重条件的路由。
在消息系统中,我们不仅想要订阅基于路由键的队列,还想订阅基于生产消息的源。这时候可以使用topic交换机。
使用主题交换机时不能采用任意写法的路由键,路由键的形式应该是由点分割的有意义的单词。例如"goods.stock.info"等。路由key最多255字节。
2、RabbitMQ防止消息丢失
1、消息持久化:
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。
所以就要对消息进行持久化处理。如何持久化,下面具体说明下:
要想做到消息持久化,必须满足以下三个条件,缺一不可。
1) Exchange 设置持久化:durable:true
2)Queue 设置持久化,new出来就是持久化的
3)Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
2、ACK确认机制:
这个使用就要使用Message acknowledgment 机制,就是消费端消费完成要通知服务端,服务端才把消息从内存删除。
ConfirmCallback 只确认消息是否正确到达 Exchange 中。
ReturnCallback 消息没有正确到达队列时触发回调,如果正确到达队列不执行。
默认情况下消息消费者是自动 ack (确认)消息的,需要设置为手动确认,原因是:自动确认会在消息发送给消费者后立即确认,这样存在丢失消息的可能
3、设置集群镜像模式:
我们先来介绍下RabbitMQ三种部署模式:
1)单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
2)普通模式:默认的集群模式,某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
3)镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的高可用方案 为什么设置镜像模式集群,因为队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据。
4、消息补偿机制:
消息补偿机制需要建立在消息要写入数据库日志,发送日志,接受日志,两者的状态必须记录。
然后根据数据库日志记录检验消息发送消费是否成功,不成功,进行消息补偿措施,重新发送消息处理。
3、RabbitMQ消息确认机制
RabbitMQ的消息确认有两种。
一种是消息发送确认。这种是用来确认生产者将消息发送给交换器,交换器传递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。
通过实现ConfirmCallBack接口,消息发送到交换器Exchange后触发回调。
spring.rabbitmq.publisher-confirms = true
通过实现ReturnCallback接口,如果消息从交换器发送到对应队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发)
spring.rabbitmq.publisher-returns = true
第二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息。
(1)确认模式
- AcknowledgeMode.NONE:不确认
- AcknowledgeMode.AUTO:自动确认
- AcknowledgeMode.MANUAL:手动确认
spring-boot中配置方法:
spring.rabbitmq.listener.simple.acknowledge-mode = manual
ACK机制:
是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。
如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
如果在集群的情况下,RabbitMQ会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务。
消息永远不会从RabbitMQ中删除,只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。
消息的ACK确认机制默认是打开的。
2.6 ElasticSearch
1、倒排索引
假设我们要进行mysql的全文搜索,可以对文档内容进行分词处理,得到结果,并且分词与文档进行映射,映射之后,我们用倒排索引的方式搜索一个词,会直接找到关键词中查找到我们的词,然后直接查找到对应的文档。
例子:我 html1,html2,html3 爱 html1,html2 爱我 html1 我爱 html2 祖国 html1 我的祖国 html1 编程 html1,html2 我爱编程 html1,html2 爱编程 html1,html2 快乐 html2 码农 html2 小码农 html2
2、分词
分词器在字符过滤器之后工作,用于把文本分割成多个标记(Token),一个标记基本上是词加上一些额外信息,分词器的处理结果是标记流,它是一个接一个的标记,准备被过滤器处理。
1,标准分词器(Standard Tokenizer)
标准分词器类型是standard,用于大多数欧洲语言,使用Unicode文本分割算法对文档进行分词。
2,字母分词器(Letter Tokenizer)
字符分词器类型是letter,在非字母位置上分割文本,这就是说,根据相邻的词之间是否存在非字母(例如空格,逗号等)的字符,对文本进行分词,对大多数欧洲语言非常有用。
3,空格分词器(Whitespace Tokenizer)
空格分词类型是whitespace,在空格处分割文本
4,小写分词器(Lowercase Tokenizer)
小写分词器类型是lowercase,在非字母位置上分割文本,并把分词转换为小写形式,功能上是Letter Tokenizer和 Lower Case Token Filter的结合(Combination),但是性能更高,一次性完成两个任务。
5,经典分词器(Classic Tokenizer)
经典分词器类型是classic,基于语法规则对文本进行分词,对英语文档分词非常有用,在处理首字母缩写,公司名称,邮件地址和Internet主机名上效果非常好。
6、中文分词:
1、IK分词器, IK分词器支持自定义词库,支持热更新分词字典
2、拼音分词器
3、项目使用 数据同步
三、微服务
3.1微服务
1、微服务的理解
微服务其实是一种架构概念,旨在通过将功能分解到各个离散的服务中去,以实现对系统方案的解耦。把一个大型的单个应用程序和服务拆分为数个甚至数十个的支持微服务,可以扩展单个组件而不是整个应用程序堆栈。
一般大型的复杂的系统,比如大型电商系统,淘宝,京东,以及高并发系统,例如大型门户网站,百度,新浪,还有一些需求不明确的更新很快的系统都可以使用微服务。
微服务主流的技术有:Dubbo阿里巴巴,SpringCloud Alibaba
微服务的十二个要素:1.一份基准代码,多份部署;2、显式声明依赖关系;3、在环境中存储配置;4、把后端服务当做附加资源;5、严格分类构建,发布和运行;6、以一个或多个无状态的进程运行应用;7、通过端口绑定提供服务;8、通过进程模型进行扩展;9、快速启动和优雅终止可最大化健壮性;10、开发环境与线上环境等价;11、把日志当做事件流;12、后台管理任务当做一次性进程运行。
2、微服务解决方案(spring cloud)
3.2 组件
1、各大组件(5个)
1、Nacos,一个更易于构建云原生应用的动态服务发现,配置管理和服务管理平台;作为注册中心,配置服务名称以及注册中心地址,启动类@EnableDiscoveryClient进行服务注册和发现。作为配置中心,实现共享的配置信息的管理,dataId命名要求服务名—多环境active-数据格式,需要开启@RefreshScope注解进行实时刷新,以及@Value进行加载共享变量
2、Gateway,微服务架构的一种简单有效的统一的API路由管理方式;作为网关中心,核心有Route路由,Predicate断言,Filter过滤器,Gateway可以请求接入作为所有API接口服务请求的接入点,业务聚合,中介策略,统一管理;一共8种路由选择原则;配置后可以通过服务的请求路径进行访问,端口号可以省略
3、OpenFeign,声明式REST服务调用,两个重要的注解@EnableFeignClients 用于开启feign功能,@FeignClient 用于定义feign 接口 。Feign是实现服务的远程调用技术。主要是作用在服务客户端,用于实现服务的调用。 远程服务参数传递: 1.键值对传输 必须使用:@RequestParam注解进行修饰 不可省略 2.对象传输 必须使用:@RequestBody;OpenFeign默认不支持MultipartFile传递,如果需要进行传递,有以下方式 1.将要上传的内容转换为Base64格式进行交互 2.使用feign-form实现 ;feign: client: config: default: connectTimeout: 10000 #设置连接的超时时间 readTimeout: 20000 #设置读取的 超时时间
4、Ribbon,客户端的负载均衡器,提供对大量的HTTP和TCP客户端的访问控制,可以实现服务的远程调用和服务的负载均衡协调。@RibbonClients启用Ribbon
5、Sentinel,以流量为切入点,从流量控制,熔断降级,系统负载保护等多个维度保护服务的稳定性。漏桶算法,令牌桶算法,计算器算法,滑动窗口算法。
6、Sleuth,SpringCloud Sleuth为SpringCloud实现了分布式跟踪解决方案,其实就是一个工具,在整个分布式系统中能跟踪一个用户请求的过程(包括数据采集,数据传输,数据存储,数据分析,数据可视化),捕获这些跟踪数据,就能构建微服务的整个调用链的视图,,这是调试和监控微服务的关键工具。
2、组件的实现原理(1-2个)
3.3 分布式
1、分布式锁
分布式锁是解决并发时资源争抢的问题,分布式事务和本地事务是解决流程化提交问题,用于分布式系统中保证不同节点之间的数据一致性。
分布式锁实现:
1.基于数据库锁实现,悲观锁和乐观锁
悲观锁就是采用行锁,表锁,操作前先上锁,适用于经常发生冲突的情况
乐观锁就是通过添加版本号,在更新时候比较版本号进行更新,比如version,适用于多读的应用类型,提高系统吞吐量。
2.基于redis实现
setnx(key,value),若key-value不存在,则成功加入缓存并返回1,否则返回0;
get(key),获取key对应的value值,若不存在直接返回null;
getset(key,value),先获取key对应的value值,若不存在则返回null,然后将旧的value更新为新的value;
expire(key,seconds),设置key-value的有效期为seconds秒。
3.基于zookeeper
利用Zookeeper不能重复创建一个节点的特性来实现,但是Zookeeper分布式锁是不常用的,编程太复杂
2、分布式事务
1、2PC:
在XA分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求,在接到Prepare请求后,每一个参与者节点会各自执行事务有关的数据更新,更新成功则返回完成消息。此时进入第二阶段。如果事务协调者收到的都是正向返回,就发送commit请求,之后各节点进行本地事务提交并释放锁资源。提交完成后,返回完成消息。
2、3PC:
XA三阶段提交在两阶段提交基础上增加了CanCommit阶段,并且引入超时机制。一旦超时,就会自动进行本地commit。
3、MQ事务:
基于MQ的事务异步确保型,需要业务系统结合MQ消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制MQ的消息状态
4、TCC补偿性事务:
是Try,Commit,Cancel三种指令的缩写,其逻辑模式类似于XA两阶段提交,实现方式是在代码层面人为实现。
5、最大努力通知型
这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知,达到通知次数后即不再通知。
6、saga
3、遇到的问题
3.4 Dubbo相关
1、Dubbo协议
Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC 分布式服务框架,现已成为 Apache 基金会孵化项目。
2、Dubbo注册中心
3、超时机制,服务容错
四、数据库
4.1 Mysql
1、存储引擎
数据库存储引擎是数据库底层软件组织,数据库管理系统使用数据引擎进行创建,查询,更新和删除数据的。
MySql常见的三种存储引擎为InnoDB、MyISAM和MEMORY。
InnoDB是事务型数据库的首选引擎,支持事务安全表,支持行锁定和外键,是MySql的默认引擎。
MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一,拥有较高的插入,查询速度,但不支持事务。
MEMORY存储引擎将表中的数据存储到内存中,未查询和引用其他表数据提供快速访问。
三种的区别:
1.事务安全:
InnoDB支持事务安全,MyISAM和MEMORY两个不支持
2.存储限制:
InnoDB有64TB的存储限制,MyISAMheMEMORY视情况而定
3.空间使用:
InnoDB对空间使用程度较高,MyISAM和MEMORY对空间使用程度较低
4.内存使用:
InnoDB和MEMORY对内存使用程度较高,MyISAM对内存使用程度较低
5.插入数据速度:
InnoDB插入数据的速度较低,MyISAM和MEMORY插入数据速度较高
6.对外建支持:
InnoDB对外建支持情况较好,MyISAM和MEMORY两个不支持外键。
2、SQl执行过程
- MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。
- 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
- SQL 等执行过程分为两类,一类对于查询等过程如下:权限校验—》查询缓存—》分析器—》优化器—》权限校验—》执行器—》引擎
- 对于更新等语句执行流程如下:分析器----》权限校验----》执行器—》引擎—redo log prepare—》binlog—》redo log commit
3、慢查询
MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阀值的语句。具体环境中,运行时间超过long_query_time值的SQL语句,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是记录运行10秒以上的语句。默认情况下,MySQL数据库并不启动慢查询日志,需要手动来设置这个参数。当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件和数据库表。
4、sql优化
1.尽量避免使用select*,返回无用的字段会降低查询效率。
优化方式:使用具体的字段代替*,只返回使用的字段
2.尽量避免使用in和not in,会导致数据库引擎放弃索引进行全表扫描
优化方式:如果是连续数值,可以用between代替。如果是子查询,可以用exists代替
3.尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描
优化方式:尽量在字段后面进行模糊查询
4.尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描
优化方式:可以给字段添加默认值0,对0值进行判断。
5、索引(使用,生效,底层原理(BTree B-Tree B+Tree))
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。举例说明索引:如果把数据库中的某一张看成一本书,那么索引就像是书的目录,可以通过目录快速查找书中指定内容的位置,对于数据库表来说,可以通过索引快速查找表中的数据。
索引一般以文件形式存在磁盘中(也可以存于内存中),存储的索引的原理大致概括为以空间换时间,数据库在未添加索引的时候进行查询默认的是进行全量搜索,也就是进行全局扫描,有多少条数据就要进行多少次查询,然后找到相匹配的数据就把他放到结果集中,直到全表扫描完。而建立索引之后,会将建立索引的KEY值放在一个n叉树上(BTree)。因为B树的特点就是适合在磁盘等直接存储设备上组织动态查找表,每次以索引进行条件查询时,会去树上根据key值直接进行搜索。
唯一,不为空,经常被查询的字段适合建立索引,
1.B+的磁盘读写代价更低
B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说lo读写次数也就降低了。
2.B+tree 的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
6、事务(ACID隔离级别,脏读,虚读,不可重复读)
事务的四个特性:
1、原子性,原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果失败则不能对数据库有任何影响。
2、一致性,事务开始前和结束后,数据库的完整性约束没有被破坏,比如A向B转账,不可能A扣了钱,B却没收到
3、隔离性,隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱结束前,B不能向这张ka转账。
4、持久性,持久性是指一个事务一旦被提交,那么对数据库的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
隔离级别:
1、读取未提交(read-uncommitted):最低的隔离级别,允许读取尚未提交的数据变更,可能导致脏读,幻读,不可重复读。
2、读取已提交(read-committed):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读和不可重复读仍可能发生。
3、可重复读(repeatable-read):对同一字段的多次读取结果都是一致的,除非数据是被本身自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
4、可串行化(序列化serializavle):最高的隔离级别,完全服从ACID的隔离级别,所有的事务依次执行,互不干扰。
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据进行了更新并提交,导致事务A多次读取同一数据时,结果因此本事务先后两次读到的数据结果不一致。
幻读:幻读解决了不可重复读,保证了同一个事务里,查询的结果都是事务开始时的状态。
7、Mysql函数和自定义函数
AVG()函数返回数值列的平均值;count()函数返回匹配指定条件的函数;max()函数返回指定列最大值;min()函数返回指定列的最小值;sum()函数返回指定列的总数;first()函数返回指定列的第一条数据;last()函数返回指定列最后一个记录的值;
自定义函数:函数必须要有返回值,内部不能写select * from tab1这种sql语句;
- – 创建一个加法计算器函数
CREATE FUNCTION jia(i INT , j INT) RETURNS INT
BEGIN
DECLARE num INT DEFAULT 0;
SET num=i+j;
RETURN(num);
4.2 Oracle
1、序列
Sequence 是oracle提供的用于产生一系列唯一数字的数据库对象。由于oracle中没有设置自增列的方法,所以我们在oracle数据库中主要用序列来实现主键自增的功能。
1、序列第一次必须先调用nextval获取一个序列值才能使用currval查看当前值
2、序列的起始值不能小于最小值
3、创建一个循环序列,则必须要设定最大值
4、如果创建带缓存的序列,缓存的值必须满足约束公式: 最大值-最小值>=(缓存值-1)*每次循环的值
2、存储过程
存储过程是一个预编译的sQL语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次sQL,使用存储过程比单纯sQL语句执行要快。
调用:
1)可以用一个命令对象来调用存储过程。2)可以供外部程序调用,比如: java程序。
优点:
1)存储过程是预编译过的,执行效率高。
2)存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯。3)安全性高,执行存储过程需要有一定权限的用户。
4)存储过程可以重复使用,可减少数据库开发人员的工作量。缺点:移植性差
3、自定义函数
create or replace function 函数名(参数1 模式 参数类型)
return 返回值类型
as
变量1 变量类型;
变量2 变量类型;
begin
函数体;
end 函数名;
4、分页
使用: --rownum关键字:oracle对外提供的自动给查询结果编号的关键字,与每行的数据没有关系。 --注意:rownum关键字只能做< <=的判断,不能进行> >=的判断
获取51到100的数据
三种分页的写法:
1.使用minus,原理就是查询出前100行的数据 减去 查询出前50行的数据
select * from
(select t.*,rownum num from DATA_TABLE_SQL t where rownum<=100 )
where num>502.查询出所有数据的rownum,然后再选择50到100的数据(不推荐)
select * from
(select t.*,rownum num from DATA_TABLE_SQL t where rownum<=100 )
where num>503.限定范围100条数据,并查询出这100条的rownum,然后再选择50到100的数据
select * from
(select t.*,rownum num from DATA_TABLE_SQL t where rownum<=100 )
where num>50
4.3 Redis
1、redis为什么那么快
首先,redis是单进程单线程的k-v内存型可持久化数据库。
单线程还能处理速度很快的原因:
1、redis 操作是基于内存的,内存的读写速度非常快
2、正是由于redis 的单线程模式,避免了线程上下文切换的损耗
3、redis采用的IO多路复用技术,可以很好的解决多请求并发的问题。多路代表
多请求,复用代表多个请求重复使用同一个线程。
2、redis常用的数据类型(8种)
1.字符串(String):最常用的,一般用于存储一个值
2.列表(List):使用list结构实现栈和队列结构
3.集合(Set) :交集,差集和并集的操作
4.有序集合(sorted set) :排行榜,积分存储等操作
5.哈希(Hash):存储一个对象数据的
3、Redis穿透,击穿,雪崩,倾斜
缓存穿透:无效ID,在redis 缓存中查不到,去查询DB,造成DB压力增大。
解决方法:
1、解决方法1:布隆过滤器,提供一个很大的Bit-Map,提供多个hash 函数,分别对查询参数值【比如UuID】,进行求hash,然后分别对多个hash结果,在对应位置对比是否全为1或者某个位置为0,一旦有一个位置标识为o,表示本次查询UUID,不存在于缓存,再去查询DB.起到一个再过滤的效果。
2、解决方法2:把无效的ID,也在redis缓存起来,并设置一个很短的超时时间。缓存雪崩:缓存同一时间批量失效,导致大量的访问直接访问DB
解决方法:
在做缓存时候,就做固定失效时间+随机时间段,保证所有的缓存不会同一时间失效缓存击穿:在缓存失效的时候,会有高并发访问失效的缓存【热点数据】
解决方法:
最简单的解决方法,就是将热点数据设置永不超时!
第二个解决方法:对访问的Key加上互斥锁,请求的Key如果不存在,则加锁,去数据库取,新请求过来,如果相同KEy,则暂停10s再去缓存取值;如果 Key不同,则直接去缓存取!缓存倾斜
热点数据放在了一个reids节点上,导致redis节点无法承受大量的请求,导致的redis宕机。解决办法:
扩展主从结构,增加从节点数量,缓解redis的压力。
可以在tomcat中做JVM缓存,在查询这个redis之前,前去查询Tomcat中的缓存。
4、Redis集群方案
1、codis:他支持在节点数量改变情况下,旧节点数据可恢复到新的hash节点
2、redis cluster3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。
3、在业务代码层实现,起几个毫无关联的redis实例,在代码层,对key进行hash计算,然后去对应的redis实例操作数据,这种方式对hash层代码要求比较高
5、Redis失效策略
不会立即删除
1.1定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。
1.2惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。
6、Redis淘汰策略
在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。(redis.conf中配置)
2.1 volatile-lru:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少使用的key。
2.2 allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
2.3 volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
2.4 allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
2.5 volatile-ttl:在内存不足时,Redis会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。
2.6 noeviction:(默认)在内存不足时,直接报错。
方案:指定淘汰机制的方式:maxmemory-policy具体策略,设置Redis的最大内存:maxmemory 字节大小
7、RESP协议
Redis 即 REmote Dictionary Server (远程字典服务);
而Redis的协议规范是 Redis Serialization Protocol (Redis序列化协议)
该协议是用于与Redis服务器通信的,用的较多的是Redis-cli通过pipe与Redis服务器联系;
协议如下:
客户端以规定格式的形式发送命令给服务器;
服务器在执行最后一条命令后,返回结果。
客户端发送命令的格式(类型):5种类型
间隔符号,在Linux下是\r\n,在Windows下是\n
简单字符串 Simple Strings, 以 "+"加号 开头
格式:+ 字符串 \r\n 字符串不能包含 CR或者 LF(不允许换行)
eg: “+OK\r\n”
注意:为了发送二进制安全的字符串,一般推荐使用后面的 Bulk Strings类型
错误 Errors, 以"-"减号 开头
格式:- 错误前缀 错误信息 \r\n 错误信息不能包含 CR或者 LF(不允许换行),Errors与Simple Strings很相似,不同的是Erros会被当作异常来看待
eg: “-Error unknow command ‘foobar’\r\n”
整数型 Integer, 以 “:” 冒号开头
格式:: 数字 \r\neg: “:1000\r\n”
大字符串类型 Bulk Strings, 以 " " 美 元 符 号 开 头 , 长 度 限 制 512 M 格 式 : "美元符号开头,长度限制512M 格式: "美元符号开头,长度限制512M格式: 字符串的长度 \r\n 字符串 \r\n
字符串不能包含 CR或者 LF(不允许换行);
eg: “$6\r\nfoobar\r\n” 其中字符串为 foobar,而6就是foobar的字符长度
“$0\r\n\r\n” 空字符串
“$-1\r\n” null
数组类型 Arrays,以 ""星号开头
格式: 数组元素个数 \r\n 其他所有类型 (结尾不需要\r\n) 注意:只有元素个数后面的\r\n是属于该数组的,结尾的\r\n一般是元素的
eg: “*0\r\n” 空数组
“*2\r\n$2\r\nfoo\r\n$3\r\nbar\r\n” 数组包含2个元素,分别是字符串foo和bar
“*3\r\n:1\r\n:2\r\n:3\r\n” 数组包含3个整数:1、2、3
“*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n” 包含混合类型的数组
“*-1\r\n” Null数组
“2\r\n3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n” 数组嵌套,外层数组包含2个数组,整理后如下:
"*2\r\n
*3\r\n:1\r\n:2\r\n:3\r\n
*2\r\n+Foo\r\n-Bar\r\n"
8、Redis的String类型优化
9、Redis的事务
Redis中的事务和MySQL中的事务有本质的区别,Redis中的事务是一个单独的隔离操作,事务中所有的命令都会序列化,按照顺序执行,事务在执行的过程中,不会被其他客户端发来的命令所打断,因为Redis服务端是个单线程的架构,不同的Client虽然看似可以同时保持连接,但发出去的命令是序列化执行的,这在通常的数据库理论下是最高级别的隔离。
Redis中的事务的特性总结
1.单独的隔离操作
事务中的所有命令都会序列化,然后按顺序执行,在执行过程中,不会被其他客户端发送的命令打断。
2.没有隔离级别的概念
队列中的命令没有被提交之前都不会执行。
3.不能保证原子性
Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,不会回滚
10、Redis实现分布式锁
1、加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
值得注意的是:
random_value
是客户端生成的唯一的字符串。
NX
代表只在键不存在时,才对键进行设置操作。
PX 5000
设置键的过期时间为5000毫秒。这样,如果上面的命令执行成功,则证明客户端获取到了锁。
2、解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候
random_value
的作用就体现出来。为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
首先,我们在pom文件中,引入Jedis。在这里,笔者用的是最新版本,注意由于版本的不同,API可能有所差异。
加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败
解锁我们通过
jedis.eval
来执行一段LUA就可以。将锁的Key键和生成的字符串当做参数传进来。
五、项目研发
5.1 管理工具
1、Git的分支(test-master-bug)
2、Git冲突(代码冲突 分支冲突)
3、Maven(常用命令 聚合 依赖范围 依赖冲突)
4、Nginx(负载均衡,算法)
1、轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。2、weight
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。2、ip_hash
每个请求按访问ip的 hash结果分配,这样每个访客固定访问一个后端服务器,可以解决的问题。3、fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。4、url_hash(第三方)
按访问url的 hash结果来分配请求,使同样的url定向到同一个后端服务器,后端服务器为缓存时比较有效
5.2 接口
1、接口测试(PostMan+Swagger)+ 单元测试(方法)+ 性能测试(Jmeter)
postman是Google开发的一款接口测试的插件,也有客户端。国内禁用Google之后,postman的插件就不好下载和使用了。postman这款接口测试工具,是一款很轻便的接口验证工具,可以通过输入请求方法、url、参数直接进行接口请求访问,验证接口是否开通,还可以查看返回的响应值查看接口开发是否正常。不过因为是Google开发的所以只支持英文版。对于英文不好的人使用起来特别难受。
Swagger是一款通过针对与后端开发人员的一款接口文档生成工具。主要通过在代码中的注释生成接口文档的工具,不过生成的接口文档是英文的
单元测试:junit 依赖,使用@Test注解,
jmeter可以进行接口测试和性能测试,但是对于做单纯的接口测试jmeter操作起来没有postman、apipost使用起来方便。jmeter重点在于压力测试,稳定性测试和负载测试。针对于接口和程序的稳定性设计的一块以软件性能为主接口测试为辅的接口测试工具。
2、项目性能指标(QPS TPS DAU MAU RT)
QPS:每秒查询率,一台服务器每秒能够响应的查询次数。 一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,即每秒的响应请求数,即最大吞吐能力。
TPS:事务数/秒。 一个事务指一个客户端向服务器发送请求,然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
DAU:DAU(Daily Active User),日活跃用户数量。常用于反映网站、互联网应用或网络游戏的运营情况。DAU通常统计一日(统计日)之内,登录或使用了某个产品的用户数(去除重复登录的用户),与UV概念相似
MAU:MAU(Month Active User):月活跃用户数量,指网站、app等去重后的月活跃用户数量
RT:响应时间,处理一次请求所需要的平均处理时间
3、全品类:SKU SPU
SKU:
SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。在服装、鞋类商品中使用最多最普遍。 例如纺织品中一个SKU通常表示:规格、颜色、款式。
SPU:
SPU = Standard Product Unit (标准化产品单元)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
4、项目管理(禅道)
5、第三方接口或平台
6、接口安全,多版本兼容,幂等性
接口安全:
1.使用MD5实现对接口加签,目的是为了防止篡改数据。 2. 基于网关实现黑明单与白名单拦截 3. 可以使用rsa非对称加密 公钥和私钥互换 4. 如果是开放接口的话,可以采用oath2.0协议 5. 使用Https协议加密传输,但是传输速度慢 6. 对一些特殊字符实现过滤 防止xss、sql注入的攻击 7. 定期使用第三方安全扫描插件 8. 接口采用dto、do实现参数转化 ,达到敏感信息脱敏效果 9. 使用token+图形验证码方法实现防止模拟请求 10. 使用对ip访问实现接口的限流,对短时间内同一个请求(ip)一直访问接口 进行限制。
多版本兼容:
第一种:The Knot:无版本,即平台的 API 永远只有一个版本,所有的用户都必须使用最新的 API,任何 API 的修改都会影响到平台所有的用户。甚至平台的整个生态系统。
第二种:Point-to-Point:点对点,即平台的 API 版本自带版本号,用户根据自己的需求选择使用对应的 API,需要使用新的 API 特性,用户必须自己升级。
第三种:Compatible Versioning:兼容性版本控制,和 The Knot 一样,平台只有一个版本,但是最新版本需要兼容以前版本的 API 行为。
幂等性:
幂等性是指同一个操作无论请求多少次,其结果都相同
解决方案参考如下:
1、数据库唯一索引,防止新增脏数据
2、数据库查询操作
3、数据库删除操作
4、数据库悲观锁乐观锁
5、缓存token机制,防止页面重复提交
6、分布式锁
杭州面试题总结:
1、描述一下HashMap的实现原理?
①HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
2、Synchronized和volatile的区别是什么?
synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile表示变量在CPU的寄存器是不确定的,必须要从主存中读取,保证多线程环境下变量的可见性,禁止指令重排序;
区别:
synchroized可以作用于类,方法,变量等,volatile只能作用于变量;
synchroized可以保证线程间的有序性,原子性和可见性,volatile只保证了可见性和有序性,不保证原子性
synchronized是线程阻塞的,volatile是线程不阻塞的
synchronized标记的变量可以被编译器优化,volatile标记的变量不会被编译器优化
synchronized关键字主要用于解决变量在多个线程之间访问资源的同步性,volatile关键字主要用于解决变量在多个线程之间的可见性;
3、Try-catch-finally,如果 catch 中return了,finally 还会执行吗?如果执行,实在return之前还是之后执行?
其实try-catch-finally中finally可以省略
如果catch中return了,finally还会执行
不管有没有异常,finally中的代码都会执行
当try,catch中有return时,finally中的代码依然会继续执行
finally是在return后面的表达式运算之后执行的,此时并没有返回运算之后的值,而是把值保存起来,不管finally对该值做任何的改变,返回的值不会变化,依然返回保存的值。
如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是finally中改变后的该属性的值
finally代码中最好不要包含return,程序会提前退出,也就是说返回的值不是try或catch的值。
4、对比下ArrayList,LinkedLit,Vector的插入效率并简述,ArrayList 插入数据一定 很慢吗?
ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,他们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快,插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上比ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要向前或向后遍历,但是插入数据时,只需要记录本项的前后项即可,所以插入速度较快。
LinkedList > ArrayList > Vector
ArrayList插入数据不一定很慢,当数据只是追加在尾部时,由于ArrayList是扩容的方式,LinkedList是需要新建立节点。当数据量很大的时侯new节点的时间会大于扩容的时间,
5、http 响应码301和302代表的什么?有什么区别?
301:Moved Permanently,被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URL之一,如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。
302:Found,请求的资源现在临时从不同的URL响应请求 。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。
区别:
301表示 被请求URL永久转移到新的url;302表示被请求URl临时转移到新的url
301搜索引擎会索引新url和新url的内容,302搜索引擎可能会索引旧url和新url页面的内容。
6、用Java写一个快速排序
- 在待排序的N个记录中任取一个元素(通常取第一个记录)作为基准,称为基准记录;
- 定义两个索引 left 和 right 分别表示“首索引” 和 “尾索引”,key 表示“基准值”;
- 首先,尾索引向前扫描,直到找到比基准值小的记录(left != righ),并替换首索引对应的值;
- 然后,首索引向后扫描,直到找到比基准值大于的记录(left != righ),并替换尾索引对应的值;
- 若在扫描过程中首索引等于尾索引(left = right),则一趟排序结束;将基准值(key)替换首索引所对应的值;
- 再进行下一趟排序时,待排序列被分成两个区:[0,left-1],[righ+1,end]
- 对每一个分区重复步骤2~6,直到所有分区中的记录都有序,排序成功。
if (leftIndex >= rightIndex) { return; } int left = leftIndex; int right = rightIndex; //待排序的第一个元素作为基准值 int key = arr[left]; //从左右两边交替扫描,直到left = right while (left < right) { while (right > left && arr[right] >= key) { //从右往左扫描,找到第一个比基准值小的元素 right--; } //找到这种元素将arr[right]放入arr[left]中 arr[left] = arr[right]; while (left < right && arr[left] <= key) { //从左往右扫描,找到第一个比基准值大的元素 left++; } //找到这种元素将arr[left]放入arr[right]中 arr[right] = arr[left]; } //基准值归位 arr[left] = key; //对基准值左边的元素进行递归排序 quickSort(arr, leftIndex, left - 1); //对基准值右边的元素进行递归排序。 quickSort(arr, right + 1, rightIndex); } }
7、Tcp为什么要三次握手,两次不行吗?为什么?
1、客户端–发送带有 SYN 标志的数据包–⼀次握⼿–服务端
2、 服务端–发送带有 SYN/ACK 标志的数据包–⼆次握⼿–客户端
3、客户端–发送带有带有 ACK 标志的数据包–三次握⼿–服务端
三次握手的目的是建立可靠的通信信道,简单来说就是数据的发送和接收,三次握手最主要的目的就是双方确认自己与对方的发送与接收都是正常的。
第一次握手,客户端什么都不能确认,服务端确认了对方的发送正常,自己接收正常;
第二次握手,客户端确认了自己发送接收正常,对方发送接受者正常,服务端确认了对方发送正常,自己接收正常;
第三次握手,客户端确认了自己发送接收正常,对方发送接收正常,服务端确认了自己发送接收正常,对方发送接收正常。
为啥不是两次或者四次:
我们假设A和B是通信的双方。我理解的握手实际上就是通信,发一次信息就是进行一次握手。
- 第一次握手: A给B打电话说,你可以听到我说话吗?
- 第二次握手: B收到了A的信息,然后对A说: 我可以听得到你说话啊,你能听得到我说话吗?
- 第三次握手: A收到了B的信息,然后说可以的,我要给你发信息啦!
在三次握手之后,A和B都能确定这么一件事: 我说的话,你能听到; 你说的话,我也能听到。 这样,就可以开始正常通信了。
注意: HTTP是基于TCP协议的,所以每次都是客户端发送请求,服务器应答,但是TCP还可以给其他应用层提供服务,即可能A、B在建立链接之后,谁都可能先开始通信。
如果两次,那么B无法确定B的信息A是否能收到,所以如果B先说话,可能后面的A都收不到,会出现问题 。
如果四次,那么就造成了浪费,因为在三次结束之后,就已经可以保证A可以给B发信息,A可以收到B的信息; B可以给A发信息,B可以收到A的信息。
8、Spring 支持几种bean的作用域
1、singleton:bean在每个spring ioc容器中只有一个实例
2、prototype:一个bean的定义可以有多个实例
3、request:每次http请求都会创建一个bean
4、session:在一个http Session中,一个bean定义对应一个实例
9、Spring Boot有哪几种读取配置的方式
1、使用@Value注解,加载单个属性值,需要在yaml或者properteis中存在配置,或者在配置中心存在配置
2、使用@ConfigurationProperties注解,加载一组属性的值,针对要加载的属性过多的情况,比@Value更加好简洁,@ConfigurationProperties(prefix = “xxx”)
3、@PropertySource+@Value,读取指定文件里的内容,如自己定义的配置文件中加载属性值
4、@PropertySource+@ConfigurationProperties,读取指定文件下的内容
@ConfigurationProperties(prefix = “xxx”), @PropertySource(value = {“Config.properties”})//指定加载的文件前缀
5、使用springBoot的Environment接口获取配置,即environment.getProperty(“xxx
10、写出几种流量控制,它的实现方式是什么?
1、令牌桶算法:系统会维护一个令牌桶,以一个恒定的速度往桶里放入令牌,这是如果有请求进来想要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量,发放令牌的速率来达到对请求的限制。如医院挂号看病,只有拿到号才可以就诊。
2、漏桶算法:思路就是,我们把水当做请求,漏桶当做是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出,即拒绝请求,以此来实现限流。
3、计算器算法:假设规定对于接口a,我们一分钟访问次数不能超过100个,我们可以设置一个技术器counter,每当一个请求过来的时候,counter就+1,如果counter的值大于100并且该请求与第一个请求得间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔大于1分钟,且counter的值还在限流范围内,那么就重置counter。
4、滑动窗口法:简单来说就是随着时间的推移,时间窗口也会持续移动,有一个计数器不断维护着窗口内的请求数量,这样就可以保证任意时间段内,都不会超过最大允许的请求数。例如当前时间窗口是0s60s,请求数是40,10s后时间窗口就变成了10s70s,请求数是60;
11、RabbitMQ中vhost的作用是什么?
vhost可以理解为虚拟broker,即mini-RabbitMQ server,其内部均含有独立的queue,exchange和binding等,但最最重要的是,其拥有独立的权限系统,可以做到vhost范围的用户控制,当然,从RabbitMQ的全局角度,vhost可以作为不同权限隔离的手段。
https://blog.csdn.net/weixin_41847891/article/details/100663850
12、RabbitMQ怎么避免消息丢失?|
1、消息持久化:
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。
所以就要对消息进行持久化处理。如何持久化,下面具体说明下:
要想做到消息持久化,必须满足以下三个条件,缺一不可。
1) Exchange 设置持久化:durable:true
2)Queue 设置持久化,new出来就是持久化的
3)Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
2、ACK确认机制:
这个使用就要使用Message acknowledgment 机制,就是消费端消费完成要通知服务端,服务端才把消息从内存删除。
ConfirmCallback 只确认消息是否正确到达 Exchange 中。
ReturnCallback 消息没有正确到达队列时触发回调,如果正确到达队列不执行。
默认情况下消息消费者是自动 ack (确认)消息的,需要设置为手动确认,原因是:自动确认会在消息发送给消费者后立即确认,这样存在丢失消息的可能
3、设置集群镜像模式:
我们先来介绍下RabbitMQ三种部署模式:
1)单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
2)普通模式:默认的集群模式,某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
3)镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的高可用方案 为什么设置镜像模式集群,因为队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据。
4、消息补偿机制:
消息补偿机制需要建立在消息要写入数据库日志,发送日志,接受日志,两者的状态必须记录。
然后根据数据库日志记录检验消息发送消费是否成功,不成功,进行消息补偿措施,重新发送消息处理。
13、数据库的索引规约你了解多少
14、Redis怎样实现分布式锁
1、加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
值得注意的是:
random_value
是客户端生成的唯一的字符串。
NX
代表只在键不存在时,才对键进行设置操作。
PX 5000
设置键的过期时间为5000毫秒。这样,如果上面的命令执行成功,则证明客户端获取到了锁。
2、解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候
random_value
的作用就体现出来。为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
首先,我们在pom文件中,引入Jedis。在这里,笔者用的是最新版本,注意由于版本的不同,API可能有所差异。
加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败
解锁我们通过
jedis.eval
来执行一段LUA就可以。将锁的Key键和生成的字符串当做参数传进来。
16、线程的生命周期如何?有使用过线程池吗,其中的参数有何意义,请手写一个并说明?
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池"中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPu,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池"中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O 请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者Io 处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程 这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice)后就处于 RUNNING(运⾏) 状态。
1、corePoolSize,线程池里最小线程数
2、maximumPoolSize、线程池里最大线程数量,超过最大线程时候会使用RejectedExecutionHandle
3、keepAliveTime,线程最大的存活时间4、unit 空闲线程存活时间单位
5、workerQueue,缓存异步任务的队列
6、threadFactory,用来构造线程池里的worker线程7、handler 拒绝策略
17、RabbitMQ怎么加速消息的处理
https://www.cnblogs.com/cky-2907183182/p/12733141.html
18、MavenJar 包冲突怎么解决
MAVEN项目运行中如果报如下错误:
Caused by:java.lang.NoSuchMethodError Caused by: java.lang.ClassNotFoundException
十有八九是Maven jar包冲突造成的
原因:
依赖传递
当我们需要A的依赖的时候,就会在pom.xml中引入A的jar包;而引入的A的jar包中可能又依赖B的jar包,这样Maven在解析pom.xml的时候,会依次将A、B 的jar包全部都引入进来。解决方案:
MAven默认处理策略:
1、最短路径优先;Maven 面对 D1 和 D2 时,会默认选择最短路径的那个 jar 包,即 D2。E->F->D2 比 A->B->C->D1 路径短 1。
2、最先声明优先:如果路径一样的话,如: A->B->C1, E->F->C2 ,两个依赖路径长度都是 2,那么就选择最先声明。
移除依赖:
1、我们可以借助Maven Helper插件中的Dependency Analyzer分析冲突的jar包,然后在对应标红版本的jar包上面点击execlude,就可以将该jar包排除出去。
2、或者手动在pom.xml中使用
<exclusion>
标签去排除冲突的jar包
19、多线程的应用
https://segmentfault.com/a/1190000018518540
https://www.cnblogs.com/chhyan-dream/p/10786043.html
20、Redis有哪些应用
1、热点数据缓存:redis访问速度快,支持的数据类型丰富,适合存储热点数据
2、限时业务的运用:redis可以使用expiremignl设置一个键的生存时间,到期自动删除
3、计数器相关问题:redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
4、排行榜相关问题:redis的sortset可以进行热点数据的排序
5、分布式锁:主要利用redis的Setnx命令
6、延时操作:进行key的时间设置,到期自动删除或者检测key的实效进行其他操作
7、分页,模糊查询:redis的set集合提供了一个zrangebylex方法,可以进行limit分页操作,也可以进行模糊查询
8、点赞,好友关系等相互关系的存储
9、队列:由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。
21、熟悉的设计模式, 你知道什么是策略者模式吗?
单例模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式;
原型模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例;
策略者模式:
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,他通过对算法进行封装,=把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理
主要优点:
多重条件语句不易维护,使用策略模式可以避免使用多重条件语句
策略模式提供了一系列的可供重用的算法族,恰当使用继承可以吧算法族的公共代码转移到父类里,从而避免重复的代码
策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的
策略模式提供对开闭原则的完美支持,可以在不修改源代码的情况下,灵活增加新算法
策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离
22、MyBatis里的dao层的方法用重载吗?
不可以进行重载,mybatis里的dao层的方法名与mapper.xml里的id名一致,id不可重复,所以方法名不可以重载
23、SpringCloud里的Nacos,各服务之间怎样相互调用的.
Nacos,一个更易于构建云原生应用的动态服务发现,配置管理和服务管理平台;作为注册中心,配置服务名称以及注册中心地址,启动类@EnableDiscoveryClient进行服务注册和发现。作为配置中心,实现共享的配置信息的管理,dataId命名要求服务名—多环境active-数据格式,需要开启@RefreshScope注解进行实时刷新,以及@Value进行加载共享变量
微服务中实现服务调用的方式:
1.Openfeign 声明式客户端调用 @EnableFeignClients @FeignClient
2.Ribbon 负载均衡客户端调用 1.负载均衡算法 2.服务调用 编码式
3.LoadRunner
25、HashSet 如何保证他唯一
HashSet:底层数据结构是哈希表
Hash保证元素的唯一性:通过元素的两个方法,HashCode和equals来完成
如果元素的HashCode值相同,再判断equals是否为true
如果元素的HashCode值不同,不会调用equals
26、HashMap1.8为啥引入红黑树
JDK1.7的存储结构是数组+链表,到了JDK1.8变成了数组+链表+红黑树
在jdk1.7中首先把元素放在数组中,后来存放的数据元素越来越多,于是就出现了链表,对于数组中的每一个元素,都可以有一条链表来存储元素,这就是有名的拉链法。
后来存储的元素越来越多,链表越来越长,在查找元素的时候效率不仅没有提高,反倒下降了不少,于是把这条链表转变成一个适合查找的树形结构,红黑树。
jdk1.7的优点是增删效率高,在jdk1.8的时候,不仅增删效率高,而且查找效率也提升了
红黑树是一个自平衡的二叉查找树,也就是说红黑二叉树的查找效率是非常高的,查找效率会从链表的O(n)降低为O()logn。
只有链表长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树:
- 红黑树的构造比链表复杂,在链表节点不多的时候,数组+链表+红黑树的结构可能不比数组+链表的结构性能高
- HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此链表长度比较长的时候转变成红黑树才会显著提高效率
27、concurrentHashMap如何保证线程安全
为什么会出现ConcurrentHashMap?
- HashTable是线程安全的,但是在高并发场景下,性能低下
- HashMap不是线程安全的
- 同步包装器虽然使用同步方法提高了部分性能,但是不适合高并发场景的性能需求
jdk7版本使用分离锁segment,实际上是一种重入锁(ReatrantLock)来保证线程安全的;segment的数量有concurrentLevel决定,默认值是16;
扩容的时候是针对单个segment扩容的,写操作也是,修改数据的时候锁定的部分,所以比较高效;size不够准确
jdk8中segment依然存在,不过不起结构上的作用,只起到保证序列化的兼容性;
内部使用volatile来保证数据存储的可见性;
利用CAS操作,在特定场景下进行无锁并发操作
同步逻辑使用Synchronized,性能得到优化,减少内存消耗
28、CAS是什么,会产生什么问题,ABA问题怎么解决
CAS是CompareAndSwap的缩写,中文意思是比较并替换
CAS指令执行时,当前仅当旧值与预期值A相等时,才可以把值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
缺点:循环时间开销大;只能保证一个共享变量的原子操作;引出ABA问题
ABA问题:
CAS算法实现的一个重要的前提是需要去除内存中某时刻的数据并在当下时刻比较并替换,在这个时间差内会导致数据的变化。
线程1在位置V中取出A,线程2也取出A,线程2将A变成了B,又将B变成了A数据,这时候线程1进行CAS操作发现内存中仍然是A,线程1提示操作成功。
解决方法:在原子引用类上加上版本号,类似于mysql的乐观锁,每个线程更改一次都需要更改版本号,那么多线程同时获取同一个版本号的时候,也只有一个线程可以更改成功。
29、如何避免MQ重复消费问题
问题等同于怎么保证幂等性
一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性
幂等性:一个数据,一个请求,重复多次,确保对应的数据不会改变
保证消息队列消费的幂等性:
- 数据写入数据库的时候,先根据主键查询一下,存在的话就更新,不存在的时候在写入
- 如果写入Redis中,每次都是set,天然幂等性。不用担心
- 让生产者发送每条数据的时候,里面加一个全局id,类似订单id的东西,消费的时候根据全局id查询是否消费过,没有的话再消费
- 基于数据库的唯一键来保证重复数据不会重复插入多条。因为唯一键约束,重复数据插入只会报错,不会导致数据库中出现脏数据
30、mysql innodb索引 加锁 行锁
InnoDB不同于MyISAM最大的两个特点就是:一是支持事务,二是支持行锁
只有通过索引条件检索数据,InnoDB才会使用行级锁,否则将使用表级锁
MySQL中的 for update 只适用于InnoDB(因为只有此引擎才有行级锁),必须开启事务,在begin和commit之间才生效。
for update 可以为数据库中的行上加一个排他锁,当一个事务的操作未完成时候,其他事务可以对这行数据进行读取但不能写入或更新,只能等待该事务结束
对有索引的键值加锁,会对所有涉及到的 数据行 加锁
多个索引时,不同的事务可以使用不同的索引锁定不同的行,不论什么索引,InnoDB都会使用行锁对数据进行加锁
32、并发编程(问的很深入)
https://blog.csdn.net/qq_34039315/article/details/78549311
33、网络编程相关的东西
网络编程:
模拟计算机通信中运输层之间的对等通信,忽略下层的打包和拆包,直接在运输层建立一条虚连接进行数据传输。主要是指网络通信,实现计算机之间的对话和文件传输等,如,QQ、P2P点对点通信等;而web主要就是B/S结构的应用,通俗一点说就是开发网站、网页,如,QQ空间、百度。
Socket:
又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议 ,socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能
34、tomcat和JDBC为什么要打破双亲委派模型
什么是双亲委派机制?
当一个类加载器收到一个类加载请求的时候,该类加载器并不会对这个类加载,而是传递该类加载器的父类加载器,只有父类加载器没有找到类信息,才会反馈给子类,由子类加载器去加载
JDBC为什么破坏双亲委派机制?
Tomcat为什么要破坏双亲委派模型?
35、如何实现线程的顺序执行
- join方式
我们直接通过在每个Thread对象后面使用join方法就可以实现线程的顺序执行。
用join方法来保证线程顺序,其实就是让main这个主线程等待子线程结束,然后主线程再执行接下来的其他线程任务。
join方法中,millis默认值为0,0秒意味着永远等待,也就是Thread执行不完,主线程就要一直等待,一直wait- ExecutorService方式
这个方法的原理就是讲线程用排队的方式扔进一个线程池中,让所有的任务以单线程的方式,按照FIFO先进先出,LIFO后进先出,优先级等特定顺序执行,但是这种方式也是存在缺点的,就是当一个线程被阻塞时,其它的线程都会受到影响被阻塞,不过依然都会按照自身调度来执行,只是会存在阻塞延迟。