Java学习笔记,持续更新

一、JAVA
对象如何分配内存?
虚拟机收到new指令触发。
1.类加载检查:会判断类是否已经被加载,如果没有被加载则需要先执行类加载流程,对象所需内存大小在类加载完后可以完全确定。
2.为对象分配内存,从堆中划分出一块确定大小的内存。
3.内存分配完后,将分配到的内存空间初始化为零(不包含对象头),保证了对象的实例字段在不赋初始值时也能直接使用。
4.为对象进行必要的设置:设置这个对象属于哪个类的实例、类的元数据、对象的哈希码、对象的GC分代年龄等信息,并存放在对象头中。
5.一个新的对象创建完毕。*堆内存是否规整是由垃圾收集器是否带有压缩整理功能决定的。
内存分配会根据堆内存是否绝对规整,分两种分配方式:
如果堆内存绝对规整,采用指针碰撞方式。分配过程:将已使用内存和未使用内存之间放一个分界点的指针,分配内存时,指针会向未使用内存方向移动,移动一段与对象大小相等的距离。
如果堆内存不绝对规整,采用空闲列表方法,分配过程:虚拟机内部维护了一个记录可用内存块的列表,在分配时从列表找一块足够大的空间划分给对象实例,并更新列表上的记录。
对象访问方式?

句柄访问:

•在Java堆中划分出一块内存作为句柄池,虚拟机栈中reference数据中存储的是对象的句柄地址,程序通过访问栈中的reference数据获取对象的句柄地址,句柄中的每条记录包含对象实例数据的指针和对象类型数据的指针,再通过句柄定位和访问对象。
优点:
reference存储稳定的句柄地址,对象移动(垃圾收集)时只会改变句柄中实例数据指针,reference不需要做改变。
缺点:
增加了指针定位的开销
场景:
适合频繁移动对象地址

直接指针访问:
•栈中reference数据存储的是对象地址,放在对象头中,程序通过访问栈中reference数据获取对象的地址,直接访问对象实例数据,再通过对象类型数据的指针访问对象类型数据。
优点:
速度快,节省了一次指针定位的开销。
缺点:
对象移动时需要重新定位。
场景:
适合频繁访问对象。

什么是逃逸分析:
逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,
如果没有被外部的方法使用,栈中分配内存空间,用来存储临时的变量。
如果被外部的方法引用了,堆上分配内存空间,实现共享。JDK 1.6就已经使用逃逸分析了。
为什么分配栈上?
减少GC次数,出栈即销毁。
什么情况下会分配在栈上?
不会被外部访问的对象。随栈帧的出栈而销毁,减轻GC的压力。
什么是TLAB?
什么是TLAB呢?本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。简单说,TLAB是为了避免多线程争抢内存,在每个线程初始化的时候,就在堆空间中为线程分配一块专属的内存。自己线程的对象就往自己专属的那块内存存放就可以了。这样多个线程之间就不会去哄抢同一块内存了。这是一块每个线程私有的内存分配区域,它存在于Eden区,TLAB空间的内存非常小,仅占有整个Eden空间的1%。jdk8默认使用的就是TLAB的方式分配内存。
java并发:
三大特性:原子性、可见性、有序性
原⼦性:
指的是⼀个操作是不可分割、不可中断的,要么全部执⾏并且执⾏的过程不会被任何因素打 断,要么就全不执⾏。

可见性:
指的是⼀个线程修改了某⼀个共享变量的值时,其它线程能够⽴即知道这个修改。

有序性:
指的是对于⼀个线程的执⾏代码,从前往后依次执⾏,单线程下可以认为程序是有序的,但 是并发时有可能会发⽣指令重排
ThreadLocal原理及使用:
线程之间隔离,每个线程都有一个ThreadLocalMap属性,key为ThreadLocal,val为传入值

set,get()方法:

createMap():

threadLoca的key是弱引用,val强引用,线程池中线程复用,生命周期较长,如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),导致val无法访问到。用完手动调用remove方法防止内存泄漏。
key为什么是弱引用:
CurrentThread 依然运行的前提下, 就算忘记调用 remove 方法也不怕被获取到value,弱引用比强引用可以多一层保障:弱引用的 key(即ThreadLocal)会GC被回收, key为null的value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

1.Thread类有⼀个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有⼀个属于⾃ ⼰的ThreadLocalMap。
2.ThreadLocalMap内部维护着Entry数组,每个Entry代表⼀个完整的对象,key是ThreadLocal的弱引⽤, value是ThreadLocal的泛型值。
3.每个线程在往ThreadLocal⾥设置值的时候,都是往⾃⼰的ThreadLocalMap⾥存,读也是以某个 ThreadLocal作为引⽤,在⾃⼰的map⾥找对应的key,从⽽实现了线程隔离。
4.ThreadLocal本身不存储值,它只是作为⼀个key来让线程往ThreadLocalMap⾥存取值。
sleep和wait区别?
从表面上看,wait和sleep方法都可以使当前线程进入阻塞状态,但是两者之间存在着本质的区别:

1.wait和sleep方法都可以使线程进入阻塞状态。
2.wait和sleep方法均是可中断方法,被中断后都会收到中断异常。
3.wait是Object的方法,而sleep是Thread特有的方法。
4.wait方法的执行必须在同步方法中进行,而sleep则不需要。
5.线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁,并且将线程添加到该对象监视器(monitor)的等待队列中
6.sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
7.sleep不需要被唤醒,但是wait是需要唤醒的
synchronized:
修饰实例方法,修饰静态方法,修饰代码块。
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁消除:
JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。无锁状态下Mark Word: 对象的hashCode+对象分代年龄+(是否位偏向锁)0+(锁标志位)01。
偏向锁:
同一个线程多次获取,避免多次获取和释放锁的上下文切换。对象头和栈帧中的锁记录里存储锁偏向的线程ID,测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。成功则获取到了锁。不成功CAS尝试获取锁,成功则获取锁,不成功则升级为轻量级锁。
轻量级锁:
虚拟机使用CAS将Mark Word拷贝到锁记录中,并且将Mark Word更新位指向Lock Record(线程的栈帧中创建)的指针。如果更新成功了,线程就有用了该对象的锁,并且对象Mark Word的锁标志位更新位00。更新失败,JVM检查当前的Mark Word中是否存在指向当前线程的栈帧的指针,如果有说明该锁已经被获取。如果有两条以上的线程竞争同一个锁,直接膨胀重量级锁,没有获得锁的线程会被阻塞。
自旋锁:
轻量级锁失败后,避免线程挂起(用户核心态转换)会自旋。在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程挂起,最后没办法只能升级为重量级锁。
重量级锁:
Synchronized的实现直接调⽤ObjectMonitor的enter和exit,这种锁被称之为重量级锁。
加锁:JVM会在monitorenter监视器入口处获取锁,在monitorexit监视器出口释放锁。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor(monitor存放持有锁的线程及等待锁的线程队列)所有权,即尝试获取对象的锁。
Synchronized中所有阻塞的线程都集中到了一个队列中,当有线程释放锁的时候,会将所有线程都唤醒。有些线程唤醒了即使得到了锁也无法执行,浪费资源。
对象头:

synchronize和lock?
1、lock是一个接口,而synchronized是java的一个关键字。
2、synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。

一篇文章讲透synchronized底层实现原理
AQS抽象同步队列:
AQS(AbstractQueuedSynchronizer)相较于Synchronized内置了多个Condition,会针对Conditon进行精确唤醒,提高了运行效率。
三个关键:
1、state:用来标记共享变量的状态,一般用volatile来修饰。
2、queue:当线程请求锁失败后,将线程包装为一个Node,加入到queue中,等待后续的唤醒操作。
3、CAS:修改state和queue中的入队操作等。CAS的全称是CompareAndSwap(比较然后交换)。
释放锁:从队列中唤醒一个等待中的线程(遇到CANCEL的直接跳过)
两种模式:
共享锁和独占锁。
独占锁:只有一个线程能执行,具体的 Java 实现有 ReentrantLock。
共享锁:多个线程可同时执行,具体的 Java 实现有 Semaphore和CountDownLatch。acquireShared(arg),指定arg数量,几个线程可以获取共享锁,<0则失败进入等待队列。
•同步队列和阻塞队列(Condition):
同步队列存放着竞争同步资源的线程的引用(不是存放线程),而等待队列(Condition)存放着待唤醒的线程的引用。
Java并发编程 之 同步队列与等待队列_CallMeJiaGu的博客-CSDN博客
ReentrantLock:
基本实现:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。用户态核心态转换,效率比非公平低。
•FairSync:
公平锁tryAcquire的流程是:
(1)检查state字段,若为0,表示锁未被占用,再检查是否要排队,不用排队会尝试占用,成功返回true,失败返回false。若不为0,检查当前占用锁的线程是否是自己,若是自己,则更新state字段,表示重入锁的次数。
(2)B和C执行tryAcquire失败,因为检查到要排队,执行addWaiter(Node.EXCLUSIVE),假设B先添加到队尾,C要执行enq(node)试着自旋+CAS初始化队列并入队。如果线程A拿着锁死死不放,那么B和C就会被挂起。
(3)B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,先判断是否为前驱节点是否为head,若前驱节点是head则尝试获取锁,拿到了锁自己变为head结点,并返回true。线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态(waitStatus)设置为SIGNAL。如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
tryRelease的过程为:
当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。

NonFairSync:
不会判断等待队列中是否已经有线程在排队了。
当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。
(1)线程在获取锁调用lock()时,非公平锁首先会进行一次CAS尝试抢锁,如果此时没有线程持有锁或者正好此刻有线程执行完释放了锁(state == 0表示当前锁未被占用),那么如果CAS成功则直接占用锁返回。
(2) 如果非公平锁在上一步获取锁失败了,那么就会进入nonfairTryAcquire(int acquires),在该方法里,如果state的值为0,表示当前没有线程占用锁或者刚好有线程释放了锁,那么就会CAS抢锁,如果抢成功了,就直接返回了,不管是不是有其他线程早就到了在阻塞队列中等待锁了。如果非公平锁 (1)(2) 都失败了,那么剩下的过程就和公平锁一样了。
(3) 从(1)(2) 可以看出,非公平锁可能导致线程饥饿,但是非公平锁的效率要高。
Java并发学习之ReentrantLock的工作原理及使用姿势
ReentrantLock和synchronized区别:

  1. synchronized 是Java的一个内置关键字,而ReentrantLock是Java的一个类。
  2. synchronized只能是非公平锁。而ReentrantLock可以实现公平锁和非公平锁两种。
    3.synchronized不能中断一个等待锁的线程,而Lock可以中断一个试图获取锁的线程。
    4.synchronized不能设置超时,而Lock可以设置超时。
    •ReenTrantLock 提供了一个 Condition 类,用来实现唤醒特定的线程; synchronized 要么随机唤醒一个线程要么唤醒全部线程。
    5.synchronized会自动释放锁,而ReentrantLock不会自动释放锁,必须手动释放,否则可能会导致死锁。
    ReentrantLock原理详解
    线程池:
    线程池: 简单理解,它就是⼀个管理线程的池⼦。
    1.它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是⼀个对象,创建⼀个对象,需要经过类加载过程,销毁⼀个对象,需要⾛GC垃圾回收流程,都是需要资源开销的。
    2.提⾼响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建⼀条线程执⾏,速度肯定慢很多。
    3.重复利⽤。 线程⽤完,再放回池⼦,可以达到复利⽤的效果,节省资源。
    四种线程池:
    (1)newCachedThreadPool可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
    使⽤场景: ⽤于并发执⾏⼤量短期的⼩任务。
    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    使⽤场景:处理CPU密集型的任务,确保CPU在⻓期被⼯作线程使⽤的情况下,尽可能的少的分配线程,即适⽤执⾏⻓期的任务。
    (3)newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
    使⽤场景:周期性执⾏任务的场景,需要限制线程数量的场景。
    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    使⽤场景:适⽤于串⾏执⾏任务的场景,⼀个任务⼀个任务地执⾏。

