面试笔记系列七之多线程+分布式系统基础知识点整理及常见面试题

介绍一下线程的生命周期及状态?

1.创建 当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。 2.就绪 当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。 3.运行 当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。添加到了线程列表中,如果OS调度选中了,就会进入到运行状态 4.阻塞 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:

  • 1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

  • 2、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

  • 3、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)。

  • 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

  • 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。

  • 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

  • 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

  • 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。

  • 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

5.死亡 线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。

  • 线程抛出一个未捕获的Exception或Error。

  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

线程的sleep、wait、join、yield如何使用?

sleep:让线程睡眠,期间会出让cpu,在同步代码块中,不会释放锁,thread类的方法,时间到后不用去争夺锁 wait(必须先获得对应的锁才能调用):让线程进入等待状态,释放当前线程持有的锁资源线程只有在notify 或者notifyAll方法调用后才会被唤醒,然后去争夺锁.Object类中的wait()方法, join:线程之间协同方式,使用场景: 线程A必须等待线程B运行完毕后才可以执行,那么就可以在线程A的代码中加入ThreadB.join(); yield:让当前正在运行的线程回到可运行状态(就绪状态),以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

sleep与yield方法的区别在于,

当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

进程调度算法

创建线程有哪些方式?

1)继承Thread类创建线程 2)实现Runnable接口创建线程 3)使用Callable和Future创建线程 4)使用线程池例如用Executor框架

什么是守护线程?

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)  任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。 User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。 注意事项: (1) thread.setDaemon(true)必须在thread.start()之前设置,否则会出现一个IllegalThreadStateException异常。只能在线程未开始运行之前设置为守护线程。 (2) 在Daemon线程中产生的新线程也是Daemon的。 (3) 不要认为所有的应用都可以分配给Daemon来进行读写操作或者计算逻辑,因为这会可能回到数据不一致的状态。

ThreadLocal的原理是什么,使用场景有哪些?

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}
    
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的载体,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

ThreadLocal有哪些内存泄露问题,如何避免?

ThreadLocal 是 Java 中的一个线程本地变量工具类,它允许我们在每个线程中存储和访问特定于该线程的数据。虽然 ThreadLocal 提供了一种方便的方式来在多线程环境下共享数据,但同时也会带来一些潜在的内存泄漏问题。

ThreadLocal 出现内存泄漏的根本原因可以归结为以下两点:

  1. 弱引用的键:ThreadLocal 内部使用线程对象作为键来存储每个线程的值。当线程结束时,其对应的 ThreadLocal 实例仍然保留了对线程对象的引用,由于 ThreadLocal 实例是被 ThreadLocalMap(ThreadLocal 的内部数据结构)引用的,这就导致了线程对象无法被垃圾回收。如果在长时间的运行过程中,线程一直存在而没有被正确清理,那么就会导致 ThreadLocalMap 中的键(即线程对象)一直存在,从而造成内存泄漏。

  2. 外部强引用持有:ThreadLocal 对象本身可能会被外部强引用持有,导致 ThreadLocal 对象本身无法被垃圾回收。如果在长时间的运行过程中不再需要使用 ThreadLocal 对象但仍然持有对其的强引用,那么 ThreadLocal 对象就无法被释放,其中包含的 ThreadLocalMap 也无法被释放,从而导致相关值无法释放。

这两个原因共同导致了 ThreadLocal 内存泄漏的问题。当没有及时清理 ThreadLocal 的引用时,ThreadLocalMap 中的键和对应的值会一直存在,并且无法被垃圾回收,从而造成内存泄漏。

为避免 ThreadLocal 内存泄漏问题,需要手动调用 remove() 方法清理对应的值,并确保在不再需要使用 ThreadLocal 对象时解除对其的强引用。合理的管理和生命周期控制对每个 ThreadLocal 对象非常重要,以确保它们能够被垃圾回收并释放相关内存。

内存泄漏问题主要出现在以下情况下:

  1. 长时间不清理:如果一个 ThreadLocal 对象被设置了值,但在不再需要时忘记进行清理,那么该线程中存储的对象可能会一直存在内存中,导致内存泄漏。

  2. 线程池使用不当:在使用线程池的情况下,如果没有手动清理或重用 ThreadLocal 对象,在线程池中的线程结束后,ThreadLocal 对象并不会自动清理,可能导致内存泄漏。

为避免 ThreadLocal 内存泄漏问题,我们可以采取以下几种措施:

  1. 及时清理:在使用完 ThreadLocal 对象后,手动调用其 remove() 方法将对应的值清除。尽量在能够确定值不再需要的时候进行清理操作。

  2. 使用弱引用:可以使用 java.lang.ref.WeakReference 来包装 ThreadLocal 对象,这样在线程结束后,ThreadLocal 对象会被垃圾回收器回收。

  3. 手动解除引用:当线程使用完 ThreadLocal 对象后,可以通过 ThreadLocal.set(null) 将 ThreadLocal 对象与值的引用解除,这样可以提醒垃圾回收器回收 ThreadLocal 对象。

  4. 使用 InheritableThreadLocal:如果在使用线程池并且希望将 ThreadLocal 的值从父线程传递到子线程时,可以考虑使用 InheritableThreadLocal,它可以自动继承父线程中的 ThreadLocal 值并传递给子线程。但要注意,InheritableThreadLocal 会带来一定的性能开销。

总之,在使用 ThreadLocal 时,要保证正确地清理和及时释放资源,以避免内存泄漏问题的发生。合理使用 ThreadLocal 可以带来很大的便利,但也需要在代码中仔细处理,以确保资源的正常释放。

AQS

AQS(AbstractQueuedSynchronizer)是Java并发包中的一个抽象基类,提供了一种实现同步器(synchronizer)的框架。AQS以队列的方式管理线程,通过内置的FIFO队列和状态变量,提供了一种简化、可扩展的机制,用于构建各种高性能的同步器,如ReentrantLock、CountDownLatch、Semaphore等。

AQS的核心思想是基于状态的获取和释放。每个AQS子类都维护一个表示状态的整数变量,线程在尝试获取资源时,首先通过检查状态来判断是否可以获取。如果状态符合获取条件,则线程可以获取到资源;否则,线程会被加入等待队列,暂时阻塞等待。当释放资源时,状态会被修改,并且被阻塞的线程会根据某种策略(如FIFO)被唤醒,继续竞争资源。

AQS框架主要由以下几个核心方法组成:

  • acquire(int arg):尝试获取资源,若获取失败则进入等待队列,一直阻塞直到被唤醒并成功获取资源。

  • release(int arg):释放资源,将状态变量修改,并唤醒等待队列中的线程。

  • tryAcquire(int arg):尝试获取资源,成功返回true,失败返回false。

  • tryRelease(int arg):尝试释放资源,成功返回true,失败返回false。

  • tryAcquireShared(int arg):尝试以共享模式获取资源。

  • tryReleaseShared(int arg):尝试以共享模式释放资源。

  • isHeldExclusively():判断当前线程是否独占资源。

通过继承AQS类,并实现自定义的同步器,可以灵活地构建各种并发操作。通常,自定义同步器需要重写上述方法来实现特定的并发控制策略。AQS提供了内部队列和状态变量的基本操作方法,简化了同步器的实现。

总结起来,AQS框架是Java并发包中的一个重要组件,通过提供内置的队列和状态变量,为构建各种高性能同步器提供了一种简洁、可扩展的框架。通过继承AQS类,并实现自定义的同步器,可以根据业务需求实现不同的并发控制策略。

内置的FIFO队列和状态变量

内置的FIFO队列和状态变量是 AQS(AbstractQueuedSynchronizer)中的两个核心组件,用于管理线程的等待和唤醒。

  1. FIFO队列: AQS内部通过一个双向链表来实现FIFO队列。该队列用于存储等待获取资源的线程,按照线程的等待顺序进行排队。在AQS中,队列中的每个节点都会持有一个线程对象的引用,并通过prevnext指针与其他节点相连。

    当线程尝试获取资源时,如果获取失败,线程会被封装成一个节点并被插入到队列的尾部。等到资源被释放时,队列头部的节点会被唤醒,并尝试重新获取资源。FIFO队列的设计保证了等待获取资源时间较长的线程先被唤醒,实现了公平性。

  2. 状态变量: AQS通过一个整型变量来表示同步器的状态。状态变量一般用于表达可获取资源的数量或控制同步行为的状态。

    在获取和释放资源的过程中,线程会基于状态变量进行判断。例如,一个计数器型的同步器可以使用状态变量来表示当前可用的资源数量。线程在尝试获取资源时,会根据状态变量判断是否可以获取。当一个线程成功获取资源时,状态变量会相应减少;当线程释放资源时,状态变量会相应增加。

    状态变量的修改一般通过CAS(Compare-and-Swap)原子操作来实现,保证了并发环境下的正确性。

