文章目录
- Java
- Java中四种引用类型及使用场景
- 集合
- HashMap源码及扩容策略
- HashMap死循环问题
- ConcurrentHashMap与Hashtable
- ConCurrentHashMap 1.8 相比 1.7
- 判断单链表是否有环,并且找出环的入口
- IO
- 线程池
- 线程池的几种创建方式
- 判断线程是否可以回收
- 线程池的7大核心参数
- 线程池的状态包括以下五种:
- 线程池怎么判断线程回收
- JVM
- GC垃圾回收
- 哪些内存需要回收
- 方法区的垃圾回收
- 垃圾收集算法
- 垃圾收集器
- 年轻代深入老年代条件
- 调优
- jmap
- jstack
- jstat
- JMM
- volitale
- 缓存一致性协议(MESI)
- 锁
- 并发包
- 常用框架
- Spring
- SpringMVC
- 注入DispatcherServlet几种方式
- DispatcherServlet调用逻辑
- SpringBoot
- 启动流程
- 配置文件加载顺序
- 配置文件加载时机
- 自动配置原理
- Dubbo
- SpringCloud
- Netty
- Seata
- Zookeeper
- XXL-JOB
- 设计模式
- 数据库
- MQ
- RabbitMq知识点
- RocketMq知识点
- Kafka知识点
- 角色
- 消息模型
- 死信队列
- 怎么避免消息丢失/消息可靠性
- 顺序消费
- 消息幂等性
- 消息持久化
- 数据积压
- 集群
- Docker
- Redis
- 持久化机制
- MongoDB
- 负载均衡
- 分布式锁
- Es
- Solr
- 计算机网络
- HTTP常见响应码
- 开发相关
- 其他
Java
JRE(Java Runtime Environment)是JAVA运行时环境,它是运行已编译Java程序所需的所有内容的集合,包括Java虚拟机(JVM),Java核心类库和一些基础的构件。
JDK(Java Development Kit)是Java的开发工具包,它不仅提供了Java程序运行所需的JRE,还提供了一系列的编译,运行等工具,如javac,java,javaw等。
JDK > JRE > JVM,所以我们在安装JDK时,通常不需要考虑JRE,JVM之类的,只要你安装好了JDK,其他两个就都有了。
Java中四种引用类型及使用场景
强引用
new 一个对象的时候,就是强引用。只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
显式地设置 置引用为 null,或者超出对象的生命周期,此时就可以回收这个对象。具体回收时机还是要看垃圾收集策略。
Object object = new Object();
软引用
非必须,但仍有用的对象,内存不足时才会回收。第一次gc不会回收,第一次回收内存还不够才会回收。在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
Object object = new Object();
SoftReference<Object> softReference = new SoftReference<>(object)
应用场景:缓存
弱引用
不管内存状态如何,总会被回收的对象。
Object object = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object);
应用场景:Java源码中的java.util.WeakHashMap中的key就是使用弱引用。
虚引用
引用与没有引用关系一样,随时会被回收,虚引用必须和引用队列一起使用。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);
public class PhantomReference<T> extends Reference<T> {/*** Returns this reference object's referent. Because the referent of a* phantom reference is always inaccessible, this method always returns* <code>null</code>.** @return <code>null</code>*/public T get() {return null;}public PhantomReference(T referent, ReferenceQueue<? super T> q) {super(referent, q);}
}
应用场景:对象销毁前的一些操作,比如说资源释放等。
- 跟踪对象被垃圾回收的状态。
- 提供机制确保对象被
finalize()
处理后执行额外清理操作。 - 与引用队列一起使用,在对象被回收时收到通知或执行清理操作。
使用实例 :
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;/*** @author liuchao* @date 2023/3/12*/
public class PhantomReferenceTest {/*** 当前对象的声明*/public static PhantomReferenceTest obj;/*** 引用队列*/static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;public static class CheckRefQueue extends Thread {@Overridepublic void run() {while (true) {if (phantomQueue != null) {PhantomReference<PhantomReferenceTest> objt = null;try {objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();} catch (InterruptedException e) {throw new RuntimeException(e);}if (objt != null) {System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");}}}}}/*** 通过此方法 复活obj对象** @throws Throwable*/@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("调用当前类的finalize方法");//复活对象obj = this;}public static void main(String[] args) {Thread t = new CheckRefQueue();//设置为守护进程t.setDaemon(true);t.start();phantomQueue = new ReferenceQueue<>();obj = new PhantomReferenceTest();//构造PhantomReferenceTest 虚引用,并指定引用队列PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(obj, phantomQueue);try {//不可获取虚引用中对象System.out.println(phantomReference.get());//销毁强引用obj = null;//进程GC,由于对象可复活,所以GC无法回收对象 (通过finalize 复活)System.gc();Thread.sleep(1000);//验证obj是否存活if (obj == null) {System.out.println("obj 被销毁");} else {System.out.println("obj 被复活");}System.out.println("第二次GC");//再次销毁强引用obj = null;System.gc();Thread.sleep(1000);//验证obj是否存活if (obj == null) {System.out.println("obj 被销毁");} else {System.out.println("obj 被复活");}} catch (Exception e) {throw new RuntimeException(e);}}
}
集合
HashMap源码及扩容策略
如果创建HashMap时不指定Capacity初始值,HashMap的默认初始化大小为16,之后每次扩充,容量会变为两倍。
HashMap会在第一次Put的时候调用resize()
初始化数组,每次put完成后再检查当前容量是否大于数组长度的0.75倍数,如果大于则再调用resize()扩容,每次扩容为当前数组长度的两倍。
put方法底层会对当前key进行hash运算,再调用putVal
去设置值,如果计算出hash对应的数组下标无值,则之间新建一个Node
节点放入;
如果有值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;/** 如果是空的table,那么默认初始化一个长度为16的Node数组*/if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length; // 1、创建table数组}/** 如果计算后的下标i,在tab数组中没有数据,那么则新增Node节点*/if ((p = tab[i = (n - 1) & hash]) == null) {tab[i] = newNode(hash, key, value, null); // 2、无哈希冲突,直接添加元素} else { // 3、存在哈希冲突,向红黑树或链表赋值/** 如果计算后的下标i,在tab数组中已存在数据,则执行以下逻辑 */Node<K, V> e;K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { /** 如果与已存在的Node是相同的key值*/e = p;}else if (p instanceof TreeNode) { /** 如果与已存在的Node是相同的key值,并且是树节点*/e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);} else { /** 如果与已存在的Node是相同的key值,并且是普通节点,则循环遍历链式Node,并对比hash和 key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。*/for (int binCount = 0; ; ++binCount) {/** 获得p节点的后置节点,赋值给e。直到遍历到横向链表的最后一个节点,即:该节点的next后置指针为null */if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);/** binCount从0开始,横向链表中第2个node对应binCount=0,如果Node链表大于8个Node,那么试图变为红黑树 */if (binCount >= TREEIFY_THRESHOLD - 1) {treeifyBin(tab, hash);}break;}/** 针对链表中的每个节点,都来判断一下,是否待插入的key与已存在的链表节点相同,如果相同,则跳出循环,并在后续的操作中,将该节点内容更新为最新的插入值 */if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {break;}p = e;}}/** 如果存在相同的key值*/if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null) {/** 则将新的value值进行更新*/e.value = value;}afterNodeAccess(e); return oldValue;}}++modCount;if (++size > threshold) {resize(); // 4、超过阈值,则进行扩容}afterNodeInsertion(evict); return null;}
链表转红黑树,两个条件,必须同时满足两个条件才能进行转换
- 条件1:单个链表长度大于等于8
- 条件2:hashMap的总长度大于64个、且树化的节点位置不能为空
红黑树转链表,两个条件,必须同时满足两个条件才能进行转换
- 条件1:树内节点数小于等于6
- 条件2:根节点为空,根节点的左右子树为空,根节点的左子树的左子树为空
HashMap源码解析
HashMap死循环问题
HashMap死循环只发生在JDK1.7版本中。
主要原因:头插法 +链表 +多线程并发 +扩容
累加到一起就会形成死循环
多线程下:建议采用ConcurrentHashMap替代
JDK1.8中,HashMap改成尾插法,解决了链表死循环的问题
博客详解
ConcurrentHashMap与Hashtable
对比 :
HashTable :
- HashTable 解决线程安全问题非常简单粗暴,就是在方法前加 synchronize 关键词,HasTable 不仅给写操作加锁
put remove clone
等,还给读操作加了锁 get
ConcurrentHashMap :
- ConcurrentHashMap 没有大量使用 synchronsize 这种重量级锁。而是在一些关键位置使用乐观锁(CAS), 线程可以无阻塞的运行。
- 读方法没有加锁
- 扩容时老数据的转移是并发执行的,这样扩容的效率更高。
ConCurrentHashMap 1.8 相比 1.7
- 去除 Segment + HashEntry + Unsafe 的实现,
- 改为 Synchronized + CAS + Node + Unsafe 的实现,其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。
用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了在加锁。 - put()方法中 初始化数组大小时,1.8不用加锁,因为用了个 sizeCtl 变量,将这个变量置为-1,就表明table正在初始化。
ConcurrentHashMap 1.8放弃了分段锁
,转而采用了一种新的实现方式。这种改变的原因是出于对分段锁局限性的考虑。以下是放弃分段锁的主要原因:
- 锁竞争:分段锁使得每个线程需要在不同的段上争夺锁,这样增加了锁的竞争,可能导致性能下降。
- 扩容时的性能影响:当ConcurrentHashMap需要进行扩容时,需要重新分配段数组并复制原有数据。这个过程需要停止所有读写操作,并持有整个ConcurrentHashMap的全局锁,这会影响性能。
- 热点数据问题:分段锁可能会导致某些小的数据结构经常被访问,从而在这些数据结构上产生激烈的锁竞争,这同样会影响整体ConcurrentHashMap的性能。
- 锁的粒度和重入:锁的粒度大小不当或者锁的重入都可能引起性能问题。分段锁的设计可能需要调整锁的粒度和处理重入问题,这本身就带来了额外的复杂性和性能开销。
为了克服上述问题,Java 8中的ConcurrentHashMap采用了CAS(CompareAndSwap)和synchronized的组合方式来保证并发安全,而不是依赖分段锁。这样的改进不仅简化了内部实现,还提高了并发性能。此外,放弃分段锁后,ConcurrentHashMap的存储空间需求也有所减少,因为不再需要维护独立的segment结构
put();get();resize();方法都做了改变
ConCurrentHashMap 1.8 相比 1.7参考博客
判断单链表是否有环,并且找出环的入口
快慢指针;s=vt(路程=速度*时间)用方程思想列等式解;
使用HashSet将遍历过的元素存入。
第一次相遇的时候将一个指针指向头节点,两个指针再同时向后每移动一个数据,再次相遇就是环的入口。
参考博客
简易解析
红黑树
IO
线程池
线程池的几种创建方式
- ThreadPoolExecutor
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。 - Executors
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
判断线程是否可以回收
工作线程回收需要满足三个条件:
- 参数allowCoreThreadTimeOut为true
- 该线程在keepAliveTime时间内获取不到任务,即空闲这么长时间(空闲+超时)
- 当前线程池大小 > 核心线程池大小corePoolSize。
线程池通过设置参数来控制线程的回收策略,其中主要的参数包括:
-
核心线程数(corePoolSize): 表示线程池中保持的最小线程数,即使它们是空闲的。
-
最大线程数(maximumPoolSize): 表示线程池中允许的最大线程数,包括核心线程数和非核心线程数。
-
线程空闲时间(keepAliveTime): 表示非核心线程在空闲状态下的最长等待时间,超过这个时间,线程可能会被回收。
-
工作队列(workQueue): 用于存放尚未执行的任务,当线程池中的线程数超过核心线程数时,任务会被放入工作队列。如果工作队列已满,新的任务可能触发创建新线程,但线程数量不会超过最大线程数。
线程池的7大核心参数
示例 :
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler){}
一、corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。
二、maximumPoolSize 线程池最大线程数量
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
三、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
四、unit 空闲线程存活时间单位
keepAliveTime的计量单位
五、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
④DelayedWorkQueue
具有优先级的无界阻塞队列
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
- CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务 - AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。 - DiscardPolicy
该策略下,直接丢弃任务,什么都不做。 - DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
线程池的状态包括以下五种:
- RUNNING(运行状态):线程池新建或调用execute()方法后,处于运行状态,能够接收新的任务,表示线程池可以接受新的任务并处理等待队列中的任务。
- SHUTDOWN(关闭状态):线程池不再接受新的任务提交,但会继续处理等待队列中的任务。当调用线程池的
shutdown()
方法时,线程池会从RUNNING状态转变为SHUTDOWN状态。 - STOP(停止状态):线程池既不接受新的任务提交,也不处理等待队列中的任务,并且会中断正在执行的任务。当调用线程池的
shutdownNow()
方法时,线程池会从(RUNNING或SHUTDOWN)状态转变为STOP状态。 - TIDYING(整理状态):当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时(所有任务都销毁了,workCount 为 0),线程池会从SHUTDOWN状态转变为TIDYING状态。在TIDYING状态下,所有的任务都已终止,线程池会执行
terminated()
方法,执行完该方法后,线程池会从TIDYING状态转变为TERMINATED状态。- SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
- 线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
- 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
- TERMINATED(终止状态/销毁状态):线程池在TIDYING状态执行完terminated()方法后,会从TIDYING状态转变为TERMINATED状态。
fdsa
转换成TIDYING
的不同:
线程池怎么判断线程回收
线程回收学习博客
线程池参数的合理设置
拒绝策略
线程池状态
怎么中断
创建销毁的过程
线程池创建基本使用
线程的5大状态:
1、 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2、 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3、运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4、 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- (01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
- (02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- (03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
JVM
包含内容:
- 类装载子系统(Class Load SubSystem)
- 运行时数据区(Run-Time Data Areas)
- 堆
- 栈
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 程序计数器
- 方法区
- 本地方法接口(Native Method Stack)
- PC寄存器(Programe Counter Register)
- 执行引擎(Execution Engine)
- 字节码解释器
对字节码采用逐行解释的方式执行 - JIT(Just In Time)编译器
- 字节码解释器
JIT(Just In Time)编译器
- 方法调用计数器:统计方法调用次数
统计方法调用的次数。默认阈值时Client模式下1500次,在Server模式下是10000次。超过这个阈值就会触发JIT编译。这个阈值可以通过-XX:CompileThreshold设定 - 回边计数器:统计循环体执行的循环次数
jvm内存分配
- 栈上分配与TLAB/内存分配的两种方法
GC垃圾回收
jdk1.8默认垃圾回收器
JDK1.8中,Parallel Scavenge 被设置为年轻代(Young Generation)的默认垃圾回收器,而 Parallel Old 是用于老年代(Tenured Generation)的垃圾回收器
GC日志内容
日志内容解析及GC案例
哪些内存需要回收
所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。
寻找回收对象的两种方式。
- 引用计数法
给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。 - 可达性分析法
通过一系列称为GC Roots
的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
可以作为GCRoots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
方法区的垃圾回收
方法区的垃圾回收主要回收两部分内容:
- 废弃常量。
以字面量回收为例,如果一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象引用了叫做“abc”的字面量,那么,如果发生垃圾回收并且有必要时,“abc”就会被系统移出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。 - 无用的类。既然进行垃圾回收,就需要判断哪些是废弃常量,哪些是无用的类,需要满足以下三个条件:
- 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
- 标记-清除(Mark-Sweep)算法
- 复制(Copying)算法
- 标记-整理(Mark-Compact)算法
- 分代收集算法
垃圾收集器
- Serial收集器()
需要STW(Stop The World),停顿时间长。
简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。 - Serial Old收集器
Serial收集器的老年代版本 - ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本 - Parallel Scavenge收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
年轻代深入老年代条件
- 躲过15次gc,达到15岁高龄之后进入老年代;
- 动态年龄判定规则,如果Survivor区域内年龄1+年龄2+年龄3+年- 龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入老年代,不一定要达到15岁
- 如果一次Young GC后存活对象太多无法放入Survivor区,此时直接计入老年代
- 大对象直接进入老年代
调优
GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化。指标参考:
a.Minor GC执行时间不到50ms;b.Minor GC执行不频繁,约10秒一次;c.Full GC执行时间不到1s;d.Full GC执行频率不算频繁,不低于10分钟1次;
GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。
在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。
总结:
开启gc日志打印,jmap -dump下载虚拟机文件,jstack分析死锁,jstat分析内存占用情况; jstat -gc 查看gc日志
CPU使用率飙高问题
1.使用top命令常看当前服务器中所有进程(jps命令可以查看当前服务器运行java进程),找到当前cpu使用率最高的进程,获取到对应的pid;
2.然后使用top -Hp pid,查看该进程中的各个线程信息的cpu使用,找到占用cpu高的线程pid
3.使用jstack pid打印它的线程信息,需要注意的是,通过jstack命令打印的线程号和通过top -Hp打印的线程号进制不一样,需要进行转换才能进行匹配,jstack中的线程号为16进制,而top -Hp打印的是10进制。
jmap
显示Java堆详细信息
jmap -heap pid
显示堆中对象的统计信息
jmap -histo:live pid
打印类加载器信息
jmap -clstats pid
生成堆转储快照dump文件
jmap -dump:format=b,file=heapdump.dump pid
jstack
jinfo pid,可以查看当前进行虚拟机的相关信息列举出来,如下图
jstat -gc pid ms,多长毫秒打印一次gc信息,打印信息如下,里面包含gc测试,年轻代/老年带gc信息等:
jmap -histo pid | head -20,查找当前进程堆中的对象信息,加上管道符后面的信息以后,代表查询对象数量最多的20个:
jmap -dump:format=b,file=xxx pid,可以生成堆信息的文件
jstack、jconsle检查死锁
jstat
jstat -<options> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
options可选值
-class:显示ClassLoader的相关信息
-compiler:显示JIT编译的相关信息
-gc:显示与GC相关信息
-gccapacity:显示各个代的容量和使用情况
-gccause:显示垃圾收集相关信息(同-gcutil),同时显示最后一次或当前正在发生的垃圾收集的诱发原因
-gcnew:显示新生代信息
-gcnewcapacity:显示新生代大小和使用情况
-gcold:显示老年代信息
-gcoldcapacity:显示老年代大小
-gcpermcapacity:显示永久代大小
-gcutil:显示垃圾收集信息
-printcompilation:输出JIT编译的方法信息
-t:在输出信息前加上一个Timestamp列,显示程序的运行时间
-h:可以在周期性数据输出后,输出多少行数据后,跟着一个表头信息
interval:用于指定输出统计数据的周期,单位为毫秒
count:用于指定一个输出多少次数据
jstat -gc 7063 500 4
7063 是进程ID ,采样时间间隔为500ms,采样数为4
JVM基本调优
GC及垃圾收集器简单解析
JMM
Java内存模型(Java Memory Model)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范(定义了程序中各个变量的访问方式)。
JMM定义了关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作
,Java虚拟机实 现时必须保证下面提及的每一种操作都是原子的、不可再分的。
主内存: 线程的共享数据区域,主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中(包括局部变量、类信息、常量、静态变量)。
工作内存: 线程私有,主要存储当前方法的所有本地变量信息(主内存中的变量副本拷贝) , 每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,即使访问的是同一个共享变量。
数据同步八大原子操作:
- lock(锁定): 作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定
- read(读取): 作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
- load(载入): 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工 作内存的变量副本中
- use(使用): 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量
- store(存储): 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作
- write(写入): 作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中
对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外
JMM 离不开原子性、可见性、有序性展开。
- 原子性
- 可见性
- 有序性
虚拟机在进行代码编译时,对改变顺序后不会对最终结果造成影响的代码,虚拟机不一定会按我们写的代码顺序运行,有可能进行重排序。实际上虽然重排后不会对变量值有影响,但会造成线程安全问题。
volitale
volatile关键字的作用主要有以下几点:
- 确保内存可见性:当一个线程修改了一个volatile变量的值,其他线程会立即看到这个改变。这确保了所有线程看到的是一致的内存映像。
- 防止指令重排序:JVM会在指令级别对程序进行重排序,以便更好地优化执行效率。但在某些情况下,这可能导致变量读取/写入操作被误排序,从而无法正确地反映出程序的意图。volatile关键字可以防止这种重排序的发生。
- 禁止共享变量缓存:大多数现代处理器都有一种名为“缓存”的技术,这种技术会缓存一部分主内存中的数据,以提高程序的运行效率。但是,如果一个变量被声明为volatile,那么处理器就会知道这个变量是用于同步的,因此不能被缓存,从而确保所有线程都能看到最新的值。
volitale 能保证可见性、有序性,不能保证原子性。
内存屏障是什么
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载新数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。
下面是基于保守策略的JMM内存屏障插入策略:
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的前面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。
volatile的实现原理-内存屏障
缓存一致性协议(MESI)
MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。
volitale
锁
synchronize 和 lock 锁
创建对象判断对象头是否开启偏向锁,开启偏向锁尝试将线程ID与对象头
volitale
并发包
JUC中常用类
ReentrantLock 可重入锁
ReentrantReadWriteLock
Semaphroe 信号量
CountDownLatch
CopyOnWriteArrayList
CyclicBarrier
Atomic原子类
AtomicReference
AQS(AbstractQueuedSynchronizer)的核心原理主要围绕三个方面:
- 同步状态(State)。AQS使用一个volatile修饰的int类型的成员变量来表示同步状态,通过该状态来控制共享资源的访问。
- 队列(Queue)。AQS使用一个基于FIFO(先进先出)原则的队列来实现线程的排队和同步。这个队列是由一系列节点(Node)组成,每个节点包含线程本身、前驱节点、后继节点以及等待状态等信息。
- 并发操作。AQS提供了多种原子操作来对同步状态进行修改,例如compareAndSetState方法用于原子地更新同步状态。
当一个线程试图获取共享资源时,它会尝试通过CAS操作来修改同步状态。如果获取成功,线程将获得资源并继续执行;如果获取失败,线程会被加入到队列的末尾并阻塞,直到同步状态发生改变(例如,有线程释放了资源)。当资源被释放时,位于队列头部的线程会被唤醒,尝试再次获取资源。这种机制使得AQS能够支持如ReentrantLock这样的同步器。1
常用框架
Spring
Spring的启动流程
-
加载Spring配置文件。即,读入并解析配置文件,构建出Spring IoC容器的初始状态。
-
初始化IoC容器。IoC容器的初始化包括对BeanFactory进行初始化、注册BeanDefinition、实例化Bean、依赖注入等过程。
-
实例化和初始化Bean。在初始化Bean时,Spring 根据Bean的定义以及配置信息,实现对Bean的实例化、属性赋值、以及初始化等操作。
-
完成IoC容器的准备工作。所有单例的Bean都已经被实例化、初始化并装配到容器中后,容器的准备工作就完成了,此时Spring框架已经可以对外提供服务。
-
执行定制化的后置处理器。Spring容器中可能会存在一些实现了BeanPostProcessor接口的定制化组件。这些组件会参与到IoC容器中Bean的生命周期过程,比如AOP、事务处理等。
-
执行自定义的初始化方法和销毁方法。容器中某些Bean可能需要在容器启动时执行自定义的初始化方法。这些方法在容器启动时就会被调用;同理,某些Bean在容器关闭时需要调用自定义的销毁方法,以清理资源。
-
容器启动后,整个应用将进入正常的工作状态。
Spring启动流程
IOC是根据什么找到对应的对象的
IOC是根据BeanName
找到对应的对象的。
IOC内存放着所有Class对应BeanName的一个关系表,如果通过Class获取实例,则先从此表查询BeanName,再从容器中获取实例。
DefaultListableBeanFactory#getBeanNamesForType
将Class类型转成BeanName,最终通过BeanName从三级缓存中获取实例
。
Spring其他相关面试题及答案
Spring中Bean的生命周期
- 实例化bean
- 属性赋值
- 各种aware接口
- BeanPostProcessor#postProcessBeforeInitialization
- InitializingBean#afterpropertiseSet
- init-method 初始化方法
- BeanPostProcessor#postProcessAfterInitialization
- 使用bean
- DiposableBean#destory / destory-method
SpringAOP原理
理解 Spring AOP 需要了解以下几个关键概念:
- 切面(Aspect): 切面是一个模块化的单元,它包含一个横跨应用程序的关注点(Concern)。在 Spring AOP 中,切面通常描述了一类横切关注点,比如日志记录、性能统计、事务管理等。切面可以被认为是一个交叉关注点(cross-cutting concern),与业务逻辑独立存在,可以在应用的多个地方进行重用。
- 连接点(Join Point): 连接点是在应用程序执行过程中可能被拦截的点。在 Spring AOP 中,连接点通常是方法的执行点。这些点可以是方法调用、方法执行过程中的特定位置,或者是异常处理的点等。
- 通知(Advice): 通知是切面在连接点上执行的动作。通知定义了在连接点处何时执行什么操作。在 Spring AOP 中,有以下几种类型的通知:
- 前置通知(Before Advice):在连接点之前执行的通知。
- 后置通知(After Advice):在连接点之后执行的通知(不考虑方法的返回结果)。
- 返回通知(After Returning Advice):在连接点正常执行后执行的通知(考虑方法的返回结果)。
- 异常通知(After Throwing Advice):在连接点抛出异常后执行的通知。
- 环绕通知(Around Advice):包围连接点的通知,可以在连接点前后自定义操作。
- 切点(Pointcut): 切点是一个表达式,它定义了哪些连接点将被匹配到并应用通知。切点表达式允许开发者选择性地将通知应用于特定的连接点。切点使用 AspectJ 切点表达式语言来定义。
- 引入(Introduction): 引入允许我们在现有的类中添加新的方法或属性。通过引入,我们可以为一个类添加一些在源代码中不存在的方法或属性,从而改变类的行为。
- 织入(Weaving): 织入是指将切面与应用程序的目标对象连接起来,并创建一个通知增强的代理对象。织入可以在编译时、类加载时或运行时进行,Spring AOP 采用运行时织入的方式。
spring源码解析之AOP原理详解
Spring中的AOP代理模式
Spring中的AOP责任链执行
BeanFactory和ApplicationContext有什么区别
SpringMVC
拦截器和过滤器的区别及实现
过滤器Filter
依赖于servlet容器,只能在 servlet容器,web环境下使用
拦截器依赖于spring容器,可以在springweb中调用,不管此时Spring处于什么环境
-
过滤器(Filter)能拿到http请求,但是拿不到处理请求方法的信息。
-
拦截器(Interceptor)既能拿到http请求信息,也能拿到处理请求方法的信息,但是拿不到方法的参数信息。
-
切片(Aspect)能拿到方法的参数信息,但是拿不到http请求信息。
注入DispatcherServlet几种方式
一 :实现WebApplicationInitializer
接口,并将实现类注入容器。
@Configuration
@ComponentScan("cn.example.springmvc.boke")
public class WebConfig {
}//使用基于Java的配置,注册并初始化一个DispatcherServlet
public class MyWebApplicationInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {//声明一个Spring-web容器AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();ctx.register(WebConfig.class);//创建并注册DispatcherServletDispatcherServlet servlet = new DispatcherServlet(ctx);//动态的添加ServletServletRegistration.Dynamic registration = servletContext.addServlet("dispatcherServlet", servlet);registration.setLoadOnStartup(1);//指定由DispatcherServlet拦截所有请求(包括静态资源,但不拦截.jsp)registration.addMapping("/");}
}
基于web.xml
来配置DispatcherServlet,如下
<web-app ....><servlet><servlet-name>dispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcherServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping>
</web-app>
三 :继承AbstractAnnotationConfigDispatcherServletInitializer
,并将实现类注入Spring容器。
//配置父子容器,其中容器使用基于注解的配置方式
public class IocInit extends AbstractAnnotationConfigDispatcherServletInitializer {//配置 DispatcherServlet 拦截的路径@Overrideprotected String[] getServletMappings() {return new String[] {"/"};}//设置根容器的配置类@Overrideprotected Class<?>[] getRootConfigClasses() {return new Class[] {RootConfig.class};}//设置子容器的配置类//如果不想形成父子容器,那么只需将下面这个getServletConfigClasses()方法返回null即可@Overrideprotected Class<?>[] getServletConfigClasses() {return new Class[] {WebConfig.class};}
}//由于我们采用的是父子容器,因此这就要求我们编写父子容器的配置文件时,根容器的配置文件(RootConfig)配置非web组件的bean,而子容器的配置文件(WebConfig)配置web组件的bean,同时,也要防止同一组件在不同容器中分别注册初始化,从而出现两个相同bean
//根容器配置类,使用excludeFilters排除掉@Controller注解标注的类和@Configuration注解标注的类,这里之所以要排除掉@Configuration注解标注的类,是为了防止根容器扫描到子容器的配置类WebConfig
@Configuration
@ComponentScan(value = "cn.example.springmvc.boke",excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class),@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class)})
public class RootConfig {
}//子容器配置类,使用includeFilters指定只扫描由@Controller注解标注的类
@Configuration
@ComponentScan(value = "cn.example.springmvc.boke",includeFilters = @ComponentScan.Filter(value = Controller.class, type = FilterType.ANNOTATION))
public class WebConfig {
}
DispatcherServlet调用逻辑
- 根据请求获取HandlerExecutionChain对象
- 根据处理器获取HandlerAdapter
- 执行handle前调用拦截器的preHandle方法,若返回false,处理结束
- 调用handler实际处理请求,获取ModelAndView对象
- 调用拦截器的postHandle方法
- 处理分发结果,渲染视图
- 调用拦截器的afterCompletion 方法
SpringBoot
SpringBoot之启动加载器
- ApplicationRunner
- CommandLineRunner
SpringBoot自定义的类加载器
SpringBoot里面默认使用的哪种代理
SpringBoot里面默认使用动态代理配置在AopAutoConfiguration
类中,类中主要方法:
Spring Boot 2.0.0.RELEASE之前
:
@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {@Configuration@EnableAspectJAutoProxy(proxyTargetClass = false)@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",matchIfMissing = true)public static class JdkDynamicAutoProxyConfiguration {}@Configuration@EnableAspectJAutoProxy(proxyTargetClass = true)@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",matchIfMissing = false)public static class CglibAutoProxyConfiguration {}}
Spring Boot 2.0.0.RELEASE之后
:
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Advice.class)static class AspectJAutoProxyingConfiguration {@Configuration(proxyBeanMethods = false)@EnableAspectJAutoProxy(proxyTargetClass = false)@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",matchIfMissing = false)static class JdkDynamicAutoProxyConfiguration {}@Configuration(proxyBeanMethods = false)@EnableAspectJAutoProxy(proxyTargetClass = true)@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",matchIfMissing = true)static class CglibAutoProxyConfiguration {}}
}
可以看到2.0之前都是用的JDK代理,2.0之后用的Cglib。
-
原因一:CGlib不需要接口
Spring动态代理默认使用CGlib,是因为它可以代理那些没有实现任何接口的类,而JDK动态代理仅能代理实现了接口的类。 -
原因二:CGlib效率高
CGlib相对于JDK动态代理来说,在代理类的创建和执行的速度上更快,因此在某些情况下,使用CGlib代理可以提高系统性能。 -
原因三:JDK代理会导致注解失效
如果Spring是JDK代理,那么就会导致某些注解失效。
强制切换代理:
- Spring可以设置
@EnableAspectJAutoProxy中proxyTargetClass
属性为false来强制使用JDK代理。 - SpringBoot的AOP 默认使用 cglib,且无法通过proxyTargetClass进行修改。
如果想修改的话,在Spring配置文件中添加spring.aop.proxy-target-class=false
。
启动流程
SpringApplication.run
方法创建一个SpringApplication
,SpringApplication构造函数里设置类加载器,判断当前容器类型(就是判断当前容器是否包含指定的类),通过getSpringFactoriesInstances
加载并设置初始化器(Initializer)和监听器(Listener),并设置主类。
再调用一个重载的run方法:获取SpringApplicationRunListeners监听器并启动,准备环境对象,在此方法里面发布一个,创建Spring容器,准Spring容器设置一些参数,执行Spring容器的刷新方法。
重写finishRefresh创建启动Tomcat或相关容器,发布ServletWebServerInitializedEvent
事件。
发布ApplicationReadyEvent
事件.
1、调用有@SpringBootApplication注解的启动类的main方法
2、通过调用SpringApplication内部的run()方法构建SpringApplication对象。创建SpringApplication对象:2.1 PrimarySources 不为空,将启动类赋值给primarySources 对象。2.2 从classpath类路径推断Web应用类型,有三种Web应用类型NONE、SERVLET、REACTIVE2.3 初始化bootstrapRegistryInitializers2.4 初始化ApplicationContextInitializer集合2.5 初始化ApplicationListener2.6 获取StackTraceElement数组遍历,通过反射获取堆栈中有main方法A的。
3、调用SpringBootApplication的run方法。
4、long startTime = System.nanoTime(); 记录项目启动时间。
5、通过BootstrapRegistryInitializer来初始化DefaultBootstrapContext
6、getRunListeners(args)获取SpringApplicationRunListeners监听器
7、 listeners.starting()触发ApplicationStartingEvent事件
8、prepareEnvironment(listeners, bootstrapContext, applicationArguments) 将配置文件读取到容器中,返回ConfigurableEnvironment 对象。
9、printBanner(environment) 打印Banner图,即SpringBoot启动时的图案。
10、根据WebApplicationType从ApplicationContextFactory工厂创建ConfigurableApplicationContext,并设置ConfigurableApplicationContext中的ApplicationStartup为DefaultApplicationStartup
11、 调用prepareContext()初始化context等,打印启动日志信息,启动Profile日志信息,并为BeanFactory中的部分属性赋值。
12、刷新容器,在该方法中集成了Tomcat容器
13、加载SpringMVC.
14、刷新后的方法,空方法,给用户自定义重写afterRefresh()
15、Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime)算出启动花费的时间。
16、打印日志Started xxx in xxx seconds (JVM running for xxxx)
17、listeners.started(context, timeTakenToStartup)触发ApplicationStartedEvent事件监听。上下文已刷新,应用程序已启动。
18、调用ApplicationRunner和CommandLineRunner
19、返回上下文。
配置文件加载顺序
classpath:/,classpath:/config/,file:./,file:./config/
如果配置了spring.config.location
则按配置的来。
配置文件加载时机
SpringBoot获取到环境上下文对象时候,会发布一个环境已准备的事件,其中一个事件监听者ConfigFileApplicationListener
会执行配置文件的加载,并设置进环境遍历上下文中。
自动配置原理
SpringBoot的启动类上有@SpringBootApplication这个注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}
), @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {......
}
由@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan注解组成。
@SpringBootConfiguration其实就是一个@Configuration,表明这是一个配置类,可以向容器注入组件。
@EnableAutoConfiguration由@AutoConfigurationPackage和@Import({AutoConfigurationImportSelector
.class})注解组成。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {......
}
@AutoConfigurationPackage内部用到了@Import导入Registrar
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {......
}
被@Configuration标注的类会在Spring刷新时候
invokeBeanFactoryPostProcessors
方法中被解析,解析入口是BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry,ConfigurationClassPostProcessor
类实现。
Dubbo
入门使用案例
SpringCloud
常用组件 :
-
Netflix Eureka:服务注册中心
-
Netflix Ribbon:客户端负载均衡
-
OpenFeign:声明式的 HTTP 客户端
-
Netflix Hystrix:断路器模式
-
Spring Cloud Gateway:网关路由
-
Spring Cloud Sleuth:分布式链路追踪
-
Spring Cloud Config:配置中心
-
Spring Cloud Bus:消息总线
-
Spring Cloud Security:安全框架
Netty
netty工作流程
Netty 起多少线程?何时启动
Netty的默认启动了电脑可用线程数的两倍,在调用了bind方法的时候执行。
Seata
Seata术语
TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器(发起者,同时也是RM的一种) 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器(每个参与事务的微服务) 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
通过TM向TC注册并开启全局事务后,调用Stock(RM)执行本地事务,并记录它的回滚日志通知TC。 然后调用Order(RM)执行本地事务,并记录回滚日志通知TC,然后有Order服务调用Account服务,Account记录本地日志后。 通知TC,当TC收到所有的事务参与者(Stock,Order,Account)的执行成功消息,然后就向各个事务参与者发送提交的消息,如果有任何一个服务有失败或超时,TC将向所有的事务参与者发送Rollback的消息。
XA 模式 :
- 一阶段:
事务协调者通知每个事物参与者执行本地事务
本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁 - 二阶段:
事务协调者基于一阶段的报告来判断下一步操作
如果一阶段都成功,则通知所有事务参与者,提交事务
如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
是一种强一致性的标准
AT模式 :
- 阶段一RM的工作:
注册分支事务
记录undo-log(数据快照)
执行业务sql并提交
报告事务状态 - 阶段二提交时RM的工作:
删除undo-log即可 - 阶段二回滚时RM的工作:
根据undo-log恢复数据到更新前
AT模式下,当前分支事务执行流程如下:
一阶段:
1)TM发起并注册全局事务到TC
2)TM调用分支事务
3)分支事务准备执行业务SQL
4)RM拦截业务SQL,根据where条件查询原始数据,形成快照。
5)RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90
6)RM报告本地事务状态给TC
二阶段:
1)TM通知TC事务结束
2)TC检查分支事务状态
a)如果都成功,则立即删除快照
b)如果有分支事务失败,需要回滚。读取快照数据({“id”: 1, “money”: 100}),将快照恢复到数据库。此时数据库再次恢复为100
A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID。
服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖。
A服务执行分支事务,向数据库做操作。
A服务开始远程调用B服务,此时XID会在微服务的调用链上传播。
B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖。
B服务执行分支事务,向数据库做操作。
全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚。
TC协调其管辖之下的所有分支事务,决定是否回滚。
Seata入门
Seata入门
Zookeeper
基本命令
ls / :查看Zookeeper中包含的key
create : 在树中的某个位置创建一个节点
delete : 删除一个节点存在:测试节点是否存在于某个位置
get data : 从节点读取数据
set data: 将数据写入节点
get children : 检索节点的子节点列表
sync : 等待数据被传播
命令基本语法 | 功能描述 |
---|---|
help | 显示所有操作命令 |
ls path | 使用 ls 命令来查看当前 znode 的子节点 [可监听]-w监听子节点变化-s 附加次级信息 |
create | 普通创建s 含有序列-e 临时(重启或者超时消失) |
get path | 获得节点的值 [可监听]-w 监听节点内容变化-s 附加次级信息 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
deleteall | 递归删除节点 |
Zookeeper入门
Zookeeper的应用场景
- 统一配置管理
- 统一命名服务
- 分布式锁
- 集群管理
- 分布式队列
- 数据发布订阅
zk中节点znode的类型
1、持久节点:创建出的节点,在会话结束后依然存在。保存数据
2、持久序号节点:创建出的节点,根据先后顺序,会在节点之后带上一个数值,越后执行数值越大,适用于分布式锁的应用场景-单调递增
3、临时节点:临时节点是在会话结束后,自动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。
4、临时序号节点:跟持久序号节点相同,适用于临时的分布式锁
5、Container节点(3.5.3版本新增):Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除
6、TTL节点:可以指定节点的到期时间,到期后被zk定时删除。只能通过系统配置zookeeper.extendedTypeEnablee=true开启
创建顺序节点命令(加上 “-s”参数):create -s /module1/app app
意思是我们创建了一个持久顺序节点“/module1/app0000000001” 如果再执行上面命令 会生成节点 “/module1/app0000000002”,同理 如果我们 create -s后面添加 -e 参数,就表示我们创建了一个临时节点。
zookeeper集群中的节点有三种角色
- Leader:处理集群的所有事务请求,集群中只有一个Leader
- Follwoer:只能处理读请求,参与Leader选举
- Observer:只能处理读请求,提升集群读的性能,但不能参与Leader选举
ZAB协议定义的四种节点状态
- Looking:选举状态
- Following:Following节点(从节点)所处的状态
- Leading:Leader节点(主节点)所处状态
ZooKeeper特性:
1、集群角色
2、原子性:更新成功或失败,没有部分结果。
3、高性能
4、高可靠:一旦应用更新了,它将从那时起一直存在,直到客户端覆盖更新。
5、顺序一致性:Zookeeper保证 来自客户端的更新将按发送顺序处理。
6、及时性: 系统的客户视图保证在特定时间范围内是最新的。
7、数据模型和分层命名空间:树结构
8、watch机制:数据变更监听机制
ZooKeeper是弱一致性,能保证最终一致性。
zookeeper使用的ZAB协议进行主从数据同步,ZAB协议认为只要是过半数节点写入成为,数据就算写成功了,然后会告诉客户端A数据写入成功,如果这个时候客户端B恰好访问到还没同步最新数据的zookeeper节点,那么读到的数据就是不一致性的,因此zookeeper无法保证写数据的强一致性,只能保证最终一致性,而且可以保证同一客户端的顺序一致性。
但也可以支持强一致性,通过sync()方法与Leader节点同步后可保证当前节点数据与Leader一致。
加锁使用
XXL-JOB
实现方式 :
- 继承抽象类IJobHandler中的execute()方法,IJobHandler还有init()和destory()方法;放入bean容器;
- 为Job方法添加注解 “@XxlJob(value=“自定义jobhandler名称”, init = “JobHandler初始化方法”, destroy = “JobHandler销毁方法”)”
路由策略:当执行器集群部署时,提供丰富的路由策略,包括:
FIRST(第一个):固定选择第一个机器;
LAST(最后一个):固定选择最后一个机器;
ROUND(轮询):;
RANDOM(随机):随机选择在线的机器;
CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片 参数开发分片任务;
任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
设计模式
Spring设计模式:
(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式:Bean默认为单例模式
(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
责任链
spring中的设计模式
Mybatis设计模式
工厂方法模式:Mybatis中的SqlSessionFactory就是使用工厂方法模式实现的。
代理模式:使用动态代理为Mapper接口创建代理对象,代理对象自动实现Mapper接口的所有方法,并将方法与SqlSession相关联。
装饰器模式:Mybatis中使用装饰器模式来实现插件功能,插件可以动态地增加或修改Sql语句的执行逻辑。
模板方法模式:MappedStatement和BaseStatementHandler都实现了一套通用的Sql语句处理逻辑,而将一些特定的处理逻辑留给子类来实现。因此,这两个模块是模板方法模式的一个典型实现。
单例模式
组合模式:例如SqlNode和各个子类ChooseSqlNode等
1、Builder模式:例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
2、工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory
3、单例模式:例如LogFactory、ErrorContext
4、代理模式:mybatis实现的核心,比如MapperProxy、ConnectionLogger、用的jdk的动态代理,还有executor.loader包使用了cglib或者javassist达到延迟加载的效果
5、组合模式:例如SqlNode和各个子类ChooseSqlNode等
6、模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler
7、适配器模式:例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现
8、装饰者模式:例如Cache包中的cache.decorators子包中的各个装饰者的实现
9、迭代器模式:例如迭代器模式PropertyTokenizer
原型模式:复制
静态内部类单例
工厂模式
策略模式:登录接口不同
动态代理的理解和具体实现
- JDK实现动态代理
jdk动态代理是jdk原生就支持的一种代理方式,它的实现原理,就是通过让target类和代理类实现同一接口,代理类持有target对象,来达到方法拦截的作用,这样通过接口的方式有两个弊端,一个是必须保证target类有接口,第二个是如果想要对target类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明,实现上略微有点限制
首先创建一个InvocationHandler
实现类,调用Proxy#newProxyInstance
方法,传入和被代理对象使用相同的类加载器
,和被代理对象具有相同的行为。实现相同的接口
,InvocationHandler
实现类,即可返回目标类的动态代理。
- Cglib实现动态代理
它的底层使用ASM在内存中动态的生成被代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理。
cglib有两种可选方式:继承和引用。第一种是基于继承实现的动态代理,所以可以直接通过super调用target方法,但是这种方式在spring中是不支持的,因为这样的话,这个target对象就不能被spring所管理,所以cglib还是才用类似jdk的方式,通过持有target对象来达到拦截方法的效果。
CGLIB的核心类:
- net.sf.cglib.proxy.Enhancer – 主要的增强类
- net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
- net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用
使用cglib需要引入依赖:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>2.2.2</version>
</dependency>
示例代码 :
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;public class CglibProxyTest {static class CglibProxyService {public CglibProxyService(){}void sayHello(){System.out.println(" hello !");}}static class CglibProxyInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object sub, Method method,Object[] objects, MethodProxy methodProxy)throws Throwable {System.out.println("before hello");Object object = methodProxy.invokeSuper(sub, objects);System.out.println("after hello");return object;}}public static void main(String[] args) {// 通过CGLIB动态代理获取代理对象的过程Enhancer enhancer = new Enhancer();// 设置enhancer对象的父类enhancer.setSuperclass(CglibProxyService.class);// 设置enhancer的回调对象enhancer.setCallback(new CglibProxyInterceptor());// 创建代理对象CglibProxyService proxy= (CglibProxyService)enhancer.create();System.out.println(CglibProxyService.class);System.out.println(proxy.getClass());// 通过代理对象调用目标方法proxy.sayHello();}
}
都无法完成Static和private方法的代理。
JDK动态代理和CGLIB动态代理的区别是什么?
支持的接口和类:
- JDK动态代理要求目标类必须实现一个或多个接口。
- CGLIB动态代理则不依赖于接口,即使目标类没有实现接口,也可以通过设置回调接口间接实现代理。
性能:
- 在早期,CGLIB动态代理的性能通常比JDK动态代理高,因为CGLIB动态代理是通过继承目标类并修改其字节码来实现代理的。
- 但是,从JDK 1.7开始,JDK动态代理的反射底层进行了优化,使得性能得到了显著提升,在某些情况下甚至超过了CGLIB动态代理。
对目标类的限制:
- JDK动态代理要求目标类不能被final修饰,且目标类中的方法也不能被final修饰。
- CGLIB动态代理则要求目标类不能被final修饰,但目标类中的方法可以。
生成代理的方式:
- JDK动态代理利用反射机制生成一个包含被代理对象所有接口的代理类,并覆盖接口中的所有方法。
JDK动态代理是基于Java反射机制实现的,它要求目标类必须实现一个或多个接口,代理对象在运行时动态创建,通过实现目标类接口的方式来代理目标类。 - CGLIB动态代理则是通过继承被代理类并修改其字节码来生成代理类,从而实现代理。
CGLIB代理则是基于ASM字节码框架实现的,它可以代理没有实现接口的目标类。CGLIB在运行时通过动态生成目标类的子类来实现代理。
适用场景:
- JDK动态代理适合于需要代理的类已经实现了接口的情况。
- CGLIB动态代理则更适合于没有实现接口的目标类,或者需要对目标类进行更深层次修改的情况。
总结: 一个是接口实现方式,一个是类的继承方式(动态生成类的子类对象)
动态代理案例解析博客
数据库
MySQL中myisam与innodb的区别:
1>.InnoDB支持事物,而MyISAM不支持事物
2>.InnoDB支持行级锁,而MyISAM支持表级锁
3>.InnoDB支持MVCC, 而MyISAM不支持
4>.InnoDB支持外键,而MyISAM不支持
5>.InnoDB不支持全文索引,而MyISAM支持。
事物的4种隔离级别
-
读未提交(RU)
-
读已提交(RC)
-
可重复读(RR)
-
串行
Explain
通过EXPLAIN,可以分析出以下结果:
表的读取顺序
数据读取操作的操作类型
哪些索引被实际使用
表之间的引用
每张表有多少行被优化器查询
id
表示查询语句的序号,自动分配,顺序递增,值越大,执行优先级越高。id相同时,优先级由上而下。select_type
列
select_type表示查询类型,常见的有SIMPLE
简单查询、PRIMARY
主查询、SUBQUERY子查询、UNION
联合查询、UNION RESULT
联合临时表结果等。- table列
table表示SQL语句查询的表名、表别名、临时表名。 - partitions列
partitions表示SQL查询匹配到的分区,没有分区的话显示NULL。 type列
type表示表连接类型或者数据访问类型,就是表之间通过什么方式建立连接的,或者通过什么方式访问到数据的。具体有以下值,性能由好到差依次是:
system > const > eq_ref > ref > ref_or_null > index_merge > range > index > ALL- system
当表中只有一行记录,也就是系统表,是 const 类型的特列。 - const
表示使用主键或者唯一性索引进行等值查询,最多返回一条记录。性能较好,推荐使用。 - eq_ref
表示表连接使用到了主键或者唯一性索引,下面的SQL就用到了user表主键id。 - ref
表示使用非唯一性索引进行等值查询。 - ref_or_null
表示使用非唯一性索引进行等值查询,并且包含了null值的行。 - index_merge
表示用到索引合并的优化逻辑,即用到的多个索引。 - range
表示用到了索引范围查询。 - index
表示使用索引进行全表扫描。 - ALL
表示全表扫描,性能最差。
- system
- possible_keys列
表示可能用到的索引列,实际查询并不一定能用到。 - key列
表示实际查询用到索引列。 - key_len列
表示索引所占的字节数。 - ref列
表示where语句或者表连接中与索引比较的参数,常见的有const(常量)、func(函数)、字段名。如果没用到索引,则显示为NULL。 - rows列
表示执行SQL语句所扫描的行数。 - filtered列
表示按条件过滤的表行的百分比。 - Extra列
表示一些额外的扩展信息,不适合在其他列展示,却又十分重要。- Using where
表示使用了where条件搜索,但没有使用索引。 - Using index
表示用到了覆盖索引,即在索引上就查到了所需数据,无需二次回表查询,性能较好。 - Using filesort
表示使用了外部排序,即排序字段没有用到索引。 - Using temporary
表示用到了临时表,下面的示例中就是用到临时表来存储查询结果。 - Using join buffer
表示在进行表关联的时候,没有用到索引,使用了连接缓存区存储临时结果。 - Using index condition
表示用到索引下推的优化特性。
- Using where
explain案例解析
explain案例解析
Mysql面试题
MVCC
readView
MySQL中有哪些锁:
- 全局锁
锁整个数据库 - 表锁
锁整个表 - 行锁
Record lock:记录锁,锁定单个行记录。
Gap lock:间隙锁,锁定某个记录范围之间的间隙。
Next-key lock:下一个键锁,是记录锁和间隙锁的组合,锁定一个记录和它之前的间隙。
分库分表会带来哪些问题
- 事务一致性问题
- 数据组装
- 跨节点分页、排序、函数问题
- 全局主键避重问题
分库分表后如何保证一致性:分布式事务
索引失效原因有哪些?
-
模糊查询的前导通配符: 当使用模糊查询(如 LIKE ‘%abc’)时,索引失效,因为通配符在前面会导致索引无法使用。
-
未使用索引字段进行过滤: 如果查询条件没有使用到创建的索引字段,数据库可能不会使用该索引。
-
数据类型不匹配: 如果查询条件的数据类型与索引字段的数据类型不匹配,数据库无法使用索引。
-
使用函数操作: 如果查询条件中对字段进行了函数操作(如 LOWER(column)),索引可能失效,因为数据库无法直接使用索引。
-
OR 运算: 在 OR 运算中,如果其中一个条件使用了索引,而另一个条件没有使用索引,整个查询可能会导致索引失效
-
使用 NOT 运算: NOT 运算通常会使索引失效,因为数据库无法使用索引来高效处理 NOT 运算。
-
表连接中的索引失效: 如果在表连接查询中,连接条件中的字段没有索引,可能导致索引失效。
引擎
常用的通用 SQL 函数
事务
ACID的理解
MySQL中哪个引擎支持ACID?哪个不支持?
表中有100万数据,如何从数据库层面做优化
如何做读写分离
Mysql主从复制
-
主数据库有个bin log二进制文件,纪录了所有增删改SQL语句。(binlog线程)
-
从数据库把主数据库的bin log文件的SQL 语句复制到自己的中继日志 relay log(io线程)
-
从数据库的relay log重做日志文件,再执行一次这些sql语句。(Sql执行线程)
从复制过程分了五个步骤进行:
-
主库的更新SQL(update、insert、delete)被写到binlog
-
从库发起连接,连接到主库。
-
此时主库创建一个binlog dump thread,把bin log的内容发送到从库。
-
从库启动之后,创建一个I/O线程,读取主库传过来的bin log内容并写入到relay log
-
从库还会创建一个SQL线程,从relay log里面读取内容,从ExecMasterLog_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db
搭建博客
MQ
相关知识点
RabbitMq知识点
生产者消息如何运转?
1、 Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
2、 Producer声明一个交换器并设置好相关属性。
3、 Producer声明一个队列并设置好相关属性。
4、 Producer通过路由键将交换器和队列绑定起来。
5、 Producer发送消息到Broker,其中包含路由键、交换器等信息。
6、 相应的交换器根据接收到的路由键查找匹配的队列。
7、 如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
8、 关闭信道。
9、 管理连接。
消息什么时候刷到磁盘?
写入文件前会有一个Buffer,大小为1M,数据在写入文件时,首先会写入到这个Buffer,如果Buffer已满,则会将Buffer写入到文件(未必刷到磁盘)。
有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每个25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘。
每次消息写入后,如果没有后续写入请求,则会直接将已写入的消息刷到磁盘:使用Erlang的receive x after 0实现,只要进程的信箱里没有消息,则产生一个timeout消息,而timeout会触发刷盘操作。
Rabbit死信的来源
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
SpringBoot发布确认
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
- NONE
禁用发布确认模式,是默认值 - CORRELATED
发布消息成功到交换器后会触发回调方法 - SIMPLE
我们先来介绍下RabbitMQ三种部署模式:
-
单节点模式: 最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
-
普通模式: 消息只会存在与当前节点中,并不会同步到其他节点,当前节点宕机,有影响的业务会瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
-
镜像模式: 消息会同步到其他节点上,可以设置同步的节点个数,但吞吐量会下降。属于RabbitMQ的HA方案
惰性队列
消息村磁盘不存内存。
一些面试题整理
RocketMq知识点
nameserver 默认使⽤ 9876 端⼝
master 默认使⽤ 10911 端⼝
slave 默认使⽤11011 端⼝
RocketMq启动流程
- 启动NameServer,NameServer起来后监听端⼝,等待Broker、Producer、Consumer连上来,相当于⼀个路由控制中⼼。
- Broker启动,跟所有的NameServer保持⻓连接,定时发送⼼跳包。⼼跳包中包含当前Broker信息(IP+端⼝等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
- 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时⾃动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的其中⼀台建⽴⻓连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择⼀个队列,然后与队列所在的Broker建⽴⻓连接从⽽向Broker发消息。
- Consumer跟Producer类似,跟其中⼀台NameServer建⽴⻓连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建⽴连接通道,开始消费消息。
Dleger⾼可⽤集群搭建
搭建集群配置文件:
#所属集群名字,名字⼀样的节点就在同⼀个集群内
brokerClusterName=rocketmq-cluster
#broker名字,名字⼀样的节点就是⼀组主从节点。
brokerName=broker-a
#brokerid,0就表示是Master,>0的都是表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=worker1:9876;worker2:9876;worker3:9876
#在发送消息时,⾃动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker ⾃动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker ⾃动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端⼝
listenPort=10911
#删除⽂件时间点,默认凌晨 4点
deleteWhen=04
#⽂件保留时间,默认 48 ⼩时
fileReservedTime=120
#commitLog每个⽂件的⼤⼩默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个⽂件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理⽂件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/opt/rocketmq/store
#commitLog 存储路径
storePathCommitLog=/opt/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/opt/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/opt/rocketmq/store/index
#checkpoint ⽂件存储路径
storeCheckpoint=/opt/rocketmq/store/checkpoint
#abort ⽂件存储路径
abortFile=/opt/rocketmq/store/abort
#限制的消息⼤⼩
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的⻆⾊
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘⽅式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
topic
:消息主题,一级消息类型,通过Topic对消息进行分类。
Tag
:消息标签,二级消息类型,用来进一步区分某个Topic下的消息分类。
key
:消息的业务标识,由消息生产者(Producer)设置,唯一标识某个业务逻辑
Tag 和 Key 的主要差别是使用场景不同,Tag 用在 Consumer 代码中,用于服务端消息过滤,Key 主要用于通过命令进行查找消息。
RocketMQ 并不能保证 message id 唯一,在这种情况下,生产者在 push 消息的时候可以给每条消息设定唯一的 key, 消费者可以通过 message key 保证对消息幂等处理。
高可用 :
- 普通集群
只保存引用,不保存队列实际内容 - 镜像集群
保存队列实际内容,主节点宕机从节点会替换。 - 仲裁队列
主从同步一致性
RocketMQ原生API使用 gitcode
整合SpringAPI
几种消息机制:
-
批量消息
-
事务消息
-
顺序消息
-
延迟消息
-
消息重试
重试的消息会进⼊⼀个 “%RETRY%”+ConsumeGroup 的队列中。然后RocketMQ默认允许每条消息最多重试16次。时间间隔与延时消息一致。 -
死信队列
普通消息
顺序消息
广播消息
延时消息
批量消息
事务消息
消息存储文件 commitLog
rocketmq 的消息存储在 commitLog 中,broker 默认会给 commitLog 申请 1G的磁盘空间。这是为了保证存储空间是有序的。
如果 commitLog 已经满了,会继续创建第二个 commitLog,且它的文件名最后几位是上一个 commitLog 的最大偏移量 masOffset。查找的时候,如果第一个文件中没找到,那么会计算它的最大偏移量,获取下一个要查找的 commitLog 的名称。
索引文件 IndexFile,索引文件是用来支持快速地查找消息的。
rocketmq 支持两种查询方式:根据key;根据time;
RocketMQ消息消费本质上是基于的拉(pull)模式
Kafka知识点
Consumer Group(CG):消费者组,由多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同。
- 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。
- 消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
生产者如何提高吞吐量
• batch.size:批次大小,默认16k
• linger.ms:等待时间,修改为5-100ms;一次拉一个,来了就走
• compression.type:压缩snappy
生产经验——生产者如何提高吞吐量
• RecordAccumulator:缓冲区大小,修改为64m
// batch.size:批次大小,默认 16Kproperties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);// linger.ms:等待时间,默认 0properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);// RecordAccumulator:缓冲区大小,默认 32M:buffer.memoryproperties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);// compression.type:压缩,默认 none,可配置值 gzip、snappy、 lz4 和 zstdproperties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
Kafka总体工作流程
(1) Kafka集群的每个broker启动之后都会向zookeeper进行注册。
(2) 注册完毕之后开始选择controller节点(争先抢占方式)。
(3) 选举出来的controller监听/brokers/ids/节点的变化。
(4) 监控完毕之后根据选举规则开始真正的选举Leader。
(5) Controller将节点的Leader信息和isr信息写到zookeeper上。
(6) 其它的controller节点会冲zookeeper上拉取数据进行同步(防止controllerLeader挂了,随时上位)。
(7) 生产者往集群发送数据,发送数据之后Leader主动与Follower进行同步(底层通过LOG进行存储,实际为segment,分为.log文件和.index文件)再进行应答。
(8) 当Leader节点挂了之后controller监控到节点变化。
(9) Controller从zookeeper上拉取Leader信息和isr信息。
(10) Controller根据拉取的信息和选举规则再重新选举Leader。
(11) 选举出来新的Leader之后更新zookeeper中的信息。
Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。
AR = ISR + OSR
ISR,表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由replica.lag.time.max.ms
参数设定,默认 30s。Leader 发生故障之后,就会从 ISR 中选举新的 Leader。
OSR,表示 Follower 与 Leader 副本同步时,延迟过多的副本(速率和leader相差大于10秒的follower)。
Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间。
- log.retention.hours,最低优先级小时,默认 7 天。
- log.retention.minutes,分钟。
- log.retention.ms,最高优先级毫秒。
- log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。
delete 日志删除:将过期数据删除; log.cleanup.policy = delete 所有数据启用删除策略
- 基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。
- 基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。
Kafka分区分配策略
- Range策略(范围)。这是最常用的分配策略,它确保每个消费者线程消费的分区数量大致相等。Range策略首先对所有分区按照序号排序,然后对消费者线程按照字典顺序排序。接下来,它为每个消费者线程分配一定数量的分区,直到所有分区被分配完毕。如果还有剩余分区,那么排序靠前的消费者线程会被分配额外的分区。
- Round Robin策略(循环)。在这种策略下,每个消费者线程依次按顺序获得一个分区。当消费者数量多于分区数量时,多余的消费者将没有分配到任何分区。把所有的 partition 和所有的consumer 都列出来,然后按照 hashcode 进行排序,最后
通过轮询算法来分配 partition 给到各个消费者。 - Sticky策略(粘性)。这种策略下,消费者会尽量保持与之前分配的分区相同。如果有新的消费者加入或有消费者退出,分区的重新分配会尽量减少,这对于需要保持状态的应用程序比较有用。
- Cooperative策略。这是Kafka 2.4.0版本引入的新策略,它通过考虑消费者的健康状况、处理速度、网络延迟等因素,动态地进行分区分配,以实现更好的负载均衡和消费者协作。
默认情况下,Kafka使用Range分配策略,但也可以通过配置参数partition.assignment.strategy
来自定义分配策略
roducer将消息推送到broker,consumer从broker拉取消息。
Kafka面试题
角色
RabbitMq
- 生产者(Publisher):发送消息的应用。
- 消费者(Consumer):接收消息的应用。
- 连接(Connection):它使用TCP进行可靠的传输,连接RabbitMQ和应用服务器。
- 信道(Channel):连接里的一个虚拟通道,通过消息队列发送或者接收消息时,都是通过信道进行的。Connection内部建立的逻辑连接,通常每个线程创建单独的Channel。
- 交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息队列里。换种理解:快递分拣中心。
- 队列(Queue):它们存储应用程序发送的消息。
- 代理(Broker):接收和分发消息的应用,RabbitMQ Server就是Message Broker。
- 消息(Message):由生产者通过RabbitMQ发送给消费者的信息。
- 绑定(Binding):绑定是交换机用来将消息路由到队列的规则。
- 路由键(Routing Key):消息的目标地址。换种理解:寄快递填写的地址。
RocketMQ
-
Producer:消息发布的⻆⾊,⽀持分布式集群⽅式部署。Producer通过MQ的负载均衡模块选择相
应的Broker集群队列进⾏消息投递,投递的过程⽀持快速失败并且低延迟。 -
Consumer:消息消费的⻆⾊,⽀持分布式集群⽅式部署。⽀持以push推,pull拉两种模式对消息进⾏消费。同时也⽀持集群⽅式和⼴播⽅式的消费,它提供实时消息订阅机制,可以满⾜⼤多数⽤户的需求。
-
NameServer:NameServer是⼀个⾮常简单的Topic路由注册中⼼,其⻆⾊类似Dubbo中的zookeeper,⽀持Broker的动态注册与发现,主要包括两个功能:
- Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供⼼跳检测机制,检查Broker是否还存活;
- 路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和⽤于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从⽽进⾏消息的投递和消费。
NameServer通常也是集群的⽅式部署,各实例间相互不进⾏信息通讯。Broker是向每⼀台NameServer注册⾃⼰的路由信息,所以每⼀个NameServer实例上⾯都保存⼀份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。多个Namesrv实例组成集群,但相互独⽴,没有信息交换。
-
BrokerServer:Broker主要负责消息的存储、投递和查询以及服务⾼可⽤保证,为了实现这些功能,Broker包含了以下⼏个重要⼦模块。
- Remoting Module:整个Broker的实体,负责处理来⾃clients端的请求。
- Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
- Store Service:提供⽅便简单的API接⼝处理消息存储到物理硬盘和查询功能。
- HA Service:⾼可⽤服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
- Index Service:根据特定的Message key对投递到Broker的消息进⾏索引服务,以提供消息的快速
查询。
Kafka
- Broker(代理)
Kafka集群通常由多个代理组成以保持负载平衡。 Kafka代理是无状态的,所以他们使用ZooKeeper来维护它们的集群状态。 一个Kafka代理实例可以每秒处理数十万次读取和写入,每个Broker可以处理TB的消息,而没有性能影响。 Kafka经纪人领导选举可以由ZooKeeper完成。
- ZooKeeper
ZooKeeper用于管理和协调Kafka代理。 ZooKeeper服务主要用于通知生产者和消费者Kafka系统中存在任何新代理或Kafka系统中代理失败。 根据Zookeeper接收到关于代理的存在或失败的通知,然后生产者和消费者采取决定并开始与某些其他代理协调他们的任务。
- Producers(生产者)
生产者将数据推送给经纪人。 当新代理启动时,所有生产者搜索它并自动向该新代理发送消息。 Kafka生产者不等待来自代理的确认,并且发送消息的速度与代理可以处理的一样快。
- Consumers(消费者)
因为Kafka代理是无状态的,这意味着消费者必须通过使用分区偏移来维护已经消耗了多少消息。 如果消费者确认特定的消息偏移,则意味着消费者已经消费了所有先前的消息。 消费者向代理发出异步拉取请求,以具有准备好消耗的字节缓冲区。 消费者可以简单地通过提供偏移值来快退或跳到分区中的任何点。 消费者偏移值由ZooKeeper通知。
消息模型
RabbitMq
消息类型 :
- direct::如果路由键完全匹配,消息就被投递到相应的队列
- fanout:如果交换器收到消息,将会广播到所有绑定的队列上
- topic :可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符
*(星号)可以代替一个单词;#(井号)可以替代零个或多个单词
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
队列具备两种模式:default 和 lazy(惰性队列
)。默认的为 default 模式。
队列的其他参数设置:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
//设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
params.put("x-max-priority", 10);
channel.queueDeclare("myqueue", false, false, false, args);
RocketMQ
- Producer:消息的发送者, 负责⽣产消息;举例:发信者
- Consumer:消息接收者,负责消费消息;举例:收信者
- Broker:暂存和传输消息,负责存储消息;举例:邮局
Broker 在实际部署过程中对应⼀台服务器,每个Broker 可以存储多个Topic的消息,每个Topic的消息也可以分⽚存储于不同的 Broker。 - NameServer:管理Broker;举例:各个邮局的管理机构
- Topic:区分消息的种类;⼀个发送者可以发送消息给⼀个或者多个Topic;⼀个消息的接收者可以订阅⼀个或者多个Topic消息。
每个 topic 默认包含4个队列,每个队列对应一个持久化文件queuelog,它存储的是每条消息在commitlog中的位置等信息。 - Message Queue:相当于是Topic的分区;⽤于并⾏发送和接收消息。⼀个queueId就代表了⼀个MessageQueue。每个Topic中的消息地址存储于多个 Message Queue 中。
RocketMQ的概念模型详解
Kafka
死信队列
消息变成死信一般是由于以下几种情况:
- 重试次数超限: 消息在处理过程中多次重试仍然失败,达到预定的重试次数上限;
- 消息被拒绝:( Basic.Reject/Basic.Nack ),并且设置 requeue 参数为 false ;
3.消息过期:消息在队列中等待时间过长,超过了设置的过期时间 ; - 队列满: 当消息队列的长度达到上限时,新的消息可能成为死信。
RocketMQ、kafka没有提供相应的设计。 kafka中的重试队列是我们通过业务实现的,自己新加几个重试主题,消费者消费失败后就将消息发给重试队列
怎么避免消息丢失/消息可靠性
消息丢失的三种情况
- 消息在传入过程中丢失
- MQ收到消息,暂存内存中,还没消费,自己挂掉了,内存中的数据搞丢
- 消费者消费到了这个消息,但还没来得及处理,就挂了,MQ以为消息已经被处理
也就是生产者丢失消息、消息列表丢失消息、消费者丢失消息;
RabbitMq
针对生产者 :
一、开启RabbitMQ事务
可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。如果 txCommit 提交成功了,则消息一定到达了 broker 了,如果在 txCommit执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务。
// 开启事务
channel.txSelect
try {// 这里发送消息
} catch (Exception e) {channel.txRollback// 这里再次重发这条消息}// 提交事务
channel.txCommit
缺点:
RabbitMQ 事务机制是同步的,你提交一个事务之后会阻塞在那儿,采用这种方式基本上吞吐量会下来,因为太耗性能。
二、使用confirm机制
事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的。
已经在事务模式的 channel 是不能再设置成 confirm 模式的,即这两种模式是不能共存的。
在生产者开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,告诉你这个消息发送OK了;如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
confirm 模式又包含三种模式:
-
普通 confirm 模式:每发送一条消息后,调用 waitForConfirms()方法,等待服务器端confirm。实际上是一种串行 confirm 了。
-
批量 confirm 模式:每发送一批消息后,调用 waitForConfirms()方法,等待服务器端confirm。
-
异步 confirm 模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后 Client 端会回调这个方法;
confirm机制代码示例-知乎
confirm机制代码示例
针对RabbitMQ服务端
一、消息持久化
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。
所以就要对消息进行持久化处理。如何持久化,下面具体说明下:
要想做到消息持久化,必须满足以下三个条件,缺一不可。
-
Exchange 设置持久化
-
Queue 设置持久化
-
Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
持久化基础参数示例
Spirng持久化示例
消息在正确存入RabbitMQ之后,还需要有一段时间(这个时间很短,但不可忽视)才能存入磁盘之中,RabbitMQ并不是为每条消息都做fsync的处理,可能仅仅保存到cache中而不是物理磁盘上,在这段时间内RabbitMQ broker发生crash, 消息保存到cache但是还没来得及落盘,那么这些消息将会丢失。那么这个怎么解决呢:镜像队列
二、设置集群镜像模式
从consumer端来说,如果这时autoAck=true,那么当consumer接收到相关消息之后,还没来得及处理就crash掉了,那么这样也算数据丢失,这种情况也好处理,只需将autoAck设置为false(方法定义如下),然后在正确处理完消息之后进行手动ack(channel.basicAck)
三、消息补偿机制
生产端首先将业务数据以及消息数据入库,需要在同一个事务中,消息数据入库失败,则整体回滚。
根据消息表中消息状态,失败则进行消息补偿措施,重新发送消息处理。
针对消费者
ACK确认机制
使用rabbitmq提供的ack机制,服务端首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。才把消息从内存删除。
这样就解决了,即使一个消费者出了问题,但不会同步消息给服务端,会有其他的消费端去消费,保证了消息不丢的case。
public static void main(String[] args) throws Exception {Channel channel = RabbitMqUtils.getChannel();System.out.println("C1 等待接收消息处理时间较短");//消息消费的时候如何处理消息DeliverCallback deliverCallback=(consumerTag, delivery)->{String message= new String(delivery.getBody());System.out.println("接收到消息:"+message);/*** 1.消息标记 tag* 2.是否批量应答未应答消息*/channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);//channel.basicNack();//channel.basicReject();};CancelCallback cancelCallback = consumerTag -> System.out.println(consumerTag+"消费者取消消费接口回调逻辑");//采用手动应答boolean autoAck=false;channel.basicConsume(ACK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
总结 :
如果需要保证消息在整条链路中不丢失,那就需要生产端、mq自身与消费端共同去保障。
-
生产端: 对生产的消息进行状态标记,开启confirm机制,依据mq的响应来更新消息状态,使用定时任务重新投递超时的消息,多次投递失败进行报警。
-
mq自身: 开启持久化,并在落盘后再进行ack。如果是镜像部署模式,需要在同步到多个副本之后再进行ack。
-
消费端: 开启手动ack模式,在业务处理完成后再进行ack,并且需要保证幂等。
RocketMQ
Broker在把消息写入日志文件的过程中,如果在刚收到消息时,Broker异常宕机了,那么内存中尚未写入磁盘的消息就会丢失了。
因此,RocketMQ持久化消息分为两种:同步刷盘和异步刷盘(默认配置)。
异步刷盘是指Broker收到消息后先存储到PageCache,然后立即通知Producer消息已存储成功,可以继续处理业务逻辑。
此后,Broker会启动一个异步线程将消息持久化到磁盘。然而,如果Broker在持久化到磁盘之前发生故障,消息将会丢失。
生产者使用同步发送:
SendReceipt sendReceipt = producer.send(message);
消费手动提交
consumer.registerMessageListener(new MessageListenerOrderly() {@Overridepublic ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {//自动提交context.setAutoCommit(false);for(MessageExt msg:msgs){System.out.println("收到消息内容 "+new String(msg.getBody()));}return ConsumeOrderlyStatus.SUCCESS;}
});
RocketMQ可靠性
Kafka
重平衡会导致消息丢失:同步+异步提交:代码块异步提交,捕获异常处理异常再同步提交
consumer使用同步提交Offset:consumer.commitSync()
Producer设置ACK:
properties.put(ProducerConfig.ACKS_CONFIG, "all");
- producer 级别:acks=all(或者 request.required.acks=-1),同时发生模式为同步 producer.type=sync
- topic 级别:设置 replication.factor>=3,并且 min.insync.replicas>=2;
- broker 级别:关闭不完全的 Leader 选举,即 unclean.leader.election.enable=false;
总结 :
rabbit
生产者 : 开启confirm、Message持久化发送
服务端: Exchange 、Queue消息持久化、设置集群镜像模式
消费者: ACK确认机制rocketmq:同步刷盘、消费者确认、同步发送kafka :
生产者 : 设置ACK
服务端:设置ACK :0,1,-1
消费者:consumer.commitSync()同步提交offset
顺序消费
要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才⾏。
RabbitMq :
需要保障以下几点:
1、发送的顺序消息,必须保证在投递到同一个队列,且这个消费者只能有一个(独占模式)
2、然后同意提交(可以合并一个大消息,或拆分多个消息,最好是拆分),并且所有消息的会话ID一致
3、添加消息属性:顺序表及的序号、本地顺序消息的size属性,进行落库操作
4、并行进行发送给自身的延迟消息(带上关键属性:会话ID、SIZE)进行后续处理消费
5、当收到延迟消息后,根据会话ID、SIZE抽取数据库数据进行处理即可
6、定时轮询补偿机制,对于异常情况
RocketMq
发生方使用 MessageQueueSelector
,消费方使用MessageListenerOrderly
Message msg = new Message("OrderTopicTest", "order_"+orderId,"KEY" + orderId,("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
//消息队列的选择器
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {//第一个参数:所有的消息,第二个参数:发送的消息,第三个参数:根据什么发送,这里面传的是orderId@Overridepublic MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {Integer id = (Integer) arg;int index = id % mqs.size();return mqs.get(index);}//同一个订单id可以放到同一个队列里面去
}, orderId);
consumer.registerMessageListener(new MessageListenerOrderly() {@Overridepublic ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {//自动提交context.setAutoCommit(true);for(MessageExt msg:msgs){System.out.println("收到消息内容 "+new String(msg.getBody()));}return ConsumeOrderlyStatus.SUCCESS;}
});
Kafka
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 消费某个主题的某个分区数据
ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
topicPartitions.add(new TopicPartition("first", 0));
kafkaConsumer.assign(topicPartitions);
消息幂等性
消息传输保证层级:
-
At most once:最多一次。消息可能会丢失,单不会重复传输。
-
At least once:最少一次。消息觉不会丢失,但可能会重复传输。
-
Exactly once: 恰好一次,每条消息肯定仅传输一次。
在互联⽹应⽤中,尤其在⽹络不稳定的情况下,消息队列 的消息有可能会出现重复,这个重复简单可以概括为以下情况:
- 发送时消息重复
- 投递时消息重复
- 负载均衡时消息重复(包括但不限于⽹络抖动、Broker 重启以及订阅⽅应⽤重启)
RocketMQ:
- RocketMQ支持消息查询的功能,只要去RocketMQ查询一下是否已经发送过该条消息就可以了,不存在则发送,存在则不发送;
- 引入Redis,在发送消息到RocketMQ成功之后,向Redis中插入一条数据,如果发送重试,则先去Redis查询一个该条消息是否已经发送过了,存在的话就不重复发送消息了;
发送的时候为Message设置一个唯一的keys
RocketMQ幂等性
Kafka服务端使用幂等性 :开启参数 enable.idempotence
默认为 true,false 关闭。
具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。所以Kafka幂等性只能保证的是在单分区单会话内不重复。
消费端 :需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)
消息持久化
rabbitmq :
定义队列、消息都为持久化才行
RocketMq
RocketMQ消息的存储分为三个部分:
- CommitLog:存储消息的元数据。所有消息都会顺序存⼊到CommitLog⽂件当中。CommitLog由多个⽂件组成,每个⽂件固定⼤⼩1G。以第⼀条消息的偏移量为⽂件名。
- ConsumerQueue:存储消息在CommitLog的索引。⼀个MessageQueue⼀个⽂件,记录当前MessageQueue被哪些消费者组消费到了哪⼀条CommitLog。
- IndexFile:为了消息查询提供了⼀种通过key或时间区间来查询消息的⽅法,这种通过IndexFile来查找消息的⽅法不影响发送与消费消息的主流程
Kafka
设置异步发送回调函数,发送失败了会有异常
设置消息重试
数据积压
Kafka
1)如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)
2)如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。从一次最多拉取500条,调整为一次最多拉取1000条
为了处理MQ队列堆积的问题,可以采取以下几种措施:
-
增加消费者的数量:通过增加消费者的数量,可以提高消息的处理速度,从而减少堆积。这有助于减少消费时间,避免因单个消费者处理速度慢而导致整个队列的堵塞。
-
优化消费者的处理逻辑:对消费者的处理逻辑进行优化,提高处理效率,减少消费时间。这样可以更快地处理消息,减少堆积的可能性。
-
监控和预警:建立监控系统,及时监测消息队列的堆积情况,当消息堆积达到一定阈值时,及时发出预警,以便及时处理。
-
增加消息队列的容量:如果消息队列的容量不足以处理高峰期的消息量,可以考虑增加消息队列的容量,以便更好地处理消息堆积。
-
增加消息队列的可用性:通过多副本或者备份机制,提高消息队列的可用性,减少由于故障或宕机导致的消息堆积。
-
重试机制:在消费者处理消息失败时,可以设置重试机制,将失败的消息重新放回队列,等待后续处理,防止消息丢失。
-
调整超时设置:调整超时设置可以缩短等待时间并提高消费速度。例如,在某些情况下,因为某些原因(例如网络延迟),MQ消费者需要等待更长时间才能接收到新的消息。
-
批量操作:通过批量操作来减少每条信息之间的交互次数也是一种有效减少MQ堆积问题的方法。例如,在生产者端,
集群
Docker
docker网络 :
- bridge :
使用docker run创建Docker容器时,可以用 --net 或 --network 选项指定容器的网络模式
●host模式:使用 --net=host 指定。
●none模式:使用 --net=none 指定。
●container模式:使用 --net=container:NAME_or_ID 指定。
●bridge模式:使用 --net=bridge 指定,默认设置,可省略。
Redis
持久化机制
Redis提供了两种持久化方式:RDB
(save、bgsave)和AOF
。
RDB是一种快照(snapshot)持久化方式
,通过将当前数据集的所有数据写入磁盘文件,实现数据的持久化。在RDB持久化过程中,Redis会生成一个压缩过的二进制文件,该文件包含了某个时间点上数据库中所有的键值对。
使用场景:定期备份数据;数据集比较大时,启动时加载数据速度更快。
AOF是一种追加写日志文件的持久化方式
,它会将每一个写操作都记录到日志文件中。Redis在启动时会通过重新执行这个日志文件中的命令来恢复数据。使用场景:实时持久化,适用于需要实时同步数据到磁盘的场景;对于需要追溯操作历史的场景,AOF更有优势。Always、everysec、no
RDB中的save和bgsave
save的优缺点
优点:
- 简单,易于理解和操作;
- 生成的RDB文件相对较小。
缺点:
-
阻塞期间,Redis不能处理任何请求,影响性能;
-
需要谨慎使用,特别是在数据量较大的情况下,可能导致阻塞时间过长。
bgsave命令是在后台异步执行的,不会阻塞主进程。执行bgsave时,Redis会fork出一个子进程,由子进程负责将数据集写入到磁盘文件。
优点:
-
不会阻塞主进程,不影响Redis的正常服务;
-
在大数据集的情况下,相对save更具优势。
缺点:
-
生成的RDB文件相对较大,占用磁盘空间;
-
子进程执行bgsave时,可能会占用一定的系统资源。
同步步骤
集群和哨兵
数据一致性
数据同步策略
数据删除和淘汰/脑裂
Redis的过期策略以及内存淘汰机制
缓存一致性
缓存雪崩、击穿和穿透
redisson
redisson使用:
// 1.设置分布式锁
RLock lock = redisson.getLock("lock");
// 2.占用锁
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
分布式读写锁 :
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
分布式信号量:
用 Redis 客户端添加了一个 key:“park”,值等于 3,代表信号量为 park,总共有三个值。
// 获取信号量(停车场)
RSemaphore park = redisson.getSemaphore("park");
// 获取一个信号(停车位)
park.acquire();
// 释放一个信号(停车位)
park.release();
注意:多次执行释放信号量操作,剩余信号量会一直增加,而不是到 3 后就封顶了。
公平锁(Fair Lock)
// 1、获取key为"fairLock"的锁对象
RLock lock = redissonClient.getFairLock("fairLock");
// 2、加锁
lock.lock();
try {// 进行具体的业务操作...
} finally {// 3、释放锁lock.unlock();
}
其他分布式锁:
联锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)可过期性信号量(PermitExpirableSemaphore)闭锁(CountDownLatch)
MongoDB
负载均衡
Nginx
分布式锁
Es
Solr
计算机网络
HTTP常见响应码
- 100 Continue
这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完 成,则忽略它。 - 101 Switching Protocol
该代码是响应客户端的 Upgrade 标头发送的,并且指示服务器也正在切换的协议。 - 102 Processing (WebDAV)
此代码表示服务器已收到并正在处理该请求,但没有响应可用。成功响应节 - 200 OK
请求成功。 - 201 Created
该请求已成功,并因此创建了一个新的资源。这通常是在PUT请求之后发送的响应。 - 202 Accepted
请求已经接收到,但还未响应,没有结果。意味着不会有一个异步的响应去表明当前请求的结果,预期另外的进程和服务去处理请求,或者批处理。 - 301 Moved Permanently
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。 - 302 Found
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。 - 400 Bad Request
1、语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。
2、请求参数有误。 - 401 Unauthorized
当前请求需要用户验证。 - 404 Not Found
请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。 - 405 Method Not Allowed
请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。 鉴于 PUT,DELETE 方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。 - 500 Internal Server Error
服务器遇到了不知道如何处理的情况。 - 502 Bad Gateway
此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。 - 504 Gateway Timeout
当服务器作为网关,不能及时得到响应时返回此错误代码。
HTTPS 是如何保证安全传输的
HTTP是RPC吗
HTTP 与 RPC 接口区别
-
HTTP(Hypertext Transfer Protocol)是一种应用层协议,它主要用于在 Web 浏览器和服务器之间传递数据。HTTP 的核心是客户端向服务器发起请求,并等待服务器响应。
-
RPC(Remote Procedure Call)是一种远程过程调用协议,它允许客户端应用程序通过网络调用远程服务器上的过程或函数。RPC 接口通常使用二进制协议来进行通信,例如 Protocol Buffers、Thrift、Msgpack 等。
HTTP 接口与 RPC 接口的区别和相同之处
-
通信协议不同:HTTP 使用文本协议,RPC 使用二进制协议。
-
调用方式不同:HTTP 接口通过 URL 进行调用,RPC 接口通过函数调用进行调用。
-
参数传递方式不同:HTTP 接口使用 URL 参数或者请求体进行参数传递,RPC 接口使用函数参数进行传递。
-
接口描述方式不同:HTTP 接口使用 RESTful 架构描述接口,RPC 接口使用接口定义语言(IDL)描述接口。
-
性能表现不同:RPC 接口通常比 HTTP 接口更快,因为它使用二进制协议进行通信,而且使用了一些性能优化技术,例如连接池、批处理等。此外,RPC 接口通常支持异步调用,可以更好地处理高并发场景。
HTTP 接口和 RPC 接口的相同之处在于,它们都是用于接口通信的协议。它们都需要定义接口、参数和返回值等信息,并通过网络进行通信。此外,它们都支持多种数据格式的编解码,可以根据需求进行灵活的选择。
运用场景 :
- HTTP 接口适用于 Web 应用程序和浏览器之间的通信。它通常用于传输 HTML、CSS、JavaScript 和其他 Web 资源,以及 RESTful 风格的 API 服务。
- RPC 接口适用于分布式系统之间的通信。它可以在多种编程语言之间进行通信,支持多种协议和数据格式。RPC 接口通常用于处理高并发、高吞吐量的场景,例如大型的分布式计算、大数据处理等。
在项目中有使用哪些RPC框架
-
Apache Dubbo
:Apache Dubbo(原阿里巴巴Dubbo)是一款高性能、轻量级的RPC框架,适用于大规模分布式系统。Dubbo使用Java注解进行服务声明,支持多种负载均衡策略和集群容错机制,提供了丰富的监控和管理功能。 -
Spring Cloud
:Spring Cloud 是一套构建分布式系统的开源框架。它提供了多个模块,包括服务发现与注册、负载均衡、断路器、智能路由等功能,基于HTTP或RPC实现了服务间的通信和调用。Spring Cloud整合了多种RPC框架,如RestTemplate、Feign、Ribbon等,使得开发者可以方便地构建分布式系统。 -
gRPC
:gRPC 是由 Google 开发的高性能、开源的RPC框架。它使用 Protocol Buffers(protobuf)作为接口定义语言(IDL),支持多种编程语言,如Java、C++、Python等。gRPC基于HTTP/2协议,支持双向流通信、多种序列化格式(如protobuf和JSON等)以及负载均衡等特性。 -
Apache Thrift
:Apache Thrift 是由 Facebook 开发和开源的跨语言RPC框架。它使用自己的IDL语言,支持多种编程语言,如Java、C++、Python、Ruby等。Thrift提供了比gRPC更丰富的功能,包括异步IO、连接池、复合类型等,适用于多种场景。 -
Apache Axis2
:Apache Axis2 是一款基于Web服务标准的RPC框架。它支持SOAP协议,通过WSDL描述服务接口,支持多种编程语言,如Java、C++、Python等。Axis2提供了高度可扩展的架构、安全性和可靠性,并支持发布和发现服务。
开发相关
接口密等性
加密算法(md5加盐、aes、rsa)
断点续传实现
shiro和security
JWT和Token
TOKEN
概念: 令牌,就是加密的字符串, 是访问资源的凭证。Token需要查库验证token 是否有效。
- 客户端使用用户名跟密码请求登录。
- 服务端收到请求,去验证用户名与密码。
- 验证成功,服务端会签发一个Token保存到(Session,redis,mysql…)中,然后再把这个 Token 发送给客户端。
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里。
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token。
- 服务端收到请求,验证密客户端请求里面带着的Token和服务器中保存的Token进行对比效验, 如果验证成功,就向客户端返回请求的数据。
JWT包含三个部分:
- Header头部
- Payload负载(载荷(payload,该token里携带的有效信息。比如用户id、名字、年龄等等))
- Signature签名。
由三部分生成JwtToken,三部分之间用“.”号做分割。
在头部信息中声明加密算法和常量,然后把header使用json转化为字符串。
在载荷中声明用户信息,同时还有一些其他的内容,再次使用json把在和部分进行转化,转化为字符串。
使用在header中声明的加密算法来进行加密,把第一部分字符串和第二部分的字符串结合和每个项目随机生成的secret字符串进行加密,生成新的字符串,此字符串是独一无二的。
解密的时候,只要客户端带着jwt来发起请求,服务端就直接使用secret进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端传过来的信息是否一致。如果一致验证成功,否则验证失败。
校验也是JWT内部自己实现的 :
1、客户端使用用户名跟密码请求登录;
2、服务端收到请求,去验证用户名与密码;
3、验证成功,服务端会签发一个JwtToken,无须存储到服务器,直接再把这个JwtToken发送给客户端;
4、客户端收到JwtToken以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
5、客户端每次向服务端请求资源的时候需要带着服务端签发的JwtToken;
6、服务端收到请求,验证密客户端请求里面带着的 JwtToken,如果验证成功,就向客户端返回请求的数据;
其他
项目哪个场景用到了动态代理,举例说明如何使用配置?
如何设计代码实现可读性、可维护性和可替代性
如何实现断点续传功能
paxso算法
BASE理论
- 基本可用(Basically Available)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。 - 软状态(Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。 - 最终一致性(Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的—种特殊情况。