ThreadPoolExecutor的七个参数:
•核心线程大小
•最大线程数
•线程存活时间
•时间单位
•线程工厂
•工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。创建需指定大小。
②LinkedBlockingQuene
基于链表的无界阻塞队列(默认最大容量为Integer.MAX),按照FIFO排序。参数maxPoolSize是不起作用的。
③SynchronousQuene
是一个同步阻塞队列,意味着生产者线程在向队列插入元素时会被阻塞,直到有消费者线程从队列中获取该元素。同样地,消费者线程在尝试从队列中获取元素时,如果没有生产者线程插入元素,则消费者线程会被阻塞。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
•拒绝策略
1)调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
2)直接丢弃任务,并抛出异常。
3)直接丢弃任务。
4)抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
•如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
•如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,加1是因为可能存在⻚缺失,更多的线程数也只能增加上下文切换,不能增加CPU利用率。
CountDownLatch和CyclicBarrier:
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。countDownLatch底层AQS,执行countdown就是state-1,执行await就是判断state是否为0,不为0加入队列阻塞,为0时头节点把剩余在队列的节点一并唤醒。CyclicBarrier借助reetrantLock和Condition实现等待唤醒功能,调用await,count-1,如果count不为0添加到队列,若count等于0,把队列节点唤醒。

自定义注解 :
ClassScaner.scanPackageByAnnotation(packagePath, NoDiGetter.class);
java自定义注解
JavaSPI:
Java SPI(Service Provider Interface)是Java官方提供的一种服务发现机制,它允许在运行时动态地加载实现特定接口的类,而不需要在代码中显式地指定该类,从而实现解耦和灵活性。
可以看一下机制图:

Java SPI
懒汉式线程安全问题:
1.加锁,效率低

  1. Double check, 指令重排序问题

3.double check + volatile

Future模式的思想:
就是在子线程进行执行的时候,主线程不阻塞继续执行。等到主线程需要子线程的结果的时候再去获取子线程的结果(此时子线程没有执行完成的话就会阻塞直至执行完成),它的核心思想是异步调用。

Future模式详解
跨域:
指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
例如:a页面想获取b页面资源,如果a、b页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。注意:跨域限制访问,其实是浏览器的限制。
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

什么是跨域?如何解决
最常见的六种跨域解决方案
SpringBoot 解决跨域问题的 5 种方案!
CPU高速缓存1-3级,缓存不一致问题:
1.总线锁:独占,若某核心在修改,其他核心不可修改。
2.缓存一致性协议MESI:M (Modified)修改、E (Exclusive)独占、S (Shared)共享、I (Invalid)失效四种状态。cpu读取共享变量前,检查数据状态。若独占,则cpu将读到的数据是最新的,无其他cpu读。若共享,数据也是最新,有被其他cpu读但没修改。若修改说明当前cpu正在修改并向其他cpu同步发送invalid状态,得到响应后才能将cpu数据写到主存,状态由修改变为独占。若无效,说明修改过了,重新从主存读数据。
优化思路:同步变异步。异步导致cpu乱序执行问题,引入内存屏障。
JMM内存模型:
Java内存模型(Java Memory Model,JMM),是⼀种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存 访问差异。一种规范,所有变量存在主存中。主存copy到线程的工作内存,工作内存改完再刷回主存。
JMM特性:
原子性:JMM保证对于基本数据类型的读写操作具有原子性,即读写操作是不可分割的。但对于复合操作(如 i++)或非原子类型的操作(如 long 和 double 的读写),JMM无法保证原子性,需要额外的同步手段来保证。
可见性:JMM保证了在一个线程将变量写入主内存后,其他线程能够看到该变量的最新值。这是通过在变量的读写操作中插入特定的内存屏障(Memory Barrier)来实现的。
有序性:JMM保证单个线程中的操作按照程序的顺序执行,并且不会发生指令重排序(Reordering)
八个原子操作:
(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到工作内存中,以便随后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
指令重排限制:
有两个规则 happens-before 和 as-if-serial 来约束。
happens-before:
•线程内的代码能够按先后顺序执行,这被称为程序次序规则。
•对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
•前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
•一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
•一个线程的所有操作都会在线程终止之前,线程终止规则。
•一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
•可传递性
as-if-serial:
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提⾼并⾏度),单线程程序的执⾏结果不能被改变。
volatile:
两个作用:可见性和有序性。
实现原理:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile的可见性:
⼀个变量被声明为volatile 时,线程在写⼊变量时不会把值缓存在寄存器或者其他地⽅,⽽是会把值刷新回主内存。当其它线程读取该共享变量 ,会从主内存重新获取最新值,⽽不是使⽤当前线程的本地内存中的值。
volatile的有序性:
通过禁止指令重排来实现的。而禁止指令重排底层是通过设置内存屏障来实现。
内存屏障:

JMM对synchronize的规定:
synchronized的可见性:
  1)线程解锁前,必须把共享变量的最新值刷新到主内存中
  2)线程加锁时,清空工作内存中共享变量的值,从主内存中重新获取最新的值
synchronized的原子性:
synchronized底层由于采用了字节码指令monitorenter和monitorexit来隐式地使用这lock和unlock两个操作,使得其操作具有原子性。
synchronized的有序性:
不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。
集合:

fail-fast机制:

Fail-safe:
Fail Safe Iterator不会抛出任何异常。这是因为它们在集合的克隆上工作,而不是在原始集合上工作,这就是它们被称为故障安全迭代器的原因。
CopyOnWriteArrayList:
通过加锁ReetrentLock + 数组拷贝+ volatile 来保证了线程安全。

已经加锁为何还要拷贝数组:
volatile 关键字修饰的是数组,在原来数组上修改无法触发可见性的,必须修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
新数组拷贝,对老数组没影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。
HashMap:
•put():
1.重新计算hash值(hash碰撞可再hash)
2.hash 值与 (tablel.length-1) 进行位与&运算
1)如果数组位置为空,插入,equals()比较key,相同则替换,不存在则尾部插入结点。
2)如果链表元素大于8,数组长度>=64转为红黑树
3)判断是否超阈值,超则扩容x2
•扩容:
1.建立新的2倍长数组
2. 遍历旧数组,重新计算在新数组中的位置。用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,元素在新数组中位置不变;否则,在新数组中的位置下标=原位置+原数组长度。(原因,新表2种可能10101,1与1为1,则原位置+原数组长度,1与0为0,则为原下标)z
3.将旧数组使用尾插法转移到新数组,重新设置扩容阈值。
•当链表长过长时会转换成红黑树,那能不能使用AVL树替代呢?
AVL(二叉搜索树)树是完全平衡二叉树,要求高度之差最多1,红黑树适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),来减少插入/删除时的平衡调整耗时,虽然会导致查询比AVL稍慢,但相比插入/删除时获取的时间是值得的。
•HashMap 在 JDK7 和 JDK8 有哪些区别?
① 数据结构:JDK7“数组+链表”,在JDK8"数组+链表+红黑树",从而降低时间复杂度(由O(n) 变成了 O(logN))
② 对数据重哈希:JDK8 之后,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是在 table 的 length较小的时候,在进行计算元素存储位置时,也让高位也参与运算。
③ 在JDK7头插法,在扩容时,多线程可能造成环形链表导致死循环。1.8尾插法,扩容是不会改变元素的相对位置
④ 扩容时重新计算元素的存储位置的方式:JDK7 : hash & (table.length-1);JDK8 使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。
⑤ JDK7 是先扩容后插入,这就导致无论这次插入是否发生hash冲突都需要进行扩容,但如果这次插入并没有发生Hash冲突的话,那么就会造成一次无效扩容;

ConcurrentHashMap:
•添加或更新键值对:put()
(1)计算 key 的哈希值
(2)如果 table 为空,则执行初始化
(3)否则,计算 key 哈希值对应的下标,并获取 table 中对应下标的头结点
(4)如果头结点为 null,则基于 CAS 尝试添加头结点
(5)否则,如果头结点不为 null,但是头结点的哈希值为 MOVED,说明目前正在执行扩容操作,则帮助扩容
(6)否则,如果头结点不为 null,且未处于扩容状态,则尝试添加或更新结点
(7)判断当前 bin 范围内结点数目是否大于阈值,如果大于阈值则执行扩容操作
•读操作不加锁
用 volatile 变量协调读写线程间的内存可见性。
•ConcurrentHashMap 在 JDK7 和 JDK8的区别?
(1)数据结构:JDK7 是 Segment数组 + HashEntry数组 + 链表,JDK8是 HashEntry数组 + 链表 + 红黑树,当链表的长度超过8和数组大于等于64,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率
(2)锁的实现:JDK7的锁是segment,是基于ReentronLock实现的,包含多个HashEntry;而JDK8 降低了锁的粒度,采用 table 数组元素作为锁,从而实现对每行数据进行加锁,进一步减少并发冲突的概率,并使用 synchronized 来代替 ReentrantLock,因为在低粒度的加锁方式中,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中ReentrantLock 可以通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。

如何保证集合不被修改:
使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
多线程下arrayList安全:
Collections.synchronizedList()
什么是Hash算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
五种IO模型:
阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型、异步IO模型;前4种为同步IO操作,只有异步IO模型是异步IO操作。
•阻塞IO模型

进程发起IO系统调用后被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

优点:不消耗CPU资源。
缺点:一个线程只能处理一个流的I/O事件,要么多线程要么fork效率低。
•非阻塞IO模型

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;一直重复查看数据是否准备好。直到内核缓冲区有数据,内核就会把数据返回进程。
缺点:进程轮询(重复)调用,消耗CPU的资源;
•IO复用模型 select、poll、epoll

多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;
IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu。
•信号驱动模型:

当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
•异步IO模型:

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。
•select、poll、epoll对比
select,poll实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替。但是它在设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在醒着的时候要遍历整个 fd 集合,而 epoll 在醒着的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。
•BIO是阻塞的,NIO是非阻塞的
BIO基于字节流和字符流进行操作的,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道事件,因此使用单个线程就可以监听多个客户端通道

大白话详解5种网络IO模型
二、分布式
分布式锁:
分布式锁应该具备哪些条件:
高可用,可重入,防止死锁,具备非阻塞锁特性。
分布式锁的三种实现方式:
•基于数据库的实现方式:
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
•基于Redis的实现方式:

•基于ZooKeeper的实现方式:
(1)线程A想获取锁在locks节点创建一个临时顺序节点lock1;
(2)线程A会查找locks节点下所有的临时顺序节点,判断自己的节点lock1是不是排序最小的那一个,如果是则成功获得锁。;
(3)这时候如果又来一个线程B前来尝试获得锁,它会在locks下再创建一个临时顺序节点lock2,跟线程A一样检查自己的节点是不是排序最小的。发现自己的节点不是最小获取锁失败,它就会监听前一个节点即lock1。
(4)当lock1节点被删除了,线程B立刻知道因为它一直监听着lock1节点,再次检查自己的节点是否最小的,是最小的获取到锁。
SpringCloud 分布式锁的多种实现
分布式事务:
2PC:
事务的提交:资源准备和资源提交,由事务协调者来协调所有事务参与者,准备阶段所有事务参与者都预留资源成功,则提交,否则协调者回滚。

一阶段:
① 协调者向参与者发送事务内容,并等待答复是否可提交。
② 各参与者执行本地事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
③ 如参与者执行成功,给协调者反馈同意,否则反馈中止。
二阶段:

若所有参与者都回复同意,协调者通知参与者commit事务,参与者commit后给协调者发送ack,协调者收到全部ack则完成。
若一阶段有参与者返回终止,或与参与者连接超时,回滚事务。回滚流程:
① 协调者向所有参与者发出 rollback请求
② 参与者利用阶段一写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
③ 参与者在完成事务回滚之后,向协调者发送回滚完成的ACK消息
④ 协调者收到所有参与者反馈的ACK消息后,取消事务
缺点:执行中所有参与者事务阻塞性、如果是协调者挂掉,可以重新选举一个协调者,但是无法解决协调者挂掉这段时间的阻塞、二阶段有人没commit成功导致数据不一致
3PC:
相比2PC改动点:
(1)在协调者和参与者中都引入超时机制
(2)在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

阶段一 CanCommit :
询问是否可以提交,返回yes,进入预提交(二阶段)。
阶段二 PreCommit :
发送预提交请求,参与者接收到 PreCommit 请求后,会执行本地事务操作,并将 undo 和 redo 信息记录到事务日志中(但不提交事务),然后返回ack。若任何一个返回no,事务abort中断。
阶段三doCommit:
预提交进入提交阶段,发送commit请求,提交完发送ack给协调方。任意一个返回no,执行abort中断,根据undolog恢复,恢复完发送ack,协调者中断事务。
优缺点:与2PC相比降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。数据不一致问题依然存在。
TCC:

一阶段:Try,锁住资源。(如下单,在try阶段不是真正减库存而是把下单的库存锁定住。
二阶段:根据第一阶段的结果决定是执行confirm还是cancel。
Confirm:执行真正的业务(执行业务,释放锁)
Cancel:是对Try阶段预留资源的释放(出问题,释放锁)
TCC如何保证最终一致性:
TCC 事务机制以 Try 为中心的,Confirm 确认 Cancel 取消都围绕 Try 展开。因此Try 阶段中的操作,其保障性是最好的,即使失败仍有Cancel可以将其执行结果撤销。
Try阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的,也就是说只要 Try 成功,Confirm 一定成功(TCC设计之初的定义)
Confirm 与 Cancel 如果失败,由TCC框架进行重试补偿,存在极低概率在CC环节彻底失败,则需要定时任务或人工介入。
TCC注意事项:
•允许空回滚
空回滚出现的原因是 Try 超时或者丢包,导致 TCC 分布式事务二阶段的 回滚,触发 Cancel 操作,此时事务参与者未收到Try,但是却收到了Cancel 请求

•防悬挂控制

网络问题导致Cancel比Try先到,由于可以空回滚,cancel回滚成功,此时应拒绝try操作否则导致数据不一致。Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果是则不执行 Try 的业务操作。
幂等控制:
由于网络原因或者重试操作都有可能导致 Try - Confirm - Cancel 3个操作的重复执行,所以使用 TCC 时需要注意这三个操作的幂等控制,通常我们可以使用事务 xid 或业务主键判重来控制。
优点:
性能提升:控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性保证数据一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
Saga事务:
核心思想:长事务拆成短事务。本地短事务均成功,提交分布式事务。某个参与者失败,由事务协调器控制反向补偿or回滚。tcc有预留动作(try),saga直接提交
saga恢复策略:
•向后恢复:撤销掉之前所有成功的子事务
•向前恢复:执行不通过的事务,会尝试重试事务(适用于必成功场景)
saga事务实现方式:
•命令协调:中央协调器OSO。如果有任何失败还负责通过向每个参与者发送命令撤销之前的操作来协调分布式的回滚。

•事件编排:第一个服务执行一个事务,然后发布一个事件,该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

Saga事务的优缺点:
(1)命令协调设计的优缺点:
① 优点:
服务间关系简单,避免循环依赖。
开发简单,只需要执行命令/回复,降低参与者的复杂性。
② 缺点:
中央协调器处理逻辑复杂难以维护。
存在协调器单点故障风险。
(2)事件编排设计的优缺点:
① 优点:
避免中央协调器单点故障风险。
当涉及的步骤较少服务开发简单,容易实现。
② 缺点:
服务之间存在循环依赖的风险。
服务间关系混乱。
分布式事务
本地消息表:
分布式事务拆成本地事务。

1)优点:
消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
方案轻量,容易实现。
2)缺点:
与具体的业务场景绑定,耦合性强,不可公用
消息数据与业务数据同库,占用业务系统资源
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限
MQ事务消息:

优缺点:
(1)优点:相比本地消息表方案,MQ 事务方案优点是:消息数据独立存储 ,降低业务系统与消息系统之间的耦合吞吐量大于使用本地消息表方案
(2)缺点:
一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。业务处理服务需要实现消息状态回查接口。
最大努力通知:

对MQ事务消息的优化,事务主动方重复通知直到达到最大次数,若被动方未收到,可以主动查询主动方的消息校对接口。适用于业务通知类型:微信交易结果就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。适合业务链较短的,最终一致性敏感度低的。
分布式事务总结:
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
本地消息表/MQ 事务:适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga 事务较适用于补偿动作容易处理的场景。
三、MySQL
脏读:
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并不一定最终存在的数据,这就是脏读。

不可重复读:
不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。

幻读:
事务 A 查询⼀个范围的结果集,另⼀个并发事务 B 往这个范围中插⼊ / 删除了数据,并静悄悄地提交,然后 事务 A 再次查询相同的范围,两次读取得到的结果集不⼀样了,这就是幻读。

隔离级别:

当前读和快照读:
快照读:简单的select操作,属于快照读,不加锁。
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;

insert into table values (…);
update table set ? where ?;
delete from table where ?;
MVCC(Multi-Version Concurrency Control):
同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。解决脏读,不可重复读,mvcc使用快照读解决了部分幻读问题,但是在修改时还是使用当前读,所以还是存在幻读问题,幻读问题最终就是使用间隙锁解决。读已提交、可重复读都有实现MVCC。
实现:
trx_id和roll_pointer把undo日志串联起来形成一个历史记录版本链。通过read-view机制与undo版本链比对机制,不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
Read-View:
m_ids :表示在⽣成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。
min_trx_id :表示在⽣成 ReadView 时当前系统中活跃的读写事务中最⼩的 事务 id ,也就是 m_ids 中的最 ⼩值。
max_trx_id :表示⽣成 ReadView 时系统中应该分配给下⼀个事务的 id 值。
creator_trx_id :表示⽣成该 ReadView 的事务的 事务 id。

如何解决幻读:
•快照读:MVCC
•当前读:间隙锁(左开右开区间)+ 行锁方式解决的。
间隙锁,锁定的区域:
根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。
where number=5的话,那么间隙锁的区间范围为(4,11);
myisam和innodb的区别:
MyISAM不支持事务,而InnoDB支持。InnoDB默认每条SQL语句会默认被封装成一个事务自动提交。
MyISAM不支持外键,而InnoDB支持。对一个包含外键的InnoDB表转为MYISAM会失败。
InnoDB是聚集索引,使用B+Tree作为索引结构主键不应该过大,因为主键太大,其他索引也都会很大。InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。MyISAM是非聚集索引。
InnoDB不保存表的具体行数,MyISAM 保存了表的总⾏数。
InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁。
自增ID可以避免B+树和频繁合并和分裂(对比使用UUID)。如果使用字符串主键和随机主键,会使得数据随机插入,效率比较差。
索引下推:
索引下推具体是在复合索引的查询中,针对特定的过滤条件而进行减少回表次数而做的优化

Redolog undolog binlog
●redo log它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于InnoDB存储引擎层产生的。
●而binlog是逻辑日志,记录内容是语句的原始逻辑,类似于“给ID=2这- -行的c字段加1”, 属于MySQL Server层。
●虽然它们都属于持久化的保证,但是则重点不同。
redo log让InnoDB存储弓|擎拥有了崩溃恢复能力。
bin log 保证了MySQL集群架构的数据一致性。
●undolog:mvcc、回滚
MySQL查询优化:
1.多用覆盖索引
2.联合索引满足最左匹配
3.索引不做函数计算
4.利用子查询优化分页多场景
MySQL锁:
Record Lock 记录锁
记录锁就是直接锁定某⾏记录。当我们使⽤唯⼀性的索引(包括唯⼀索引和聚簇索引)进⾏等值查询且精准匹配到⼀ 条记录时,此时就会直接将这条记录锁定。例如 select * from t where id =6 for update; 就会将 id=6 的记录锁定。
分库分表:
垂直拆分:
按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
将一个表按照字段分成多表,每个表存储其中一部分字段。
大字段效率低:数据大读取时间长、跨页查找。可将热门数据和冷门数据拆分减少争抢。
优点:业务层次清晰、方便监控维护扩展、提升IO效率
解决方案:
•把不常用的字段单独放在一张表;
•把text,blob等大字段拆分出来放在附表中;
•经常组合查询的列放在一张表中;

水平拆分
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。优化单一表数据量过大而产生的性能问题。
数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。
分库规则:
雪花算法:
注意:依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。

分库分表迁移:

读写分离:
主写从读。
主从复制过程:
①当Master节点进行insert、update、delete操作时,会按顺序写入到binlog中。
②salve从库连接master主库,Master有多少个slave就会创建多少个binlog dump线程。
③当Master节点的binlog发生变化时,binlog dump 线程会通知所有的salve节点,并将相应的binlog内容推送给slave节点。
④I/O线程接收到 binlog 内容后,将内容写入到本地的 relay-log。
⑤SQL线程读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做出对应的操作。

主从复制数据丢失:
主库在执行完事务后不立刻返回结果给客户端,需要等待至少一个从库接收到并写到relay log中才返回结果给客户端。
主从复制延迟:
主机与从机之间的物理延迟是无法避免的,既然无法避免就可以考虑尝试通过缓存等方式,降低新修改数据被立即读取的概率。

MySQL随机取样:
SELECT * FROM your_table ORDER BY RAND() LIMIT 100;
上述 SQL 查询的步骤是先将表中所有记录按照随机顺序排序,然后选择前 100 条记录。这样确实可以获取随机的 100 条记录,但是当数据量非常大时,排序会占用大量资源,性能可能不够好。