通过内置的FIFO队列和状态变量,AQS能够实现并发控制的核心功能。FIFO队列管理等待获取资源的线程,保证了公平性;状态变量用于表示同步器的状态,并在获取和释放资源时进行状态的更新和控制。这两个组件共同协作,实现了线程的等待和唤醒机制,提供了强大的同步能力。

AQS实现加锁和解锁

-在 AQS(AbstractQueuedSynchronizer)中,实现加锁和解锁的核心方法是 acquirerelease。这两个方法会被子类进行重写,以实现具体同步器的加锁和解锁逻辑。下面详细介绍一下在 AQS 中加锁和解锁的实现方式:

加锁(acquire)过程:

  1. 尝试获取资源(tryAcquire):在加锁过程中,首先会调用自定义的 tryAcquire 方法尝试获取资源。如果成功获取资源,则直接返回,线程可以继续执行临界区代码;如果获取失败,则进入阻塞状态。

  2. 阻塞并入队(enqueue):如果 tryAcquire 失败,当前线程会将自己加入 AQS 维护的阻塞队列中,同时会以一种 node 节点的形式表示线程的状态,并最终以 FIFO(先进先出)的方式将线程排队等待获取资源。

  3. 自旋与阻塞:在队列中等待的线程可能采用自旋(spin)方式或者使用阻塞的方式来等待资源,具体的策略由具体的同步器决定。

  4. 状态记录:AQS 内部会根据线程的状态和队列中的顺序,记录和维护资源的状态,以便后续恢复和协调资源的分配。

解锁(release)过程:

  1. 尝试释放资源(tryRelease):在释放锁时,会调用自定义的 tryRelease 方法来尝试释放资源。这个方法通常会更新同步器的状态和状态队列,并唤醒可能正在等待资源的线程。

  2. 线程唤醒:如果成功释放资源,会唤醒阻塞队列中等待资源的线程,让它们有机会再次尝试获取资源。

  3. 状态更新:在加锁和解锁的过程中,同步器的状态会被更新和维护,以确保线程的正确同步和竞争。状态的更新可能包括资源的数量、线程状态等。

通过以上加锁和解锁的过程,AQS 通过内部的队列和状态记录,实现了多线程对资源的协调和同步控制。通过合理实现这些方法,可以定制出适应不同场景需求的高效同步器,帮助开发者实现复杂的并发控制逻辑。

AQS 状态的介绍

在 AQS(AbstractQueuedSynchronizer)中,状态的作用非常重要,它通常用来表示同步器所管理的资源的状态或可用数量,以及线程的获取和释放状态。具体来说,状态的作用包括:

  1. 资源状态标识:状态可以用来标识同步器管理的资源的状态,例如一个锁管理的是独占资源时,状态可以表示被锁住的状态。这个状态的具体含义可以根据具体的同步器来定义,比如 0 表示未被占用,1 表示已被占用。

  2. 线程等待判断:通过状态,可以判断线程是否可以获取资源。在加锁时,线程会通过状态的值来决定是否可以获取资源,从而决定是继续执行还是被阻塞。在解锁时,更新状态可能会唤醒正在等待的线程。

  3. 协调线程操作:状态的改变会影响到同步器内部队列的操作,进而影响到线程的等待和唤醒。状态的变化通常会触发队列中线程的唤醒和重新竞争资源的过程,从而实现多线程之间的协调和竞争。

  4. 实现特定的同步语义:状态的设计可以使得同步器实现特定的同步语义,比如可重入锁的状态表示当前锁被重复获取的次数,读写锁的状态可以表明读线程和写线程的数量等。

综上所述,AQS 中的状态起着非常关键和核心的作用,通过状态的维护和更新,AQS 可以实现多线程间的同步协调和资源管理。开发者可以根据具体的需求和场景来合理设计和使用状态,从而实现高效的同步控制和并发处理。

在 AQS(AbstractQueuedSynchronizer)中,一般情况下可以将状态分为两种:独占模式(Exclusive mode)和共享模式(Shared mode)。这些状态通常是通过一个整型变量来表示的,具体取值和含义可能会根据实际使用情况而定。以下是常见情况下状态的表示和含义:

独占模式(Exclusive mode):
  1. 0:表示当前资源未被任何线程占用,可以被当前线程获取。

  2. >0:表示当前资源已经被占用,值通常表示占用资源的线程个数或者深度。

  3. -1:表示当前资源已经被占用,且当前线程已经获取了资源(通常用于可重入锁)。

共享模式(Shared mode):
  1. 0:表示当前没有任何线程占用资源,可以被多个线程同时获取。

  2. >0:表示当前资源已经被占用,值通常表示占用资源的线程个数或者深度。

  3. -1:表示当前资源已经被占用,并且当前线程已经获取了资源(通常用于计数信号量)。

以上状态仅供参考,具体同步器的实现中,这些状态的含义和取值可能会有所差异。在使用 AQS 或者自定义同步器时,需要根据具体场景和需求来理解和使用状态,确保状态的变化和含义符合预期,从而实现正确的多线程同步和协调。

AQS是如何实现reentrantLock、CountDownLatch、Semaphore 这三种功能的

在 Java 中,ReentrantLock、CountDownLatch 和 Semaphore 三种功能的实现都是基于 AQS(AbstractQueuedSynchronizer)的。下面简单介绍一下它们是如何利用 AQS 实现的:

ReentrantLock:

ReentrantLock 是一个可重入的互斥锁,在 AQS 的基础上实现了锁的功能。ReentrantLock 内部通过继承 AbstractQueuedSynchronizer 并重写其 tryAcquiretryRelease 方法来实现独占锁的功能。具体实现中,tryAcquire 在获取锁的时候会判断锁的重入次数,tryRelease 在释放锁的时候会减少重入次数,直到重入次数为 0 才会真正释放锁。

CountDownLatch:

CountDownLatch 是一种同步工具,等待一个或多个线程执行完毕后才能继续执行。CountDownLatch 内部利用 AQS 的状态实现等待线程数量的计数功能,当计数为零时释放阻塞线程。主要是通过继承 AbstractQueuedSynchronizer,并在其中实现 tryAcquireSharedtryReleaseShared 方法来实现计数和等待的逻辑。

Semaphore:

Semaphore 是一种控制同时访问特定资源的线程数量的同步工具,它可以用来实现资源池或者限流。Semaphore 内部也是基于 AQS 进行实现的,通过状态变量表示可用资源的数量,当获取资源时会尝试获取许可,当释放资源时会释放许可。重点是实现 tryAcquireSharedtryReleaseShared 方法以实现资源数量的控制和线程等待逻辑。

总的来说,ReentrantLock、CountDownLatch 和 Semaphore 这三种功能的实现都是建立在 AQS 的基础上,通过重写 AQS 的相关方法,实现了不同类型的同步功能。开发者在使用这些类时,无需关注具体的实现细节,只需要了解其提供的功能及如何正确使用即可。

对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问 堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分 配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了 要还给操作系统,要不然就是内存泄漏。 在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚 拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及 数组都在这里分配内存。 栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语 言里面显式的分配和释放。 目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己 的内存空间,而不能访问别的进程的,这是由操作系统保障的。 在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。

如何预防死锁?

  1. 首先需要将死锁发生的是个必要条件讲出来:

    1. 互斥条件 同一时间只能有一个线程获取资源。

    2. 不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占

    3. 请求和保持条件 线程等待过程中不会释放已占有的资源

    4. 循环等待条件 多个线程互相等待对方释放资源

  2. 死锁预防,那么就是需要破坏这四个必要条件

    1. 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论

    2. 破坏不可剥夺条件

      1. 一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行

  3. 破坏请求与保持条件

    1. 第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源

    2. 第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源

  4. 破坏循环等待条件

    1. 采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

