Java后台面试相关知识点解析

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,其他两个就都有了。

String、StringBuffer 和 StringBuilder 的区别?

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 {
        @Override
        public 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
     */
    @Override
    protected 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);
        }
 
 
    }
}

java中的线程

每当使用java命令执行一个带main方法的类时,就会启动JVM(应用程序),实际上就是在操作系统中启动一个JVM进程,JVM启动时,必然会创建以下5个线程:

  • main 主线程,执行我们指定的启动类的main方法
  • Reference Handler 处理引用的线程;线程的任务就是将一个不用的对象打上标记,放到回收对象的队列中,以便于Finalizer线程来进行释放内存操作。
  • Finalizer 调用对象的finalize方法的线程,就是垃圾回收的线程; JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收。
  • Signal Dispatcher 分发处理发送给JVM信号的线程
  • Attach Listener 负责接收外部的命令的线程

AttachListener(请求接收线程):AttachListener线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求JVM给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在JVM启动的时候没有初始化,那么,则会在用户第一次执行JVM命令时,得到启动。

Signal Dispatcher(信号转发线程):前面我们提到第一个Attach Listener线程的职责是接收外部JVM命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部JVM命令时,进行初始化工作。

区别:可以看出以上的AttachListener线程和Signal Dispatcher线程是属于两者相互解耦的线程,有点类似单一职责且加入了命令模式的概念在里面,一个抓门负责接受(AttachListener)之后进行封装后转发给执行线程(Signal DIspatcher)统一进行执行和派遣工作。如同邮局收件和运件属于两个部门。

Java是线程的抢占式的吗

Java中的线程并不是完全抢占式的。

在Java中,多线程的实现是一种抢占式的机制,但并不是所有情况下都是如此。抢占式策略是指系统会分配给每个执行任务的线程一个很小的时间段用于执行任务,当该时间段用完后,系统会剥夺其CPU使用权,交给其他线程执行。但是,Java中的线程调度器是基于优先级的,也就是说,优先级高的线程会获得更多的执行机会,但这并不意味着它可以一直占用CPU,当高优先级的线程处于等待或阻塞状态时,低优先级的线程也可以得到执行。

此外,Java中的线程还有非抢占式调度的情况。例如,在使用synchronized关键字或ReentrantLock等锁机制时,当一个线程获得了锁并正在执行临界区的代码时,其他需要该锁的线程即使优先级再高,也必须等待该线程释放锁后才能执行。这种情况下,线程的调度就不是抢占式的。

因此,Java中的线程调度方式既有抢占式的特点,也有非抢占式的特点,具体取决于线程的优先级、锁机制等因素。

集合

ThreadLocal的内存泄露的原因分析以及如何避免

在这里插入图片描述

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部 强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

解决ThreadLocal内存泄露的措施主要包含以下几点:

  • 及时移除
    最简单的解决方式是,在不再需要访问 ThreadLocal 存储的数据时,及时调用 ThreadLocal.remove() 方法来清除线程局部变量。

  • 使用try-finally块确保remove()
    在代码结构中采用 try-finally 块,在 try 块中访问线程局部变量,然后在 finally 块中调用 remove()。这样即使出现异常,也能保证局部变量被清除。

  • 自定义ThreadLocal匿名内部类
    创建 ThreadLocal 变量时,覆盖 ThreadLocal 的 initialValue()remove() 方法,以确保在书写代码时关注资源的清理。

  • 减少使用ThreadLocal
    如果能不使用 ThreadLocal 以避免其中的陷阱,则通过其他方式传递方法间共享的数据也是一种办法。

  • 线程池监控
    监控使用线程池时线程的回收情况,当确定线程不会再被复用,清除它所持有的 ThreadLocal 变量。

由于Key是弱引用,因此ThreadLocal可以通过key.get()==null来判断Key是否已经被回收,如果Key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。
ThreadLocal会在以下过程中自发清理过期节点:

  • 调用set()方法时,采样清理、全量清理,扩容时还会继续检查。
  • 调用get()方法,没有直接命中,向后环形查找时。
  • 调用remove()时,除了清理当前Entry,还会向后继续清理。

解决示例

学习博客

解决示例

图解

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改成尾插法,解决了链表死循环的问题

博客详解

ArrayList 内部使用数组来存储元素,当 ArrayList 容量不足时,会自动进行扩容。ArrayList 的扩容机制是在当前容量不足时,创建一个新的更大容量的数组,并将原数组中的元素复制到新数组中。默认情况下,每次进行扩容时,新数组的容量是原数组容量的 1.5 倍,即增长率是 50%。
LinkedList 是一个双向链表,每个节点包含了前一个节点和后一个节点的引用。当添加元素时,LinkedList 不需要进行扩容操作,因为它的存储结构是动态的,可以根据需要灵活地分配节点

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将遍历过的元素存入。

第一次相遇的时候将一个指针指向头节点,两个指针再同时向后每移动一个数据,再次相遇就是环的入口。

参考博客

简易解析

线程安全的集合

Java中有多种线程安全的List,常见的包括:

  1. CopyOnWriteArrayList:这个类是线程安全的,因为它在读操作时不需要锁,因为它拷贝了一份原数组,对拷贝的数组进行操作,写操作时需要锁,因为在写操作时需要修改原数组。

  2. Collections.synchronizedList(List< T> list)。使用这种方法我们可以获得线程安全的List容器,它和Vector的区别在于它采用了同步代码块实现线程间的同步。通过分析源码,它的底层使用了新的容器包装原始的List。

  3. ConcurrentHashMap:这个类是线程安全的,它使用了锁分离技术,通过在每个节点上进行锁定,来使不同的操作可以并发进行而不会相互影响。

  4. ConcurrentLinkedQueue:这个类是线程安全的,它使用了非阻塞算法,通过使用循环CAS(Compare-And-Swap)操作,来确保不会出现竞争条件,从而实现并发安全。

  5. BlockingQueue:这个类是线程安全的,它支持生成者和消费者模式,通过使用等待/通知机制,来实现阻塞队列的线程安全操作。

  6. Vector:这个类是线程安全的,它的所有方法都是同步的,但是在效率上不如前面几种线程安全的List。

需要注意的是,虽然这些类是线程安全的,但并不代表它们的使用一定是正确的。在多线程环境下,仍然需要注意线程间的通信、同步等问题,以保证程序的正确性和性能。

三种线程安全的List

CopyOnWriteArrayList源码解析

红黑树

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): 用于存放尚未执行的任务,当线程池中的线程数超过核心线程数时,任务会被放入工作队列。如果工作队列已满,新的任务可能触发创建新线程,但线程数量不会超过最大线程数。

线程池提交任务的流程大致如下:

  • 任务提交。当线程池对象接收到一个新任务(实现Runnable接口的对象)时,首先判断当前线程池中的线程数是否小于核心线程数(corePoolSize)。
  • 核心线程执行。如果线程数小于核心线程数,线程池会创建一个新线程,并立即执行提交的任务。
  • 任务队列判断。如果核心线程数已达到,但工作队列(workQueue)尚未满,线程池会将新任务加入到队列中。
  • 非核心线程执行。如果工作队列已满,且当前线程数未超过最大线程数(maximumPoolSize),线程池会创建新的非核心线程来执行任务。
  • 拒绝策略。如果工作线程数已达到最大线程数,并且工作队列也已满,线程池会根据预设的拒绝策略来处理新任务。常见的拒绝策略包括直接抛出异常(AbortPolicy)、丢弃最旧的任务再尝试执行当前任务(DiscardOldestPolicy)等。

线程池的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的不同:

在这里插入图片描述

线程池怎么判断线程回收

线程回收学习博客

线程回收学习源码图解
回收解析博客

死锁