SELECT * FROM your_table ORDER BY RAND() LIMIT CEIL((SELECT COUNT(*) FROM your_table) * 0.00001);
这个例子中的 0.00001 是你希望获取的随机行的百分比,可以根据实际需要进行调整。这种方法避免了对整个表进行排序,提高了性能。
四、Redis
Redis为什么快:
1)基于内存;
2)单线程减少上下文切换,同时保证原子性;
3)IO多路复用;
4)高级数据结构(如 SDS、Hash以及跳表等)。

缓存穿透: 缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在⼀样。查询数据不存在,例如没有id为-1的数据。
解决:
1.布隆过滤器(数据多误判多,无法删除),多个hash函数,算出元素位置,哈希碰撞,如果不存在一定不存在,存在可能不存在。
2.空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),⽐较有效的⽅法是针对这类数据设置⼀个较短的过期时间,让其⾃动剔除。
3.缓存层和存储层的数据会有⼀段时间窗⼝的不⼀致,可能会对业务有⼀定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不 ⼀致。这时候可以利⽤消息队列或者其它异步⽅式清理缓存中的空对象。

缓存击穿:缓存过期,伴随大量对该 key 的请求。
解决:热点数据不过期,加锁,第一次查库,后面查缓存。

缓存雪崩:大量数据同时失效。
解决:热点数据不过期,分散失效时间。
一致性hash:将新增or删除机器导致的数据失效影响减少,环状结点,环的元素由 [0, 2^32 -1] 范围的整数组成,按照哈希值计算位于哪里,往前找到的第一个点就是。避免数据倾斜,添加虚拟节点映射。

Redis删除策略:

持久化文件对过期策略的处理?
过期 key 是不会写入 RDB 和 AOF 文件,同时数据恢复时也会做过期验证。
Redis 有哪些内存淘汰机制?
和操作系统页面置换算法类似,选择一定的数据淘汰策略。
内存淘汰机制
从已设置过期时间的数据集:
1)volatile-lru:最近最少使用。
2)volatile-lfu:使用频率最低的数据。
3)volatile-random:任意数据。
4)volatile-ttl:将要过期的。
从数据集:
5)all keys-lru:最近最少使用的数
6)all keys-lfu:使用频率最低的数据。
7)all keys-random:任意选择数据。
8)no-eviction(驱逐):不会驱逐数据(不会淘汰数据),返回错误,这也是默认策略。 兜底删除策略
渐进式rehash:
Redis是通过全局hash表来存储key-value键值对的,Redis中主要通过链式哈希、渐进式rehash方法来解决hash冲突问题。
链式哈希:随着hash冲突可能越来越多,就会导致某些hash冲突链过长,进而导致链上的元素查找耗时长,效率降低。
rehash:增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

•什么时机触发 Rehash 操作?
缩容: Redis 定时任务 serverCron 会在每个周期内检查 bucket 的使用情况。当存放 key 的数量和总 bucket 数的比例小于 HASHTABLE_MIN_FILL(10%),触发缩容 Rehash 操作。
扩容:在每次调用 dictAddRaw 新增数据时,会检查 bucket 的使用比例。扩容的条件是以下之一:
dict_can_resize = 1 (该参数会在有 COW 操作的子进程运行时更新为 0,防止在子进程操作过程中触发 Rehash,导致内核进行大量的 Page 复制操作)
当前存放的 key 的数量与 bucket 数量的比例超过了 dict_force_resize_ratio(5)
•什么时机实际执行 Rehash 函数?
定时任务: Redis 定时任务 serverCron 会在每个周期内执行 1ms 渐进式Rehash 操作。
附着于其他操作:在 Redis 执行 客户端请求dictAddRaw, dictGenericDelete, dictFind, dictGetSomeKeys 和 dictGetRandomKey 等操作前会执行 Rehash 操作。

RDB:
指定的时间间隔内将内存中的数据集快照写入磁盘。默认持久化方式。将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。支持同步(save 命令)、后台异步(bgsave:fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束,父进程继续接受写命令。阻塞只发生在fork阶段,一般时间很短)以及自动配置三种方式触发。
优点:
文件紧凑,全量备份,适合备份和灾难恢复
异步处理,主进程不需要磁盘IO操作
恢复大数据集时的速度比 AOF快
缺点:
一次全量备份,存储的二进制序列化形式,存储上非常紧凑。且在快照持久化期间修改的数据不会被保存,可能丢失数据。
AOF:
写命令追加到文件中。Redis 提供了 bgrewriteaof 命令,作用是 fork 出一条新进程将内存中的数据以命令的方式保存到临时文件中,完成对AOF 文件的重写(压缩原始大文件)。
AOF 也有三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步。
优点:
保护数据不丢失,一般 AOF 隔 1 秒通过一个后台线程执行一次 fsync 操作。
无磁盘寻址的开销,写入性能高,文件不易破损。
后台重写操作,也不会影响客户端的读写。
命令可读,适合紧急恢复。
缺点:
AOF数据快照文件比RDB更大,且 恢复速度慢。
AOF开启后,支持的写 QPS 会比RDB支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件。
原生批命令 (mset, mget) 与 Pipeline 区别?
1)原生批命令是原子性的,而 pipeline 是非原子操作。
2)原生批命令一命令多个 key, 但 pipeline 支持多命令(存在事务),非原子性。
3)原生批命令是服务端实现,而 pipeline 需要服务端与客户端共同完成。
全量复制:

增量复制:
增量复制的过程主要是master每执行一个写命令就会向slave发送相同的写命令,slave接受并执行写命令,从而保持主从一致。
支持无磁盘的复制,子进程直接将RDB通过网络发送给从服务器。
Redis集群搭建:
一主一从,一主多从,多主多从级联。
主从同步特点:
主从复制实现高可用

哨兵机制(sentinel):
哨兵作用:主从复制存在⼀个问题,没法完成⾃动故障转移。所以我们需要⼀个⽅案来完成⾃动故障转移,它就是Redis Sentinel(哨兵)。

缺点:(1)主从服务器的数据要经常进行主从复制,这样会造成性能下降;
(2)当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不可用的。
集群(Cluster):
集群解决⾼可⽤和分布式问题
数据分区:数据分区 (或称数据分⽚) 是集群最核⼼的功能。集群将数据分散到多个节点,⼀⽅⾯ 突破了 Redis 单机内存⼤⼩的限制,存储容量⼤⼤增加;另⼀⽅⾯ 每个主节点都可以对外提供读服务和写服务,⼤ ⼤提⾼了集群的响应能⼒ 。
⾼可⽤: 集群⽀持主从复制和主节点的 ⾃动故障转移 (与哨兵类似),当任⼀节点发⽣故障时,集群仍然可以对外提供服务。
所有redis至少三主三从(即多主多从)。集群模式原理:虚拟槽分区,16384个槽。只有 Master 才拥有槽的所有权,如果是某个 Master 的 slave,这个slave只负责槽的使用,但是没有所有权。
当有实例上/下线,会通知集群更新hash槽映射关系。数据迁移完毕,客户端收到moved命令,更新本地缓存,如果迁移中,会返回ack命令告诉客户端去哪台新Redis实例请求。

服务端路由:分1024个hash槽,映射信息存zk,proxy缓存一份至本地。异步迁移会大key拆分。

部分同步:
主服务器端为复制流维护一个内存缓冲区。主从服务器都维护一个复制偏移量和master run id ,当连接断开时,从服务器会重新连接上主服务器,然后请求继续复制,假如主从服务器的两个master run id相同,并且指定的偏移量在内存缓冲区中还有效,复制就会从上次中断的点开始继续。如果其中一个条件不满足,就会进行完全重新同步(在2.8版本之前就是直接进行完全重新同步)。因为主运行id不保存在磁盘中,如果从服务器重启了的话就只能进行完全同步了。

故障转移:
故障发现:
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信⼀直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。当某个节点判断另⼀个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
故障恢复:
1.资格检查
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
2.准备选举时间
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执⾏后续流程。
3.发起选举
当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
4.选举投票
持有槽的主节点处理故障选举消息。投票过程其实是⼀个领导者选举的过程,如集群内有N个持有槽的主节 点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给⼀个 从节点,因此只能有⼀个从节点 获得N/2+1的选票,保证能够找出唯⼀的从节点。
5. 替换主节点
当从节点收集到⾜够的选票之后,触发替换主节点操作。
主从复制过程中数据丢失:
1.异步丢失:master同步过程中挂掉
2.脑裂丢失:两主
减少数据丢失的配置:
min-slaves-to-write 1
min-slaves-max-lag 10
所以应对脑裂的解决办法应该是去限制原主库接收请求,Redis提供了两个配置项。
min-slaves-to-write:与主节点通信的从节点数量必须大于等于该值,否则主节点拒绝写入。
min-slaves-max-lag:主节点与从节点通信的ACK消息延迟必须小于该值,否则主节点拒绝写入。
Redis基本数据类型:
1.String:k-v映射表,set,get,mset(设置多个KV对,一次传太多导致阻塞),mget(get多个val),setex(设置一个键值对,并为其设置过期时间。若存在即覆盖),setnx(若给定的 key 已经存在,则 SETNX 不做任何动作),incr(+1),decr(-1),incrby(数值+增量),decrby(数值+减量)。
使用场景:计数,限次(自增超阈值则xxx),共享Session,缓存功能。
底层数据结构:SDS(简单动态字符串),编码int,raw,embstr
•raw与embstr编码效果是相同的,不同在于内存分配与释放,raw两次,embstr一次。
•embstr内存块连续
•int编码和embstr编码如果做追加字符串等操作,满足条件下会被转换为raw编码;embstr编码的对象是只读的,一旦修改会先转码到raw。
2.Hash:k-v键值对集合,hset,hget,hdel,hexsist,hgetall,hincrby,hlens,hkeys,hmget,hmset。
使用场景:缓存信息。
底层结构:zipList,hashTable(底层字典)
3.List:链表,可头插or尾插。有序,可重复。lpush,lpushx,lpop,lrange(指定区间元素),lindex,rpush,rpop,
使用场景:消息队列,⽂章列表
栈: lpush+lpop 队列:lpush+rpop 有限集合:lpush+ltrim 消息队列: lpush+brpop.
底层结构:
zipList:压缩列表,每个压缩里列表节点保存了一个列表元素
linkedList:双端链表,每个双端链表节点都保存了一个字符串对象,在每个字符串对象内保存了一个列表元素
4.Set:无序String集合,不可重复,无序。sadd,scard,sdiff,smembers,smove,spop,sinter(返回2集合的交集)。场景:交并集特性,社交领域求出多个用户的共同好友,共同感兴趣的领域等。
底层结构:intset编码:集合对象底层实现是整数集合,所有元素都保存在整数集合中。hashSet
5.Sorted Set:set的有序集合,zadd,zcount,zcard,zrank(按分高低排序),zrem,场景:同set,还可排行榜等。
底层结构:zipList,skipList。
skiplist编码底层跳跃表(有序,On)和字典(无序查询复杂度O1)两种;每个跳跃表节点都保存一个集合元素,并按分值从小到大排列;字典的每个键值对保存一个集合元素,字典的键保存元素的成员,字典的值保存分值。
常用操作命令
底层数据结构:
六种底层数据结构:简单动态字符串、双向链表、字典、跳跃表、整数集合和压缩列表。

  1. 简单动态字符串SDS