为什么要使用线程池?

为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池.

线程池种类

线程池的7大核心参数是什么?

  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数。

  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目。

  3. keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放。

  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等。

  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。

  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。

  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略。

(1)抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy。

(2)由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy。

(3)丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy。

(4)丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy。

线程池线程复用的原理是什么?

思考这么一个问题:任务结束后会不会回收线程? 答案是:allowCoreThreadTimeOut控制

/java/util/concurrent/ThreadPoolExecutor.java:1127
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {...执行任务...}
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
首先线程池内的线程都被包装成了一个个的java.util.concurrent.ThreadPoolExecutor.Worker,然后这个worker会马不停蹄的执行任务,执行完任务之后就会在while循环中去取任务,取到任务就继续执行,取不到任务就跳出while循环(这个时候worker就不能再执行任务了)执行 processWorkerExit方法,这个方法呢就是做清场处理,将当前woker线程从线程池中移除,并且判断是否是异常的进入processWorkerExit方法,如果是非异常情况,就对当前线程池状态(RUNNING,shutdown)和当前工作线程数和当前任务数做判断,是否要加入一个新的线程去完成最后的任务(防止没有线程去做剩下的任务).
那么什么时候会退出while循环呢?取不到任务的时候(getTask() == null).下面看一下getTask方法
​
private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
​
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
​
            //(rs == SHUTDOWN && workQueue.isEmpty()) || rs >=STOP
            //若线程池状态是SHUTDOWN 并且 任务队列为空,意味着已经不需要工作线程执行任务了,线程池即将关闭
            //若线程池的状态是 STOP TIDYING TERMINATED,则意味着线程池已经停止处理任何任务了,不在需要线程
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                //把此工作线程从线程池中删除
                decrementWorkerCount();
                return null;
            }
​
            int wc = workerCountOf(c);