线程死锁条件:

  • 互斥条件:指在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程完全释放。

  • 请求和保持条件:指进程已经保持至少一个资源,又提出新的请求,而该资源已经被其他进程占有,此时请求已经阻塞,但又不释放自己的资源。

  • 不剥夺条件:指进程已获得的资源,在未使用完之前不能被剥夺,只能等使用完后自己释放。

  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

死锁的避免

预防死锁:

  • 破坏互斥条件:使用资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
  • 破坏请求和保持条件:采用静态分配的方式。意思是在进程执行之前就申请需要的全部资源,直至所有的资源全部获得后才开始执行,如果有一个资源不能获得,则也不给该进程分配其他的资源。
  • 破坏不剥夺条件:指当某进程获得了部分资源,但得不到其他资源,就释放已获得的资源,但是只适用于内存和处理器资源。
  • 破坏环路等待条件:给系统的所有资源编号,规定进程所请求的资源顺序必须按照资源的编号依次进行。

设置加锁顺序:按照相同的顺序来请求锁。
支持定时的锁(超时放弃)

死锁及解决

线程池参数的合理设置
拒绝策略
线程池状态
怎么中断
创建销毁的过程

线程池创建基本使用

Java 线程池线程复用

线程的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()方法,该线程结束生命周期。

结束线程的三种方法:

// 方法1:使用stop()方法,不推荐
public void stopThreadWithStop() {
    Thread t = new Thread(new Runnable() {
        public void run() {
            while (true) {
                System.out.println("Thread running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    t.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.stop();  // 不推荐,可能会导致一些不可预见的问题
}
 
// 方法2:使用interrupt()方法,推荐
public void interruptThreadWithInterrupt() {
    Thread t = new Thread(new Runnable() {
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 处理中断异常,退出循环
                    Thread.currentThread().interrupt();
                }
            }
        }
    });
    t.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.interrupt(); // 推荐,可以通过抛出InterruptedException来退出循环
}
 
// 方法3:使用返回值控制循环,推荐
public void stopThreadWithReturnValue() {
    Thread t = new Thread(new Runnable() {
        public void run() {
            while (true) {
                System.out.println("Thread running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (/* 某个条件满足,表示需要结束线程 */) {
                    break; // 退出循环
                }
            }
        }
    });
    t.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 通过改变条件来控制循环退出
}

JVM

元数据空间的大小
在Java 在8和之前的版本中,永久代的尺寸是固定的,JVM的启动参数-XX:PermSize-XX:MaxPermSize配置。默认情况下,-XX:PermSize和-XX:MaxPermSize值均为64MB。

然而,在Java 8.在后续版本中,元空间的大小是动态的,不再局限于固定的大小。元空间的大小由JVM的启动参数组成-XX:MetaspaceSize-XX:MaxMetaspaceSize配置。默认情况下,-XX:MetaspaceSize值为21MB,而-XX:MaxMetaspaceSize值是无限大的。

jvm堆空间默认多大
JVM堆空间的默认大小取决于不同的JVM实现和操作系统。对于Oracle JDK和OpenJDK,默认的初始堆空间大小是物理内存的1/64,最大堆空间是物理内存的1/4。但是,这些默认值可以通过启动JVM时指定-Xms和-Xmx参数来分别调整初始堆大小和最大堆大小。

例如,要设置JVM的初始堆空间为512MB,最大堆空间为1024MB,可以这样启动应用程序:

java -Xms512m -Xmx1024m YourApplication

栈内存大小的默认值通常为 512KB 到 1024KB 之间。-Xmx2g -Xms512m可配置

GC垃圾回收
哪些内存需要回收
方法区的垃圾回收
垃圾收集算法
垃圾收集器
年轻代进入老年代条件
内存担保机制
字符串常量由永久代转移到堆中
三色标记算法

JVM组成
以上问题参考另一篇博客

  1. 老年代空间担保机制是谁给谁担保?
    我理解的是老年代给新生代的S区做担保。
  2. 为什么要有老年代空间担保机制?或者说空间担保机制的目的是什么?
    目的:避免频繁的进行FullGC。
  3. 如果没有老年代空间担保机制会有什么不好?
    如果没有这个担保机制,就会直接执行Full GC,这样对性能的影响频次会增加。

Gc日志

如果你项目是jar包,可以按照下面方式指定这些GC参数即可。

下面这些参数意思是把GC日志记录到/opt/app/abc-user/ard-user-gc-%t.log 这个文件里,每个文件大小为20M,一共生成5个文件,超过的话则覆盖。

java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -Xloggc:/opt/ard-user-gc-%t.log -jar abg-user.jar
  • -Xloggc:/opt/app/ard-user/ard-user-gc-%t.log 设置日志目录和日志名称

  • -XX:+UseGCLogFileRotation 开启滚动生成日志

  • -XX:NumberOfGCLogFiles=5 滚动GC日志文件数,默认0,不滚动

  • -XX:GCLogFileSize=20M GC文件滚动大小,需开启UseGCLogFileRotation

  • -XX:+PrintGCDetails 开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况

  • -XX:+ PrintGCDateStamps 记录系统的GC时间

  • -XX:+PrintGCCause 产生GC的原因(默认开启)

调优

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

AQS

AQS(AbstractQueuedSynchronizer)抽象队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它。核心原理主要围绕三个方面:

  • 同步状态(State)。AQS使用一个volatile修饰的int类型的成员变量来表示同步状态,通过该状态来控制共享资源的访问。
  • 队列(Queue)。AQS使用一个基于FIFO(先进先出)原则的队列来实现线程的排队和同步。这个队列是由一系列节点(Node)组成,每个节点包含线程本身、前驱节点、后继节点以及等待状态等信息。
  • 并发操作。AQS提供了多种原子操作来对同步状态进行修改,例如compareAndSetState方法用于原子地更新同步状态。

当一个线程试图获取共享资源时,它会尝试通过CAS操作来修改同步状态。如果获取成功,线程将获得资源并继续执行;如果获取失败,线程会被加入到队列的末尾并阻塞,直到同步状态发生改变(例如,有线程释放了资源)。当资源被释放时,位于队列头部的线程会被唤醒,尝试再次获取资源。这种机制使得AQS能够支持如ReentrantLock这样的同步器。

核心在于volatile int state(代表共享资源,用于标识是否持有锁)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。如果获取同步资源失败时,会将当前线程及等待信息等构建成一个Node,将Node放到FIFO队列里,同时阻塞当前线程,当线程将同步状态state释放时,会把FIFO队列中的首节点唤醒,使其获取同步状态state。

AQS为什么采用双向链表?

  • FIFO(先进先出)的顺序:
    双向链表可以保持线程加入等待队列的顺序,即先加入的线程排在队列前面,后加入的线程排在队列后面。这有助于实现公平性,即等待时间较长的线程更有机会先获得锁。
  • 便于在两端进行操作:
    双向链表支持在队列的两端进行高效的操作,例如在头部添加新节点、在尾部移除节点等。这对于在等待队列中的线程状态的管理和维护是非常有用的。
  • 节点的前驱和后继信息:
    每个等待队列节点都有指向其前驱节点和后继节点的引用。这使得在等待队列中的线程状态的变化可以高效地传播,例如当前驱节点释放锁时,可以唤醒后继节点。这对于实现线程的唤醒和阻塞是关键的。
  • 简化队列操作:
    双向链表的结构可以简化在队列中的节点插入、删除和移动等操作,而这些操作是在多线程环境下需要高效完成的。
    使用双向链表作为等待队列的底层数据结构,是为了提供高效的队列操作,保持线程等待的有序性,同时支持在节点之间传播状态信息,从而实现了 AQS 的核心同步机制。这种设计使得 AQS 能够灵活地支持各种同步器的实现。

AQS详细解析

并发包

JUC中常用类

ReentrantLock 可重入锁
ReentrantReadWriteLock
Semaphroe 信号量
CountDownLatch
CopyOnWriteArrayList
CyclicBarrier
Atomic原子类
AtomicReference

CountDownLatch使用

其他

使用反射有什么意义,会影响性能么

error和exception有什么区别

常用框架

Spring

Spring的启动流程

  1. 准备环境对象,事件监听者。

  2. 加载Spring配置文件。即读入并解析配置文件,构建初始Spring IOC容器。

  3. 对创建的Spring IOC容器进行一些初始化配置

  4. 调用Spring IOC容器的postProcessBeanFactory,留给子类实现。

  5. 实例化并执行BeanFactoryPostProcessor相关接口,包括解析配置类。并将BeanFactoryPostProcessor相关类放入特定容器。

  6. 初始化IOC容器。IOC容器的初始化包括对BeanFactory进行初始化、注册BeanDefinition、实例化Bean、依赖注入等过程。

  7. 实例化和初始化Bean。在初始化Bean时,Spring 根据Bean的定义以及配置信息,实现对Bean的实例化、属性赋值、以及初始化等操作。

  8. 完成IoC容器的准备工作。所有单例的Bean都已经被实例化、初始化并装配到容器中后,容器的准备工作就完成了,此时Spring框架已经可以对外提供服务。

  9. 执行定制化的后置处理器。Spring容器中可能会存在一些实现了BeanPostProcessor接口的定制化组件。这些组件会参与到IoC容器中Bean的生命周期过程,比如AOP、事务处理等。

  10. 执行自定义的初始化方法和销毁方法。容器中某些Bean可能需要在容器启动时执行自定义的初始化方法。这些方法在容器启动时就会被调用;同理,某些Bean在容器关闭时需要调用自定义的销毁方法,以清理资源。

  11. 容器启动后,整个应用将进入正常的工作状态。

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有什么区别


Spring AOP失效的几种场景:

  • 方法不是public:Spring AOP通过代理来实现,只有在代理的类(方法)是public时,AOP才能生效。

  • 方法是final:final方法不能被覆盖,AOP的实现依赖于创建代理对象,final方法阻止了这一点。

  • 方法是静态的:静态方法不通过Spring管理的bean实例调用,而是直接通过类调用,因此不会触发AOP。

  • 使用了CGLib代理:如果Spring配置为使用CGLib代理(通过<aop:aspectj-autoproxy proxy-target-class=“true”/>),则只有当目标类没有实现接口时,或者接口中的方法不是public时,CGLib代理才会生效。

  • 切面表达式不匹配:如果切面的pointcut表达式没有正确匹配到目标方法,AOP将不会被触发。

  • 同一个类中的方法调用:一个类中的方法调用本类中的另一个方法,不会触发AOP,因为Spring AOP基于代理,而代理方法调用是在不同的代理实例之间进行的。

  • 异步执行:如果方法通过异步执行(如使用@Async注解),则在方法执行时,可能没有经过Spring管理的代理,因此AOP不会生效。

  • 使用@Transactional注解时,确保方法不是被声明为final类型,否则会失效。解决方法:

    • 确保被代理的方法是public。

    • 不要将方法声明为final。

    • 不要使用静态方法。

    • 确保切面表达式正确匹配目标方法。

    • 如果需要在同一个类中调用方法被AOP,可以将该方法提取到单独的服务类中。

    • 确保异步执行的方法不是最终的目标方法,而是它的被调用方法。

    • 如果使用@Transactional注解,确保方法不是final。

    • aop切面顺序导致事务不能正确回滚
      事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常(在切面中catch住没有往外抛),事务就无法回滚
      解法1:异常原样抛出
      解法2:手动设置 TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

Spring事物失效的场景:

  1. 方法内的自调用
  2. 方法是private的
  3. 方法是final 的
  4. 单独的线程方法调用
  5. 异常被吃掉
  6. 类没有被spring管理
  7. 数据库不支持事务

Spring中事务失效的场景

总结 :
1、私有方法调用
2、静态方法调用
3、final方法调用
4、类内部自调用
5、内部类方法调用

SpringMVC

拦截器和过滤器的区别及实现

过滤器Filter依赖于servlet容器,只能在 servlet容器,web环境下使用
拦截器依赖于spring容器,可以在springweb中调用,不管此时Spring处于什么环境

  1. 过滤器(Filter)能拿到http请求,但是拿不到处理请求方法的信息。

  2. 拦截器(Interceptor)既能拿到http请求信息,也能拿到处理请求方法的信息,但是拿不到方法的参数信息。

  3. 切片(Aspect)能拿到方法的参数信息,但是拿不到http请求信息。

注入DispatcherServlet几种方式

一 :实现WebApplicationInitializer接口,并将实现类注入容器。

@Configuration
@ComponentScan("cn.example.springmvc.boke")
public class WebConfig {
}
 
//使用基于Java的配置,注册并初始化一个DispatcherServlet
public class MyWebApplicationInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //声明一个Spring-web容器
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(WebConfig.class);
 
        //创建并注册DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ctx);
        //动态的添加Servlet
        ServletRegistration.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 拦截的路径
    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }
 
    //设置根容器的配置类
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {RootConfig.class};
    }
 
    //设置子容器的配置类
    //如果不想形成父子容器,那么只需将下面这个getServletConfigClasses()方法返回null即可
    @Override
    protected 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调用逻辑

  1. 根据请求获取HandlerExecutionChain对象
  2. 根据处理器获取HandlerAdapter
  3. 执行handle前调用拦截器的preHandle方法,若返回false,处理结束
  4. 调用handler实际处理请求,获取ModelAndView对象
  5. 调用拦截器的postHandle方法
  6. 处理分发结果,渲染视图
  7. 调用拦截器的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应用类型NONESERVLETREACTIVE
   	2.3 初始化bootstrapRegistryInitializers
   	2.4 初始化ApplicationContextInitializer集合
   	2.5 初始化ApplicationListener
   	2.6 获取StackTraceElement数组遍历,通过反射获取堆栈中有main方法A的。