•减少内存重分配次数
SDS通过空间预分配和惰性空间释放两种优化策略来减少内存重分配次数。
1)空间预分配
对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
2)惰性空间释放
当SDS缩短时,程序并不会立即回收缩短后多出来的空间,而是使用free属性将这些字节的数量记录起来,等待将来使用。
兼容部分 C 字符串函数、二进制安全、常数复杂度获取字符串长度
2. 双向链表结构
双向链表结构:
链表中的节点结构:

Redis链表的实现特性:
•双端
•无环
•带表头指针和表尾指针
•带链表长度计数器
•多态(保存各种不同类型的值)
3. 字典:
每个键独一无二的,根据键查找值,通过键更新值,根据键删除整个键值对等。
实现结构:
一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

next属性是指向另一个哈希表节点的指针,可以将多个哈希值相同的键值对连接在一起来解决键冲突的问题。

哈希算法:
当要将一个新的键值对添加到字典里面时,先计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上。
若果字典被用做数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
当元素增加会使用渐进式rehash扩容。渐进式的rehash将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上。(同redis的渐进式rehash目的,分散压力)
4. 跳跃表
•跳跃表是一个双向链表,每个节点都包含score和ele值
•节点按照score值排序,score值一样则按照ele字典排序
•每个节点都可以包含多层指针,层数是1到32之间的随机数
•不同层指针到下一个节点的跨度不同,层级越高,跨度越大
•增删改查效率与红黑树基本一致,实现却更简单

  1. 压缩列表:

不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存

分布式锁的七种方案:
不同分布式锁不同进程对共享资源的操作
•方案一:SETNX + EXPIRE
缺点:非原子操作,永久不释放。
•方案二:SETNX + value值是(系统时间+过期时间)
缺点:没唯一标识,可能被其他释放。
•方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)
•方案四:SET的扩展命令(SET EX PX NX)
NX :表示key不存在的时候,才能set成功。EX:过期时间,秒。PX: 过期时间,毫秒。XX: 仅当key存在时设置值。
缺点:被别的线程误删。
•方案五:SET EX PX NX + 校验唯一随机值,再释放锁,value值设置当前线程唯一key
•方案六: 开源框架~Redisson
问题:锁过期释放,业务没执行完,定时守护线程,每隔10s watch dog检查锁是否还存在,存在延长,防止提前释放。
•方案七:多机实现的分布式锁Redlock
问题:master拿到锁,未同步slave挂掉,slave升为主,第二个线程拿到锁。多master部署,按顺序向5个master节点请求加锁,根据设置的超时时间来判断,是不是要跳过该master节点。大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即加锁成功。如果获取锁失败(超时,过期),所有master解锁。
双写一致性:
•延时双删:
1 先删除缓存
2 再更新数据库
3 休眠一会(比如1秒),再次删除缓存。
删除缓存重试机制的大致步骤:
•写请求更新数据库
缓存因为某些原因,删除失败
把删除失败的key放到消息队列
消费消息队列的消息,获取要删除的key
重试删除缓存操作
五、Dubbo
Dubbo 是一款高性能 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题。
架构分层:
•接口服务层:业务逻辑相关,provider和consumer接口和实现
•配置层:初始化配置信息,管理Dubbo配置。
•服务代理层:服务接口透明代理,将服务接口转换为可远程调用的代理对象。
•服务注册层:封装服务地址的注册和发现。
•路由层:封装多个提供者的路由和负载均衡,并桥接注册中心。
•监控层:PRC调用次数和调用时间监控。
•远程调用层:封装RPC调用的具体过程。
•信息交换层(Exchange):封装请求响应模式。
•网络传输层:将网络传输封装成统一接口。
•数据序列层:网络传输的序列化和反序列化。
角色:

1.服务提供者(Provider)
生产者必须依赖容器(container)才能正常启动,所以需container先启动,默认依赖spring容器
2.服务消费者(Consumer)
3.注册中心(Registry)
4.监控中心(Monitor)
核心配置:
dubbo:service 服务配置 dubbo:reference 引用配置
dubbo:protocol 协议配置 dubbo:application 应用配置
dubbo:module 模块配置 dubbo:registry 注册中心配置
dubbo:monitor 监控中心配置 dubbo:provider 提供方配置
dubbo:consumer 消费方配置 dubbo:method 方法配置
dubbo:argument 参数配置
集群容错方案:

负载均衡策略:
•随机(按权重设置随机概率)
•轮询(按公约后权重设轮询比率)
•最少活跃调用(响应快的提供者接受越多请求,响应慢的接受越少请求)
•一致性hash(一致性Hash,相同参数的请求总是发到同一个服务提供者(相同参数默认是指请求的第一个参数)根据服务提供者ip设置hash环,携带相同的参数总是发送的同一个服务提供者,若服务挂了,则会基于虚拟节点平摊到其他提供者上)
注册中心挂了,consumer还能不能调用provider?
可以的,启动 dubbo 时,消费者会从 zookeeper 拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。
Dubbo服务调用流程:
读取配置实例化服务者,proxy代理层封装服务接口供调用(封装时调用Protocol定义协议格式), Proxy 封装成 Invoker(真实服务调用的实例),invoker包装成Exporter方便在注册中心暴露。
消费者建立好实例会去注册中心拿ip端口及proxy通过invoker调用服务方。

消费者向zk拉取到服务列表后在本地保存记录
RPC底层原理:
全称remote procedure call,中译为远程过程调用。用白话来讲就是在别的进程上执行计算任务,因为不在当前进程上执行,是为远程。

rpc的目的或是我们为什么需要rpc?
为了能让这两个进程能够协同工作,就需要rpc技术了。

实现rpc所涉及到的底层技术
1.通信技术(网络IO、Network IO)
套接字(Socket): Socket套接字其本质是一个文件,外部Client端给某个Socket传递数据本质就是向这个文件写入数据,那我们的Server端再从Socket文件取数据这就是我们网络IO数据传输的一个本质。socket文件里数据的读写功能是由OS提供的。
从文件(Socket)里读数据的方式是Netty: Netty底层使用nio,对其使用者封装了一些便利好用的接口方便Java开发者去进行网络编程。
2.网络协议(字节数据解析技术)
使用的协议是TCP,编码与解码
编码:encode(String) → byte[],把字符串转换成字节数组;
解码:decode(byte[]) → String,把字节数组恢复成字符串;

序列化与反序列化技术(Serialization & Deserialization)
serialize(obj) → byte[],把对象转换成字节数组;
deserialize(byte[]) → obj,从字节数组中恢复对象;

RPC调用时需要传递的核心信息
无非就是调用方需要告诉被调用方我到底要调用哪个接口的哪个方法,调用参数等信息。
浅析rpc的原理及所用到的基本底层技术
六、Zookeeper
作用:
ZooKeeper 是一个开源的分布式协调服务框架,解决分布式架构数据一致性问题,应用场景有注册/配置中心、分布式锁、分布式队列、集群选举、分布式屏障、发布/订阅等场景。
数据结构:
1.文件系统
2.目录和子目录。
核心功能:
监听通知机制,如果对某个节点进行监听,当这个节点被删改时,监听方会感知到修改消息。
监听到七种变化:
1、None:连接建立事件
2、NodeCreated:节点创建
3、NodeDeleted:节点删除
4、NodeDataChanged:节点数据变化
5、NodeChildrenChanged:子节点列表变化
6、DataWatchRemoved:节点监听被移除
7、ChildWatchRemoved:子节点监听被移除
ZK数据如何持久化:
数据都是在内存中的,记录事务日志or快照方式
注册中心:
1.服务提供者启动时,会将其服务名称,ip地址注册到配置中心。
2.消费者第一次调用服务时,通过注册中心找到相应的服务的IP地址列表,并缓存到本地,后续消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从IP列表中取一个服务提供者的服务器调用服务。
3.当提供者的某台服务器宕机或下线时,相应的ip会从服务提供者IP列表中移除。同时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
4.当某个服务的所有服务器都下线了,那么这个服务也就下线了。
5.提供者某台服务器上线时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
6.服务提供方可以根据服务消费者的数量来作为服务下线的依据。
感知服务的下线&上线
下线:zookeeper提供了“心跳检测”功能(2s一次),它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除。
服务消费者会去监听相应路径,一旦路径上的数据增加或减,zookeeper都会通知服务消费方、服务提供者地址列表已经发生改变,从而进行更新。
ZK分布式锁:

(一) ZooKeeper的节点有次序编号,而这个生成的次序编号,是上一个生成的次序编号加一。
(二) ZooKeeper节点的递增有序性,为了确保公平,可以简单的规定:编号最小节点获得锁。
(三)ZooKeeper的节点监听机制,锁的传递有序而且高效。每个线程抢占锁之前,先尝试创建ZNode。释放锁删除创建的Znode。创建成功后,如果不是最小的节点,等前一个Znode的通知。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候。第一个通知第二个依次向后。只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,自己就获得锁。
另外,ZooKeeper的内部优越的机制,能保证由于网络异常或者其他原因,集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系,这个临时Znode也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。
(四)ZooKeeper的节点监听机制,能避免羊群效应。
后面监听前面的方式,可以避免羊群效应。羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,会给服务器带来压力,所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反应。
•优点:能有效的解决分布式问题,不可重入问题,使用简单。
•缺点:因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同步到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。
分布式锁主流的方案有两种:
(1)基于Redis的分布式锁
(2)基于ZooKeeper的分布式锁
两种锁,分别适用的场景为:
(1)基于ZooKeeper的分布式锁,适用于高可靠(高可用)而并发量不是太大的场景;
(2)基于Redis的分布式锁,适用于并发量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。
七、Spring
IOC(Inversion of Control):
即控制反转,对象的创建,赋值,管理工作都交给代码之外的容器实现。原理:Spring的IoC的实现原理就是工厂模式加反射机制。
DI(Dependency Injection):
即依赖注入,对象之间的依赖由容器在运行期决定,即容器动态的将某个依赖注入到对象之中。Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性。
AOP(Aspect Orient Programming):
Aspect Orient Programming直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。AOP代理主要分为静态代理和动态代理。
动态代理:
jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。
CGLIB通过继承做动态代理,如果某个类被标记为final,无法用CGLIB动态代理的。

xml配置切点切面等,在目标类织入增强逻辑,拦截器等。

AOP的底层实现-CGLIB动态代理和JDK动态代理_田有朋的博客-CSDN博客_cglib代理和jdk动态代理

Spring使用的模式:
1)单例模式: 在Spring中定义的bean默认是单例模式。
2)工厂模式:Spring使用工厂模式通过BeanFactory、ApplicationContext创建Bean对象。
3)代理模式:Spring AOP功能的实现是通过代理模式中的动态代理实现的。
4)策略模式:Spring中资源访问接口Resource的设计是一种典型的策略模式。Resource接口是所有资源访问类所实现的接口,Resource 接口就代表资源访问策略,但具体采用哪种策略实现,Resource 接口并不理会。客户端程序只和 Resource 接口耦合,并不知道底层采用何种资源访问策略,这样客户端程序可以在不同的资源访问策略之间自由切换。
5)适配器模式:Spring AOP的增强或通知使用到了适配器模式。SpringMVC中也是用到了适配器模式适配Controller。
6)装饰器模式:Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源,项目需要连接多个数据库,这种模式让我们可以根据客户需求切换不同的数据源。
7)模板模式:Spring中jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,就是用到了模板模式。
8)观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。