​
            //allowCoreThreadTimeOut:当没有任务的时候,核心线程数也会被剔除,默认参数是false,官方推荐在创建线程池并且还未使用的时候,设置此值
            //如果当前工作线程数 大于 核心线程数,timed为true
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            
            //(wc > maximumPoolSize || (timed && timedOut)):当工作线程超过最大线程数,或者 允许超时并且超时过一次了
            //(wc > 1 || workQueue.isEmpty()):工作线程数至少为1个 或者 没有任务了
            //总的来说判断当前工作线程还有没有必要等着拿任务去执行
            //wc > maximumPoolSize && wc>1 : 就是判断当前工作线程是否超过最大值
            //或者 wc > maximumPoolSize && workQueue.isEmpty():工作线程超过最大,基本上不会走到这,
            //      如果走到这,则意味着wc=1 ,只有1个工作线程了,如果此时任务队列是空的,则把最后的线程删除
            //或者(timed && timedOut) && wc>1:如果允许超时并且超时过一次,并且至少有1个线程,则删除线程
            //或者 (timed && timedOut) && workQueue.isEmpty():如果允许超时并且超时过一次,并且此时工作                    队列为空,那么妥妥可以把最后一个线程(因为上面的wc>1不满足,则可以得出来wc=1)删除
            if ((wc > maximumPoolSize  || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    //如果减去工作线程数成功,则返回null出去,也就是说 让工作线程停止while轮训,进行收尾
                    return null;
                continue;
            }
​
            try {
                //判断是否要阻塞获取任务
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
    
//综上所述,如果allowCoreThreadTimeOut为true,并且在第1次阻塞获取任务失败了,那么当前getTask会返回null,不管是不是核心线程;那么runWorker中将推出while循环,也就意味着当前工作线程被销毁
​

通过上面这个问题可以得出一个结论:当你的线程池参数配置合理的时候,执行完任务的线程是不会被销毁的,而是会从任务队列中取出任务继续执行!

描述一下线程安全活跃态问题?

线程安全的活跃性问题可以分为 死锁、活锁、饥饿

  1. 活锁 就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试

    1. 我们开发中使用的异步消息队列就有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。

    2. 解决活锁的方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试

  2. 饥饿 就是 线程因无法访问所需资源而无法执行下去的情况

    1. 饥饿 分为两种情况:

      1. 一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态

      2. 另一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行

    2. 解决饥饿的问题有几种方案:

      1. 保证资源充足,很多场景下,资源的稀缺性无法解决

      2. 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源

      3. 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短

  3. 死锁 线程在对同一把锁进行竞争的时候,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁

线程安全的竞态条件有哪些?

  1. 同一个程序多线程访问同一个资源,如果对资源的访问顺序敏感,就称存在竞态条件,代码区成为临界区。 大多数并发错误一样,竞态条件不总是会产生问题,还需要不恰当的执行时序

  2. 最常见的竞态条件为

    1. 先检测后执行执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题,见一种可能 的解决办法就是:在一个线程修改访问一个状态时,要防止其他线程访问修改,也就是加锁机制,保证原子性

    2. 延迟初始化(典型为单例)

程序开多少线程合适?

  1. CPU 密集型程序,一个完整请求,I/O操作可以在很短时间内完成,CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0

    1. 单核CPU: 一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0。单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程。

    2. 多核 : 如果是多核CPU 处理 CPU 密集型程序,我们完全可以最大化的利用 CPU 核心数,应用并发编程来提高效率。CPU 密集型程序的最佳线程数就是:理论上线程数量 = CPU 核数(逻辑),但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1(经验值),计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作

  2. I/O 密集型程序,与 CPU 密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说 I/O 操作占比很大部分,等待时间较长,线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程

    1. I/O 密集型程序的最佳线程数就是: 最佳线程数 = CPU核心数 (1/CPU利用率) = CPU核心数 (1 + (I/O耗时/CPU耗时))

    2. 如果几乎全是 I/O耗时,那么CPU耗时就无限趋近于0,所以纯理论你就可以说是 2N(N=CPU核数),当然也有说 2N + 1的,1应该是backup

    3. 一般我们说 2N + 1 就即可

线程池拒绝策略

的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略: 线程池拒绝策略是在线程池无法继续接受新任务时,如何拒绝新任务并告知调用者的一种策略。当线程池的工作队列已满且线程数达到上限时,可以采用以下几种拒绝策略:

  1. AbortPolicy(默认策略): 这是线程池默认的拒绝策略。当线程池无法接受新任务时,会抛出RejectedExecutionException异常给调用者。

  2. CallerRunsPolicy: 当线程池无法接受新任务时,新任务会由提交任务的线程执行。这样做可以一定程度上降低对系统的压力,但也会影响提交任务的线程的性能。

  3. DiscardOldestPolicy: 当线程池无法接受新任务时,会丢弃工作队列中最旧的任务(即最先加入队列的任务)。然后尝试添加新任务。

  4. DiscardPolicy: 当线程池无法接受新任务时,会默默地丢弃新任务,不给任何提示和告知。

除了上述内置的拒绝策略,也可以自定义拒绝策略,实现RejectedExecutionHandler接口,并实现其中的rejectedExecution()方法,根据具体需求进行拒绝策略的定制。

选择合适的拒绝策略需要根据实际应用场景和需求。例如,如果对任务提交的性能要求较高,可以使用CallerRunsPolicy;如果对任务的可靠性要求较高,可以使用DiscardOldestPolicy;如果不希望丢失任务,可以自定义拒绝策略进行记录和处理。

线程池的队列满了之后

瞬间任务特别多,你可以无限制的不停地创建额外的线程出来,一台机器上可能有很多很多很多线程,每个线程都有自己的栈内存,占用一定的内存资源,会导致内存资源耗尽,系统也会崩溃。

即使内存没有崩溃,也会导致机器的cpu load(cpu负载)特别高。

假设【maximumPoolSize】最大线程数,设置为200。可能会导致任务被拒绝掉,很多任务无法被执行。

无界队列用的比较多,实际情况下得看系统业务的具体负载。具体情况具体分析

可以自定义一个拒绝策略:

自定义一个reject策略,如果线程池无法执行更多的任务了,此时建议你可以把这个任务信息持久化写入磁盘里去,后台专门启动一个线程,后续等待你的线程池的工作负载降低了,他可以慢慢的从磁盘里读取之前持久化的任务,重新提交到线程池里去执行

8核16G的内存 一般可以

创建多少线程呢?

根据你的业务场景来去设计,核心线程数=CPU核数*(执行时间/(执行时间+等待时间))

执行时间:代码中运算

等待时间:比如调用dubbo接口等待响应

2、如果使用无界队列,那么可能会导致OOM甚至宕机。 如果使用有界队列,然后设置max线程数=max那么会导致创建很多线程,也可能导致服务器崩溃。 所以要根据具体的场景以及具体的压测数据,来设定这些参数。最后就是我们可以手动去实现一个拒绝策略,将请求持久化一下,然后后台线程去等线程池负载降下来了后再读出来继续执行。

线程池execute提交任务做了什么事  线程池的状态

这个问题,有一张很经典的图可以说明execute的执行流程:(为任务分配线程)

Java线程池(一):运行阶段可以修改参数吗

可以

poolExecutor.execute(() -> { // 执行修改 poolExecutor.setCorePoolSize(10); poolExecutor.setMaximumPoolSize(cpuSize * 5); poolExecutor.setKeepAliveTime(60, TimeUnit.SECONDS); poolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); });

调整核心线程数

上硬核代码:

public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    // case 1. 如果当前正在使用的核心线程数多余修改后的核心线程数,中断一部分线程
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
    // case 2. 如果是增大核心线程的数量,视情况则增加工作线程
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}
复制代码

细说一下,首先,核心线程数量corePoolSize的数量肯定会设置成为我们想要的数值,就case 1来说,调用interruptIdleWorkers来中断线程,但是中断是有条件的,如果当前线程在执行任务,此时是不可中断的,我理解这样的线程最终会在keepAliveTime时间内结束处理的任务,不管有没有正确完成,在后面的某一个时间点内核心线程会调整到我们具体设置的值上:

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}
​
private void interruptIdleWorkers(boolean onlyOne) {
    // 只可有一个线程操作
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 循环线程集合,将处于空闲状态的线程中断掉
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}
​
// 其中workers定义:
private final HashSet<Worker> workers = new HashSet<Worker>();
复制代码

如果是调大核心线程数,比如由5个调整到10个,框架并不是立马又启动5个线程,而是结合观察阻塞队列里面的任务数,根据代处理的任务数来创建新的worker线程:如果阻塞队列里面有两个任务代处理,那么会新增两个核心线程,如果为0个,那一个都不创建。

ReentrantLock和synchronized的区别

废话区别:单词不一样。。。

核心区别:ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式

效率区别:如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。

底层实现区别:实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor

功能向的区别:ReentrantLock的功能比synchronized更全面。ReentrantLock支持公平锁和非公平锁ReentrantLock可以指定等待锁资源的时间。

选择哪个:如果你对并发编程特别熟练,推荐使用ReentrantLock,功能更丰富。如果掌握的一般般,使用synchronized会更好

synchronized和lock有哪些区别?

区别类型synchronizedLock
存在层次Java的关键字,在jvm层面上是JVM的一个接口
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放1、以获取锁的线程执行完同步代码,释放锁2、线程执行发生异常,jvm会让线程释放在finally中必须释放锁,不然容易造成线程死锁
锁类型锁可重入、不可中断、非公平可重入、可判断 可公平(两者皆可)
性能少量同步适用于大量同步
支持锁的场景1. 独占锁1. 公平锁与非公平锁

Lock对比Synchronized 支持以非阻塞方式获取锁 可以响应中断 可以限时

ABA问题遇到过吗,详细说一下?

  1. 有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成B。

  2. 首先线程1获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新(CompareAndSwap),成功将变量的值从A更新成B。

  3. 更新完毕之后,恰好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。

  4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后有更新成了B。但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。

̵Synchronized原理

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。

讲解synchronized的一个内存语义,这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。

Synchronized修饰静态变量和普通变量的区别

这里主要涉及到类对象(static方法),对象方法(非static方法)

我们知道,当synchronized修饰一个static方法时,多线程下,获取的是类锁(即Class本身,注意:不是实例);

当synchronized修饰一个非static方法时,多线程下,获取的是对象锁(即类的实例对象)

所以,当synchronized修饰一个static方法时,创建线程不管是new JoinThread()还是new Thread(new JoinThread()),在run方法中执行inc()方法都是同步的;

相反,当synchronized修饰一个非static方法时,如果用new JoinThread()还是new Thread(new JoinThread())方式创建线程,就无法保证同步操作,因为这时

inc()是属于对象方法,每个线程都执有一个独立的对象实例new JoinThread(),所以多线程下执行inc()方法并不会产生互斥,也不会有同步操作

1.Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。

   Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间只能有一个人打开一个小房间,然后用完了还回去,再由JVM 去分配下一个获得钥匙的人。

情况1:同一个对象在两个线程中分别访问该对象的两个同步方法

结果:会产生互斥。

解释:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。

情况2:不同对象在两个线程中调用同一个同步方法

结果:不会产生互斥。

解释:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙,

2.Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

情况1:用类直接在两个线程中调用两个不同的同步方法

结果:会产生互斥。

解释:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。

注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。

情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法

结果:会产生互斥。

解释:因为是一个对象调用,同上。

情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法

结果:不会产生互斥。

volatile和Synchronized

除了Synchronized,还能怎么保证线程安全

1 Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock 2 atomic 原子性+1 3 threadlocal 注意复杂对象及集合的clear,防止内存溢出 4 volatile 内存可见(不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁),仅仅set或者get的场景是适合volatile的)

5 可重入读写锁 ReentrantReadWriteLock(1、只有一个线程可以获取到写锁。在获取写锁时,只有没有任何线程持有任何锁才能获取成功; 2、如果有线程正持有写锁,其他任何线程都获取不到任何锁; 3、没有线程持有写锁时,可以有多个线程获取到读锁。)

volatile的可见性和禁止指令重排序怎么实现的?

  • 可见性: volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性 写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

  • 禁止指令重排序: volatile是通过内存屏障来禁止指令重排序 JMM内存屏障的策略

    • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

    • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

    • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

    • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

Happens-Before规则是什么?

Happens-Before 规则是并发编程中的一个重要概念,用来描述不同操作之间的先后顺序关系。在 Java 内存模型中,Happens-Before 规则定义了一组规则,用于确定一个操作是否能观察到另一个操作的结果,从而确保多线程程序的正确性和一致性。

具体来说,Happens-Before 规则包括以下几个方面:

  1. 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。

  2. 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

  3. volatile规则:对一个volatile变量的写,happens-before于任意后续对一个volatile变量的读。

  4. 传递性:若果A happens-before B,B happens-before C,那么A happens-before C。

  5. 线程启动规则:Thread对象的start()方法,happens-before于这个线程的任意后续操作。

  6. 线程终止规则:线程中的任意操作,happens-before于该线程的终止监测。我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  7. 线程中断操作:对线程interrupt()方法的调用,happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到线程是否有中断发生。

  8. 对象终结规则:一个对象的初始化完成,happens-before于这个对象的finalize()方法的开始。

Happens-Before 规则帮助程序员理解并发程序中操作之间的执行顺序,确保在多线程环境中操作能够按照预期顺序执行,避免出现数据不一致或不确定的结果。合理地遵循 Happens-Before 规则可以减少竞态条件(Race Condition)等并发编程中常见的问题,提高程序的稳定性和可靠性。

分布式系统

作为一个 Java 开发工程师,处理分布式事务是一个常见的挑战。以下是一些处理分布式事务的常见方法:

  1. 两阶段提交(Two-Phase Commit,2PC):2PC 是一种常见的分布式事务协议,它包括协调器和参与者两个角色。在该协议中,协调器协调所有参与者的提交或回滚操作。尽管 2PC 有效地保证了数据的一致性,但它依赖于协调器的单点故障和网络延迟的问题。

  2. 补偿事务(Compensating Transaction):补偿事务是通过反向操作来撤销之前已经执行的操作。当一个操作无法成功提交时,可以执行相应的补偿操作来回滚已经执行的操作。这种方法要求开发人员预先定义补偿操作,并确保补偿操作的一致性和正确性。

  3. 消息队列(Message Queue):使用消息队列来处理分布式事务可以提高系统的可靠性和性能。将事务操作封装为消息并将其发送到消息队列中,然后由后续的消费者进行处理。如果某个消费者失败,可以重新消费该消息或者将其发送到死信队列以便进行后续处理。

  4. 分布式数据库:选择合适的分布式数据库可以简化事务处理。一些分布式数据库系统具有内置的分布式事务管理功能,例如 Google Spanner、Apache Cassandra 或 TiDB 等。它们能够处理分布式事务并提供一致性和可用性保证。

  5. 基于消息的最终一致性(Eventual Consistency):在某些场景下,可以接受一定的数据不一致性,并基于最终一致性来处理分布式事务。通过异步更新数据副本,最终达到一致的状态。这种方式可以提高系统的吞吐量和性能,但需要在业务逻辑上进行妥善处理。

需要根据具体需求选择合适的事务处理方法。每种方法都有其优缺点,需要权衡各个方面的因素来做出决策。此外,还可以考虑使用一些分布式事务管理框架,如 Seata、HumiFly 等,来简化分布式事务的处理过程。

分布式id生成方案有哪些?

UUID,数据库主键自增,Redis自增ID,雪花算法。

描述优点缺点
UUIDUUID是通用唯一标识码的缩写,其目的是让分布式系统中的所有元素都有唯一的辨识信息,而不需要通过中央控制器来指定唯一标识。1. 降低全局节点的压力,使得主键生成速度更快; 2. 生成的主键全局唯一; 3. 跨服务器合并数据方便。1. UUID占用16个字符,空间占用较多; 2. 不是递增有序的数字,数据写入IO随机性很大,且索引效率下降
数据库主键自增MySQL数据库设置主键且主键自动增长1. INT和BIGINT类型占用空间较小; 2. 主键自动增长,IO写入连续性好; 3. 数字类型查询速度优于字符串1. 并发性能不高,受限于数据库性能; 2. 分库分表,需要改造,复杂; 3. 自增:数据和数据量泄露
Redis自增Redis计数器,原子性自增使用内存,并发性能好1. 数据丢失; 2. 自增:数据量泄露
雪花算法(snowflake)大名鼎鼎的雪花算法,分布式ID的经典解决方案1. 不依赖外部组件; 2. 性能好时钟回拨

雪花算法生成的ID由哪些部分组成?

  1. 符号位,占用1位。

  2. 时间戳,占用41位,可以支持69年的时间跨度。

  3. 机器ID,占用10位。

  4. 序列号,占用12位。一毫秒可以生成4095个ID。

分布式锁在项目中有哪些应用场景?

使用分布式锁的场景一般需要满足以下场景:

  1. 系统是一个分布式系统,集群集群,java的锁已经锁不住了。

  2. 操作共享资源,比如库里唯一的用户数据。

  3. 同步访问,即多个进程同时操作共享资源。

分布锁有哪些解决方案?

  1. Reids的分布式锁,很多大公司会基于Reidis做扩展开发。setnx key value ex 10s,Redisson。

    watch dog.

  2. 基于Zookeeper。临时节点,顺序节点。

  3. 基于数据库,比如Mysql。主键或唯一索引的唯一性。

基于 ZooKeeper 的分布式锁实现原理是什么?

顺序节点特性:

使用 ZooKeeper 的顺序节点特性,假如我们在/lock/目录下创建3个节点,ZK集群会按照发起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003,最后一位数是依次递增的,节点名由zk来完成。

临时节点特性:

ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。

根据ZK中节点是否存在,可以作为分布式锁的锁状态,以此来实现一个分布式锁,下面是分布式锁的基本逻辑:

  1. 客户端1调用create()方法创建名为“/业务ID/lock-”的临时顺序节点。

  2. 客户端1调用getChildren(“业务ID”)方法来获取所有已经创建的子节点。

  3. 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,如果是第一,那么就认为这个客户端1获得了锁,在它前面没有别的客户端拿到锁。

  4. 如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁。

ZooKeeper和Reids做分布式锁的区别?

Reids:

  1. Redis只保证最终一致性,副本间的数据复制是异步进行(Set是写,Get是读,Reids集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有部分数据没有复制过去可能会 「丢失锁」 情况,故强一致性要求的业务不推荐使用Reids,推荐使用zk。

  2. Redis集群各方法的响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明显上升(公网集群影响因素偏大),但是极限qps可以达到最大且基本无异常

ZooKeeper:

  1. 使用ZooKeeper集群,锁原理是使用ZooKeeper的临时顺序节点,临时顺序节点的生命周期在Client与集群的Session结束时结束。因此如果某个Client节点存在网络问题,与ZooKeeper集群断开连接,Session超时同样会导致锁被错误的释放(导致被其他线程错误地持有),因此ZooKeeper也无法保证完全一致。

  2. ZK具有较好的稳定性;响应时间抖动很小,没有出现异常。但是随着并发量和业务数量的提升其响应时间和qps会明显下降。

总结:

  1. Zookeeper每次进行锁操作前都要创建若干节点,完成后要释放节点,会浪费很多时间;

  2. 而Redis只是简单的数据操作,没有这个问题。

zookeeper的watcher特性

ZooKeeper的Watcher(观察者)特性是其分布式协调服务中非常重要的一部分。Watcher允许客户端能够接收和处理ZooKeeper服务端上节点的变化事件,实现实时的数据同步和协调。

具体来说,ZooKeeper的Watcher特性有以下几个重要点:

  1. 监听节点变化:在创建ZooKeeper客户端时,可以注册一个Watcher对象,用于监听指定节点的变化。当该节点的数据发生变化、被创建、被删除或其子节点发生变化时,ZooKeeper服务端会将这些事件通知给注册了Watcher的客户端。

  2. 一次性触发:每个Watcher只能被触发一次,也就是说,当一个Watcher接收到节点变化的通知后,它就会被移除,需要重新注册才能继续监听。

  3. 顺序性:ZooKeeper保证了Watcher的有序性。具体来说,如果在一个节点上注册了多个Watcher,那么这些Watcher将按照注册的先后顺序被通知。这样可以确保处理节点变化事件的顺序一致,避免不一致的问题。

  4. 实现实时同步:通过Watcher,客户端可以实时地感知到节点的变化,从而能够及时地更新自己的数据或采取相应的行动。这对于分布式系统中需要实现数据同步和协调的场景非常重要。

Watcher特性的作用是实现分布式系统中的实时数据同步和协调。通过注册Watcher,客户端能够及时地获取节点变化的通知,从而可以根据实际业务需求进行相应的处理。Watcher在ZooKeeper中广泛应用于分布式锁、配置管理、命名服务等场景,为分布式应用程序的开发和运维提供了便利。

Watcher在ZooKeeper中被设计为一种轻量级的通知机制。它的轻量性表现在以下几个方面:

  1. 建立和维护开销较低:当客户端注册Watcher时,它们只需要发送一个请求给ZooKeeper服务端,在服务端进行注册即可。客户端在接收到节点变化通知后,不需要保持持久的连接,因此不需要额外的资源用于维护连接状态。

  2. 数据传输量低:Watcher通知中只包含发生变化的节点的相关信息,如节点路径、变化类型等,并不包含节点的具体数据内容。因此,Watcher通知的数据传输量通常非常小,可以在网络中快速传输。

    但需要注意以下几点:

    1. Watcher的触发是异步的:当节点发生变化时,并不能保证Watcher能够立即被触发。ZooKeeper服务端会将通知推送给客户端,但触发的时间可能会受到网络延迟等因素的影响。

    2. Watcher的处理应尽快完成:由于Watcher在同一个会话中只能触发一次,因此客户端在处理Watcher通知时应尽快完成相应的逻辑,以保持及时的响应能力。

    客户端注册 Watcher 实现

    1. 创建ZooKeeper连接: 首先,需要创建一个ZooKeeper连接对象,并指定ZooKeeper服务端的地址和会话超时时间。例如:

      ZooKeeper zooKeeper = new ZooKeeper("localhost:2181", 5000, null);

    2. 注册Watcher: 在需要监听节点变化的地方,可以通过调用ZooKeeper对象的方法来注册Watcher。例如,注册一个用于监听指定节点"/myNode"的变化的Watcher:

      zooKeeper.exists("/myNode", new Watcher() {
          @Override
          public void process(WatchedEvent event) {
              // 处理节点变化事件的逻辑
              System.out.println("Node changed: " + event.getPath());
          }
      });

    3. 处理Watcher事件: 定义Watcher的process()方法中,可以编写具体的逻辑来处理节点变化事件。例如,在上述的Watcher中,当指定的节点发生变化时,会打印出节点路径。 可以根据实际需求进行相应的数据更新、业务操作等。

    需要注意的是,注册Watcher的方法中通常还会包含其他参数,用于控制Watcher的行为,如是否触发默认的Watch(设为true时,会在节点变化时收到通知)、指定Watcher的路径是否存在等。

MySQL如何做分布式锁?

在Mysql中创建一张表,设置一个 主键或者UNIQUE KEY 这个 KEY 就是要锁的 KEY(商品ID),所以同一个 KEY 在mysql表里只能插入一次了,这样对锁的竞争就交给了数据库,处理同一个 KEY 数据库保证了只有一个节点能插入成功,其他节点都会插入失败。

DB分布式锁的实现:通过主键id 或者 唯一索性 的唯一性进行加锁,说白了就是加锁的形式是向一张表中插入一条数据,该条数据的id就是一把分布式锁,例如当一次请求插入了一条id为1的数据,其他想要进行插入数据的并发请求必须等第一次请求执行完成后删除这条id为1的数据才能继续插入,实现了分布式锁的功能。

这样 lock 和 unlock 的思路就很简单了,伪代码:

def lock :
    exec sql: insert into locked—table (xxx) values (xxx)
    if result == true :
        return true
    else :
        return false
​
def unlock :
    exec sql: delete from lockedOrder where order_id='order_id'

计数器算法是什么?

计数器算法,是指在指定的时间周期内累加访问次数,达到设定的阈值时,触发限流策略。下一个时间周期进行访问时,访问次数清零。此算法无论在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性,再结合key的过期时间,即可轻松实现。

从上图我们来看,我们设置一分钟的阈值是100,在0:00到1:00内请求数是60,当到1:00时,请求数清零,从0开始计算,这时在1:00到2:00之间我们能处理的最大的请求为100,超过100个的请求,系统都拒绝。

这个算法有一个临界问题,比如在上图中,在0:00到1:00内,只在0:50有60个请求,而在1:00到2:00之间,只在1:10有60个请求,虽然在两个一分钟的时间内,都没有超过100个请求,但是在0:50到1:10这20秒内,确有120个请求,虽然在每个周期内,都没超过阈值,但是在这20秒内,已经远远超过了我们原来设置的1分钟内100个请求的阈值。

滑动时间窗口算法是什么?

为了解决计数器算法的临界值的问题,发明了滑动窗口算法。在TCP网络通信协议中,就采用滑动时间窗口算法来解决网络拥堵问题。

滑动时间窗口是将计数器算法中的实际周期切分成多个小的时间窗口,分别在每个小的时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的小时间窗口的总的请求数即可。

在上图中,假设我们设置一分钟的请求阈值是100,我们将一分钟拆分成4个小时间窗口,这样,每个小的时间窗口只能处理25个请求,我们用虚线方框表示滑动时间窗口,当前窗口的大小是2,也就是在窗口内最多能处理50个请求。随着时间的推移,滑动窗口也随着时间往前移动,比如上图开始时,窗口是0:00到0:30的这个范围,过了15秒后,窗口是0:15到0:45的这个范围,窗口中的请求重新清零,这样就很好的解决了计数器算法的临界值问题。

在滑动时间窗口算法中,我们的小窗口划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。

漏桶限流算法是什么?

漏桶算法的原理就像它的名字一样,我们维持一个漏斗,它有恒定的流出速度,不管水流流入的速度有多快,漏斗出水的速度始终保持不变,类似于消息中间件,不管消息的生产者请求量有多大,消息的处理能力取决于消费者。

漏桶的容量=漏桶的流出速度*可接受的等待时长。在这个容量范围内的请求可以排队等待系统的处理,超过这个容量的请求,才会被抛弃。

在漏桶限流算法中,存在下面几种情况:

  1. 当请求速度大于漏桶的流出速度时,也就是请求量大于当前服务所能处理的最大极限值时,触发限流策略。

  2. 请求速度小于或等于漏桶的流出速度时,也就是服务的处理能力大于或等于请求量时,正常执行。

    漏桶算法有一个缺点:当系统在短时间内有突发的大流量时,漏桶算法处理不了。

令牌桶限流算法是什么?

令牌桶算法,是增加一个大小固定的容器,也就是令牌桶,系统以恒定的速率向令牌桶中放入令牌,如果有客户端来请求,先需要从令牌桶中拿一个令牌,拿到令牌,才有资格访问系统,这时令牌桶中少一个令牌。当令牌桶满的时候,再向令牌桶生成令牌时,令牌会被抛弃。

在令牌桶算法中,存在以下几种情况:

  1. 请求速度大于令牌的生成速度:那么令牌桶中的令牌会被取完,后续再进来的请求,由于拿不到令牌,会被限流。

  2. 请求速度等于令牌的生成速度:那么此时系统处于平稳状态。

  3. 请求速度小于令牌的生成速度:那么此时系统的访问量远远低于系统的并发能力,请求可以被正常处理。

    令牌桶算法,由于有一个桶的存在,可以处理短时间大流量的场景。这是令牌桶和漏桶的一个区别。

你设计微服务时遵循什么原则?

  1. 单一职责原则:让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚。

  2. 服务自治原则:每个服务要能做到独立开发、独立测试、独立构建、独立部署,独立运行。与其他服务进行解耦。

  3. 轻量级通信原则:让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。比如采用RESTful风格,利用消息队列进行通信等。

  4. 粒度进化原则:对每个服务的粒度把控,其实没有统一的标准,这个得结合我们解决的具体业务问题。不要过度设计。服务的粒度随着业务和用户的发展而发展。

总结一句话,软件是为业务服务的,好的系统不是设计出来的,而是进化出来的。

CAP定理是什么?

CAP定理,又叫布鲁尔定理。指的是:在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • C:一致性(Consistency),数据在多个副本中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据是一致的。

  • A:可用性(Availability),系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。

  • P:分区容错性(Partition tolerance),在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成了不同的独立区域,这就是网络分区。

    我们来详细分析一下CAP,为什么只能满足两个。看下图所示:

    用户1和用户2分别访问系统A和系统B,系统A和系统B通过网络进行同步数据。理想情况是:用户1访问系统A对数据进行修改,将data1改成了data2,同时用户2访问系统B,拿到的是data2数据。

    但是实际中,由于分布式系统具有八大谬论:

    • 网络相当可靠

    • 延迟为零

    • 传输带宽是无限的

    • 网络相当安全

    • 拓扑结构不会改变

    • 必须要有一名管理员

    • 传输成本为零

    • 网络同质化

    我们知道,只要有网络调用,网络总是不可靠的。我们来一一分析。

    1. 当网络发生故障时,系统A和系统B没法进行数据同步,也就是我们不满足P,同时两个系统依然可以访问,那么此时其实相当于是单机系统,就不是分布式系统了,所以既然我们是分布式系统,P必须满足。

    2. 当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让用户2通过系统B正确的拿到data2,那么此时是满足C,就必须等待网络将系统A和系统B的数据同步好,并且在同步期间,任何人不能访问系统B(让系统不可用),否则数据就不是一致的。此时满足的是CP。

    3. 当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让系统B能继续提供服务,那么此时,只能接受系统A没有将data2同步给系统B(牺牲了一致性)。此时满足的就是AP。

我们在前面学过的注册中心Eureka就是满足 的AP,它并不保证C。而Zookeeper是保证CP,它不保证A。在生产中,A和C的选择,没有正确的答案,是取决于自己的业务的。比如12306,是满足CP,因为买票必须满足数据的一致性,不然一个座位多卖了,对铁路运输都是不可以接受的。

BASE理论是什么?

由于CAP中一致性C和可用性A无法兼得,eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性。它由于如下3种特征:

  • Basically Available(基本可用):分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用。

  • Soft state(软状态):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。、

  • Eventually consistent(最终一致性):最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

BASE理论并没有要求数据的强一致性,而是允许数据在一定的时间段内是不一致的,但在最终某个状态会达到一致。在生产环境中,很多公司,会采用BASE理论来实现数据的一致,因为产品的可用性相比强一致性来说,更加重要。比如在电商平台中,当用户对一个订单发起支付时,往往会调用第三方支付平台,比如支付宝支付或者微信支付,调用第三方成功后,第三方并不能及时通知我方系统,在第三方没有通知我方系统的这段时间内,我们给用户的订单状态显示支付中,等到第三方回调之后,我们再将状态改成已支付。虽然订单状态在短期内存在不一致,但是用户却获得了更好的产品体验。

2PC提交协议是什么?

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

可以进一步将准备阶段分为以下三个步骤:

1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。

2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)

3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

接下来分两种情况分别讨论提交阶段的过程。

当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。

2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”完成”消息。

4)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。