3、调用SpringBootApplication的run方法。
4long startTime = System.nanoTime(); 记录项目启动时间。
5、通过BootstrapRegistryInitializer来初始化DefaultBootstrapContext
6getRunListeners(args)获取SpringApplicationRunListeners监听器
7、 listeners.starting()触发ApplicationStartingEvent事件
8prepareEnvironment(listeners, bootstrapContext, applicationArguments) 将配置文件读取到容器中,返回ConfigurableEnvironment 对象。
9printBanner(environment) 打印Banner图,即SpringBoot启动时的图案。
10、根据WebApplicationTypeApplicationContextFactory工厂创建ConfigurableApplicationContext,并设置ConfigurableApplicationContext中的ApplicationStartupDefaultApplicationStartup
11、 调用prepareContext()初始化context等,打印启动日志信息,启动Profile日志信息,并为BeanFactory中的部分属性赋值。
12、刷新容器,在该方法中集成了Tomcat容器
13、加载SpringMVC.
14、刷新后的方法,空方法,给用户自定义重写afterRefresh()
15Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime)算出启动花费的时间。
16、打印日志Started xxx in xxx seconds (JVM running for xxxx)
17、listeners.started(context, timeTakenToStartup)触发ApplicationStartedEvent事件监听。上下文已刷新,应用程序已启动。
18、调用ApplicationRunnerCommandLineRunner
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 {
  ......
}

总结:@SpringBootApplication注解导入了一个AutoConfigurationImportSelector类,该类实现了DeferredImportSelector接口,会在Spring扫描配置时候执行其selectImports方法,该实现类方法会通过SPI机制扫描出类路径中META-INF目录下的spring.factories文件EnableAutoConfiguration.class对应的实现类注册进容器。