Spring注册bean的方式:

Spring容器启动流程:
•实例化BeanFactory工厂,生成Bean对象
•实例化BeanDefinitionReader,用于对特定注解(如@Service)的类转化成 BeanDefinition 对象,(BeanDefinition存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
•实例化ClassPathBeanDefinitionScanner,路径扫描查找 bean 对象
•将配置类的BeanDefinition注册到容器中
•调用refresh()方法刷新容器
BeanFactory是Spring最底层接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。
ApplicationContext为BeanFactory的子类,功能更全,如支持国际化。
BeanFactory:延迟加载,ApplicationContext:容器注入式创建所有bean
Spring Bean的生命周期

实例化-属性赋值-初始化-销毁,流程图:

1.bean的实例化,是通过反射的方式实现的。
2.bean的属性注入,是通过populateBean()实现的。
3.调用aware接口的相关方法(实现BeanName、BeanFactory、ApplictionContext对象的属性设置)
4.调用BeanPostProcessor的前置处理
5.调用initMethond方法,判断是否实现InitializingBean接口,如果有的话就实现afterPropertiesSet方法。
6.调用BeanPostProcessor的后置处理,aop就是这块实现的
7.获取完整的bean对象,可以通过getBean来获取。
8.当容器关闭的时候,就会调用销毁流程,调用DisposableBean的destory()。

Spring Bean 生命周期 (实例结合源码彻底讲透)
bean作用域:
(1)singleton:默认单例,每个容器中只有一个bean的实例。
(2)prototype:一个bean多个实例。
(3)request:每个request请求创建一个实例,请求完成后bean会失效并被垃圾回收器回收。
(4)session:同一个session会话共享一个实例,不同会话使用不同的实例。
(5)global-session:所有会话共享一个实例。
Spring框架中的Bean是否线程安全?不安全如何处理
(1)prototype,每次都创建一个新对象,无共享,因此不会有线程安全问题。
(2)singleton,所有的线程都共享一个单例Bean,存在问题。如果单例Bean无状态(也就是只查询)则线程安全。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。
解决:用ThreadLocal解决线程安全问题,为每个线程提供独立的变量副本,不同线程只操作自己线程的副本变量,空间换时间。同步锁时间(排队等待)换空间。
Spring基于xml注入bean的几种方式:
set()方法注入;property
构造器注入:①通过index设置参数的位置;②通过type设置参数类型;constructor-args
静态工厂注入;factory-method
实例工厂;factory-bean factory-method
循环依赖:
第三级缓存目的:延迟代理对象的创建
循环依赖时,A不能构造函数方式注入B,而B不受限制,即可以通过三种注入方式的任意一种注入主bean对象。
•实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
•为 A 创建一个 Bean工厂,并放入到 singletonFactories 中。
•发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
•实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
•为 B 创建一个 Bean工厂,并放入到 singletonFactories 中。
•发现 B 需要注入 A 对象,此时在一级、二级未发现对象
•A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A还是一个半成品,并没有完成属性填充和执行初始化方法)
•将对象 A 注入到对象 B 中。
•对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
•对象 A 得到对象B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
•对象 A完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A。

二级缓存来解决循环依赖意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,Spring设计原则在 Bean 初始化完成之后创建代理。所以选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,因为如果不提前创建代理对象,那么注入的就是原始对象,这样就会产生错误。
自动装配:
基于XML或注解。
注解:
1.@Autowired(by Name)
@Autowired 注解默认按类型匹配进行依赖注入,但可以结合 @Qualifier 注解按名称匹配进行精确注入。
@Resource(先by Type找不到by Name)
Spring事务:
TransactionTemplate:编程式事务,代码块级别
@Transactional:声明式事务,方法级别
Spring的事务是如何实现的,和事务的失效场景。
底层就是Aop来实现的,分别为声明式事务和编程式事务(几乎不用)
1.首先要生成代理对象,但是事务不是直接通过通知来实现的,而是通过TransactionInterceptor来间接实现的,然后调用invoke来实现具体的逻辑
2.解析各个方法上面的属性
3.当需要开启事务的时候,获取数据库连接,关闭自动提交
4.执行业务逻辑
5.如果执行失败了则事务rollback,如果成功则commit。
失效场景
1.方法用final修饰
2.方法内部调用(有事务注解的被没有事务注解的调用)
3.未被spring容器管理
事务传播机制:
用threadLocal实现,出现新线程则事务传播失效。

事务隔离级别:
读未提交、读已提交、可重复读、串行化
隔离级别 脏读 不可重复读 幻读
Read Uncommited 读取未提交 是 是 是
Read Commited 读取已提交 否 是 是
Repeatable Read 可重复读 否 否 是
Serialzable 可串⾏化 否 否 否

八、SpringMVC
封装servlet细节,属性名与参数名一致即可封装到bean。
SpringMVC执行流程:

(1)用户发送请求至前端控制器DispatcherServlet;
(2)DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler;
(3)HandlerMapping处理器映射器根据请求url找到具体的处理器Handler,生成处理器对象及处理器拦截器(如果有则生成),一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器,请求执行Handler;
(5)HandlerAdapter 经过适配调用 具体处理器进行处理业务逻辑;
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。
SpringMVC常见面试题总结

九、SpringBoot
Spring Boot是一个基于Spring框架的开源框架,它简化了基于Java的企业级应用程序的开发。Spring Boot的设计目标是使Spring应用程序的开发变得更加容易、更快捷,同时提供了一种约定大于配置的方式,减少了开发者在配置上的烦恼。简化配置。
核心注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:启用组件扫描,自动扫描并注册标有@Component、@Service、@Repository等注解的类。
SpringBoot自动配置原理:
在启动类有一个@SpringBootApplication注解,在这个注解中组合了一个@EnableAutoConfiguration注解,这个注解的作用是打开自动装配,在这个注解中又包含了@Import({AutoConfigurationImportSelector.class})注解,在对应的selectImports方法中会读取META/INF目录下的spring.factories文件中需要被自动装配的所有配置类,然后通过META-INF下面的spring-configuration-metadata.properties文件做条件过滤(减少没必要加载的类),最后返回的就是自动装配的对象。
SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽_SunAlwaysOnline的博客-CSDN博客_springboot自动装配原理
SpringBoot启动流程:

十、Mybatis
Dao的工作原理:
Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,根据类的全限定名+方法名,定位到唯一一个MapperStatement并调用执行器执行所代表的sql,然后将sql执行结果返回。
Xml映射文件,id是否可以重复?
namespace不同可以,namespace + id做主键
分页插件原理:
自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
延迟加载原理:
使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Mybatis架构:

Mybatis的一级、二级缓存
(1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
(2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
(3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。
Mybatis工作流程:

十一、RocketMQ

1.NameServer 先启动
2.Broker 启动时向 NameServer 注册
3.生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),然后根据负载均衡算法从列表中选择一台Broker 进行消息发送。
4.NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到Broker 宕机(使用心跳机制, 如果检测超120S),则从路由注册表中将其移除。
5.消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),但是消费者选择从 Broker 中 订阅消息,订阅规则由 Broker 配置决定
作用:
解耦(不耦合调用系统),流量削峰(缓存大量请求),异步(降低 响应时间)
缺点:
依赖度高可用性降低,复杂度提高(消息丢失重复消费),数据一致性问题(A成功,B失败)
角色:
Producer: 产生消息
Producer Group:多个发送同一类消息的生产者称之为一个生产者组
Consumer:后台系统负责异步消费
Consumer Group:消费同一类消息的多个 Consumer 实例组成一个消费者组
Message:每个message必须指定一个topic,有一个可选的 Tag 设置,消费端可以基于 Tag 进行过滤消息。
Topic:Producer将消息发往指定的Topic,Consumer订阅该Topic就可以收到这条消息。⽐如⼀个电商系统可以分为:交易消息、物流消息 等,⼀条消息必须有⼀个 Topic 。
Tag:标签,子主题(二级分类)对topic的进一步细化,用于区分同一个主题下的不同业务的消息。⽐如交易消息⼜可以分为:交易创建消息、交易完成 消息等,⼀条消息可以没有 Tag 。
Broker:负责接收并存储消息,同时提供Push/Pull接口来将消息发送给Consumer。Broker同时提供消息查询的功能,可以通过MessageID和MessageKey来查询消息。Borker会将自己的Topic配置信息实时同步到NameServer
Queue:Topic和Queue是1对多的关系,一个Topic下可以包含多个Queue,主要用于负载均衡,Queue数量设置建议不要比消费者数少。发送消息时,用户只指定Topic,Producer会根据Topic的路由信息选择具体发到哪个Queue上。Consumer订阅消息时,会根据负载均衡策略决定订阅哪些Queue的消息
Offset:RocketMQ在存储消息时会为每个Topic下的每个Queue生成一个消息的索引文件,每个Queue都对应一个Offset记录当前Queue中消息条数
NameServer:NameServer可以看作是RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。各 NameServer 之间不会互相通信, 各自都有完整的路由信息,即无状态。
Producer/Consumer :通过查询NameServer提供的接口获取Topic对应的Broker的地址和Topic-Queue的路由配置
Broker : 发送心跳包包含当前Broker信息(ip、port等)到NameServer, 实时更新Topic信息到NameServer
消息分片:Queue是Topic在一个Broker上的分片等分为指定份数后的其中一份,是负载均衡过程中资源分配的基本单元。
Broker.conf配置文件:
删除时间(4:00am),集群名,broker名,nameserver地址,对外端口,commitLog大小,消息大小,刷盘方式,broker角色(slave,异步复制master,同步双写master),发/拉消息线程数。
发消息方式:
同步发送,异步发送(SendCallback结果回调),单向发送。
消费消息模式:
集群模式:一条消息同一个消费者组中只有一个消费者会消费到(公司每个系统集群为一个消费者组)。
广播模式:一条消息同一个消费者组中每个消费者都要消费。
顺序消息:
控制消息发到同一个queue,顺序生产:producer.send(msg, new MessageQueueSelector(){},msgid),顺序消费:每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序。
延时消息:
setDelayTimeLevel,18个等级,1s-2h,消息的消费比存储时间晚10秒。
批量消息:
同一个topic(不能延时,不能超4MB,超则分割)
过滤消息:
一个消息只能有一个标签,MessageSelector.bySql来使用sql筛选消息。
事务消息:
正常事务发送与提交阶段
•生产者发送一个半消息给broker(半消息是指的暂时不能消费的消息)
•服务端响应成功
•开始执行本地事务
•根据本地事务的执行情况执行Commit或者Rollback
事务信息的补偿流程
•如果broker长时间没有收到本地事务的执行状态,会向生产者发起一个回查的操作请求
•生产者收到确认回查请求后,检查本地事务的执行状态
•根据检查后的结果执行Commit或者Rollback操作。补偿阶段主要是用于解决生产者在发送Commit或者Rollbacke操作时发生超时或失败的情况。
3)事务消息状态
事务消息共有三种状态,提交状态、回滚状态、中间状态:
TransactionStatus.CommitTransaction:提交事务,它允许消费者消费此消息。
TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
TransactionStatus.Unknown:中间状态,它代表需要检查消息队列来确定状态。
创见事务监听器
消息存储:
顺序写,无论哪个topic的queue都写。
生产者将消息推送给对应的Broker之后,Broker会持久化到对应的commitLog文件,然后ReputMessageService线程会定时以CommitLog文件为基础来更新ConsumeQueue(包括Commitlog 中的偏移位置)文件和index文件;作为消费者,会根据ConsumeQueue的offset信息,拉取对应queue的数据消费,所以,当消息生产者提交的消息存储在Commitlog文件中,ConsumeQueue、IndexFile需要及时更新,否则消息无法及时被消费。
在 RocketMQ 中不论是 CommitLog 还是 CosumerQueue 都采用了 mmap。