2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”回滚完成”消息。

4)协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

  不管最后结果如何,第二阶段都会结束当前事务。

2PC提交协议有什么缺点?

  1. 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

  2. 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

  3. 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

3PC提交协议是什么?

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

1.发送中断请求 协调者向所有参与者发送abort请求。

2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

pre阶段参与者没收到请求,rollback。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

3.响应反馈 事务提交完之后,向协调者发送Ack响应。

4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

1.发送中断请求 协调者向所有参与者发送abort请求

2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

2PC和3PC的区别是什么?

1、引入超时机制。同时在协调者和参与者中都引入超时机制。

2、三阶段在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

  • TCC解决方案是什么?

    TCC(Try-Confirm-Cancel)是一种常用的分布式事务解决方案,它将一个事务拆分成三个步骤:

    • T(Try):业务检查阶段,这阶段主要进行业务校验和检查或者资源预留;也可能是直接进行业务操作。

    • C(Confirm):业务确认阶段,这阶段对Try阶段校验过的业务或者预留的资源进行确认。

    • C(Cancel):业务回滚阶段,这阶段和上面的C(Confirm)是互斥的,用于释放Try阶段预留的资源或者业务。

TCC空回滚是解决什么问题的?

在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么此时Cancel就不能正常执行,因为Try没有对数据进行修改,如果Cancel进行了对数据的修改,那就会导致数据不一致。 ​ 解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。建议TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入一条记录,表示Try阶段执行了。Cancel接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

