定期总结-2023
2023-10-23 11:19:04 0 举报
AI智能生成
登录查看完整内容
java
作者其他创作
大纲/内容
原子性(atomicity,或称不可分割性)
一致性(consistency)
隔离性(isolation,又称独立性)
持久性(durability)
ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:
BASE柔性事务是指 Basic Available(基本可用)、Soft-state(软状态/柔性事务)、Eventual Consistency(最终一致性)。从广义上来看,像XA事务其实也是属于一种柔性事务。但是一般情况下,BASE柔性事务特指Seata框架提供的柔性事务,因为BASE实际上是集成了阿里对于分布式事务的所有研究,而阿里的这些研究成果,最终都沉淀到了Seata框架中。ShardingSphere中对于柔性事务的支持,其实也是更多的基于Seata的AT模式,来实现的两阶段提交。这里要注意的是,虽然XA和AT都是基于两阶段协议提供的实现,但是AT模式相比XA模式,简化了对于资源锁的要求,所以可以认为在大部分的业务场景下,AT模式比XA模式性能稍高。
BASE柔性事务
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
使用锁就是为了互斥临界资源,没有办法破坏
互斥条件
一次性申请所有需要的资源
请求与保持条件
占用部分资源的线程,如果在获取资源时获取不到,就主动释放它占有的资源
不剥夺条件
将系统中的所有资源统一编号,进程提出资源申请时必须按照资源的编号顺序(升序)提出。这样可以确保系统不会出现死锁,因为资源按照编号顺序分配,不会出现循环等待
按照循序申请资源,按照反序释放资源,破坏循环等待条件
循环等待
造成死锁的四个必要条件及其各自的避免死锁方案
@Trasactional
记忆
底层数据结构是数组,连续内存,所以查询快(指根据下标访问),增删慢
线程不安全
效率高
ArrayList
底层数据结构是链表,无需连续内存,所以查询慢(要沿着链表遍历),增删快
Linkedlist
底层数据结构是数组,所以查询快增删慢
线程安全
效率低
Vector
List
底层数据结构是哈希表
HashSet
底层数据结构由链表和哈希表组成
由链表保证元素有序
由哈希表保证元素唯一
LinkedHashSet
底层数据结构是红黑树。(是一种自平衡的二叉树)
根据比较的返回值是否是0来决定保证元素唯一性
让元素所属的类实现Comparable接口
自然排序(元素具备比较性)
让集合接收一个Comparator的实现类对象
比较器排序(集合具备比较性)
两种排序方式
TreeSet
Set
Collection
哈希表依赖两个方法:hashCode()和equals()
底层数据结构是哈希表。线程不安全,效率高
头插法
多线程死锁问题
数组+链表
尾插法
1.树化 :hash冲突的值放链表,当数组长度大于64,且链表长度超过阈值8,则转成红黑树,否则先进行扩容2.树退化: a.扩容时,原来红黑树上一部分数据可能会转移到别的槽中,当红黑树中的元素小于等于6时,退化成链表b.调用remove方法删除数据时,删除之前校验红黑树根节点 左儿子 左孙子 右儿子是否存在,如果有一个不存在,退化成链表
DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash表默认初始容量 16,有参构造函数:可以指定容量。会根据指定的正整数找到不小于指定容量的2的幂数MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量,如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE( 2^31-1 ,即永远不超出阈值了)DEFAULT_LOAD_FACTOR = 0.75f;默认加载因子,加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量,默认情况下是16x0.75=12时,会触发扩容操作TREEIFY_THRESHOLD = 8;链表转红黑树阈值UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容。
重要成员变量
HashMap的put方法过程
数组+链表+红黑树
jdk8及其之后
jdk8之前数据结构
属于 JUC 包下的一个集合类,可以实现线程安全
● Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它● ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
hashtable区别
Segment 段(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
1.7
Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突
1.8
concurrentHashMap(☆☆☆)
子主题
Segment
ConcurrentHashMap的数据结构与HashMap基本类似,区别在于:1、内部在数据写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作;2、扩容时老数据的转移是并发执行的,这样扩容的效率更高。
Java7 ConcurrentHashMap基于ReentrantLock实现分段锁
重要的成员
Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现
在CopyOnWrite机制中,当一个线程要修改共享数据时,会先将共享数据的副本复制出来,进行修改。修改完成后,再将修改后的数据复制回原来的位置,用新的数据替换旧的数据。在此过程中,其他线程可以继续读取旧的数据,不会受到影响。
核心思想:读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。
1、CopyOnWrite适用于读多写少的情况,最大程度的提高读的效率;2、CopyOnWrite是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;3、如何使其他线程能够及时读到新的数据,需要使用volatile变量;4、写的时候不能并发写,需要对写操作进行加锁;
关键点
CopyOnWrite机制(写时复制)
java.util.concurrent.ConcurrentHashMap
多线程使用(不在此体系下)
不得不聊
HashMap
LinkedHashMap
Hashtable
TreeMap
Map(Map集合的数据结构仅仅针对键有效,与值无关。存储的是键值对形式的元素,键唯一,值可重复。)
java集合
demo
1) 继承Thread
2)实现Runnable接口
Callable可以返回任务的执行结果。
之前都在使用 Future,要么只能用 get 方法阻塞,要么就用 isDone 来判断,JDK1.8 之后新增了 CompletableFuture 用于异步编程,它针对 Future 的功能增加了回调能力,可以简化异步编程。
CompletableFuture 主要包含四个静态方法去创建对象,主要区别在于 supplyAsync 返回计算结果,runAsync 不返回,另外两个方法则是可以指定线程池,如果不指定线程池则默认使用 ForkJoinPool,默认线程数为CPU核数。
串行
AND 聚合
Or 聚合
方法
CompletableFuture
异步结果
3) 使用 Callable接口 (可以使用CompletableFuture )
匿名内部类
实现方式
图
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
新生状态(New)
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态: 1. 新建线程:调用start()方法,进入就绪状态; 2. 阻塞线程:阻塞解除,进入就绪状态; 3. 运行线程:调用yield()方法,直接进入就绪状态; 4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
就绪状态(Runnable)
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
运行状态(Running)
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞: 1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。 2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。 3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。 4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
阻塞状态(Blocked)
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
死亡状态(Terminated)
五态
线程状态(生命周期)
固定数量线程池
FixedThreadPool
可缓存线程池特点:具有自动回收多余线程的功能
CachedThreadPool
支持定时及周期性任务执行的线程池
ScheduledThreadPool
SingleThreadExecutor
以上4种线程池的构造函数的参数
FixedThreadPool和SingleThreadExecutor的Queue是LinkedBlockingQueue?
CachedThreadPool使用的Queue是SynchronousQueue?
span style=\
以上4种线程池对应的阻塞队列分析
这个线程池和之前的都有很大不同
一般是不加锁的
子任务
执行顺序不能保障
窃取
适用于递归
workStealingPool是JDK1.8加入的
ThreadPoolExecutor的重要参数包括:corePoolSize:核心线程数,即使没有任务需要执行,线程池也会保持这些线程。对于长时间等待的任务,线程池不会创建超过这个数的线程。maximumPoolSize:当线程数大于或等于核心线程数,且任务队列已满时,线程池会创建新的线程,直到线程数量达到这个数。keepAliveTime:当线程空闲时间达到这个参数所设置的时间时,线程会被销毁,直到线程数量等于核心线程数。unit:keepAliveTime参数的时间单位。workQueue:执行前用于保存任务的队列。如果这个队列已满,新提交的任务将被拒绝。threadFactory:用于创建新线程的工厂。handler:当拒绝处理任务时的处理策略。对于ThreadPoolExecutor的参数设置建议与优化,以下是一些建议:根据业务需求设定corePoolSize和maximumPoolSize。corePoolSize建议设置为通常执行任务的线程数,maximumPoolSize建议设置为最高可能执行的线程数。可以通过监控线程池的状态和使用情况,根据实际需求进行调整。根据任务类型和需求设定keepAliveTime。如果任务需要立即执行,keepAliveTime可以设置为0;如果任务可能需要等待一段时间才能被执行,可以将keepAliveTime适当设置,以避免创建过多的线程。根据任务类型和需求选择适当的workQueue。不同的任务队列适用于不同的场景,需要根据实际需求选择适当的队列,例如LinkedBlockingQueue、ArrayBlockingQueue等。使用ThreadPoolExecutor的allowCoreThreadTimeOut方法。这个方法可以在指定时间内让核心线程空闲并自动关闭,以避免资源浪费。根据实际需求设定handler。如果任务不能被接受,可以根据实际情况设定对应的拒绝策略,例如直接抛出异常、存储到磁盘等。总之,ThreadPoolExecutor的参数设置需要根据实际业务需求和系统资源情况进行调整和优化,以达到最佳的性能和资源利用率。
建议
1最大线程数maximumPoolSize2核心线程数corePoolSize3活跃时间keepAliveTime4阻塞队列workQueue5拒绝策略RejectedExecutionHandler
线程池的核心参数
1当提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务2当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队3当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁4如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
ne-oli ne-alignment=\"left\" style=\
拒绝策略主要有四种:
当提交一个新任务到线程池时,具体的执行流程
ThreadPoolExecutor
自定义线程池
常见线程池的特点和用法
线程池Executor
多线程
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。类装载方式,有两种 :1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,2.显式装载, 通过class.forname()等方法,显式加载需要的类Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销
JVM加载Class文件的原理机制
是虚拟机自身的一部分,用来加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用
负责加载\\lib\\ext目录或Java.ext.dirs系统变量指定的路径中的JAR类包
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
应用类加载器:AppClassLoader也叫做系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。主要就是加载你自己写的那些类一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
类加载器
加载:根据查找路径找到相应的 class 文件然后导入;验证:检查加载的 class 文件的正确性;准备:给类中的静态变量分配内存空间;解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;初始化:对静态变量和静态代码块执行初始化工作。
类装载的执行过程
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到JVM 内存,然后再转化为 class 对象
先找父亲加载,不行再由儿子自己加载
解释
为什么要设计双亲委派机制?沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
原因
loadClass
重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
打破双亲委派机制
双亲委派模型
类加载器子系统
字节码执行引擎
系统
堆中主要存储的是实际创建的对象,也就是会存储通过new关键字创建的对象,堆中的对象能够被多个线程共享。堆中的数据不需要事先明确生存期,可以动态的分配内存,不再使用的数据和对象由JVM中的GC机制自动回收。对JVM的性能调优一般就是对堆内存的调优。
Java中基本类型的包装类:Byte、Short、Integer、Long、Float、Double、Boolean、Character类型的数据是存储在堆中的。
堆一般会被分成年轻代和老年代。而年轻代又会被进一步分为1个Eden区和2个Survivor区。在内存分配上,如果保持默认配置的话,年轻代和老年代的内存大小比例为1 : 2,年轻代中的1个Eden区和2个Survivor区的内存大小比例为:8 : 1 : 1。
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
(1)对新生代的对象的收集称为minor GC;(2)对旧生代的对象的收集称为Full GC;(3)程序中主动调用System.gc()强制执行的GC为Full GC。
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)(3)弱引用:在GC时一定会被GC回收(4)虚引用:由于虚引用只是用来得知对象是否被GC
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有标记的对象。它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
标记-清除算法
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。
复制算法
复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。
标记-整理算法
当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。这样就可以根据各个年代的特点采用不同的收集算法。根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
分代收集算法
垃圾收集算法
垃圾回收
Java堆
栈一般又叫作线程栈或虚拟机栈,一般存储的是局部变量。在Java中,每个线程都会有一个单独的栈区,每个栈中的元素都是私有的,不会被其他的栈所访问。栈中的数据大小和生存期都是确定的,存取速度比较快。
在Java中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。
程序在执行过程中,会在栈中为不同的方法创建不同的栈帧,在栈帧中又包含了:局部变量表、操作数栈、动态链接和方法出口。
栈中一般会存储对象的引用,这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。注意:这里说的是这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。因为在JVM中,如果开启了逃逸分析和标量替换,则可能不会再在堆上创建对象,可能会将对象直接分配到栈上,也可能不再创建对象,而是进一步分解对象中的成员变量,将其直接在栈上分配空间并赋值。
虚拟机栈
方法区(官方叫做永久代)
程序计数器也叫作PC计数器,只要存储的是下一条将要执行的命令的地址。
程序计数器
本地方法栈相对来说比较简单,就是保存native方法进入区域的地址。例如,在Java中创建线程,调用Thread对象的start()方法时,会通过本地方法start0()调用操作系统创建线程的方法。此时,本地方法栈就会保存start0()方法进入区域的内存地址。
本地方法栈
Java7
把方法区永久删除了,取而代之的是元空间,使用的是直接内存
区域是JDK1.8中划分出来的。主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应的Class实例的引用等信息。方法区中的信息能够被多个线程共享。
例如,在程序中声明的常量、静态变量和有关于类的信息等的引用,都会存放在方法区,而这些引用所指向的具体对象 一般都会在堆中开辟单独的空间进行存储,也可能会在直接内存中进行存储。
方法区(元空间)
Java8
Java6和6之前,常量池是存放在方法区(永久代)中的。Java7,将常量池是存放到了堆中。Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间(方法区)中,而字符串常量池依然存放在堆中。
各版本常量池的位置
jdk1.8之前
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
jdk1.8之后
看似好像没多大区别,其实元空间和永久代一个很大不同是如果不指定大小,随着更多类的创建,虚拟机会耗光所有可用的系统内存。
整个永久代有一个JVM本身设置的固定大小上下限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,然后元空间仍可能存在溢出,但几率比之前小了很多
总结
移除永久代前后调整方法区大小所设置的参数
对比
运行时数据区(内存结构)需要找时间去官网校验
本地接口
组件
组成(JVM包含两个系统和两个组件)
-Xss:每个线程的栈大小-Xms:设置堆的初始可用大小,默认物理内存的1/64 -Xmx:设置堆的最大可用大小,默认物理内存的1/4-Xmn:新生代大小-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
参数
日均百万级订单交易系统如何设置JVM参数
优化
就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。
让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc
思想
查看内存信息,实例个数以及占用内存大小
num:序号instances:实例数量bytes:占用空间大小class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
jmap -histo 14660 #查看历史生成的实例jmap -histo:live 14660 #查看当前存活的实例,执行过程中可能会触发一次full gc
用jvisualvm命令工具导入该dump文件分析
堆内存dump
Jmap
用jstack加进程id查找死锁
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
1,使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如196632,按H,获取每个线程的内存情况3,找到内存和cpu占用最高的线程tid,比如196644,转为十六进制得到 0x4cd0,此为线程id的十六进制表示5,执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
找出占用cpu最高的线程堆栈信息
Jstack
查看正在运行的Java应用程序的扩展参数
Jinfo
查看堆内存各部分的使用量,以及加载类的数量
Jstat
Arthas
调优工具
一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
内存泄露到底是怎么回事
调优
加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证:校验字节码文件的正确性
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
简单说loadClass的类加载过程有如下几步加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
对象创建的主要流程:
不考虑JVM的逃逸分析情况,新创建的对象是存放在JVM堆空间中年轻代的Eden区
Java中创建的对象是存储在JVM中的哪个区域的?
由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收
给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
引用计数算法(计数器)
通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
可达性分析算法(有向图)
有两种算法可以判定对象是否存活
对象“已死”的判定算法
JVM
Error(错误)
表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。
RuntimeException运行时异常
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。
“try/catch”捕获异常
当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。
方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
“throws”声明异常
异常的处理方式有两种
这类异常在编译时就必须做出处理,否则无法通过编译。
CheckedException已检查异常
在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
自定义异常类只需从Exception类或者它的子类派生一个子类即可。
自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
自定义异常类
自定义异常类的使用
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。2.处理异常不可以代替简单测试---只在异常情况下使用异常机制。3.不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。4.异常往往在高层处理
使用异常机制的建议
自定义异常
子类
Exception(异常)
异常
JavaSE
依赖注入DI(Dependency Injecttion)
控制反转IOC(Inversion Of Control)
Bean的生命周期
Spring 框架
SpringMVC
HTTP协议的四种传参方式
修饰请求参数,注解用于接收HTTP的body,默认是使用JSON的格式
@RequestBody
修饰返回值,注解用于在HTTP的body中携带响应数据,默认是使用JSON的格式。如果不加该注解,spring响应字符串类型,是跳转到模板页面或jsp页面的开发模式。说白了:加上这个注解你开发的是一个数据接口,不加这个注解你开发的是一个页面跳转控制器。
@ResponseBody
PostMapping等同于@RequestMapping的method等于POST。同理:@GetMapping、@PutMapping、@DeleteMapping也都是简写的方式
注解是所有常用注解中,最有看点的一个注解,用于标注HTTP服务端点。
@RequestMapping
告诉Spring,被该注解标注的类是一个Spring的Bean,需要被注入到Spring的上下文环境中
该类里面所有被RequestMapping标注的注解都是HTTP服务端点
开发中最常使用的注解,它的作用有两层含义
@Controller
作为Controller的作用,将控制器类注入到Spring上下文环境,该类RequestMapping标注方法为HTTP服务端点。
作为ResponseBody的作用,请求响应默认使用的序列化方式是JSON,而不是跳转到jsp或模板页面。
相当于 @Controller和@ResponseBody结合。它有两层含义
@RestController
@RestController与@Controller
DeleteMapping(\"/article/{id}\")public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {
@PathVariable
@PostMapping(\"/article\")public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) {
用于接收普通表单方式或者ajax模拟表单提交的参数数据。
@RequestParam
@PathVariable 与@RequestParam
RequestParam只能接收平面的、一对一的参数
接收复杂嵌套对象
用于接收HTTP请求中的文件参数,通常用于文件上传功能。使用该注解时,需要在方法参数中声明MultipartFile类型的参数,Spring MVC框架会自动将上传的文件转换为MultipartFile类型的对象
@Requestpart
lombok的@Slf4j注解
常用注解
SpringBoot默认
改变子属性在JSON序列化中的默认定义的顺序。如:param1在先,param2在后。
@JsonPropertyOrder(value={\"pname1\
排除某个属性不做序列化与反序列化
@JsonIgnore
为某个属性换一个名称,体现在JSON数据里面
@JsonProperty(anotherName)
排除为空的元素不做序列化反序列化
@JsonInclude(JsonInclude.Include.NON_NULL)
指定日期类型的属性格式
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
通常会对日期类型转换,进行全局配置,而不是在每一个java bean里面配置
@JsonFormat(pattern = \"yyyy-MM-dd HH:mm:ss\
常用注解这些注解通常用于标注java实体类或实体类的属性
当JSON字符串代表的对象的字段多于类定义的字段时,使用readValue会抛出UnrecognizedPropertyException异常,在类的定义处加上@JsonIgnoreProperties(ignoreUnknown = true)可以解决这个问题。
手动数据转换
配置文件的方式
通过代码的方式
全局配置
Jackson
阿里巴巴
FastJSON
Gson
JSON数据处理
junit4和junit5中,注解的写法有些许变化
junit测试框架
swagger3
@ConditionOnXXXXXXX
装载顺序
application.properties
application.yml
全局配置文件
SpringBoot入口启动类使用了SpringBootApplication,实际上就是开启了自动配置功能@EnableAutoConfiguration
@ConfigurationProperties
@Value
比较
注意:1.对象需要成为Bean对象,使用注解@Component2.需要@Autowired,new对象是没有值的
@Value (\"#{systemProperties['java.home']}\") private String javaHome;
获取JAVA_HOME目录
@Value (\"#{systemProperties['user.dir']}\") private String userDir;
获取系统用户工作目录
https://docs.spring.io/spring-framework/docs/4.3.10.RELEASE/spring-framework-reference/html/expressions.html
SpEL结合 @Value注解读取系统环境变量
获取配置值
全局配置文件:application.yml开发环境配置文件:application-dev.yml测试环境配置文件:application-test.yml生产环境配置文件:application-prod.yml
通过配置application.yml
spring.profiles.active
切换环境的方式
profile不同环境使用不同配置
file:./config/ (当前项目路径config目录下);file:./ (当前项目路径下);classpath:/config/ (类路径config目录下);classpath:/ (类路径下).
也可以通过配置spring.config.location来改变默认配置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置
命令行参数来自java:comp/env的JNDI属性Java系统属性(System.getProperties())操作系统环境变量RandomValuePropertySource配置的random.*属性值jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件jar包外部的application.properties或application.yml(不带spring.profile)配置文件jar包内部的application.properties或application.yml(不带spring.profile)配置文件@Configuration注解类上的@PropertySource通过SpringApplication.setDefaultProperties指定的默认属性
https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-external-config
加载优先级
Jasypt
防君子不防小人
配置文件敏感字段加密
配置管理
加载数据库驱动建立数据库连接创建数据库操作对象定义操作的 SQL 语句执行数据库操作获取并操作结果集关闭对象,回收资源
JdbcTemplate
使用jdbc操作数据库
https://docs.jboss.org/hibernate/annotations/3.4/reference/zh_cn/html_single/#entity
实体Model类
数据操作接口
//注意这个方法的名称,jPA会根据方法名自动生成SQL执行 Article findByAuthor(String author);
service层接口实现
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference
使用方法和生产成 SQL 如下表所示
等等
JPA(不推荐)
Mybatis Genenrator
Mybatis-plus
PageHelper分页插件
mybatis: configuration: mapUnderscoreToCamelCase: true
实体类属性userName对应SQL的字段user_name;实体类属性userId对应SQL的字段user_id;
<mapper namespace=\"data.UserMapper\"> <resultMap type=\"data.User\" id=\"userResultMap\"> <!-- 用id属性来映射主键字段 --> <id property=\"id\" column=\"user_id\"/> <!-- 用result属性来映射非主键字段 --> <result property=\"userName\" column=\"user_name\"/> </resultMap></mapper>
第一种:xml的属性映射举例resultMap
@Select(\"select * from t_user where user_name = #{userName}\")@Results( @Result(property = \"userId\
第二种:通过注解 @Results 和 @Result
@Select(\
第三种:通过SQL字段别名来完成映射
其他的实现方式都很不友好,都需要写一个查询SQL,做一套映射配置
查询结果属性映射的最佳实践
这样就会自动扫描com.zimug.**.mapper目录下面的所有XXXXMapper文件,并且完成自动注入。不需要在每一个Mapper上面都加@Mapper注解
使用@MapperScan
Mybatis
对比选型
主流ORM持久层框架选型
Spring中七种事务传播行为
跨库
分布式事务
多个数据源
事务管理
整合数据库
如果你的业务,可以用一个实体类对象,就可以贯穿持久层到展现层,就没有必要做映射赋值转换,也没有必要去分VO、BO、PO。比如:单表表格数据展现、修改、新增
专指Spring包下面的BeanUtils
Dozer
转换工具
java bean的赋值转换
在JPA(mogodb等)Repository接口上面加上RepositoryRestResource注解,path是Rest接口资源的基础访问路径
Spring Data REST
实现rest接口的最快方式
OpenAPI
接口文档
screw(螺丝钉)
一键生成数据库文档
/static: classpath:/static//public: classpath:/public//resources: classpath:/resources//META-INF/resources:classpath:/META-INF/resources/
默认配置
可以通过spring.resources.static-locations配置指定静态文件的位置。但是要特别注意,一旦自己指定了静态资源目录,系统默认的静态资源目录就会失效。所以系统默认的就已经足够使用了,尽量不要自定义
如果在配置的静态资源目录中有favicon.ico文件,SpringBoot会自动将其设置为应用图标。
favicon.ico图标
SpringBoot支持静态和模板欢迎页,它首先在静态资源目录查看index.html文件做为首页,若未找到则查找index模板。
欢迎页面
依赖
引入 webjars-locator 值后可以省略版本号
测试
WebJars管理css&js
静态资源
jsp(糅合三种元素:Java代码、动态的数据、HTML代码结构)
freemarker
Thymeleaf
velocity
java模板引擎(数据模型与业务代码分离)
前后端分离技术
VUE、angularjs、reactjs
前端工程化
模板引擎选型
观察者模式
关注特定事物的创建、销毁以及变化并做出回调动作
具有异步的特性
ServletContext Listener:application 级别,整个应用只存在一个,所有用户使用一个ServletContextHttpSession Listener:session 级别,同一个用户的浏览器开启与关闭生命周期内使用的是同一个sessionServletRequest Listener:request 级别,每一个HTTP请求为一个request
监听三大域对象的创建和销毁事件
HttpSessionAttributeListenerServletContextAttributeListenerServletRequestAttributeListener
还可以监听域对象中属性发生修改的事件
以用来统计在线人数和在线用户、统计网站访问量、系统启动时初始化信息等
在启动类中加入@ServletComponentScan进行自动注册
全局Servlet组件扫描注解
实现
Servlet 监听器
在客户端的请求访问后端资源之前,拦截这些请求。在服务器的响应发送回客户端之前,处理这些响应。
目的
基于一定的授权逻辑,对HTTP请求进行过滤,从而保证数据访问的安全。比如:判断请求的来源IP是否在系统黑名单中对于一些经过加密的HTTP请求数据,进行统一解密,方便后端资源进行业务处理或者我们社交应用经常需要的敏感词过滤,也可以使用过滤器,将触发敏感词的非法请求过滤掉
场景
可以过滤所有请求
能够改变请求的数据内容
特点
@WebFilter时Servlet3.0新增的注解,原先实现过滤器,需要在web.xml中进行配置,而现在通过此注解,启动启动时会自动扫描自动注册
编写Filter类
在启动类加入@ServletComponentScan注解
通过过滤器的java类名称,进行顺序的约定,比如LogFilter和AuthFilter,此时AuthFilter就会比LogFilter先执行,因为首字母A比L前面
无法指定过滤器的先后执行顺序
FilterRegistrationBean可以
注意
方式一:利用WebFilter注解配置
Filter的注册代码
方式二:FilterRegistrationBean
Servlet 过滤器
@WebServlet
原始servlet进行开发
preHandle 表示被拦截的URL对应的控制层方法,执行前的自定义处理逻辑postHandle 表示被拦截的URL对应的控制层方法,执行后的自定义处理逻辑,此时还未将modelAndView进行页面渲染。afterCompletion 表示此时modelAndView已做页面渲染,执行拦截器的自定义处理。
三个方法
拦截器Interceptor
作用是类似的
规范不同:Filter是在Servlet规范中定义的组件,在servlet容器内生效。而拦截器是Spring框架支持的,在Spring 上下文中生效。
拦截器可以获取并使用Spring IOC容器中的bean,但过滤器就不行。因为过滤器是Servlet的组件,而IOC容器的bean是Spring框架内使用,拦截器恰恰是Spring框架内衍生出来的。
拦截器可以访问Spring上下文值对象,如ModelAndView,过滤器不行。基于与上一点同样的原因。
过滤器在进入servlet容器之前处理请求,拦截器在servlet容器之内处理请求。过滤器比拦截器的粒度更大,比较适合系统级别的所有API的处理动作。比如:权限认证,Spring Security就大量的使用了过滤器。
拦截器相比于过滤器粒度更小,更适合分模块、分范围的统一业务逻辑处理。比如:分模块的、分业务的记录审计日志。(使用拦截器实现统一访问日志的记录)
比如说:我们在Filter中使用注解,注入一个测试service,结果为null。因为过滤器无法使用Spring IOC容器bean
场景有一些区别
HandlerInterceptor
继承WebMvcConfigurerAdapter注册拦截器(WebMvcConfigurerAdapter类已经被废弃,请实现WebMvcConfigurer接口完成拦截器的注册)
拦截器与过滤器的核心区别
CustomFilter : customFilter 请求处理之前----doFilter方法之前过滤请求CustomHandlerInterceptor : preHandle:请求前调用CustomHandlerInterceptor : postHandle:请求后调用CustomHandlerInterceptor : afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调CustomFilter : customFilter 请求处理之后----doFilter方法之后处理响应
通过输出结果分析一下拦截器、过滤器中各接口函数的执行顺序
请求链路
使用消息队列中间件的发布订阅模式
JDK自带的java.util.EventListener
1.写代码向ApplicationContext中添加监听器2.使用Component注解将监听器装载入spring容器3.在application.properties中配置监听器4.通过@EventListener注解实现事件监听(推荐)
Spring环境下的实现事件发布监听的方法
...
实现事件监听机制有很多方法
实现CommandLineRunner、ApplicationRunner接口
CommandLineRunner、ApplicationRunner的核心用法是一致的,就是用于应用启动前的特殊代码执行。ApplicationRunner的执行顺序先于CommandLineRunner;ApplicationRunner将参数封装成了对象,提供了获取参数名、参数值等方法,操作上会方便一些
应用启动的监听
事件监听
自定义事件的发布与监听
Servlet域对象与属性变化监听
嵌入式容器的配置与应用
Controller、Service、DAO层拦截异常转换为自定义异常,不允许将异常私自截留。必须对外抛出
统一数据响应代码,使用http状态码,不要自定义。自定义不方便记忆,HTTP状态码程序员都知道。但是太多了程序员也记不住,在项目组规定范围内使用几个就可以。比如:200请求成功,400用户输入错误导致的异常,500系统内部异常,999未知异常
自定义异常里面有message属性,用对用户友好的语言描述异常的发生情况,并赋值给message.
不允许对父类Excetion统一catch,要分小类catch,这样能够清楚地将异常转换为自定义异常传递给前端
开发规范
ExceptionTypeEnum 枚举异常分类,将异常分类固化下来,防止开发人员思维发散。
AjaxResponse 用于响应HTTP 请求的统一数据结构。
数据结构
通用异常处理逻辑
监听所有的Controller,一旦Controller抛出CustomException,就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。处理方法很简单就是使用AjaxResponse.error(e)包装为通用的接口数据结构返回给前端
作用
@ControllerAdvice
实现ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。也就是说,ResponseBodyAdvice 的处理过程在全局异常处理的后面
业务状态与HTTP协议状态一致(让业务状态与HTTP协议Response状态码一致)
最终代码
全局异常处理器
java的JSR 303: Bean Validation规范
JSR 303只是个规范,并没有具体的实现,目前通常都是才有hibernate-validator进行统一参数校验
JSR303定义的校验类型
Hibernate Validator 附加的 constraint
参数合法性校验
当用户输入参数不符合注解给出的校验规则的时候,会抛出BindException或MethodArgumentNotValidException
Assert断言与IllegalArgumentException
对这两种异常(BindException或MethodArgumentNotValidException)做全局处理,防止重复编码
使用org.springframework.util.Assert断言,如果不满足条件就抛出IllegalArgumentException
友好的数据校验异常处理(用户输入异常的全局处理)
服务端数据校验异常处理逻辑
用面向切面的方式,将Exception转换为ModelAndViewException。全局异常处理器拦截ModelAndViewException,返回ModelAndView,即error.html页面切入点是带@ModelView注解的Controller层方法
非前后端分离的应用
AOP完美处理页面跳转异常
统一全局异常处理@ControllerAdvice
JDK java.util.logging
过时的函数库,已经停止更新,不推荐使用,相比之下,性能和功能也是最差的
log4j
Spring Boot 默认的日志记录框架使用的是 Logback
性能上还是不及 Log4j2,因此,在现阶段,日志记录首选 Log4j2
%d{HH:mm:ss.SSS}:日志输出时间(red)%thread:输出日志的进程名字,这在Web应用以及异步任务处理中很有用 (green)%-5level:日志级别,并且使用5个字符靠左对齐 (highlight高亮蓝色)%logger:日志输出类的名字 (boldMagenta粗体洋红色)%msg:日志消息 (cyan蓝绿色)%n:平台的换行符
日志格式占位符
一、application配置文件实现日志配置
需求复杂的话
因为logback是spring boot的默认日志框架,所以不需要引入maven依赖,直接上logback-spring.xml放在resources下面
异步日志配置
二、使用logback-spring.xml实现日志配置
配置
Logback
一、引入maven依赖
在resources目录下新建一个log4j2-spring.xml文件,放在src/main/resources目录下即可被Spring Boot应用识别
上文两个Appender,一个叫做CONSOLE用于输出日志到控制台,一个叫做FILE-APPENDER输出日志到文件PatternLayout用于指定输出日志的格式,[%d][%p][%t][%C] %m%n 这些占位符将结合下文测试结果为大家介绍Policies用于指定文件切分参数TimeBasedTriggeringPolicy默认的size是1,结合filePattern定义%d{yyyy-MM-dd},则每天生成一个文件(最小的时间切分粒度是小时)<SizeBasedTriggeringPolicy size=\"100 MB\"/> 当文件大小到100MB的时候,切分一个新的日志文件<DefaultRolloverStrategy max=\"20\"/>表示文件最大的存档数量,多余的将被删除上文中的日志格式占位符号
<PatternLayout pattern=\
%d : date时间%p : 日志级别%t : thread线程名称%C: class类文件名称%msg:日志信息%n换行%style{%throwable}{red} 加样式,异常信息红色显示
占位符
二、添加配置文件log4j2-spring.xml
不同的环境使用不同的配置
在application-dev.yml里面使用log4j2-dev.xml配置文件
比如:我们需要三个log4j2 xml文件:log4j2-dev.xml 开发环境日志配置log4j2-prod.xml 生产环境日志配置log4j2-test.xml 测试环境日志配置
三、自定义配置文件
一、引入disruptor
在应用启动类里面使用System.setProperty
通过启动参数来设置全局异步日志
两种方式
二、 全局异步模式
采用异步/同步混合模式不需要配置Log4jContextSelector
在log4j2 xml里面对Loggers配置进行改造,加入AsyncLogger也就是异步日志,只针对com.zimug.boot.launch包(假如已知这个包对处理性能要求比较高)下的代码产生的日志采用异步模式,其他的日志仍然使用同步模式
三、异步/同步混合模式
log4j2
框架
每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对于日志框架的耦合性要求。有了SLF4J这个门面之后,程序员永远都是面向SLF4J编程,可以实现简单快速地替换底层的日志框架而不会导致业务代码需要做相应的修改
commons-logging
一般的Java项目而言,日志框架会选择slf4j-api作为门面
@Slf4j 注解来自动生成上面那个变量,默认的变量名是 log,如果我们想采用惯用的 LOGGER 变量名,那么可以在工程的 main/java 目录中增加 lombok.config 文件,并在文件中增加 lombok.log.fieldName=LOGGER 的配置项即可
引入Lombok
slf4j-api
日志门面
SLF4J + Log4j2
推荐的日志记录选型
TRACE:追踪。一般上对核心系统进行性能调试或者跟踪问题时有用,此级别很低,一般上是不开启的,开启后日志会很快就打满磁盘的。DEBUG:调试。这个大家应该不陌生了。开发过程中主要是打印记录一些运行信息之类的。INFO:信息。这个是最常见的了,大部分默认就是这个级别的日志。一般上记录了一些交互信息,一些请求参数等等。可方便定位问题,或者还原现场环境的时候使用。此日志相对来说是比较重要的。WARN:警告。这个一般上是记录潜在的可能会引发错误的信息。比如启动时,某某配置文件不存在或者某个参数未设置之类的。ERROR:错误。这个也是比较常见的,一般上是在捕获异常时输出,虽然发生了错误,但不影响系统的正常运行。但可能会导致系统出错或是宕机等。日志级别从小到大为trace<debug<info<warn<error<fatal,由于通常日志框架默认日志级别设置为INFO,因此1.3.小节中样例trace和debug级别的日志都看不到
日志级别(trace<debug<info<warn<error<fatal)
appender:主要控制日志输出到哪里,比如:文件、数据库、控制台打印等logger: 用来设置某一个包或者具体某一个类的日志打印级别、以及指定appenderroot:也是一个logger,是一个特殊的父logger。所有的子logger最终都会将输出流交给root,除非在子logger中配置了additivity=\"false\"。rollingPolicy:所有日志都放在一个文件是不好的,所以可以指定滚动策略,按照一定周期或文件大小切割存放日志文件。RolloverStrategy:日志清理策略。通常是指日志保留的时间。异步日志:单独开一个线程做日志的写操作,达到不阻塞主线程的目的。同步日志,主线程要等到日志写磁盘完成之后,才能继续向下执行异步日志,主线程写日志只是将日志消息放入一个队列,之后就继续向下执行,这个过程是在内存层面完成的。之后由专门的线程从队列中获取日志数据写入磁盘,所以不阻塞主线程。主线程(核心业务代码)执行效率很高。
常见术语概念
针对当前系统的每一次接口访问,要记录是什么人访问的(用户名)、什么时间访问的、访问耗时多长时间、使用什么HTTP method方法访问的、访问结果如何等。可以称为审计日志。将访问记录审计日志,输出到一个单独的日志文件access.log
需求
定义访问日志内容记录实体类
获取ip访问地址的工具类
自定义日志拦截器
拦截器注册
LoggerFactory.getLogger(\"ACCESS-LOG\") 代码去配置文件里面找一个name为ACCESS-LOG的Logger配置。该Logger是一个AsyncLogger,指向的输出目标是ACCESS-APPENDERACCESS-APPENDER是一个日志文件输出配置,日志文件是access-log.log
ACCESS-LOG\"的日志Logger定义
拦截器实现统一访问日志
日志
在 Spring Boot 入口类上配置 @EnableAsync 注解开启异步处理
创建任务抽象类 AbstractTask,并分别配置三个任务方法 doTaskOne(),doTaskTwo(),doTaskThree()
环境准备
定义 Task 类,继承 AbstractTask,三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10 秒内)
单元测试
简单示例
任务一、任务二、任务三顺序的执行
同步调用
通过 异步调用 的方式来 并发执行
在方法上配置 @Async 注解,将原来的 同步方法 变为 异步方法
注意:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
异步调用
假设我们需要统计一下三个任务 并发执行 共耗时多少
创建 AsyncCallBackTask 类,声明 doTaskOneCallback(),doTaskTwoCallback(),doTaskThreeCallback() 三个方法,对原有的三个方法进行包装
使用 Future<T> 来返回 异步调用 的 结果
循环调用 Future 的 isDone() 方法等待三个 并发任务 执行完成,记录最终执行时间
异步回调
创建一个 线程池配置类TaskConfiguration ,并配置一个 任务线程池对象taskExecutor
AbortPolicy,用于被拒绝任务的处理程序,它将抛出RejectedExecutionException。CallerRunsPolicy,用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。DiscardOldestPolicy,用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。DiscardPolicy,用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
拒绝策略
AsyncExecutorTask类,三个任务的配置和 AsyncTask 一样,不同的是 @Async 注解需要指定前面配置的 线程池的名称taskExecutor
定义线程池
由于在应用关闭的时候异步任务还在执行,导致类似 数据库连接池 这样的对象一并被 销毁了,当 异步任务 中对 数据库 进行操作就会出错
重新设置线程池配置对象,新增线程池 setWaitForTasksToCompleteOnShutdown() 和 setAwaitTerminationSeconds() 配置
setWaitForTasksToCompleteOnShutdown(true): 该方法用来设置 线程池关闭 的时候 等待 所有任务都完成后,再继续 销毁 其他的 Bean,这样这些 异步任务 的 销毁 就会先于 数据库连接池对象 的销毁。setAwaitTerminationSeconds(60): 该方法用来设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
解决方案
优雅地关闭线程池
为异步任务规划线程池
异步任务
@EnableScheduling //开启定时任务
入口main方法上加注解
开启定时任务方法
每隔多长时间执行一次。(开始------->X时间------>再开始)。如果间隔时间小于任务执行时间,上一次任务执行完成下一次任务就立即执行。如果间隔时间大于任务执行时间,就按照每隔X时间运行一次
fixedRate
当任务执行完毕后一段时间再次执行。(开始--->结束(隔一分钟)开始----->结束)。上一次执行任务未完成,下一次任务不会开始
fixedDelay
fixedDelay和fixedRate,单位是毫秒
灵活
第一位,表示秒,取值0-59第二位,表示分,取值0-59第三位,表示小时,取值0-23第四位,日期天/日,取值1-31第五位,日期月份,取值1-12第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思,另外:1表示星期天,2表示星期一。第七位,年份,可以留空,取值1970-2099
特殊的符号
含义
https://cron.qqe2.com/
在线工具
cron表达式
不同定时方式的解析
所有的定时任务使用的都是一个线程,所以彼此互相影响
实现定时任务
解决定时任务单线程运行的问题
通过@Scheduled实现定时任务
引入对应的 maven依赖
创建一个任务类Job
将之前创建的定时任务添加到定时调度里面
创建 Quartz 定时配置类
Job:一个仅包含一个void execute(JobExecutionContext context)Abstract方法的简单接口。在实际开发中,要执行的任务是通过实现接口自定义实现的。JobExecutionContext提供调度上下文信息。
核心概念
深入解析
使用quartz提供的API完成配置任务的增删改查将任务的配置保存在数据库中
原理
可能是版本bug,有的时候自动建表不会生效,自己去quartz-scheduler-x.x.x.jar里面找一下建表sql脚本:classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql,然后执行
创建一个定时任务相关实体类用于保存定时任务相关信息到数据库当中
创建定时任务暂停,修改,启动,单次启动工具类
动态配置代码实现
quartz动态定时任务(数据库持久化)
quartz简单定时任务(内存持久化)
定时任务
任务
引入 commons-pool 2 是因为 Lettuce 需要使用 commons-pool 2 创建 Redis 连接池
引入依赖包
单例模式连接配置
注意,当我们使用spring boot连接哨兵模式的redis集群,连接的是sentinel节点,而不是redis服务实例节点
哨兵模式连接配置
集群模式连接配置
整合
使用redisTemplate操作数据
使用Redis Repository操作数据
使用
自定义缓存到期时间
redis 缓存配置
为redis的key设置过期时间
异常导致锁没有释放
获取锁的同时设置过期时间
获取锁与设置过期时间操作不是原子性的
在释放锁之前判断一下,这把锁是不是自己的那一把,如果是别人的锁你就不要动
锁过期之后被别的线程重新获取与释放
使用redis lua脚本(lua脚本是在一个事务里面执行的,可以保证原子性)
锁的释放不是原子性的
目前我们的程序获取不到锁,就无限的重试,是不是应该在重试一定的次数之后就抛出异常?在有限的时间内通过异常给用户一个友好的响应。比如:程序太忙,请您稍后再试!程序A没有执行完成,锁定的key就过期了。虽然过期之后会自动释放锁,但是我的程序A的确没有执行完成啊,也没有异常抛出,就是执行的时间比较长,这个时候是不是应该对锁定的key进行续期?笔者对于分布式锁自动续期的这个功能也不是特别感冒,我觉得程序超过了我们设置的过期时间(比如说60s)一定是出现了问题,如果不是离线大数据批处理,一个程序执行60秒还没完成那一定是出问题了,你给我抛出异常就可以了。对于一个出问题的程序一直续期和死锁没什么区别。所以实现一个分布式锁,不是我们想的那么简单,在高并发的环境下需要考虑的问题会复杂得多。怎么办?实际上分布式锁的细节时间有很多的现成的解决方案
其他的问题?
分布式锁实现过程中的问题
RedisLockRegistry是spring-integration-redis中提供redis分布式锁实现类
基于Redisson实现分布式锁原理(Redission是一个独立的redis客户端,是与Jedis、Lettuce同级别的存在)
RedisLockRegistry通过本地锁(ReentrantLock)和redis锁,双重锁实现;Redission通过Netty Future机制、Semaphore (jdk信号量)、redis锁实现。RedisLockRegistry和Redssion都是实现的可重入锁。RedisLockRegistry对锁的刷新没有处理(续期),Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。
比较完整优秀的分布式锁实现
redis分布式锁
redis缓存
spring cache缓存
适用于单体应用的缓存
EhCache缓存
单个应用的session应用
同一IP(域名),不同端口,在同一个浏览器cookies是共享的。不同IP(域名)的Cookies,在同一个浏览器Cookies肯定不共享的。对于这种情况需要在集群应用的前面加上负载均衡器逆向代理,如:nginx,haproxy。让客户端看上去访问的是同一个IP(代理IP),从而浏览器认为基于这个IP的Cookies是共享的。SESSION正常是由Servlet容器来维护的(内存里面,每个服务器内存是不共享的),这样SESSION就无法共享。如果希望Session共享,就需要把sessionID的存储放到一个统一的地方,如:redis。SessionID的维护交给Spring session则更加方便。除了Cookies可以维持Sessionid,Spring Session还提供了了另外一种方式,就是使用header传递SESSIONID。目的是为了方便类似于手机这种没有cookies的客户端进行session共享
不要把跨域请求和cookies跨域名的概念搞混了。同源策略是要求IP、端口、协议全一致,不一致的请求就是跨域请求。但cookies是可以跨域共享的,但是不能跨域名(IP)共享
集群应用的Session共享
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30 * 60 * 1000)
启动类上方加上注解,启动SpringSession管理应用的session,并设置session数据的有效期30分钟
.配置启用Redis的httpSession
集成Spring session
配置redis链接信息(application.yml)
在同一台机器上启动多个实例,ip相同所以session是共享的。如果你在不同的服务器上启动多个实例(IP)不同,需要在应用前方加上负载均衡逆向代理才可以实现session共享
集群多节点应用session共享
缓存
文件上传目录自定义配置
文件上传的Controller实现
MinIO
FastDFS
分布式
文件系统
为了避免轮询
开启websocket功能
WebSocket的ws协议是基于HTTP协议实现的WebSocket的wss协议是基于HTTPS协议实现的一旦你的项目里面使用了https协议,你的websocket就要使用wss协议才可以
兼容HTTPS协议
全双工(双向通信)通信:WebSocket
服务端主动推送:SSE (Server Send Event)(应用较少)
服务器推送
邮件发送
应用程序监控管理
ActiveMQ
NameServer:消息队列的大脑,同时监控消息队列,存储关于队列的基本信息,如各个队列的分布,ip、服务地址、目前的健康状况、队列的处理进度等。多个Nmaeserver之间没有通信。broker:队列服务,负责接受请求并分发请求。数据存储、消息分发的负载均衡。可以是master-slave的主从结构。console-ng:消息队列的监控控制台
架构
安装nameserver和broker
RocketMQ控制台
防火墙开放端口
定义一个常量配置类
对外暴露一个接口用于发送消息
接口
消费者需要实现RocketMQListener<T>接口
消费监听
新建一个RocketConsumer2,如果和RocketConsumer 是同一个组consumerGroup ,一条消息二者选其一消费一次,而且呈现负载均衡分布。即:同一个消费者组的消费者订阅同一话题,组内只能消费一次消息
新建一个RocketConsumer2,如果和RocketConsumer 不是同一个组consumerGroup ,一条消息被消费两次。即:不同消费者组订阅同一话题,组内只消费一次,但多组消费多次
消费者组
消费者主动从Broker拉取消息
Pull消费模式
Broker主动将消息推送给消费者
Push消费模式
实现2种消费模式
RocketMQ
消息队列的整合与使用
SpringBoot
简化开发
Spring
3.熟悉SQL语言编写、调优,对事务、索引、MVCC机制等有深入理解,拥有线上慢SQL优化、使ShardingSphere进行分库分表经验
4.熟悉Redis、RabbitMQ,Elasticsearch,了解Redis性能优化,数据一致性方案
5.熟悉版本控制工具 Git、SVN
6.熟悉Netty、Nginx、Tomcat、docker
7.熟悉windows、linux部署等相关操作
8.熟悉前端知识,前端三剑客、BootStrap及其插件、Echerts、VUE、element等
9.了解SpringCloud alibaba技术体系,对Nacos、Sentinel有研究、服务注册与发现、服务限流、降级、熔断等
2PC的实现需要引入一个协调者角色,通常由一个单独的节点担任。协调者负责发起事务的开始,并在准备阶段询问所有参与者是否准备提交事务,在提交阶段下发提交或回滚的指令。参与者必须回复事务的执行结果,包括成功或失败。协调者根据所有参与者的回复做出决策,如果所有参与者都回复成功,则提交事务;如果有任何参与者回复失败,则回滚事务。
2PC(Two-Phase Commit):两阶段提交协议是一种分布式事务协议,用于保证分布式系统中的事务的原子性和一致性。它分为准备阶段和提交阶段,准备阶段中协调者询问所有参与者是否准备提交事务,提交阶段中协调者下发事务提交或回滚的指令。该协议可以避免单点故障,但也存在阻塞和同步等待的问题。
TCC的实现分为Try、Confirm和Cancel三个阶段。在Try阶段,执行事务的准备工作,例如预留资源、创建临时数据等。在Confirm阶段,尝试提交事务并生成确认消息,同时将操作转化为已确认的消息并保存。在Cancel阶段,发送取消消息,取消已确认的消息操作。TCC的实现需要针对每个业务进行定制化开发,实现较为复杂。
本地消息表的实现需要引入一个本地数据库表来存储待发送的消息。发送方将消息写入本地数据库表中,接收方从表中读取消息并处理。通过数据库的事务和锁定机制来保证消息的可靠性和一致性。
本地消息表:本地消息表是一种消息传递机制,用于保证分布式系统中的消息可靠性。它使用本地数据库表来存储待发送的消息,通过数据库的事务和锁定机制来保证消息的可靠性和一致性。当发送方发送消息时,将消息写入本地数据库表中,接收方从表中读取消息并处理。这种方式可以避免消息丢失和重复处理的问题。
可靠消息最终一致性的实现需要借助消息队列和重试机制。发送方将消息发送到消息队列中,接收方从队列中读取消息并处理。如果处理失败,可以通过重试机制重新发送消息,直到达到一定的重试次数或者成功处理为止。在实现中需要设置合理的重试次数和控制重试的频率。
可靠消息最终一致性:可靠消息最终一致性是一种消息传递机制,用于实现分布式系统中的最终一致性。它使用消息队列和重试机制来保证消息的可靠性和一致性。发送方将消息发送到消息队列中,接收方从队列中读取消息并处理。如果处理失败,可以通过重试机制重新发送消息,直到达到一定的重试次数或者成功处理为止。这种方式可以实现最终一致性,但需要合理设置重试次数和控制重试的频率。
最大努力通知的实现不需要特殊的机制,只需要发送方尽可能多次地发送通知,接收方也尽可能多次地接收通知并处理即可。这种方式无法保证通知的可靠性,但可以最大程度地提高通知的成功率。
最大努力通知:最大努力通知是一种消息传递机制,用于实现分布式系统中的通知可靠性。它通过尽可能多次地发送通知来保证接收方能够收到通知。发送方可以多次发送通知,接收方也需要尽可能多次地接收通知并处理。这种方式无法保证通知的可靠性,但可以最大程度地提高通知的成功率。
10.了解分布式事务解决方案,2PC、TCC、本地消息表、可靠消息最终一致性、最大努力通知等实现方案
基础
从左到右遍历,进行比较,找出最小值,赋值给最左边元素,将最左边元素值,赋值给此元素,依次遍历
遍历左到右除去最后一个元素
第一个循环
为了找出最值下标
遍历左到右除去第一个元素
第二个循环
去除元素算是个优化,因为不需要和本身进行比较,比较不同位置的元素就行
将所有元素从左到右遍历,分为两个循环
找出最小值的下标
进行判断
根据下标将最小值元素和第一层遍历元素进行互换
代码
当然 找出最大进行选择排序也可
复杂度O(N²)
选择排序
第一趟排序能得出最后一个元素一定是最大元素
第二趟排序能得出倒数第二个元素为第二大元素
所以需要元素个数-1趟,最后一个元素不需要冒泡了
O(n)
最好
O(N²)
平均
时间复杂度
冒泡排序
插入排序
排序
基础算法
分析
定期总结-2023
0 条评论
回复 删除
下一页