存储结构:
消息物理存储文件是CommitLog,ConsumeQueue 是消息的逻辑队列,类似索引文件,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件。顺序写
刷盘机制:
1.同步刷盘:在返回写成功状态时,消息已经被写入磁盘。消息写入内存的 PageCache 后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
2.异步刷盘:在返回写成功状态时,消息可能只是被写入了内存的 PageCache;当内存里的消息量积累到一定程度时,统一触发写磁盘。
零拷贝:

  1. 从磁盘复制数据到内核态内存;
  2. 从内核态内存复制到⽤户态内存;
  3. 然后从⽤户态内存复制到⽹络驱动的内核态内存;
  4. 最后是从⽹络驱动的内核态内存复制到⽹卡中进⾏传输。

所以,可以通过零拷⻉的⽅式,减少⽤户态与内核态的上下⽂切换和内存拷⻉的次数,⽤来提升I/O的性能。零拷 ⻉⽐较常⻅的实现⽅式是mmap,这种机制在Java中是通过MappedByteBuffer实现的。

mmap 的方式,可以省去向用户态的内存复制。
消费高可用:
master角色的broker用于写,slave角色只读。当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。
发送高可用:
把Topic的多个MsgQueue创建在多个Broker组上(相同Broker名称,不同 brokerId的机器组成一个Broker组),当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。 不支持把Slave自动转成Master,如果机器资源不足要手动停止Slave角色的Broker,更改配置文件,用新的配置文件启动Broker。
主从复制:
同步(双写)复制:
Master 和 Slave 均写成功后才反馈给客户端写成功状态。优:在同步复制方式下,如果 Master 出故障,Slave上有全部的备份数据,容易恢复。缺:同步复制会增大数据写入延迟,降低系统吞吐量。
异步复制:
Master 写成功即刻反馈给客户端写成功状态。优:较低的延迟和较高的吞吐量。缺:如果 Master 出了故障,有些数据因为没有被写入 Slave可能丢失。
负载均衡:
Producer负载均衡:默认会轮询所有的MsgQueue发送。
Consumer负载均衡:consumer平摊queue。
消息重试:
对于顺序消息,当消费者消费消息失败后,会消息重试(每次间隔时间为 1 秒),会出现消息消费被阻塞的情况。因此务必保证应用能够及时监控并处理消费失败的情况,避免阻塞。对于无序消息(普通、定时、延时、事务消息),消费失败时,设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费;广播方式消费失败后,不再重试,继续消费新的消息。默认重试16次。
多消费组重试:
假设有 A 消费者组和 B 消费者组,当 A 和 B 同时监听同一个 topic 时,A 和 B 都获得了同一消息,但是 A 消费失败了(return Action.ReconsumeLater),B 却消费成功了。然后重试的时候,rocketMQ 只会把该消息再发送给 A 消费者组,不会再发送给 B 消费者组了。
死信队列:
超过最大重试次数仍失败单独放一个队列,3天内不处理自动删除。一个队列对应一个groupid,多个topic的死信消息。可在控制台重发。
消息幂等:
消息key作为业务唯一主键去重。
流量控制:
生产者流控:因为broker处理能力达到瓶颈;
消费者流控:因为消费能力达到瓶颈。
1.生产者流控:(注意,生产者流控,不会尝试消息重投。)
commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,返回流控。
如果开启transientStorePoolEnable == true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,返回流控。
broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,返回流控。
broker通过拒绝send 请求方式实现流量控制。
2.消费者流控:(消费者流控的结果是降低拉取频率。)
消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。
消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。
消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。
消息堆积:

可以新建⼀个临时的Topic,临时的Topic多设置⼀些 Message Queue,然后先⽤⼀些消费者把消费的数据丢到临时的Topic,因为不⽤业务处理,只是转发⼀下消息很快的,上线N台Consumer同时消费临时Topic中的数据。好了后恢复原来consumer。
原因:机器性能不足、生产者业务暴增、消费者挂了。

预映射机制和文件预热:
内存预映射机制:Broker 会针对磁盘上的各种 CommitLog、ConsumeQueue 文件预先分配好MappedFile,提前对一些可能接下来要读写的磁盘文件,提前使用 MappedByteBuffer 执行 mmap() 函数完成内存映射,这样后续读写文件的时候,就可以直接执行了(减少一次 CPU 拷贝 )。
文件预热:在提前对一些文件完成内存映射之后,因为内存映射不会直接将数据从磁盘加载到内存里来,那么后续在读,取尤其是 CommitLog、ConsumeQueue 文件时候,可能会频繁的从磁盘里加载数据到内存中去。所以,在执行完 mmap() 函数之后,还会进行 madvise() 系统调用,就是提前尽可能将磁盘文件加载到内存里去。(读磁盘 -> 读内存)

RocketMQ为什么性能好?
•数据分散多集群(数据分片)
在RocketMQ中,整个集群环境由多个分散的小集群组成,所以topic首先会分片到多个小集群中,然后每个小集群内部又会分成多个msgqueue。这两层的分片很大程度上提升了系统的性能。
•顺序写磁盘、磁盘预读
RocketMQ存储模型设计时,设计成每个broker实例上所有的topic共用一个commitlog文件。从而保证全局顺序写。同时索引格式设计是固定长度,充分利用操作系统磁盘预读特性提升性能。
•mmap内存映射
RocketMQ commitlog在保证顺序写的情况下,通过采用mmap方式来加速读数据过程。使其随机读也不会大幅度影响系统性能。

十二、Kafka
零拷贝:2 次上下文切换和2次数据拷贝
四次拷贝:磁盘到内核缓冲区(DMA),内核缓冲区到用户缓冲区(cpu),用户缓冲区到socket缓冲区(CPU),socket到网卡(DMA)。零拷贝省略2次CPU拷贝。
DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。
•通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
•只将缓冲区描述符和数据长度传到 socket 缓冲区,而内核缓存中的数据则通过网卡的 SG-DMA 控制器直接拷贝到网卡的缓冲区里,这样就减少了一次数据拷贝;
mmp:mmap是一种实现内存映射文件的方法。
即:将一个文件映射到用户进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。内核空间对这段区域的修改也直接反映到用户空间,从而可以实现不同用户进程间的文件共享。
sendfile:将内核缓冲区的文件描述符/长度信息发送到socket缓冲区,省略一次cpu拷贝。
无法全局有序,可以分区有序

角色介绍:

Producer:消息生产者,向Kafka中发布消息的角色。
Consumer:消息消费者,即从Kafka中拉取消息消费的客户端。
Consumer Group:消费者组,消费者组则是一组中存在多个消费者,消费者消费Broker中当前Topic的不同分区中的消息,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
Broker:经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
Topic:主题,可以理解为一个队列,生产者和消费者都是面向一个Topic
Partition:分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)
Replica:副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
Leader:每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
Follower:每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。

partition:类似RocketMQ的分片。
Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。副本最大数量是10个,且不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。
kafka依赖ZK,RockerMQ有NameSvr
一个topic有多个partition如何找到是哪个?轮询或者hash key或者指定。
partition结构:
每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。分段+索引提升查找效率。

Messag存储结构:
offset:offset是一个占8byte的有序id号,它可以唯一确定每条消息在parition内的位置!
消息大小:消息大小占用4byte,用于描述消息的大小。
消息体:消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。
生产者ISR:
每个partition收到消息都要返回ack给生产者。
存储策略:
无论消息是否被消费,kafka都会保存所有的消息。那对于旧数据有什么删除策略呢?
1、 基于时间,默认配置是168小时(7天)。
2、 基于大小,默认配置是1073741824(10GB)。
消费:
消费者与分区(partition)数量一致。
发送ACK的时机:
确保有follower与leader同步完成,leader在发送ack,这样可以保证在leader挂掉之后,follower中可以选出新的leader(主要是确保follower中数据不丢失)
follower同步完成多少才发送ack:
•半数以上的follower同步完成,即可发送ack。
优点是延迟低;缺点是选举新的leader的时候,容忍n台节点的故障,需要2n+1个副本(因为需要半数同意,所以故障的时候,能够选举的前提是剩下的副本超过半数),容错率为1/2
•全部的follower同步完成,才可以发送ack。
优点是容错率高,选举新的leader的时候,容忍n台节点的故障只需要n+1个副本即可,因为只需要剩下的一个人同意即可发送ack了。
缺点是延迟高,因为需要全部副本同步完成才可。
kafka选择的是第二种,因为在容错率上面更加有优势,同时对于分区的数据而言,每个分区都有大量的数据,第一种方案会造成大量数据的冗余。虽然第二种网络延迟较高,但是网络延迟对于Kafka的影响较小。
ISR:

消息丢失?应答ACK机制
0:代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。低延迟。At Most Once
1:代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。
-1(all):代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。At Least Once

选举:
每个Topic都有一个ISR列表,直接取ISR列表的第一个作为leader,如果当前挂的就是第一个,则选择后面一个作为leader。
数据一致性问题(故障转移):
follower故障:follower发生故障后会被临时踢出ISR,等待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步,等待该follower的LEO大于等于该partition的HW,即follower追上leader之后,就可以重新加入ISR了。
leader故障:leader发生故障之后,会从ISR中选出一个新的leader,为了保证多个副本之间的数据的一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader中同步数据。

幂等性:
Producer不论向Server发送了多少次重复数据,Server端都只会持久化一条数据。
消费者offset的存储:
由于Consumer在消费过程中可能会出现断电宕机等故障,Consumer恢复以后,需要从故障前的位置继续消费,所以Consumer需要实时记录自己消费到了那个offset,以便故障恢复后继续消费。
概念:
ISR:速率和leader相差低于10s的follower的集合
OSR:速率和leader相差大于10s的follwer
AR:所有分区的follower
有哪些情形会造成重复消费,漏消费?
先消费后提交offset,如果消费完宕机了,则会造成重复消费。
先提交offset,还没消费就宕机了,则会造成漏消费。
指定了一个 offset, Kafka Controller 怎么查找到对应的消息?
offset表示当前消息的编号,首先可以通过二分法定位当前消息属于哪个.index文件中,随后采用seek定位的方法查找到当前offset在.index中的位置,此时可以拿到初始的偏移量。通过初始的偏移量再通过seek定位到.log中的消息即可找到。
Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线、所有topic的分区副本分配和leader的选举等工作。Controller的工作管理是依赖于zookeeper的。
kafka为什么快?
•数据分片/分区(提高并行度)
Kafka 将消息分成多个 partition,增加了并行处理的能力。
•顺序写磁盘(提升写性能)
Producer 发送的消息顺序追加到文件中,Consumer 从 Broker 自带偏移量读取消息。这两者可以充分利用磁盘的顺序写和顺序读性能,速度远快于随机读写。
•数据零拷贝(减少上下文切换和拷贝次数)
mmap 持久化文件:Broker 写入数据,并非真正的 flush 到磁盘上了,而是写入到 mmap 中。
sendfile 读取:Customer 从 Broker 读取 数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送。
•批处理(提升IO性能,减少网络传输开销)
Producer 发送多个消息到同一分区,通过批量发送可以减少系统性能开销。
batch.size:默认积压到 16K 就会批量发送
linger.ms:设置一定延迟来收集更多消息。默认 0ms ,即有消息就立马发送。
上述两个条件有任一条件满足,就会触发批量发送。
•数据压缩:
Kafka 支持三种压缩算法:lz4 gzip snappy。
Kafka和RocketMQ对比:

十三、JVM
java源文件到执行的过程:
•编译:语法分析、语义分析、注解处理生成字节码文件。
•加载:class文件加载到jvm,分为:装载-链接-初始化。
装载:类加载器根据双亲委派机制加载到jvm,创建出class对象以及类信息存储到方法区。
链接:验证-准备-解析。验证java类是否符合规范-为类变量分配内存空间并赋默认值-将符号引用转为直接引用。
初始化:为静态变量赋正确初始值。
•解释:把字节码转换为操作系统识别的指令。2种方式:字节码计时器、即时编译器。热点代码做编译,非热点直接解释。热点探测检测(计数器和抽样)是否热点代码,计数器超过阈值触发即时编译。即时编译器将热点代码保存,无需重复解释。
•执行:os把解释器解析出的指令码调用系统的硬件执行最终的程序指令。
双亲委派机制:

JVM内存结构:

线程共享:堆、方法区
方法区:类相关信息(版本、字段、方法、接口、父类信息)、常量池(静态常量池:字符串常量池、字面量、符号引用、运行时常量池、类加载时生成的符号引用)

真正意义上字符串常量池在堆中存储,元空间可能有引用堆中字符串常量,运行时常量池在方法区中
堆:

线程不共享:程序计数器、虚拟机栈(局部变量表、操作数栈、动态链接、返回地址)、本地方法栈(本地方法C/C++)
动态链接:编译时无法确定类型,运行时发生改变需要动态链接指针。
静态链接:符号引用转为直接引用。
解析阶段即是虚拟机将常量池内的符号引用(一组符号来描述所引用的目标)替换为直接引用的过程。

永久代换元空间原因:
永久代 在虚拟机受到限定的内存大小限制,很容易发生内存溢出。
如何判断对象不再使用:
•引用计数法:被引用则+1,引用失效则-1,计数器为0,说明不再被引用可以回收。缺点:若循环依赖,无法判断是否可回收。
•可达性分析:GC Roots,也就是根对象作为起始节点集合,从根节点开始,根据引用关系向下搜索,若没被引用则可回收,老年代不回收。

垃圾回收器:
年轻代(标记复制):Seria、Parallel Scavenge、ParNew
老年代:Serial Old、Parallel Old、CMS

垃圾回收算法:
标记清除

标记复制

标记整理

进入老年代:对象太大、太老(每minorGC一次年龄+1,默认值15),当eden区不足触发minor GC,

CMS垃圾回收器:

并发标记删除特点:并发,GC时用户线程不会完全停止工作,部分场景与GC并发执行。无论何种垃圾回收器STW无法避免。
设计目标:避免老年代GC长时间卡顿。
五个步骤:初始标记、并发标记、并发预清理、重新标记、并发清除。
初始标记(STW):标记GC Roots能直接关联到的对象,速度非常快。
并发标记:进行GC Roots Tracing ,就是从GC Roots开始找到它能引用的所有对象的过程。
重新标记(STW):为了修成并发标记期间因用户程序继续运作导致标记产生变动的一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间要短。
并发清除:在整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程与用户线程一起并发执行的。
优点:并发收集、并发清除、低停顿。
缺点:对CPU要求高,无法处理浮动垃圾、产生大量空间碎片、并发阶段会降低吞吐量。
对CPU敏感,并发阶段虽然不会导致用户线程暂停,但是它总是要线程执行,还是会占用CPU资源,(一定程度上也是,吞吐量的下降)
无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次 gc 的时候清理掉。
产生大量空间碎片、并发阶段会降低吞吐量。
OOM与JVM退出:
尽管OutOfMemoryError本身不会导致JVM退出,但一下几种情况可能会
1.未捕获的OOM:
如果OutOfMemoryError在应用程序中未被捕获,并传播到主线程,那么主线程将终止,可能导致整个应用程序的终止。
2.连续的OOM:
在第一个OutOfMemoryError之后,如果程序继续运行并再次尝试分配内存,可能会连续出发多个OOM,使得程序无法继续执行。
3.JVM内部错误:
在某些情况下,如JVM的内部进程(例如Finalizer)遭遇OutOfMemoryError,JVM可能会决定退出。
性能调优:

参数:
•jps:运行的进程状态信息
•jstack:某个Java进程内的线程堆栈信息
1.找出进程id :ps -ef | pcr-core | grep -v grep
2.找出最费cpu进程:ps -Lfp pid
3.printf “%x\n” 21742 打印16进制pid
4.jstack 21711 | grep 54ee
•jmap:查看堆内存使用状况,一般结合jhat用
jmap -heap pid 查看进程堆内存使用情况
•查看垃圾回收频率,再次确认:jstat -gcutil 16190 1000
•jconsole:可以观测堆内存使用量、线程数、类加载数和CPU占用率;内存选项可以查看堆中各个区域的内存使用量和左下角的详细描述(内存大小、GC情况等);线程选项可以查看当前JVM加载的线程,查看每个线程的堆栈信息,还可以检测死锁;VM概要描述了虚拟机的各种详细参数。

调优经验总结:
1.GC的时间足够的小
2.GC的次数足够的少
3.发生Full GC的周期足够的长
4.如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。
5.一般来说,Survivor 区的空间不够,或者占用量达到 50%时,就会使对象进入年老代(不管它的年龄有多大),老年代避免存在命短的大对象。
触发FullGC情况:
•程序执行了System.gc() //建议jvm执行fullgc,并不一定会执行
•执行了jmap -histo:live pid命令 //这个会立即触发fullgc
•在执行minor gc的时候进行的一系列检查
执行Minor GC的时候,JVM会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小。
如果大于,则直接执行Minor GC(这个时候执行是没有风险的)。
如果小于了,JVM会检查是否开启了空间分配担保机制,如果没有开启则直接改为执行Full GC。
如果开启了,则JVM会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小,如果小于则执行改为执行Full GC。
如果大于则会执行Minor GC,如果Minor GC执行失败则会执行Full GC
•使用了大对象 //大对象会直接进入老年代
•在程序中长期持有了对象的引用 //对象年龄达到指定阈值也会进入老年代

JVM性能调优经验贴:
JVM性能调优_滴哩哩哩滴哩哩哩哒哒的博客-CSDN博客_jvm调优场景

十四、SpringCloud
Seata分布式事务:
默认AT模式是二阶段提交的事务:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
分布式事务处理过程的一ID+三组件模型,一ID即Transaction ID XID,全局唯一的事务ID。
三组件:
1.TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动 全局事务提交或回滚。
2.TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
3.RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈已注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
然后我们看一下Seata处理流程

nacos:
Nacos经典7道面试题
OpenFeign:
OpenFeign与Ribbon源码分析总结

Ribbon:
https://blog.51cto.com/u_15064638/2873996

服务降级、熔断、降级
服务降级:fallback,如果系统不可用有兜底的方法,给予消费者识别。
服务熔断:保险丝,直接拒绝访问。服务降级–>进行熔断–>恢复调用链路
服务限流:limit 流量太多,不要让服务器打满。 sentinel流控两个维度,一个qps,还有一个是线程数的阈值
十五、Docker
Docker 由镜像、镜像仓库、容器三个部分组成
镜像: 跨平台、可移植的程序+环境包
镜像仓库: 镜像的存储位置,有云端仓库和本地仓库之分,官方镜像仓库地址(https://hub.docker.com/)
容器: 进行了资源隔离的镜像运行时环境
十六、Elasticsearch
Elasticsearch 是一个开源的分布式搜索引擎,它属于 Elastic Stack(前身是ELK Stack,由 Elasticsearch、Logstash 和 Kibana 组成)。Elasticsearch 被设计用于实时搜索、分析和存储大规模数据集。
ik_max_word:文本最细粒度拆分
ik_smart: 文本最粗粒度拆分
十七、计算机网络
计算机网络常见面试题总结
Http为什么要加密?
因为http的内容是明文传输的,明文数据会经过中间代理服务器、路由器、wifi热点、通信服务运营商等多个物理节点,被劫持内容就暴露,还可以篡改传输的信息且不被双方察觉。
对称加密:
浏览器只要预存好世界上所有HTTPS网站的密钥就行了,不现实
非对称加密:
公钥加密的内容必须用私钥才数字证书。
https采用的加密方式:

某网站拥有用于非对称加密的公钥A、私钥A’。
浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器。
服务器拿到后用私钥A’解密得到密钥X。
这样双方就都拥有密钥X了,且别人无法知道它。之后双方所有数据都用密钥X加密解密。
数字证书:
网站在使用HTTPS前,需要向“CA机构”申请颁发一份数字证书,数字证书里有证书持有者、证书持有者的公钥等信息,服务器把证书传输给浏览器,浏览器从证书里取公钥就行了,证书就如身份证一样,可以证明“该公钥对应该网站”。然而这里又有一个显而易见的问题了,证书本身的传输过程中,如何防止被篡改?即如何证明证书本身的真实性?身份证有一些防伪技术,数字证书怎么防伪呢?解决这个问题我们就基本接近胜利了!
如何放防止数字证书被篡改?
我们把证书内容生成一份“签名”,比对证书内容和签名是否一致就能察觉是否被篡改。这种技术就叫数字签名:
数字签名能解开,同样,私钥加密的内容只有公钥能解开。
数字签名:
数字签名的制作过程:
CA拥有非对称加密的私钥和公钥。
CA对证书明文信息进行hash。
对hash后的值用私钥加密,得到数字签名。
明文和数字签名共同组成了数字证书,这样一份数字证书就可以颁发给网站了。
浏览器拿到服务器传来的数字证书后,如何验证它是不是真的?(有没有被篡改、掉包)
浏览器验证过程:
拿到证书,得到明文T,数字签名S。
用CA机构的公钥对S解密(由于是浏览器信任的机构,所以浏览器保有它的公钥。详情见下文),得到S’。
用证书里说明的hash算法对明文T进行hash得到T’。
比较S’是否等于T’,等于则表明证书可信。
十八、操作系统
操作系统:硬件之上的软件,主要:进程管理、内存管理、文件管理、设备管理,提供用户接口
四类I/O操作:直接访问、中断、DMA直接内存访问、通道控制。
用户态内核态:
内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

上下文切换:cpu分配给另一个进程,保存现场(内存空间指针、指令执行位置)

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值