如何解决TCC幂等问题?

为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段Confirm和Cancel接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。 解决思路在上述 分支事务记录中增加执行状态,每次执行前都查询该状态。

分布式锁。

如何解决TCC中悬挂问题?

悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。 出现原因是在调用分支事务Try时,由于网络发生拥堵,造成了超时,TM就会通知RM回滚该分布式事务,可能回滚完成后,Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后无法继续处理。 解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try。

可靠消息服务方案是什么?

可靠消息最终一致性方案指的是:当事务的发起方(事务参与者,消息发送者)执行完本地事务后,同时发出一条消息,事务参与方(事务参与者,消息的消费者)一定能够接受消息并可以成功处理自己的事务。

这里面强调两点:

  1. 可靠消息:发起方一定得把消息传递到消费者。

  2. 最终一致性:最终发起方的业务处理和消费方的业务处理得完成,达成最终一致。

最大努力通知方案的关键是什么?

  1. 有一定的消息重复通知机制。因为接收通知方(上图中的我方支付系统)可能没有接收到通知,此时要有一定的机制对消息重复通知。

  2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

什么是分布式系统中的幂等?

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

例如,“getUsername()和 setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现. 我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

操作:查询,set固定值。逻辑删除。set 固定值。

流程:分布式系统中,网络调用,重试机制。