被@Configuration标注的类会在Spring刷新时候invokeBeanFactoryPostProcessors方法中被解析,解析入口是BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry,ConfigurationClassPostProcessor类实现。

Spring Boot解决跨域

Spring Boot启动慢如何分析

Dubbo

Dubbo支持多传输协议(Dubbo、Rmi、http、redis等等),Dubbo的服务端需要配置开放的Dubbo接口,通过TCP长连接的方式进行通信,服务粒度是方法级的。
Dubbo支持的:

  • 随机(Random):随机选择一个可用的服务提供者。
  • 最少活跃数(LeastActive):选择活跃数最少的服务提供者,即处理请求最少的服务提供者。
  • 一致性哈希(ConsistentHash):根据请求参数的hash值,按照顺时针方向路由到相邻节点。
  • 轮询(RoundRobin):按公平轮询的方式选择服务提供者。
  • 加权轮询(WeightedRoundRobin):按照服务提供者的权重进行轮询,权重越大被选中的概率越高。
  • 加权随机(WeightedRandom):根据权重随机选择服务提供者。

springboot

# 服务端口号
server.port=8080

# Dubbo应用名
dubbo.application.name=demo-provider

# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

# 服务提供者协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

# 服务提供者接口类全限定名
dubbo.scan.base-packages=com.example.demo.service

# 服务提供者负载均衡策略
# 随机(Random)、最少活跃数(LeastActive)、一致性哈希(ConsistentHash)、轮询(RoundRobin)
# 加权轮询(WeightedRoundRobin)、加权随机(WeightedRandom)
dubbo.loadbalance=leastactive

入门使用案例
基础配置

SpringCloud

常用组件 :

  1. Netflix Eureka:服务注册中心

  2. Netflix Ribbon:客户端负载均衡;随机、规则轮询、空闲策略、响应时间策略。

  3. OpenFeign:声明式的 HTTP 客户端

  4. Netflix Hystrix:断路器模式

  5. Spring Cloud Gateway:网关路由

  6. Zuul:网关管理,由 Zuul 向相应服务转发网关请求。

  7. Spring Cloud Sleuth:分布式链路追踪

  8. Spring Cloud Config:配置中心

  9. Spring Cloud Bus:消息总线

  10. Spring Cloud Security:安全框架

怎么服务降级:

Hystrix在方法上标注降级方法:@HystrixCommand(fallbackMethod =“hystrixGet”)
OpenFeign在调用接口上使用@FeignClient(value = “SPRINGCLOUD-PROVIDER-DEPT”,fallbackFactory = DeptClientServiceFallBackFactory.class),指定降级的类。

SpringCloudAlibaba

Spring Cloud Alibaba的常用组件:

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。

  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。

  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

Nacos作为注册中心应该选择是CP还是AP?

  • CP:如果注册中心是CP的,当我们向注册中心注册实例或移除实例时,都要等待注册中心集群中的数据达到一致后,才算注册或移除成功,这是比较耗时的,随着业务应用规模的增大,应用频繁的上下线,那么就会导致注册中心的压力比较大,会影响到服务发现的效率以及服务调用。
  • AP:如果注册中心是AP的,注册中心集群不管出现了什么情况都是可以提供服务的,即使节点之间数据出现了不一致,例如拉取到了一个已经下线了的服务节点,但是现在一般的微服务框架或组件都提供了服务容错和重试功能,也可以避免这个问题。对于注册中心而言不需要消耗太多的资源来实时的保证数据一致性,保证最终一致性就可以了,这样注册中心的压力会小一点。

Dubbo与Feign的区别

Nacos中的AP与CP
Nacos中的AP与CP实现原理
SpringCloudAlibaba相关知识点
SpringCloudAlibaba相关知识点60+

Nacos的distro协议和raft协议

Netty

什么是Netty:
Netty是一个高性能的网络编程框架,基于NIO的非阻塞式IO模型,可以帮助开发者快速开发高性能、高可靠性的网络应用程序。

  • 第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是对 NIO 网络模。型的封装,提供了简单易用的 API,我们可以利用这些封装好的API 快速开发自己的网络程序。
  • 第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、内存池等,因此性能会比 NIO 更高。
  • 第三:Netty 可以支持多种通信协议,如 Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。

Netty 线程模型
在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的。我们实现服务端的时候,一般会初始化两个线程组:
bossGroup:接收连接。
workerGroup :负责具体的处理,交由对应的 Handler 处理。

  • 单线程模型 :
    EventLoopGroup 只包含一个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;一个线程需要执行处理所有的 accept、read、decode、process、encode、send事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。

  • 多线程模型:
    EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理accept、read、decode、process、encode、send 事件。满足绝大部分应用场景,并发连接量不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。

  • 主从多线程模型:
    EventLoopGroup 包含多个 EventLoop,Boss 是主 Reactor,Worker 是从 Reactor,它们分别使用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,SubNIO线程池负责具体处理 I/O 读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型

单线程模型:

 //1.eventGroup既用于处理客户端连接,又负责具体的处理。
  EventLoopGroup eventGroup = new NioEventLoopGroup(1);
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
            boobtstrap.group(eventGroup, eventGroup)  
            //......

多线程模型:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
  //3.给引导类配置两大线程组,确定了线程模型
  b.group(bossGroup, workerGroup)
    //......

主从多线程模型:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
  //2.创建服务端启动引导/辅助类:ServerBootstrap
  ServerBootstrap b = new ServerBootstrap();
  //3.给引导类配置两大线程组,确定了线程模型
  b.group(bossGroup, workerGroup)
    //......

三种线程模式图示

使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2

Netty 起多少线程?何时启动:Netty的默认启动了电脑可用线程数的两倍,在调用了bind方法的时候执行。

netty零拷贝
netty工作流程
select、poll、epoll 区别有哪些
NIOEventLoopGroup源码
reactor模式(I/O 复用结合线程池)

Netty相关面试题
netty实现长连接
添加链接描述

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

TCC模式。这是一种最终一致性的两阶段提交协议,需要业务实现Try、Confirm和Cancel三个操作。它适用于需要业务侵入但灵活度高的场景。

SAGA模式。这是一种长事务模式,通过状态机编排或注解方式实现业务逻辑,适用于无法干涉第三方服务事务的场景。它需要业务实现正向和反向两个操作,同样具有业务侵入性

A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID。
服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖。
A服务执行分支事务,向数据库做操作。
A服务开始远程调用B服务,此时XID会在微服务的调用链上传播。
B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖。
B服务执行分支事务,向数据库做操作。
全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚。
TC协调其管辖之下的所有分支事务,决定是否回滚。

Seata入门

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一致。

ZooKeeper是一个CP(一致性+分区容错性)系统。这意味着:

  • 一致性:任何时刻对ZooKeeper的访问请求都能得到一致的数据结果。
  • 分区容错性:系统对网络分割具备容错性,即使部分节点出现故障,也能继续提供服务。
  • 可用性:ZooKeeper不能保证每次服务请求的可用性。

加锁使用