幂等有哪些技术解决方案?

1.查询操作

查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select 是天然的幂等操作;

2.删除操作

删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回 0,删除的数据多条,返回结果多个。

3.唯一索引

防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建多个资金账户,那么给资金账户表中的用户 ID 加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可。

4.token 机制

防止页面重复提交。

业务要求:页面的数据只能被点击提交一次;

发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交;

解决办法:集群环境采用 token 加 redis(redis 单线程的,处理需要排队);单 JVM 环境:采用 token 加 redis 或 token 加 jvm 锁。

处理流程:

  1. 数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间;

  2. 提交后后台校验 token,同时删除 token,生成新的 token 返回。

token 特点:要申请,一次有效性,可以限流。

注意:redis 要用删除操作来判断 token,删除成功代表 token 校验通过。

  1. traceId

    操作时唯一的。

对外提供的API如何保证幂等?

举例说明: 银联提供的付款接口:需要接入商户提交付款请求时附带:source 来源,seq 序列号。

source+seq 在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源 source,一个是来源方序列号 seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。

注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理。

分布式系统

从严格意义上来说,一个系统由多个独立的进程组成,而且进程之间有数据交互的逻辑,那么,不管这几个进程是否被部署在一台主机上,这样的系统都可以叫作分布式系统。

分布式微服务项目你是如何设计的?

我一般设计成两层:业务层和能力层(中台),业务层接受用户请求,然后通过调用能力层来完成业务逻辑。

认证 (Authentication) 和授权 (Authorization)的区别是什么?

Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。 Authorization(授权) 发生在 Authentication(认证) 之后。授权,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。 这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

Cookie 和 Session 有什么区别?如何使用Session进行身份验证?

Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。

那么,如何使用Session进行身份验证?

很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下:

用户向服务器发送用户名和密码用于登陆系统。 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。 服务器向用户返回一个 SessionID,写入用户的 Cookie。 当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。 使用 Session 的时候需要注意下面几个点:

依赖Session的关键业务一定要确保客户端开启了Cookie。 注意Session的过期时间

为什么Cookie 无法防止CSRF攻击,而token可以?

CSRF(Cross Site Request Forgery)一般被翻译为 跨站请求伪造 。那么什么是 跨站请求伪造 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科学理财,年盈利率过万</> 进行Session 认证的时候,我们一般使用 Cookie 来存储 SessionId,当我们登陆后后端生成一个SessionId放在Cookie中返回给客户端,服务端通过Redis或者其他存储工具记录保存着这个Sessionid,客户端登录以后每次请求都会带上这个SessionId,服务端通过这个SessionId来标示你这个人。如果别人通过 cookie拿到了 SessionId 后就可以代替你的身份访问系统了。

Session 认证中 Cookie 中的 SessionId是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,我们使用 token 的话就不会存在这个问题,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。

什么是 Token?什么是 JWT?如何基于Token进行身份验证?

我们知道 Session 信息需要保存一份在服务器端。这种方式会带来一些麻烦,比如需要我们保证保存 Session 信息服务器的可用性、不适合移动端(依赖Cookie)等等。

有没有一种不需要自己存放 Session 信息就能实现身份验证的方式呢?使用 Token 即可!JWT (JSON Web Token) 就是这种方式的实现,通过这种方式服务器端就不需要保存 Session 数据了,只用在客户端保存服务端返回给客户的 Token 就可以了,扩展性得到提升。

JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。

下面是 RFC 7519 对 JWT 做的较为正式的定义。

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)

JWT 由 3 部分构成:

Header :描述 JWT 的元数据。定义了生成签名的算法以及 Token 的类型。 Payload(负载):用来存放实际需要传递的数据 Signature(签名):服务器通过Payload、Header和一个密钥(secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。 在基于 Token 进行身份验证的的应用程序中,服务器通过Payload、Header和一个密钥(secret)创建令牌(Token)并将 Token 发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:Authorization: Bearer Token。

用户向服务器发送用户名和密码用于登陆系统。 身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。 用户以后每次向后端发请求都在Header中带上 JWT。 服务端检查 JWT 并从中获取用户相关信息。

单体架构的缺陷

好处显而易见:通常只建立一个Project工程即可,当系统比较小时,开发、部署、测试等工作都更加简单快捷,容易实现项目上线的目标。但随着系统的快速迭代,就会产生一些难以调和的矛盾和发现先天的缺陷。

● 过高耦合的风险:服务越来越多,不停地变化,由于都在一个进程中,所以一个服务的失败或移除,都将导致整个系统无法启动或正常运行的系统性风险越来越大。

● 新语言与新技术引入的阻力:单体架构通常只使用一种开发语言,并且完全使用一种特定的框架,运行在一个进程内,从而导致新语言和新技术很难被引入。在互联网应用时代,多语言协作开发是主流,特别是对于复杂的大系统、大平台。各种新技术层出不穷,拒绝新技术就意味着技术上的落后,从而可能逐步被市场抛弃。

● 水平扩展的问题:单体架构从一开始就没有考虑分布式问题,或者即使考虑了但仍然开发为单体架构,所以遇到单机性能问题时,通常难以水平扩展,往往需要推倒重来,代价比较大。

● 难以可持续发展:随着业务范围的快速拓展,单体架构通常难以复用原有的服务,一个新业务的上线,通常需要重新开发新服务、新接口,整个团队长期被迫加班是必然的结果,老板则怀疑技术团队及Leader的能力。

分布式架构下,Session 共享有什么方案?

  1. 不要有session:但是确实在某些场景下,是可以没有session的,其实在很多接口类系统当中,都提倡【API无状态服务】;也就是每一次的接口访问,都不依赖于session、不依赖于前一次的接口访问;

  2. 存入cookie中:将session存储到cookie中,但是缺点也很明显,例如每次请求都得带着session,数据存储在客户端本地,是有风险的;

  3. session同步:对个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;

  4. 使用Nginx(或其他复杂均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;

  5. 我们现在的系统会把session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器),服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制),不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/415477.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flutter 人机验证实战

先看效果 基本思路 接口进行触发是否进行图像验证&#xff0c;验证后将结果携带到接口里面去&#xff0c;进行人机验证 使用的技术(可惜只有web版本的) 验证码2.0智能人机验证(VAPTCHA)- 安全、易用、完全免费手势验证码VAPTCHA是基于人工智能和大数据的次世代人机验证解决方案…

HTML列表

想要在HTML中实现列表功能&#xff0c;无序用<ul>&#xff0c;有序用<ol>&#xff0c;有手就行。 效果图&#xff1a; CODE: <!DOCTYPE html> <html> <body><h2>一个无序 HTML 列表</h2><ul><li>咖啡</li><…

网络爬虫的危害,如何有效的防止非法利用

近年来&#xff0c;不法分子利用“爬虫”软件收集公民隐私数据案件屡见不鲜。2023年8月23日&#xff0c;北京市高级人民法院召开北京法院侵犯公民个人信息犯罪案件审判情况新闻通报会&#xff0c;通报侵犯公民个人隐私信息案件审判情况&#xff0c;并发布典型案例。在这些典型案…

Apache Paimon Append Scalable表解析

1.Append Scalable Table a) 定义 在表属性中配置 ‘bucket’ ‘-1’&#xff0c;将进入 “unaware-bucket mode”&#xff0c;在此模式下不再有桶的概念&#xff0c;也不保证流任务读取数据的顺序&#xff0c;可以将此表视为批量离线表&#xff0c;所有记录都将进入一个目录…

Codeforces Round 929 (Div. 3)

Codeforces Round 929 (Div. 3) Codeforces Round 929 (Div. 3) A. Turtle Puzzle: Rearrange and Negate 题意&#xff1a;可以对整数数组进行两个操作&#xff0c;一是随意重新排列或保持不变&#xff0c;二是选择连续子段元素符号倒转&#xff0c;求可能最大的所有元素和…

hadoop学习中遇到的问题一

由于看视频总是断断续续&#xff0c;经常遇到各种报错&#xff0c;现将遇到的问题进行总结。 hadoop学习中遇到的问题&#xff1a;hadoop拒绝连接 hadoop安装好之后&#xff0c;在本地浏览器输入地址http://192.168.222.102:9870&#xff0c;提示拒绝连接。在网上找了很多相关…

【Quarto】Markdown导出PPT

title: “Quarto Basics” mainfont: “LXGW WenKai Mono” format: revealjs: theme: default incremental: true pptx: incremental: true html: code-fold: true beamer: incremental: true aspectratio: 169 QUARTO 这段代码是一个 YAML 头部&#xff08;front matter&…

Unity(第十一部)场景

游戏有多个场景组成&#xff08;新手村&#xff0c;某某副本&#xff0c;主城&#xff09; 场景是有多个物体组成&#xff08;怪物&#xff0c;地形&#xff0c;玩家等&#xff09; 物体是有多个组件组成&#xff08;刚体组件&#xff0c;自定义脚本&#xff09; 创建场景 编辑…

刷题笔记 洛谷 P1162 填涂颜色

思路来自 大佬 hat.openai.com/c/9c30032e-5fb9-4677-8c15-9ea6530dc6db 题目链接 P1162 填涂颜色 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路 搜索 首先 在外面围上一圈0开始搜素 因为题目说将封闭区域内的0变成2 我们可以在外面进行搜索 把外面所有可以搜索…

【LabVIEW 】串口如何读取长度不一致的字符串

工程经验 1、在循环中&#xff0c;加入定时器&#xff0c;这样可以一段时间读取一次。 2、只要获取完整的一帧数据&#xff0c;就可以进行过滤筛选。

Leetcode—82. 删除排序链表中的重复元素 II【中等】

2024每日刷题&#xff08;117&#xff09; Leetcode—82. 删除排序链表中的重复元素 II 实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val…

【踏雪无痕的痕四】——0到底是有还是没有?

目录 一、背景介绍三、过程1.0的历史发展&#xff1f;2.如何将0讲给一个刚上一年级的孩子&#xff1f;3.0的边界和意义&#xff1f;4.那四年&#xff0c;到底在培养什么&#xff1f;和0有什么关系&#xff1f; 四、总结 一、背景介绍 最近在看一年级数学&#xff0c;其中介绍到…

几种新能源汽车(纯电、插混、油混、增程)的区别

纯电&#xff1a;顾名思义就是仅用电池驱动。 插混&#xff1a;汽车具备两套独立的动力系统&#xff1a;油动和电动。该种汽车可充电可加油&#xff0c;用油还是用电自己决定。他的系统结构图如下图&#xff1a; 油混&#xff1a;也称为油电混合。他的特点是可加油不可充电&…

前后端分离项目Vue+node.js二手商品交易系统74qb3

校园二手交易网络的开发和使用在不同的地方是有着差别的。在初高中&#xff0c;校园二手交易网也就是简单的买卖物品&#xff1b;但在大学中&#xff0c;通过买卖自己的物品可以建立联系成为朋友&#xff0c;也就是说校园二手交易网不仅仅是一个交易物品的平台&#xff0c;同时…

重拾前端基础知识:CSS

重拾前端基础知识&#xff1a;CSS 前言选择器简单选择器属性选择器组合选择器 插入CSS内嵌样式&#xff08;Inline Style&#xff09;内部样式&#xff08;Internal Style&#xff09;外部样式&#xff08;External Style&#xff09; 层叠颜色背景颜色文本颜色RGB 颜色HEX 颜色…

JS api基础初学

web api基础 变量声明有三个var let 和const 我们应该用那个呢&#xff1f; 首先var先排除&#xff0c;老派写法&#xff0c;问题很多&#xff0c;可以淘汰掉... let or const? 建议&#xff1a;const优先&#xff0c;尽量使用const&#xff0c;原因是&#xff1a; con…

JMeter学习(一)工具简单介绍

一、JMeter 介绍 Apache JMeter是100%纯JAVA桌面应用程序&#xff0c;被设计为用于测试客户端/服务端结构的软件(例如web应用程序)。它可以用来测试静态和动态资源的性能&#xff0c;例如&#xff1a;静态文件&#xff0c;Java Servlet,CGI Scripts,Java Object,数据库和FTP服务…

我在使用 Copilot 时遇到了许可证验证错误。

如果使用的是 Copilot&#xff0c;并收到以下错误消息&#xff0c;请按以下步骤进行操作&#xff1a; We encountered a problem validating your Copilot license. For more information, see https://aka.ms/copilotlicensecheck 请确保使用的是正确的帐户 请确保已使用具…

信钰证券|昨夜,“金龙”大涨

当地时间2月27日&#xff0c;我国资产自开盘一路走高&#xff0c;抢手中概股普涨&#xff0c;纳斯达克我国金龙指数涨2.10%。其中&#xff0c;抱负轿车涨超11%&#xff0c;网易涨超5%&#xff0c;爱奇艺、微博涨超4%。 美股方面&#xff0c;三大指数涨跌纷歧。到收盘&#xff…

npm淘宝镜像报错certificate has expired

1、概述 vue项目使用npm install命令时&#xff0c;突然报错&#xff1a;“...certificate has expired” 2、解决 1.清空缓存&#xff1a;npm cache clean --force 2.修改镜像&#xff08;管理员运行命令行&#xff09;&#xff1a;npm config set registry https:/…
最新文章