zookeeper分布式锁实现

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(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片			参数开发分片任务;
任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
源码仓库地址Release Download
https://github.com/xuxueli/xxl-jobDownload
http://gitee.com/xuxueli0323/xxl-jobDownload

XXL-JOB入门使用案例

xxl-job源码解析

设计模式

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 {
        @Override
        public 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

MySQL中myisam与innodb的区别:

1>.InnoDB支持事物,而MyISAM不支持事物
2>.InnoDB支持行级锁,而MyISAM支持表级锁
3>.InnoDB支持MVCC, 而MyISAM不支持
4>.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
      表示全表扫描,性能最差。
  • 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
      表示用到索引下推的优化特性。

explain案例解析

explain案例解析

Mysql面试题

MVCC

readView

锁使用案例

分库分表会带来哪些问题

  • 事务一致性问题
  • 数据组装
  • 跨节点分页、排序、函数问题
  • 全局主键避重问题

分库分表后如何保证一致性:分布式事务

索引失效原因有哪些?

  1. 模糊查询的前导通配符: 当使用模糊查询(如 LIKE ‘%abc’)时,索引失效,因为通配符在前面会导致索引无法使用。

  2. 未使用索引字段进行过滤: 如果查询条件没有使用到创建的索引字段,数据库可能不会使用该索引。

  3. 数据类型不匹配: 如果查询条件的数据类型与索引字段的数据类型不匹配,数据库无法使用索引。

  4. 使用函数操作: 如果查询条件中对字段进行了函数操作(如 LOWER(column)),索引可能失效,因为数据库无法直接使用索引。

  5. OR 运算: 在 OR 运算中,如果其中一个条件使用了索引,而另一个条件没有使用索引,整个查询可能会导致索引失效

  6. 使用 NOT 运算: NOT 运算通常会使索引失效,因为数据库无法使用索引来高效处理 NOT 运算。

  7. 表连接中的索引失效: 如果在表连接查询中,连接条件中的字段没有索引,可能导致索引失效。

mysql中如何使用sql实现递归

递归查询通常使用 " WITH RECURSIVE " 语句实现。

使用示例

oracle的递归查询用的是 start with connt by的方式,这个语式标识的 start with 表示开始查找的节点,connet by prior 表示关联条件。

引擎
常用的通用 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

搭建博客

可重复读一定解决了幻读问题吗:
不一定,如果可重复读下使用select * from table for update等排他锁的话,会变成当前读,会有幻读问题。

mysql如何实现避免幻读

  1. 在快照读读情况下,mysql通过mvcc来避免幻读。
  2. 在当前读读情况下,mysql通过next-key(加表锁或间隙锁)来避免幻。

MySQL索引B+数

聚簇索引和非聚簇索引概念
聚簇索引:将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据
非聚簇索引:将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置

mysql相关知识点

Mysql面试详细知识点查看另一篇文章

MyCat

MyCat概念说明

  • 逻辑库(schema):
    通常对实际应用来说,并不需要知道数据库中间件的存在,业务开发人员只需要知道数据库的概念,所以数据库中间件可以被看做是一个或多个数据库集群构成的逻辑库。

  • 逻辑表(table):
    既然有逻辑库,那么就会有逻辑表,分布式数据库中,对应用来说,读写数据的表就是逻辑表。逻辑表,可以是数据切分后,分布在一个或多个分片库中,也可以不做数据切分,不分片,只有一个表构成。

    • 分片表:是指那些原有的很大数据的表,需要切分到多个数据库的表,这样,每个分片都有一部分数据,所有分片构成了完整的数据。 总而言之就是需要进行分片的表。

    • 非分片表:一个数据库中并不是所有的表都很大,某些表是可以不用进行切分的,非分片是相对分片表来说的,就是那些不需要进行数据切分的表。

  • 分片节点(dataNode):
    数据切分后,一个大表被分到不同的分片数据库上面,每个表分片所在的数据库就是分片节点(dataNode)。

  • 节点主机(dataHost):
    数据切分后,每个分片节点(dataNode)不一定都会独占一台机器,同一机器上面可以有多个分片数据库,这样一个或多个分片节点(dataNode)所在的机器就是节点主机(dataHost),为了规避单节点主机并发数限制,尽量将读写压力高的分片节点(dataNode)均衡的放在不同的节点主机(dataHost)。

  • 分片规则(rule):
    前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则,这样按照某种业务规则把数据分到某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度。

分库分表标准:

  • 存储占用100G+
  • 数据增量每天200w+
  • 单表条数1亿条+

MyCat配置文件

  • Schema.xml
    Schema.xml作为MyCat中重要的配置文件之一,管理着MyCat的逻辑库、表、分片规则、DataNode以及DataSource。弄懂这些配置,是正确使用MyCat的前提。这里就一层层对该文件进行解析。
    • schema 标签用于定义MyCat实例中的逻辑库
    • Table 标签定义了MyCat中的逻辑表
    • dataNode 标签定义了MyCat中的数据节点,也就是我们通常说所的数据分片。
    • dataHost标签在mycat逻辑库中也是作为最底层的标签存在,直接定义了具体的数据库实例、读写分离配置和心跳语句。
  • Server.xml
    server.xml几乎保存了所有mycat需要的系统配置信息。最常用的是在此配置用户名、密码及权限。
  • rule.xml
    rule.xml里面就定义了我们对表进行拆分所涉及到的规则定义。我们可以灵活的对表使用不同的分片算法,或者对表使用相同的算法但具体的参数不同。这个文件里面主要有tableRule和function这两个标签。在具体使用过程中可以按照需求添加tableRule和function。

schema 示例

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">

	<schema name="DB01" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
			<table name="tb_order" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" splitTableNames ="true"/>
	</schema>

	<dataNode name="dn1" dataHost="dhost1" database="db01" />
	<dataNode name="dn2" dataHost="dhost2" database="db01" />
	<dataNode name="dn3" dataHost="dhost3" database="db01" />

	<dataHost name="dhost1" maxCon="1000" minCon="10" balance="0"
			  writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100">
		<heartbeat>select user()</heartbeat>

		<writeHost host="hostM1" 
		           url="jdbc:mysql://192.168.200.210:3306??serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true" 
		           user="root"
				   password="1234">
		</writeHost>
	</dataHost>
	
	
	<dataHost name="dhost2" maxCon="1000" minCon="10" balance="0"
			  writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100">
		<heartbeat>select user()</heartbeat>

		<writeHost host="hostM1" 
		           url="jdbc:mysql://192.168.200.213:3306??serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true" 
		           user="root"
				   password="1234">
		</writeHost>
	</dataHost>
	
	
	<dataHost name="dhost3" maxCon="1000" minCon="10" balance="0"
			  writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100">
		<heartbeat>select user()</heartbeat>

		<writeHost host="hostM1" 
		           url="jdbc:mysql://192.168.200.214:3306??serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true" 
		           user="root"
				   password="1234">
		</writeHost>
	</dataHost>
	

</mycat:schema>

入门学习及配置
mycat详细配置参数解析

mycat配置示例

主从切换配置

dataHost标签中对balancewriteTypeswitchTypeslaveThreshold属性的配置

	<dataNode name="dn1" dataHost="localhost1" database="db1" />
    <dataNode name="dn2" dataHost="localhost1" database="db2" />
    <dataNode name="dn3" dataHost="localhost1" database="db3" />
    
    <dataHost name="localhost1" maxCon="1000" minCon="10" balance="1"
       writeType="0" dbType="mysql" dbDriver="native" switchType="2"  slaveThreshold="100">

       <heartbeat>show slave status</heartbeat>
       <writeHost host="hostM" url="主服务器IP地址:3306" user="root" password="root">
           <readHost host="hostS" url="从服务器IP地址:3306" user="root" password="root" />
       </writeHost>

    </dataHost>

Balance参数设置:

  1. balance=“0”, 所有读操作都发送到当前可用的writeHost上。

  2. balance=“1”,所有读操作都随机的发送到readHost。

  3. balance=“2”,所有读操作都随机的在writeHost、readhost上分发

WriteType参数设置:

  1. writeType=“0”, 所有写操作都发送到可用的writeHost上。

  2. writeType=“1”,所有写操作都随机的发送到readHost。

  3. writeType=“2”,所有写操作都随机的在writeHost、readhost分上发。

readHost是从属于writeHost的,即意味着它从那个writeHost获取同步数据,因此,当它所属的writeHost宕机了,则它也不会再参与到读写分离中来,即“不工作了”,这是因为此时,它的数据已经“不可靠”了。基于这个考虑,目前mycat 1.3和1.4版本中,若想支持MySQL一主一从的标准配置,并且在主节点宕机的情况下,从节点还能读取数据,则需要在Mycat里配置为两个writeHost并设置banlance=1。

switchType 目前有三种选择:

  • -1:表示不自动切换
  • 1 :默认值,自动切换
  • 2 :基于MySQL主从同步的状态决定是否切换

Mycat心跳检查语句配置为 show slave status ,dataHost 上定义两个新属性: switchType=“2” 与slaveThreshold=“100”,此时意味着开启MySQL主从复制状态绑定的读写分离与切换机制。Mycat心跳机制通过检测 show slave status 中的"Seconds_Behind_Master", “Slave_IO_Running”, “Slave_SQL_Running” 三个字段来确定当前主从同步的状态以及Seconds_Behind_Master主从复制时延。

简单使用案例
主从搭建原文

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:批次大小,默认 16K
 properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
 // linger.ms:等待时间,默认 0
 properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
 // RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
 properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
 // compression.type:压缩,默认 none,可配置值 gzip、snappy、 lz4 和 zstd
 properties.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包含了以下⼏个重要⼦模块。

    1. Remoting Module:整个Broker的实体,负责处理来⾃clients端的请求。
    2. Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
    3. Store Service:提供⽅便简单的API接⼝处理消息存储到物理硬盘和查询功能。
    4. HA Service:⾼可⽤服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
    5. 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

  1. Producer:消息的发送者, 负责⽣产消息;举例:发信者
  2. Consumer:消息接收者,负责消费消息;举例:收信者
  3. Broker:暂存和传输消息,负责存储消息;举例:邮局
    Broker 在实际部署过程中对应⼀台服务器,每个Broker 可以存储多个Topic的消息,每个Topic的消息也可以分⽚存储于不同的 Broker。
  4. NameServer:管理Broker;举例:各个邮局的管理机构
  5. Topic:区分消息的种类;⼀个发送者可以发送消息给⼀个或者多个Topic;⼀个消息的接收者可以订阅⼀个或者多个Topic消息。
    每个 topic 默认包含4个队列,每个队列对应一个持久化文件queuelog,它存储的是每条消息在commitlog中的位置等信息。
  6. Message Queue:相当于是Topic的分区;⽤于并⾏发送和接收消息。⼀个queueId就代表了⼀个MessageQueue。每个Topic中的消息地址存储于多个 Message Queue 中。

RocketMQ的概念模型详解

Kafka

死信队列

消息变成死信一般是由于以下几种情况:

  1. 重试次数超限: 消息在处理过程中多次重试仍然失败,达到预定的重试次数上限;
  2. 消息被拒绝:( Basic.Reject/Basic.Nack ),并且设置 requeue 参数为 false ;
    3.消息过期:消息在队列中等待时间过长,超过了设置的过期时间 ;
  3. 队列满: 当消息队列的长度达到上限时,新的消息可能成为死信。

kafka没有提供相应的设计。 kafka中的重试队列是我们通过业务实现的,自己新加几个重试主题,消费者消费失败后就将消息发给重试队列

怎么避免消息丢失/消息可靠性

消息丢失的三种情况

  1. 消息在传入过程中丢失
  2. MQ收到消息,暂存内存中,还没消费,自己挂掉了,内存中的数据搞丢
  3. 消费者消费到了这个消息,但还没来得及处理,就挂了,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() {
   @Override
   public 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持久化发送
服务端: ExchangeQueue消息持久化、设置集群镜像模式
消费者: ACK确认机制
 
rocketmq:同步刷盘、消费者确认、同步发送
 
kafka :
生产者 : 设置ACK
服务端:设置ACK01-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
    @Override
    public 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() {
    @Override
    public 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堆积问题的方法。例如,在生产者端,


RocketMQ【面试】

Docker

docker网络 :

  • bridge :
    在这里插入图片描述

使用docker run创建Docker容器时,可以用 --net 或 --network 选项指定容器的网络模式

●host模式:使用 --net=host 指定。
●none模式:使用 --net=none 指定。
●container模式:使用 --net=container:NAME_or_ID 指定。
●bridge模式:使用 --net=bridge 指定,默认设置,可省略。

docker启动失败的原因怎么排查

  1. 检查Docker守护进程是否正在运行。可以通过运行sudo systemctl status docker命令来查看Docker服务的状态。
  2. 检查系统资源是否已经耗尽,例如内存、磁盘空间等。可以通1. 过运行free -h和df -h命令来查看系统资源的使用情况。
  3. 检查Docker日志文件以查看是否有任何错误消息。Docker日志文件通常位于/var/log/docker.log或/var/log/syslog中。
  4. 检查Docker镜像和容器的状态。可以通过运行docker ps -a和docker images命令来查看Docker容器和镜像的列表。
  5. 检查网络配置是否正确。有时候网络配置不正确会导致Docker启动失败。可以通过运行docker network ls和docker network inspect <network_name>来查看网络配置。
  6. 检查Docker配置文件是否正确。可以通过编辑/etc/docker/daemon.json文件来检查Docker的配置选项是否正确。
  7. 如果以上方法都无法解决问题,可以尝试重新安装Docker。可以使用适合您系统的安装程序重新安装Docker。
docker访问宿主机访问不通如何处理

DockerFile

常用的Dockerfile指令:

  • FROM:指定基础镜像。这是Dockerfile中的第一条指令,如果不指定基础镜像,可以使用FROM scratch。基础镜像是指构建新镜像的基础层,后续的指令都会在这个基础层上执行。
  • RUN:运行指定的命令。这个指令用于在基础镜像上执行命令。有两种格式,一种是直接跟shell命令,另一种是类似于函数调用。需要注意的是,Dockerfile中的每个RUN指令都会创建一个新的镜像层,因此过多的RUN指令会导致镜像变得臃肿。
  • CMD:定义容器启动时要运行的命令。这个指令用于设置容器启动时运行的命令。有两种格式,一种是直接跟命令和参数,另一种是使用可执行文件和参数。
  • ENV:设置环境变量。这个指令用于在构建过程中设置环境变量,这些变量可以在后续的RUN和CMD指令中使用。
  • ADD:从宿主机向容器中复制文件。这个指令用于将宿主机上的文件或目录复制到容器中。默认情况下,ADD命令只能复制Dockerfile文件所在目录或其子目录中的文件。如果需要复制其他路径的文件,可以在Dockerfile中使用WORKDIR命令设置目标路径。
  • WORKDIR:设置工作目录。这个指令用于在容器中设置工作目录,后续的命令都会在这个目录下执行。

ADD与COPY:
COPY指令和ADD指令的唯一区别在于:是否支持从远程URL获取资源。
COPY指令只能从执行docker build所在的主机上读取资源并复制到镜像中。而ADD指令还支持通过URL从远程服务器读取资源并复制到镜像中。
相同需求时,推荐使用COPY指令。ADD指令更擅长读取本地tar文件并解压缩。

ADD指令有如下的优越性:

  1. 如果源路径是个文件,且目标路径是以 / 结尾, 则docker会把目标路径当作一个目录,会把源文件拷贝到该目录下。
    如果目标路径不存在,则会自动创建目标路径。

  2. 如果源路径是个文件,且目标路径是不是以 / 结尾,则docker会把目标路径当作一个文件。
    如果目标路径不存在,会以目标路径为名创建一个文件,内容同源文件;
    如果目标文件是个存在的文件,会用源文件覆盖它,当然只是内容覆盖,文件名还是目标文件名。
    如果目标文件实际是个存在的目录,则会源文件拷贝到该目录下。 注意,这种情况下,最好显示的以 / 结尾,以避免混淆。

  3. 如果源路径是个目录,且目标路径不存在,则docker会自动以目标路径创建一个目录,把源路径目录下的文件拷贝进来。
    如果目标路径是个已经存在的目录,则docker会把源路径目录下的文件拷贝到该目录下。

  4. 如果源文件是个归档文件(压缩文件),则docker会自动帮解压。

K8s组件
DockerFile指令示例
DockerFile编写

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从2.8版本开始,使用 PSYNC命令 替代了旧版的SYNC命令来执行 复制操作 。

缓存一致性:延迟双删,MQ异步同步

Redis复制原理解析

同步步骤

psync&sync及同步流程

集群和哨兵
数据一致性
数据同步策略
数据删除和淘汰/脑裂
Redis的过期策略以及内存淘汰机制
缓存一致性
redisson

缓存雪崩、击穿和穿透
另一篇-主从复制&哨兵&集群搭建解析&淘汰策略

主从、集群和哨兵

  • 主从复制:主从复制是高可用redis的基础,哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实
    现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。主从节点复制成功建立后,可以使用info replication命令查看复制相关状态
  • 哨兵模式
    主从切换技术的方法是:当服务器宕机后,需要手动一台从机切换为主机,这需要人工干预,不仅费时费力而且还
    会造成一段时间内服务不可用。为了解决主从复制的缺点,就有了哨兵机制。哨兵的核心功能是在主从复制的基础上,哨兵引入了主节点的自动故障转移。
  • 集群,即Redis Cluster,是Redis 3.0开始引入的分布式存储方案。
    集群由多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护,从节点只进行主节点数据和状态信息的复制。
    集群的每个节点负责一部分哈希槽,每个Key通过CRC16校验后对16384取余来决定放置哪个哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作

Redis sentinel是如何进行监控的
sentinel通过在配置文件中配置 sentinel monitor 选项来指定要监控的redis master节点的地址,然后在启动sentinel时,会创建与redis master节点的连接并向master节点发送一个info命令,master节点在收到info命令后,会将自身节点的信息和自己下面所有的slave节点的信息返回给sentinel,sentinel收到反馈后,会与新的slave节点创建连接,接下来就会每隔10秒钟向所有的redis节点发送info命令来获取最新的redis主从结构信息。

主从复制同步流程

全量数据同步

  1. slave节点根据配置的master节点信息,连接上master节点,并向master节点发送SYNC命令;

  2. master节点收到SYNC命令后,执行BGSAVE命令异步将内存数据生成到rdb快照文件中,同时将生成rdb文件期间所有的写命令记录到一个缓冲区,保证数据同步的完整性;

  3. master节点的rdb快照文件生成完成后,将该rdb文件发送给slave节点;

  4. slave节点收到rdb快照文件后,丢弃所有内存中的旧数据,并将rdb文件中的数据载入到内存中;

  5. master节点将rdb快照文件发送完毕后,开始将缓冲区中的写命令发送给slave节点;

  6. slave节点完成rdb文件数据的载入后,开始执行接收到的写命令。

增量数据同步
Redis2.8版本之前,是不支持增量数据同步的,只支持全量同步。增量数据同步是指slave节点初始化完成后,master节点执行的写命令同步到slave节点的过程。该过程比较简单,master节点每执行一个写命令后就会将该命令发送给slave节点执行,从而达到数据同步的目的。

Redis分布式锁与集群下分布式锁与问题

主从与哨兵搭建、集群的搭建

主从同步详细解析-知乎

分布式锁

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

Nginx 负载均衡策略:

  • 轮询(Round Robin):啥都不配就是默认
  • 加权轮询(Weighted Round Robin): weight=3
  • IP Hash : ip_hash
  • 最小连接数(Least Connections): least_conn
  • 随机(Random): random;

Linux中Nginx防护基本命令

LVS

术语 :
a、VIP:虚拟ip地址virturalipaddress用于给客户端计算机提供服务的ip地址
b、RIP:realip,集群下面节点使用的ip地址,物理ip地址
c、DIP:directorip用于连接内外网络的ip,物理网卡上的ip地址,他是负载均衡器上的ip
d、CIP:clientip客户端用户计算机请求集群服务器的ip地址,该地址用作发送给集群的请求的源ip地址

LVS工作模式

LVS有三种工作模式:NAT,DR,TUN,DR是三种工作模式中性能最高的,TUN次之。

  1. NAT(NetworkAddress Translation)即网络地址映射模式
    NAT(Network Address Translation)即网络地址转换,其作用是通过数据报头的修改,使得位于企业内部的私有IP地址可以访问外网,以及外部用用户可以访问位于公司内部的私有IP主机。LVS负载调度器可以使用两块网卡配置不同的IP地址,一块设置为私钥IP与内部网络通过交换设备相互连接,e第二块设备为外网IP与外部网络联通。

  2. DR(DirectRouting)即直接路由模式
    直接路由模式(DR模式)要求调度器与后端服务器必须在同一个局域网内,VIP地址需要在调度器与后端所有的服务器间共享,因为最终的真实服务器给客户端回应数据包时需要设置源IP为VIP地址,目标IP为客户端IP,这样客户端访问的是调度器的VIP地址,回应的源地址也依然是该VIP地址(真实服务器上的VIP),客户端是感觉不到后端服务器存在的。

  3. TUN(IP Tunneling)即IP隧道模式
    LVS(TUN)的思路就是将请求与响应数据分离,让调度器仅处理数据请求,而让真实服务器响应数据包直接返回给客户端。LVS(TUN)模式要求真实服务器可以直接与外部网络连接,真实服务器在收到请求数据包后直接给客户端主机响应。后端响应数据直接返回给客户端,不再经过LVS

工作模式图解
三种工作模式解析

LVS负载均衡调度算法

  1. 轮询rr:Real Server轮流提供服务
  2. 加权轮询wrr:根据服务器性能设置权重,权重大的得到的请求更多
  3. 最少连接lc:根据Real Server的连接数分配请求
  4. 加权最少连接wlc:类似于wrr,根据权重分配请求
  5. 基于局部的最少连接

Haproxy

Haproxy负载均衡调度算法

  • 轮询(Round Robin):每个请求按顺序轮流分配给后端服务器,这是最常用的策略之一。

  • 最少连接(Least Connections):请求被分配给当前连接数最少的后端服务器。这种策略适用于后端服务器处理能力相近的情况。

  • 最少响应时间(Least Time):请求被分配给当前响应时间最短的后端服务器。这种策略适用于后端服务器处理能力不均衡的情况。

  • 哈希(Hash):请求被分配给通过对某个属性进行哈希计算得到的桶中的后端服务器。这种策略适用于后端服务器数量较多的情况。

分布式锁

Es

分词器 :

  • 标准分词器
    特点: 按照单词分词 英文统一转为小写 过滤标点符号 中文单字分词
  • Simple 分词器
    特点: 英文按照单词分词 英文统一转为小写 去掉符号 中文按照空格进行分词
  • Whitespace 分词器
    特点: 中文 英文 按照空格分词 英文不会转为小写 不去掉标点符号

IK分词器
ik_smart: 会做最粗粒度的拆分
ik_max_word: 会将文本做最细粒度的拆分

IK支持自定义扩展词典和停用词典
pinyin分词器

扩展词典就是有些词并不是关键词,但是也希望被ES用来作为检索的关键词,可以将这些词加入扩展词典。
停用词典就是有些词是关键词,但是出于业务场景不想使用这些关键词被检索到,可以将这些词放入停用词典。

Solr

Linux

/etc/sysconfig/network-script/ # 此目录下是系统启动最初始化网络的信息

查看端口:

ps -ef | grep <进程关键字>

linux 查看端口

使用netstat命令。使用netstat -tuln命令可以查看系统中所有TCP和UDP端口的状态,包括端口号、本地地址、外部地址和进程ID等,其中-t表示TCP,-u表示UDP,-l表示仅显示监听状态的端口,-n表示以数字形式显示端口号。

top查看服务器负载,内存消耗;
df -h查看硬盘
netstat -tunpl 查看对外服务和端口

常用命令

常用命令示例

网络操作

计算机网络

在这里插入图片描述

在这里插入图片描述

各层含义:

  • 应用层:是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。
  • 表示层:提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。
  • 会话层:就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。
  • 传输层:传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务。
  • 网络层:本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。
  • 数据链路层:将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。
  • 物理层: 实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。

网络模型

TCP/IP协议、HTTP协议

TCP/IP

HTTP

请求响应模式
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准。通常,由HTTP客户端发起一个请求(浏览器、爬虫等),创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息。

请求响应报文:

在这里插入图片描述

组成示例:
在这里插入图片描述

若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;

响应报文:

在这里插入图片描述

响应示例 :
在这里插入图片描述

HTTP常见请求方法:

HTTP 协议中共定义了八种方法或者叫“动作”来表明对 Request-URI 指定的资源的不同操作方式,具体介绍如下:

  • OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*'的请求来测试服务器的功能性。
  • HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
  • GET:向特定的资源发出请求。
  • POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
  • PUT:向指定资源位置上传其最新内容。
  • DELETE:请求服务器删除 Request-URI 所标识的资源。
  • TRACE:回显服务器收到的请求,主要用于测试或诊断。
  • CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。

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
    当服务器作为网关,不能及时得到响应时返回此错误代码。

HTTP解析

HTTPS 是如何保证安全传输的
常见的媒体格式类型
以application开头的媒体格式类型

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框架

  1. Apache Dubbo:Apache Dubbo(原阿里巴巴Dubbo)是一款高性能、轻量级的RPC框架,适用于大规模分布式系统。Dubbo使用Java注解进行服务声明,支持多种负载均衡策略和集群容错机制,提供了丰富的监控和管理功能。

  2. Spring Cloud:Spring Cloud 是一套构建分布式系统的开源框架。它提供了多个模块,包括服务发现与注册、负载均衡、断路器、智能路由等功能,基于HTTP或RPC实现了服务间的通信和调用。Spring Cloud整合了多种RPC框架,如RestTemplate、Feign、Ribbon等,使得开发者可以方便地构建分布式系统。

  3. gRPC:gRPC 是由 Google 开发的高性能、开源的RPC框架。它使用 Protocol Buffers(protobuf)作为接口定义语言(IDL),支持多种编程语言,如Java、C++、Python等。gRPC基于HTTP/2协议,支持双向流通信、多种序列化格式(如protobuf和JSON等)以及负载均衡等特性。

  4. Apache Thrift:Apache Thrift 是由 Facebook 开发和开源的跨语言RPC框架。它使用自己的IDL语言,支持多种编程语言,如Java、C++、Python、Ruby等。Thrift提供了比gRPC更丰富的功能,包括异步IO、连接池、复合类型等,适用于多种场景。

  5. Apache Axis2:Apache Axis2 是一款基于Web服务标准的RPC框架。它支持SOAP协议,通过WSDL描述服务接口,支持多种编程语言,如Java、C++、Python等。Axis2提供了高度可扩展的架构、安全性和可靠性,并支持发布和发现服务。

在浏览器输入url后会发生的过程:

  1. DNS对域名进行解析;
  2. 建立TCP连接(三次握手);
  3. 发送HTTP请求;
  4. 服务器处理请求;
  5. 返回响应结果;
  6. 关闭TCP连接(四次挥手);
  7. 浏览器解析HTML;
  8. 浏览器布局渲染;

Cookie 和 Session 的区别

在这里插入图片描述

开发相关

接口密等性
加密算法(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,如果验证成功,就向客户端返回请求的数据;

序列化协议

  • XML
    优点:人机可读性好,可指定元素或特性的名称。
    缺点:序列化数据只包含数据本身
    以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
    适用场景:当做配置文件存储数据,实时数据转换。

  • JSON,是一种轻量级的数据交换格式。
    优点:兼容性高、数据格式比较简单,易于读写、
    序列化后数据较小,可扩展性好,兼容性好、与 XML 相比,其协议比较简单,解析速度比
    较快。
    缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销比
    较大。
    适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于 Web browser 的Ajax 请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。

  • Fastjson,采用一种“假定有序快速匹配”的算法。
    优点:接口简单易用、目前 java 语言中最快的 json 库。
    缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。
    适用场景:协议交互、Web 输出、Android 客户端

  • Thrift,不仅是序列化协议,还是一个 RPC 框架。
    优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
    缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
    适用场景:分布式系统的 RPC 解决方案

  • Avro,Hadoop 的一个子项目,解决了 JSON 的冗长和没有 IDL 的问题。
    优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用 RPC、支持跨编程语言实现。
    缺点:对于习惯于静态类型语言的用户不直观。
    适用场景:在 Hadoop 中做 Hive、Pig 和 MapReduce 的持久化数据格式。

  • Protobuf,将数据结构以.proto 文件进行描述,通过代码生成工具可以生成对应数据结构的
    POJO 对象和 Protobuf 相关的方法和属性。
    优点:序列化后码流小,性能高、结构化数据存储格式(XML JSON 等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。
    缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持 Java 、C++ 、python。
    适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问属性、适
    合应用层对象的持久化

  • 其它

    • protostuff 基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可
    • Jboss marshaling 可以直接序列化 java 类, 无须实 java.io.Serializable 接口
    • Message pack 一个高效的二进制序列化格式
    • Hessian 采用二进制协议的轻量级 remoting onhttp 工具
    • kryo 基于 protobuf 协议,只支持 java 语言,需要注册(Registration),然后序列化(Output),
      反序列化(Input)

以上协议Netty都支持

其他

BASE理论

  • 基本可用(Basically Available)
    基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
    电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
  • 软状态(Soft State)
    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
  • 最终一致性(Eventual Consistency)
    最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的—种特殊情况。

加密算法:

  • 在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。
    优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
    缺点: 没有非对称加密安全.
    用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。

  • 非对称加密
    两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密
    解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.
    加密与解密:私钥加密,持有公钥才可以解密;公钥加密,持有私钥才可解密
    签名:私钥签名, 持有公钥进行验证是否被篡改过.
    优点: 非对称加密与对称加密相比,其安全性更好;
    缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
    用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.

常见的对称加密算法有: AES(微信用)、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256
不可逆加密算法(摘要):MD5、SHS

共识算法

CSRF攻击?如何防⽌?
cookie设置为HttpOnly,随机令牌、Referer 校验和 SameSite Cookie 等

项目哪个场景用到了动态代理,举例说明如何使用配置?
如何设计代码实现可读性、可维护性和可替代性
如何实现断点续传功能
paxso算法
在开发过程中遇到过什么印象很深刻的问题

面试题目

Java面试时问在项目开发时遇到最难的是什么问题,怎么解决的

大厂面试题整理

消息推送平台设计

2024大厂Java高级面试题及答案,java面试题基础篇

互联网大厂Java面试题1500+附答案详解(2023版)

Redis面试题

大厂最全1100道Java面试题及答案整理(2023最新版)

100道Java高频面试题(阿里面试官整理)

100 道 Linux 面试题

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值