java笔录

Linux

https://www.cnblogs.com/yjd_hycf_space/p/7730690.html

ps -eLf | grep java 显示所有java线程

java设计模式

http://c.biancheng.net/design_pattern/

JVM虚拟机


在这里插入图片描述

JVM分为五个区

  • 方法区(Method Area)
    方法区存放了要加载的类的信息(如类名、修饰符等)、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。方法区是全局共享的,在一定条件下也会被GC。当方法区超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。[在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation)方法区上执行GC的情况很少,因此方法区被称为持久代的原因之一]
    运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常量和引用。一般来说,常量的分配在编译时就能确定,但也不全是,也可以存储在运行时期产生的常量。比如String类的intern()方法,作用是String类维护了一个常量池,如果调用的字符”hello”已经在常量池中,则直接返回常量池中的地址,否则新建一个常量加入池中,并返回地址。

  • 堆区(Heap)
    堆区主要用于存放对象实例及数组,所有new出来的对象都存储在该区域。 堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由所有线程共享,在虚拟机启动时创建。
    堆内存分区
    内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)
    在这里插入图片描述

  • 新生代(Youn Generation):

新生代适合生命周期较短,快速创建和销毁的对象.大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpaceToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。一般占据堆的1/3空间当新生代进行垃圾回收时会出发Minor GC(也称作Youn GC)。
(新生代 详解)

                            - Eden区:

Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收

					       - ServivorTo:

保留了一次MinorGC过程中的幸存者

                	      - ServivorFrom:

上一次GC的幸存者,作为这一次GC的被扫描者。
当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。

           - MinorGC的过程:采用复制算法。(下文GC算法有介绍)

1.首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)
2.同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)
3.然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

在这里插入图片描述

  • 旧生代(Old Generation):

旧生代适合生命周期较长的对象,旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。MajorGC采用标记—清除算法

  • 持久代(Permanent Generation):

持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法).在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

  • 虚拟机栈(VM Stack)
    虚拟机栈占用的是操作系统内存,每个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。
    虚拟机栈定义了两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足时,抛出OutOfMemoryError。

  • 本地方法栈(Native Method Stack)
    本地方法栈用于支持native方法的(带有native字样的方法都是c写的)执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。

  • 程序计数器(Program Counter Register)
    程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念模型,各种JVM所采用的方式不一样。字节码解释器工作时,就是通过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。
    每个程序计数器只能记录一个线程的行号,因此它是线程私有的。
    如果程序当前正在执行的是一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,则计数器的值为空,此内存区是唯一不会抛出OutOfMemoryError的区域。

GC机制 算法

  • 查找算法
    经典的引用计数算法,每个对象添加到引用计数器,每被引用一次,计数器+1,失去引用,计数器-1,当计数器在一段时间内为0时,即认为该对象可以被回收了。但是这个算法有个明显的缺陷:当两个对象相互引用,但是二者都已经没有作用时,理应把它们都回收,但是由于它们相互引用,不符合垃圾回收的条件,所以就导致无法处理掉这一块内存区域。因此,Sun的JVM并没有采用这种算法,而是采用一个叫——根搜索算法,如图:
    在这里插入图片描述
    基本思想是:从一个叫GC Roots的根节点出发,向下搜索,如果一个对象不能达到GC Roots的时候,说明该对象不再被引用,可以被回收。如上图中的Object5、Object6、Object7,虽然它们三个依然相互引用,但是它们其实已经没有作用了,这样就解决了引用计数算法的缺陷。

补充概念,在JDK1.2之后引入了四个概念:强引用、软引用、弱引用、虚引用。
强引用:new出来的对象都是强引用,GC无论如何都不会回收,即使抛出OOM异常。

      Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());

而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
软引用:只有当JVM内存不足时才会被回收。
非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用:只要GC,就会立马回收,不管内存是否充足。
第二次垃圾回收时回收,可以通过如下代码实现

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器
虚引用:可以忽略不计,JVM完全不会在乎虚引用,你可以理解为它是来凑数的,凑够”四大天王”。它唯一的作用就是做一些跟踪记录,辅助finalize函数的使用。
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据的回收比例是不同的,根据IBM的统计,数据如下图所示:在这里插入图片描述

  • 常见的GC算法:复制、标记-清除和标记-压缩

复制:复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域,如图所示:
在这里插入图片描述
当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。
标记-清除:该算法采用的方式是从跟集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。标记和清除的过程如下:
在这里插入图片描述
上图中蓝色部分是有被引用的对象,褐色部分是没有被引用的对象。在Marking阶段,需要进行全盘扫描,这个过程是比较耗时的。
在这里插入图片描述
清除阶段清理的是没有被引用的对象,存活的对象被保留。
标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。
标记-压缩:该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针,如下图所示
在这里插入图片描述

垃圾收集器(选读)

  • 串行收集器(Serial GC)
    Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。比较适合于只有一个处理器的系统。在串行处理器中minor和major GC过程都是用一个线程进行回收的。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,而且事实上,它并没有让我们失望,几十毫秒的停顿,对于我们客户机是完全可以接受的,该收集器适用于单CPU、新生代空间较小且对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。

  • ParNew GC
    基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以被用于服务端上(server),同时它可以与CMS GC配合,所以,更加有理由将他用于server端。

  • Parallel Scavenge GC
    在整个扫描和复制过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的应用,是server级别的默认GC方式。

  • CMS (Concurrent Mark Sweep)收集器
    该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的应用就适合这种收集器,因为其高并发、高响应的特点,CMS是基于标记-清楚算法实现的。
    CMS收集器的优点:并发收集、低停顿,但远没有达到完美;
    CMS收集器的缺点:
    a.CMS收集器对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。
    b.CMS收集器无法处理浮动垃圾,可能出现“Concurrnet Mode Failure”,失败而导致另一次的Full GC。
    c.CMS收集器是基于标记-清除算法的实现,因此也会产生碎片。

  • G1收集器
    相比CMS收集器有不少改进,首先,基于标记-压缩算法,不会产生内存碎片,其次可以比较精确的控制停顿。

  • Serial Old收集器
    Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

  • Parallel Old收集器
    Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

  • RTSJ垃圾收集器
    RTSJ垃圾收集器,用于Java实时编程。


多线程并发编程

线程

https://blog.csdn.net/weixin_40271838/article/details/79998327
线程的生命周期
在这里插入图片描述
1.用new Thread()的方法新建一个线程,创建完成后,线程就进入就绪状态(Runnable状态),
2.1线程进入抢占cpu资源的状态,当线程抢到CPU资源后进入运行状态
3.1线程运行,当线程运行结束后或者非常态调用stop()方法之后,线程就进入了死亡状态.
2.2第二中情况:线程遇到一下几种情况的时候容易造成堵塞,当线程主动调用sleep()方法,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态
3.2线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源.

如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程
ps:
1.创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
2.就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
3.运行(running)状态: 执行run()方法
4.阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
5.死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)
https://blog.csdn.net/vbirdbest/article/details/81282163
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

线程的创建

https://blog.csdn.net/m0_37840000/article/details/79756932
1.继承Thread类实现重写run()方法
1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2】创建Thread子类的实例,也就是创建了线程对象
3】启动线程,即调用线程的start()方法

代码实例:
public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

2.实现Runnable接口
1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3】第三部依然是通过调用线程对象的start()方法来启动线程

代码实例:
public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者    new Thread(new MyThread2()).start();
  }
}

3.Callable和Future创建线程

1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码实例:
public class Main {
    public static void main(String[] args) throws Exception {
    	 // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
        return sum;
    }
}

三种方式比较:

Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行

当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

线程池

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
创建时,一般使用它的子类:ThreadPoolExecutor.

ThreadPoolExecutor属性
public ThreadPoolExecutor(
int corePoolSize,//核心线程数(线程池中线程的数量)
int maximumPoolSize,(最大线程数,包括核心线程数,剩下的任务放在放在阻塞队列中)
long keepAliveTime,//非核心线程空闲存活时间
TimeUnit unit,// 空闲线程存活时间的单位
BlockingQueue workQueue, //阻塞队列,存放最大线程数-核心线程数的剩余任务数
ThreadFactory threadFactory,//线程生产工厂类对象
RejectedExecutionHandler handler//任务数异常时解决方案
)

线程池图解
在这里插入图片描述
corePoolSize就是线程池中的核心线程数,在没有用的时候,也不会被回收
maximumPoolSize就是线程池中可以容纳的最大线程的数量
keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间.除了核心线程,其余线程都有存活时间.
util,就是计算这个时间的一个单位
workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了知乎,拒绝执行某些任务

线程池的执行流程
在这里插入图片描述
任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
工作队列
如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将累积起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。常见的工作队列有以下几种,前三种用的最多
1.ArrayBlockingQueue:列表形式的工作队列,必须要有初始队列大小,有界队列,先进先出。
**2.LinkedBlockingQueue:**链表形式的工作队列,可以选择设置初始队列大小,有界/无界队列,先进先出。
3.SynchronousQueue: SynchronousQueue不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入SynchronousQueue中, 必须有另一个线程正在等待接受这个元素. 如果没有线程等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建 一个线程, 否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交 给执行它的线程,而不是被首先放在队列中, 然后由工作者线程从队列中提取任务. 只有当线程池是无解的或者可以拒绝任务时,SynchronousQueue才有实际价值.
4.PriorityBlockingQueue:优先级队列,有界队列,根据优先级来安排任务,任务的优先级是通过自然顺序或Comparator(如果任务实现了Comparator)来定义的。

**5.DelayedWorkQueue:**延迟的工作队列,无界队列。

handler的拒绝策略:
第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
第二种DisCardPolicy:不执行新任务,也不抛出异常
第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
第四种CallerRunsPolicy:直接调用execute来执行当前任务

线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的
在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。
Executors提供的线程工厂有两种

1.DefaultThreadFactory默认线程工厂,创建一个新的、非守护的线程,并且不包含特殊的配置信息。
2.PrivilegedThreadFactory通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限、 AccessControlContext、ContextClassLoader。如果不使用privilegedThreadFactory, 线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限。
3.自定义线程工厂可以自己实现ThreadFactory接口来定制自己的线程工厂方法。

线程池的工作流程

1.默认情况下,创建完线程池后并不会立即创建线程, 而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
2.当线程数小于核心线程数时,每提交一个任务就创建一个线程来执行,即使当前有线程处于空闲状态,直到当前线程数达到核心线程数。
3.当前线程数达到核心线程数时,如果这个时候还提交任务,这些任务会被放到队列里,等到线程处理完了手头的任务后,会来队列中取任务处理。
4.当前线程数达到核心线程数并且队列也满了,如果这个时候还提交任务,则会继续创建线程来处理,直到线程数达到最大线程数。
5.当前线程数达到最大线程数并且队列也满了,如果这个时候还提交任务,则会触发饱和策略。
6.如果某个线程的控线时间超过了keepAliveTime,那么将被标记为可回收的,并且当前线程池的当前大小超过了核心线程数时,这个线程将被终止。

四种常见的线程池:

CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
只有非核心线程,最大线程数很大(Int.Max(values)),它会为每一个任务添加一个新的线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收。缺点就是没有考虑到系统的实际内存大小。

public static ExecutorService newCachedThreadPool(int threads)
    {
    return newFixedThreadPool(threads,Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
    }

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
唯一一个有延迟执行和周期重复执行的线程池。它的核心线程池固定,非核心线程的数量没有限制,但是闲置时会立即会被回收。

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
只有一个核心线程,通过指定的顺序将任务一个个丢到线程,都乖乖的排队等待执行,不处理并发的操作,不会被回收。确定就是一个人干活效率慢。

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程.
有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。

 public static ExecutorService newFixedThreadPool(int threads)
    {
    return newFixedThreadPool(threads,threads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    }

线程池的基本使用:

1.package com.joonwhee.concurrent;
2. 
3.import java.util.ArrayList;
4.import java.util.List;
5.import java.util.concurrent.Callable;
6.import java.util.concurrent.ExecutorService;
7.import java.util.concurrent.Executors;
8.import java.util.concurrent.Future;
9.import java.util.concurrent.FutureTask;
10.import java.util.concurrent.LinkedBlockingQueue;
11.import java.util.concurrent.ScheduledExecutorService;
12.import java.util.concurrent.ThreadPoolExecutor;
13.import java.util.concurrent.TimeUnit;
15./**
3. * 线程池的基本使用
4. * @author JoonWhee
5. * @Date 2018年1月21日
6. */
20.public class ThreadPoolExecutorTest {
7.    /**
8.     * 创建一个线程池(完整入参): 
9.     * 核心线程数为5 (corePoolSize), 
10.     * 最大线程数为10 (maximumPoolSize), 
11.     * 存活时间为60分钟(keepAliveTime), 
12.     * 工作队列为LinkedBlockingQueue (workQueue),
13.     * 线程工厂为默认的DefaultThreadFactory (threadFactory), 
14.     * 饱和策略(拒绝策略)为AbortPolicy: 抛出异常(handler).
15.     */
16.    private static ExecutorService THREAD_POOL = new ThreadPoolExecutor(5, 10, 60, TimeUnit.MINUTES,
17.            new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(),
18.            new ThreadPoolExecutor.AbortPolicy());
19.    /**
20.     * 只有一个线程的线程池 没有超时时间, 工作队列使用无界的LinkedBlockingQueue
21.     */
22.    private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
23.    // private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
24.    /**
25.     * 有固定线程的线程池(即corePoolSize = maximumPoolSize) 没有超时时间,
26.     * 工作队列使用无界的LinkedBlockingQueue
27.     */
28.    private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
29.    // private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5, Executors.defaultThreadFactory());
30.    /**
31.     * 大小不限的线程池 核心线程数为0, 最大线程数为Integer.MAX_VALUE, 存活时间为60秒 该线程池可以无限扩展,
32.     * 并且当需求降低时会自动收缩, 工作队列使用同步移交SynchronousQueue.
33.     */
34.    private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
35.    // private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
36.    /**
37.     * 给定的延迟之后运行任务, 或者定期执行任务的线程池
38.     */
39.    private static ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
40.    // private static ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5, Executors.defaultThreadFactory());
41.    public static void main(String args[]) throws Exception {
42. 
43.        /**
44.         * 例子1: 没有返回结果的异步任务
45.         */
46.        THREAD_POOL.submit(new Runnable() {
47.            @Override
48.            public void run() {
49.                // do something
50.                System.out.println("没有返回结果的异步任务");
51.            }
52.        });
53.        
54.        /**
55.         * 例子2: 有返回结果的异步任务
56.         */
57.        Future<List<String>> future = THREAD_POOL.submit(new Callable<List<String>>() {
58.            @Override
59.            public List<String> call() {
60.                List<String> result = new ArrayList<>();
61.                result.add("JoonWhee");
62.                return result;
63.            }
64.        });
65.        List<String> result = future.get(); // 获取返回结果
66.        System.out.println("有返回结果的异步任务: " + result);
67.        
68.        /**
69.         * 例子3: 
70.         * 有延迟的, 周期性执行异步任务
71.         * 本例子为: 延迟1秒, 每2秒执行1次
72.         */
73.        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
74.            @Override
75.            public void run() {
76.                System.out.println("this is " + Thread.currentThread().getName());
77.            }
78. 
79.        }, 1, 2, TimeUnit.SECONDS);
80.        
81.        /**
82.         * 例子4: FutureTask的使用
83.         */
84.        Callable<String> task = new Callable<String>() {
85.            public String call() {
86.                return "JoonWhee";
87.            }
88.      
89.        };      
90.        FutureTask<String> futureTo = new FutureTask<String>(task);
91.        THREAD_POOL.submit(futureTo);
92.        System.out.println(futureTo.get()); // 获取返回结果
112.//        System.out.println(futureTo.get(3, TimeUnit.SECONDS));  // 超时时间为3秒
93.    }
114.} 

线程常用的方法 查看(point)
https://blog.csdn.net/vbirdbest/article/details/81282163

锁大概有以下名词:
自旋锁 ,自旋锁的其他种类,阻塞锁,可重入锁 ,读写锁 ,互斥锁 ,悲观锁 ,乐观锁 ,公平锁 ,偏向锁, 对象锁,线程锁,锁粗化, 锁消除,轻量级锁,重量级锁, 信号量,独享锁,共享锁,分段锁
我们所说的锁的分类其实应该按照锁的特性和设计来划分
从并发的角度来讲,按照线程安全的三种策略看,主要内容都集中在互斥同步里,我们所讨论的锁也集中在这个部分。这个部分的锁都是悲观锁,第二个部分是非阻塞同步,这个部分也就一种通过CAS进行原子类操作,这个部分可以看成乐观锁,其实也就是不加锁。第三个部分是无同步方案,包括可重入代码和线程本地存储

锁的分类

常见的锁

Synchronized和Lock
Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁
ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。
https://www.cnblogs.com/yumingxing/articles/9581586.html
https://blog.csdn.net/nalanmingdian/article/details/77800355
还需要添加…
ReentrantLock的高级操作
中断等待
ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

ReentrantLock获取锁定有三种方式:
lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false
tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
lockInterruptibly如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到获取锁定,或者当前线程被别的线程中断

乐观锁和悲观锁 (point)

悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁( 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和
ReentrantLock 等独占锁就是悲观锁思想的实现

乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
使用场景
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的
时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以

可重入锁

ReentrantLock 和 synchronized 都是可重入锁。

class Main{
    public synchronized void method1() {
        method2();
    }     
    public synchronized void method2() {     
    }
}

在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。如果不具有可重入性,则会造成死锁。

可中断锁

Lock 是可中断锁,而synchronized 不是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁

synchronized 是非公平锁,它无法保证线程获取锁的顺序。ReentrantLock 与 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

公平锁即尽量以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久(最先请求)的的线程会获得该锁,这种就是公平锁。

public ReentrantLock() {
     sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
}

如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

读写锁

读写锁使得多个读操作不会发生冲突。

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

public void get(Thread thread) {
        ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        rwlock.readLock().lock();
        try {
            long start = System.currentTimeMillis();         
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwlock.readLock().unlock();
        }
}

死锁

是指两个或两个以上的线程在执行过程中,
因争夺资源而造成的一种互相等待的现象

1)两个以上的线程
2)争夺共享的资源
3)它们各自不释放手中资源,除非有外力协助


Spring

SpringMvc

在这里插入图片描述
Spring MVC流程

  1. 用户发送请求至前端控制器 DispatcherServlet(Filters过滤器 过滤用户请求)
  2. DispatcherServlet收到请求调用HandlerMapping(可以理解为一个注册中心 作用是找到指定的controller的信息)处理映射器
  3. 处理映射器找到具体的处理器(HandlerMthod=具体的controller类[目标对象]+类中具体的方法[目标哦啊方法]),生成处理器对象以及处理拦截器(如何则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器指导指定的完成业务,HandlerInterceptors试拦截器(比如实现 12306定时访问的功能)
  5. 指定的controller执行业务完成,通过HandlerAdapter返回ModelAndView给DispatcherServlet
  6. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  7. ViewReslover解析后返回具体View
  8. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)响应用户
    在这里插入图片描述

分布式、高并发、多线程区别?

什么是分布式?
分布式更多的一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。该领域需要解决的问题极多,在不同的技术层面上,又包括:分布式文件系统、分布式缓存、分布式数据库、分布式计算等,一些名词如Hadoop、zookeeper、MQ等都跟分布式有关。从理念上讲,分布式的实现有两种形式:
水平扩展:当一台机器扛不住流量时,就通过添加机器的方式,将流量平分到所有服务器上,所有机器都可以提供相当的服务;
垂直拆分:前端有多种查询需求时,一台机器扛不住,可以将不同的需求分发到不同的机器上,比如A机器处理余票查询的请求,B机器处理支付的请求。
2什么是高并发?
相对于分布式来讲,高并发在解决的问题上会集中一些,其反应的是同时有多少量:比如在线直播服务,同时有上万人观看。
高并发可以通过分布式技术去解决,将并发流量分不到不同的物理服务器上。但除此之外,还可以有很多其他优化手段:比如使用缓存系统,将所有的,静态内容放到CDN等;还可以使用多线程技术将一台服务器的服务能力最大化。
3什么是多线程?
多线程是指从软件或者硬件上实现多个线程并发执行的技术,它更多的是解决CPU调度多个进程的问题,从而让这些进程看上去是同时执行(实际是交替运行的)。
这几个概念中,多线程解决的问题是最明确的,手段也是比较单一的,基本上遇到的最大问题就是线程安全。在JAVA语言中,需要对JVM内存模型、指令重排等深入了解,才能写出一份高质量的多线程代码。
总结一下:
●分布式是从物理资源的角度去将不同的机器组成一个整体对外服务,技术范围非常管且难度非常大,有了这个基础,高并发、高吞吐等系统很容易构建;
● 高并发是从业务角度去描述系统的能力,实现高并发的手段可以采用分布式,也可以采用诸如缓存、CDN等,当然也包括多线程;
● 多线程则聚焦于如何使用编程语言将CPU调度能力最大化。
分布式与高并发系统,涉及到大量的概念和知识点,如果没有系统的学习,很容易会杂糅概念而辨识不清,在面试与实际工作中都会遇到困难。


nginx

nginx是一款web和反向代理服务器

反向代理解析:
反向代理和正向代理的区别就是:正向代理代理客户端,反向代理代理服务器。
  反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。(我们只需要访问www.baidu.com这个网址 具体访问哪个服务器的ip 是反向代理服务器自动帮我们转发的 我们不需要你知道服务器的ip)
一、 静态代理
nginx擅长处理静态文件,是很好的图片、文件服务器,把静态资源放在nginx上,可以是应用动静分离,提高性能

二、 负载均衡
Nginx通过反向代理可以实现服务的负载均衡,避免了服务器单节点故障,把请求按照一定的策略转发到不同的服务器上,达到负载的效果。

1、轮询
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载

2、加权轮询
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同.给配置高、负载低的机器配置更高的权重,让其处理更多的请

3、ip_hash(源地址哈希法)
根据获取客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客户端要访问服务器的序号。当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问

4、随机
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。

5、least_conn(最小连接数法)
由于后端服务器的配置不尽相同,对于请求的处理有快有慢,最小连接数法根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
三、 限流
Nginx的限流模块,是基于漏桶算法实现的,在高并发的场景下非常实用(自行百度)

四、 缓存
1、浏览器缓存,静态资源缓存用expire。
2、代理层缓存
五、 黑白名单
1、不限流白名单


Dubbo

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。只有在分布式的时候,才有dubbo这样的分布式服务框架的需求.说白了就是个远程服务调用的分布式框架(告别Web Service模式中的WSdl,以服务者与消费者的方式在dubbo上注册)
Dubbo 是一个分布式、高性能、透明化的 RPC 服务框架,提
供服务自动注册、自动发现等高效服务治理方案, 可以和
Spring 框架无缝集成。

默认使用 dubbo 协议

dubbo架构图如下所示:
在这里插入图片描述
节点角色说明:
Provider: 暴露服务的服务提供方。
Consumer: 调用远程服务的服务消费方。
Registry: 服务注册与发现的注册中心。
Monitor: 统计服务的调用次调和调用时间的监控中心。
Container: 服务运行容器。

调用关系说明:
0 服务容器负责启动,加载,运行服务提供者。

  1. 服务提供者在启动时,向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  4. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  5. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

在这里插入图片描述
流程说明:
 Provider(提供者)绑定指定端口并启动服务
 指供者连接注册中心,并发本机 IP、端口、应用信息和提供服务信息
发送至注册中心存储
 Consumer(消费者),连接注册中心 ,并发送应用信息、所求服务信
息至注册中心
 注册中心根据 消费 者所求服务信息匹配对应的提供者列表发送至
Consumer 应用缓存。
 Consumer 在发起远程调用时基于缓存的消费者列表择其一发起调
用。
 Provider 状态变更会实时通知注册中心、在由注册中心实时推送至
Consumer

设计的原因:
 Consumer 与 Provider 解偶,双方都可以横向增减节点数。
 注册中心对本身可做对等集群,可动态增减节点,并且任意一台宕掉
后,将自动切换到另一台
 去中心化,双方不直接依懒注册中心,即使注册中心全部宕机短时间
内也不会影响服务的调用
 服务提供者无状态,任意一台宕掉后,不影响使用

Dubbo 的服务调用流程?

在这里插入图片描述

Dubbo 有些哪些注册中心?

一般Dubbo推荐使用Zookeeper作为注册中心(zookeeper后面有详解)
nginx和dubbo微服务架构图:适用于人手不足
在这里插入图片描述

dubbo 服务负载均衡策略?

l Random LoadBalance
随机,按权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比
较均匀,有利于动态调整提供者权重。(权重可以在 dubbo 管控台配置)
l RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调
到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
l LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的
调用前后计数差会越大。
l ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节
点,平摊到其它提供者,不会引起剧烈变动。缺省只对第一个参数 Hash,如果要修改,请配置

Dubbo 的注册中心集群挂掉,发布者和订阅者之间还能通信么?

可以的,启动 dubbo 时,消费者会从 zookeeper 拉取注册的生产者
的地址接口等数据,缓存在本地。
每次调用时,按照本地存储的地址进行调用
注册中心对等集群,任意一台宕机后,将会切换到另一台;注册中心全部宕机后,服务的提供者和消费者仍能通过本
地缓存通讯。服务提供者无状态,任一台 宕机后,不影响使用;服务提供者全部宕机,服务消费者会无法使用,并无
限次重连等待服务者恢复;
挂掉是不要紧的,但前提是你没有增加新的服务,如果你要调用新的服务,则是不能办到的。

Dubbo 的集群容错方案有哪些?

默认Failover Cluster
Failover Cluster
 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但
重试会带来更长延迟。
Failfast Cluster
 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写
操作,比如新增记录。
Failsafe Cluster
 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操
作。
Forking Cluster
 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较
高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最
大并行数。
Broadcast Cluster
 广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通
知所有提供者更新缓存或日志等本地资源信息。

Dubbo 在安全机制方面是如何解决?

Dubbo 通过 Token 令牌防止用户绕过注册中心直连,然后在注册中
心上管理授权。Dubbo 还提供服务黑白名单,来控制服务所允许的调
用方。


ZooKeeper

ZooKeeper 是一个 分布式 的,开放源码的分布式 应用程序协调服务
①Zookeeper 可以被用作注册中心。 ②Zookeeper 是 Hadoop 生态系统的一员;③构建 Zookeeper 集群的时候,使用的服务器最好是奇数台。”
ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Zookeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心(主要是作为dubbo的注册中心)
主要提供:1、集群管理:容错、负载均衡。2、配置文件的集中管理3、集群的入口。功能
在这里插入图片描述

zookeeper 的使用场景

分布式协调 分布式锁 元数据/配置信息管理 HA高可用性

分布式协调
这个其实是 zookeeper 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知,完美解决。

在这里插入图片描述

分布式锁
举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
在这里插入图片描述
有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是 保持独占 ,另一个是 控
制时序 。
对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁 ,通过 createznode 的方式来实现。所有客户
端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的
distribute_lock 节点就释放出锁。
对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选
master 一样, 编号最小的获得锁 ,用完删除,依次方便。

获取分布式锁的流程
在这里插入图片描述
在获取分布式锁的时候在 locker 节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用
createNode 方法在 locker 下创建临时顺序节点,
然后调用 getChildren(“locker”)来获取 locker 下面的所有子节点,注意此时不用设置任何 Watcher。客户
端获取到所有的子节点 path 之后,如果发现自己创建的节点在所有创建的子节点序号最小,那么就认为该客户
端获取到了锁。如果发现自己创建的节点并非 locker 所有子节点中最小的,说明自己还没有获取到锁,此时客
户端需要找到 比自己小的那个节点 ,然后对其调用 exist() 方法,同时对其注册事件监听器。之后,让这个被关
注的节点删除,则客户端的 Watcher 会收到相应通知,此时再次判断自己创建的节点是否是 locker 子节点中
序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。当
前这个过程中还需要许多的逻辑判断。
在这里插入图片描述

元数据/配置信息管理
ookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么?

在这里插入图片描述
HA高可用性
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。
在这里插入图片描述

zookeeper watch 机制

Watch 机制官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch 的数据发生了改变的时
候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。
Zookeeper 机制的特点:
1、一次性触发数据发生改变时,一个 watcher event 会被发送到 client,但是 client 只会收到一次这样的信
息 。
2、watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是 异步 的,这就存在一个问题,不同
的客户端和服务器之间通过 socket 进行通信,由于 网络延迟或其他因素导致客户端在不通的时刻监听到事件 ,
由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode 发生了
变化 。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,
而无法保证强一致性 。
3、数据监视 Zookeeper 有数据监视和子数据监视 getdata() and exists()设置数据监视,getchildren()设置了
子节点监视。
4、注册 watcher getData、exists、getChildren
5、触发 watcher create、delete、setData
6、 setData() 会触发 znode 上设置的 data watch(如果 set 成功的话)。一个成功的 create() 操作会触发被
创建的 znode 上的数据 watch,以及其父节点上的 child watch。而一个成功的 delete() 操作将会同时触发一
个 znode 的 data watch 和 child watch(因为这样就没有子节点了),同时也会触发其父节点的 child
watch。
7、当一个客户端 连接到一个新的服务器上 时,watch 将会被以任意会话事件触发。当 与一个服务器失去连接 的
时候,是无法接收到 watch 的。而当 client 重新连接 时,如果需要的话,所有先前注册过的 watch,都会被重
新注册。通常这是完全透明的。只有在一个特殊情况下, watch 可能会丢失 :对于一个未创建的 znode 的
exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这
个 watch 事件可能会被丢失。
8、Watch 是轻量级的,其实就是本地 JVM 的 Callback ,服务器端只是存了是否有设置了 Watcher 的布尔类型

为什么最好使用奇数台服务器构成 ZooKeeper 集群?

我们知道在Zookeeper中 Leader 选举算法采用了Zab协议。Zab核心思想是当多数 Server 写成功,则任务数据写成功。
①如果有3个Server,则最多允许1个Server 挂掉。
②如果有4个Server,则同样最多允许1个Server挂掉。
既然3个或者4个Server,同样最多允许1个Server挂掉,那么它们的可靠性是一样的,所以选择奇数个ZooKeeper Server即可,这里选择3个Server。12341234

ZooKeeper的重要概念

● ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
● 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
● ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。
● ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
● ZooKeeper有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。
● ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提交数据节点监听服务。

ZooKeeper 特点

● 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
● 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
● 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
● 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。

ZooKeeper 集群角色介绍

Zookeeper 的核心是 原子广播 ,这个机制保证了 各个 Server 之间的同步 。实现这个机制的协议叫做 Zab 协
议 。Zab 协议有两种模式,它们分别是 恢复模式(选主) 和 广播模式(同步) 。当服务启动或者在领导者崩溃
后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 Server 完成了和 leader 的状态同步以后,恢复
模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态

在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色。如下图所示
在这里插入图片描述
ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
在这里插入图片描述


MQ

什么是消息队列中间件
消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等等功能,其作为分布式系统架构中的一个重要组件,有着举足轻重的地位。
在这里插入图片描述

ActiveMQ

Kafka

资料消息服务第二卷有Kafka高可用

RocketMQ

RabbitMQ

https://www.cnblogs.com/panchanggui/p/10332876.html

RabbitMQ的5种模式与实例
1.1 简单模式Hello World
在这里插入图片描述
功能:一个生产者P发送消息到队列Q,一个消费者C接收

生产者实现思路:

创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,使用通道channel向队列中发送消息,关闭通道和连接。
在这里插入图片描述
消费者实现思路

创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue, 创建消费者并监听队列,从队列中读取消息。

在这里插入图片描述

1.2 工作队列模式Work Queue
在这里插入图片描述
功能:一个生产者,多个消费者,每个消费者获取到的消息唯一,多个消费者只有一个队列

任务队列:避免立即做一个资源密集型任务,必须等待它完成,而是把这个任务安排到稍后再做。我们将任务封装为消息并将其发送给队列。后台运行的工作进程将弹出任务并最终执行作业。当有多个worker同时运行时,任务将在它们之间共享。
生产者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,使用通道channel向队列中发送消息,2条消息之间间隔一定时间,关闭通道和连接

在这里插入图片描述

消费者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,创建消费者C1并监听队列,获取消息并暂停10ms,另外一个消费者C2暂停1000ms,由于消费者C1消费速度快,所以C1可以执行更多的任务。
在这里插入图片描述
1.3 发布/订阅模式Publish/Subscribe
在这里插入图片描述
功能:一个生产者发送的消息会被多个消费者获取。一个生产者、一个交换机、多个队列、多个消费者
生产者:可以将消息发送到队列或者是交换机。
消费者:只能从队列中获取消息。
如果消息发送到没有队列绑定的交换机上,那么消息将丢失。
交换机不能存储消息,消息存储在队列中

生产者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,使用通道channel创建交换机并指定交换机类型为fanout,使用通道向交换机发送消息,关闭通道和连接。
在这里插入图片描述
消费者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,绑定队列到交换机,设置Qos=1,创建消费者并监听队列,使用手动方式返回完成。可以有多个队列绑定到交换机,多个消费者进行监听。
在这里插入图片描述
1.4 路由模式Routing
在这里插入图片描述
说明:生产者发送消息到交换机并且要指定路由key,消费者将队列绑定到交换机时需要指定路由key

生产者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,使用通道channel创建交换机并指定交换机类型为direct,使用通道向交换机发送消息并指定key=b,关闭通道和连接。
在这里插入图片描述
消费者实现思路:

创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,绑定队列到交换机,设置Qos=1,创建消费者并监听队列,使用手动方式返回完成。可以有多个队列绑定到交换机,但只要绑定key=b的队列key接收到消息,多个消费者进行监听。
在这里插入图片描述
1.5 通配符模式Topics
在这里插入图片描述
说明:生产者P发送消息到交换机X,type=topic,交换机根据绑定队列的routing key的值进行通配符匹配;符号#:匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor

符号*:只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor

生产者实现思路:

创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,使用通道channel创建交换机并指定交换机类型为topic,使用通道向交换机发送消息并指定key=key.1,关闭通道和连接。
在这里插入图片描述
消费者实现思路:
创建连接工厂ConnectionFactory,设置服务地址127.0.0.1,端口号5672,设置用户名、密码、virtual host,从连接工厂中获取连接connection,使用连接创建通道channel,使用通道channel创建队列queue,绑定队列到交换机,设置Qos=1,创建消费者并监听队列,使用手动方式返回完成。可以有多个队列绑定到交换机,凡是绑定规则符合通配符规则的队列均可以接收到消息,比如key.*,key.#,多个消费者进行监听。

在这里插入图片描述

RabbitMQ之消息确认回执和拒绝(Point)

https://blog.csdn.net/weixin_43430036/article/details/85221619
(保证消息的可靠性传输)
消费消息确认
从安全角度考虑,网络是不可靠的,消费者是有可能在处理消息的时候失败。而我们总是希望我们的消息不能因为处理失败而丢失,基于此原因,rabbitmq提供了一个消息确认(message acknowledgements) 的概念:当一个消息从队列中投递给消费者(consumer)后,消费者会通知一下消息中间件(rabbitmq),这个可以是系统自动autoACK的也可以由处理消息的应用操作。

当 “消息确认” 被启用的时候,rabbitmq不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。

为了解决这个问题,rabbitmq提供了2种处理模式来解决这个问题:

  • 自动确认模式(automatic acknowledgement model):当RabbbitMQ将消息发送给应用后,消费者端自动回送一个确认消息。(使用AMQP方法:basic.deliver或basic.get-ok)。

  • 显式确认模式(explicit acknowledgement model):RabbbitMQ不会完全将消息从队列中删除,直到消费者发送一个确认回执(acknowledgement)后再删除消息。(使用AMQP方法:basic.ack)。

显式确认模式
在显式确认模式下,消费者可以自由选择什么时候发送确认回执(acknowledgement)。消费者可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执。
如果一个消费者在尚未发送确认回执的情况下挂掉了,那rabbitmq会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。
消费者在获取队列消息时,可以指定autoAck参数,采用显式确认模式,需要指定autoAck = flase,在显式确认模式,RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。如果断开连接,RabbitMQ也没有收到ACK,则Rabbit MQ会安排该消息重新进入队列,等待投递给下一个消费者。
在显式确认模式,确认回执的案例如下:

//设置非自动回执
boolean autoAck = false;
Channel finalChannel = channel;
try {
    channel.basicConsume("test", autoAck, "test-consumer-tag",
        new DefaultConsumer(finalChannel) {
            @Override
            public void handleDelivery(String consumerTag,
                           Envelope envelope,
                           AMQP.BasicProperties properties,
                           byte[] body)
            throws IOException
            {
	            //发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的 
                long deliveryTag = envelope.getDeliveryTag();
                //第二个参数是批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
                finalChannel.basicAck(deliveryTag, true);
            }
       });
} catch (IOException e) {
     e.printStackTrace();
}

上面我们显式的成功回执了我们的消息,但是假如我们发现我们的消费者处理不了这个消息需要其他的消费者处理怎么办呢,我们还可以拒绝消息
确认消息API

	 /**
     * Acknowledge one or several received
     * messages.
     * @param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
     * @param multiple 批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
     * @throws java.io.IOException if an error is encountered
     */
    void basicAck(long deliveryTag, boolean multiple) throws IOException;

拒绝消息API

    /**
     * Reject a message. 
     * @param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
     * @param requeue 是否重回队列 如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
     * @throws java.io.IOException if an error is encountered
     */
    void basicReject(long deliveryTag, boolean requeue) throws IOException;

我们似乎只能拒绝一条消息,后面rabbitMQ又补充了basicNack一次对多条消息进行拒绝

	/**
     * Reject one or several received messages.
     * @param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
     * @param multiple 批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行拒绝; 如果值为false,则只对当前收到的消息进行拒绝
     * @param requeue 是否重回队列 如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
     * @throws java.io.IOException if an error is encountered
     */
    void basicNack(long deliveryTag, boolean multiple, boolean requeue)
            throws IOException;

这里我们需要注意一下,如果我们的队列目前只有一个消费者,请注意不要拒绝消息并放回队列导致消息在同一个消费者身上无限循环无法消费的情况发生。

发送消息确认
我们除了要考虑消费者消息失败可能失败的情况,我们还需要考虑,消息的发布者在将消息发送出去之后,消息到底有没有正确到达消息中间件呢,如果没到达,我们又需要怎么处理呢?

RabbitMQ为我们提供了两种方式来解决这个问题:

  • 事务:通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案。(性能太差,基本不用
  • confirm:通过将channel设置成confirm模式来实现;
    事务
    RabbitMQ事物的处理包装channel调用代码为:
Tx.SelectOk txSelect() throws IOException;
Tx.CommitOk txCommit() throws IOException;
Tx.RollbackOk txRollback() throws IOException;

txSelect主要用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务。
只要我们使用过事物的同学都知道,事物没有什么好解释的,我这边就给大家一个案例,不了解事物的同学也不要着急,因为它性能太差,官方主动弃用,所以我们不了解也无所谓,重点关注第二种:

try {
    // 开启事务
    channel.txSelect();
    // 往test队列中发出一条消息
    channel.basicPublish("test", "test", null, messageBodyBytes);
    // 提交事务
    channel.txCommit();
} catch (Exception e) {
    e.printStackTrace();
    // 事务回滚
    try {
        channel.txRollback();
    } catch (IOException e1) {
        e1.printStackTrace();
    }
}

confirm
下面,我们重点说一下confirm机制:

在confirm机制下,我们可以将channel设置成confirm模式,一旦channel进入confirm模式,所有在该channel上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理;

confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;

confirm机制和transaction事务模式是不能够共存的,已经处于transaction事务模式的channel不能被设置为confirm模式,同理,反过来也一样。通常我们可以通过调用channel的confirmSelect方法将channel设置为confirm模式。如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意生产者当前channel信道设置为confirm模式。

SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
try {
	//设置为confirm模式
    channel.confirmSelect();
} catch (IOException e) {
    e.printStackTrace();
}
//设置监听
channel.addConfirmListener(new ConfirmListener() {

    //处理消息成功回执
   @Override
   public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Ack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
        if (multiple) {
            confirmSet.headSet(deliveryTag + 1).clear();
        }else {
            confirmSet.remove(deliveryTag);
        }
    }

     //处理失败回执
    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
        if (multiple) {
            confirmSet.headSet(deliveryTag + 1).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
    }

});

for(int i =0;i<100;i++) {
    // 查看下一个要发送的消息的序号
    long nextSeqNo = channel.getNextPublishSeqNo();
    try {
        channel.basicPublish("test", "test", MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes);
    } catch (IOException e) {
        e.printStackTrace();
    }
    confirmSet.add(nextSeqNo);
}

RabbitMQ的高可用性

Rabbit是基于主从(非分布式)做高可用性的,RabbitMQ有三种模式:单机模式 普通集群模式 镜像集群模式

1.单机模式
单机模式就是自己玩耍使用的,没人生产就使用单机模式
2.普通集群模式
普通集群模式就是在多台计算机上启动多个MQ实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(queue的配置信息,用来找到queu实例),消费的时候,若连接到另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。不能实现高可用性

3.镜像集群模式

惊醒集群模式即是RabbitMQ高可用模式,
跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
在这里插入图片描述
保证MQ的消息的可靠性传输 就是上文的消息确认

RabbitMQ处理数据丢失问题
设置持久化有两个步骤:
创建 queue 的时候将其设置为持久化
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
第二个是发送消息的时候将消息的 deliveryMode 设置为 2
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。
持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,这样即使MQ挂了,生产者收不到ack,就会重发一次.
消费端弄丢了数据
消费者刚消费到,还没处理,结果进程挂了.关闭自动ack,在代码执行完毕在在程序里返回ack.
在这里插入图片描述


reids缓存

redis介绍

redis 是单线程工作模型 redis快速的原因

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞 I/O 多路复用机制

I/O 多路复用机制(网上栗子)
打一个比方:小曲在 S 城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。
经营方式一
客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。
慢慢的小曲就发现了这种经营方式存在下述问题:
几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。
随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了。
快递员之间的协调很花时间。
综合上述缺点,小曲痛定思痛,提出了下面的经营方式。
经营方式二
小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。
最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。
上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢?
在上述比喻中:
每个快递员→每个线程
每个快递→每个 Socket(I/O 流)
快递的送达地点→Socket 的不同状态
客户送快递请求→来自客户端的请求
小曲的经营方式→服务端运行的代码
一辆车→CPU 的核数
于是我们有如下结论:
经营方式一就是传统的并发模型,每个 I/O 流(快递)都有一个新的线程(快递员)管理。
经营方式二就是 I/O 多路复用。只有单个线程(一个快递员),通过跟踪每个 I/O 流的状态(每个快递的送达地点),来管理多个 I/O 流。
下面类比到真实的 Redis 线程模型,如图所示:
在这里插入图片描述
简单来说,就是我们的 redis-client 在操作的时候,会产生具有不同事件类型的 Socket。
在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
需要说明的是,这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库,大家可以自行去了解。

Redis 的数据类型,以及每种数据类型的使用场景
String
这个没啥好说的,最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。
Hash
这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。
我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。
List
使用 List 的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。
Set
因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用 JVM 自带的 Set 进行去重?
因为我们的系统一般都是集群部署,使用 JVM 自带的 Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
Sorted Set
Sorted Set多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。
可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。最后一个应用就是可以做范围查找。

Redis 的过期策略以及内存淘汰机制

            - Redis 采用的是定期删除+惰性删除策略。
  • 定期删除+惰性删除是如何工作?以及这种策略的弊端

定期删除,Redis 默认每个 100ms 检查,是否有过期的 Key,有过期 Key 则删除。
需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查(如果每隔 100ms,全部 Key 进行检查,Redis 岂不是卡死)。
因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。
也就是说在你获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

Question:如果定期删除没删除 Key。然后你也没即时去请求 Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高。那么就应该采用内存淘汰机制。
在 redis.conf 中有一行配置: # maxmemory-policy volatile-lru
淘汰策略(一般使用lru算法)

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用
    Key,去随机删。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把
    Redis 既当缓存,又做持久化存储的时候才用。不推荐。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。

PS:如果没有设置 expire 的 Key,不满足先决条件(prerequisites);那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。

Redis 有哪些适合的场景

(1)会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(sessioncache),用 Redis 缓存会话比其他存储(如
Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的
购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为
人知的商业平台 Magento 也提供 Redis 的插件。
(2)全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实
例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地
FPC。
再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。
此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快
速度加载你曾浏览过的页面。
(3)队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列
平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop
操作。
如果你快速的在 Google 中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的
就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用
Redis 作为 broker,你可以从这里去查看。
(4)排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(SortedSet)也使
得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。
所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“user_scores”,我们只需要像
下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执
行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可
以在这里看到。
(5)发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见
人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建
立聊天系统!

redis 的持久化

RDB(Redis DataBase:在不同的时间点将 redis 的数据生成的快照同步到磁盘等介质上):内存
到硬盘的快照,定期更新。缺点:耗时,耗性能(fork+io 操作),易丢失数据。
AOF(Append Only File:将 redis 所执行过的所有指令都记录下来,在下次 redis 重启时,只
需要执行指令就可以了):写日志。缺点:体积大,恢复速度慢。

缓存雪崩问题

什么是缓存雪崩?
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

  • 使用互斥锁,但是该方案吞吐量明显下降了
    一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:
    在这里插入图片描述
    加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

  • 给缓存的失效时间,加上一个随机值,避免集体失效。

  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下
    在这里插入图片描述
    记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;它的过期时间比缓存标记的时间延长1倍

  • 事发前:实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),尽量避免Redis挂掉这种情况发生。

  • 事发中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)

  • 事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

缓存击穿问题

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存
这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

在这里插入图片描述
这就是缓存穿透:请求的数据在缓存大量不命中,导致请求走数据库。
缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!

解决缓存穿透
解决缓存穿透也有两种方案:
由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截 [将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据
会被这个 bitmap 拦截掉,从而避免了对 DB 的查询]
,不合法就不让这个请求到数据库层!
当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。
这种情况我们一般会将空对象设置一个较短的过期时间,过期时间会很短,最长不超过五分钟。

缓存和数据库双写一致性问题

一致性问题还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。
这问题的前提是,如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。


  • Cache Aside Pattern保证缓存与数据库的双写一致性

1.读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

2.更新的时候,先删除缓存,然后再更新数据库

使用过 Redis 做异步队列么,你是怎么用的?有什么缺点?

一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep
一会再重试。
缺点:
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq 等。
能不能生产一次消费多次呢?
使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。

使用 redis 如何设计分布式锁?

redis:
1.线程 A setnx(上锁的对象,超时时的时间戳 t1),如果返回 true,获得锁。
2.线程 B 用 get 获取 t1,与当前时间戳比较,判断是是否超时,没超时 false,若超时执行第 3 步;
3.计算新的超时时间 t2,使用 getset 命令返回 t3(该值可能其他线程已经修改过),如果
t1==t3,获得锁,如果 t1!=t3 说明锁被其他线程获取了。
4.获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,
不用处理(防止删除其他线程的锁)。


MySql

关键字的执行顺序

【一】数据分组(group by ):
1.group by 和where

select 列a,聚合函数(聚合函数规范) from 表明 where 过滤条件 group by 列a

2.group by 和having

select 列a,聚合函数(聚合函数规范) from 表明 group by 列a having 过滤条件 ;

3.group by 和order by

select 列a,聚合函数(聚合函数规范) from 表明 group by 列a order by 条件;

【二】使用having字句对分组后的结果进行筛选,语法和where差不多:having 条件表达式

需要注意having和where的用法区别:
1.having只能用在group by之后,对分组后的结果进行筛选(即使用having的前提条件是分组)。
2.where肯定在group by 之前,即也在having之前。
3.where后的条件表达式里不允许使用聚合函数,而having可以。
4.having后只能跟group by后边字段条件 或者 非group by字段的聚合函数条件(按组查询);

【三】当一个查询语句同时出现了where,group by,having,order by的时候,执行顺序:

Group By 和 Having, Where ,Order by这些关键字是按照如下顺序进行执行的:Where, Group By, Having, Order by。
SELECT的语法顺序就是起执行顺序

FROM
WHERE (先过滤单表/视图/结果集,再JOIN)
GROUP BY
HAVING (WHERE过滤的是行,HAVING过滤的是组,所以在GROUP之后)
ORDER BY


Git

Git 的分支管理

https://www.jianshu.com/p/c9d1d43f17ac\

https://www.cnblogs.com/spec-dog/p/11043371.html

创建项目时(一般是服务型项目,工具型或辅助型项目可以简单一些),会针对不同环境创建三个常设分支:

  1. develop:开发环境的稳定分支,公共开发环境基于该分支构建。
    在这里插入图片描述
  2. pre-release:测试环境的稳定分支,测试环境基于该分支构建

一旦develop分支上的功能到了发布日期,就从develop分支fork一个发布分支(一般叫release)。release分支用于预发布测试,所以从这个时间点开始后新的功能不再加到这个分支上,release分支只应该做bug修复、文档生成和其他面向发布的任务。一旦release测试完毕并准备发布后,将其合并到master分支并分配一个版本号打上Tag。另外,release上做的bug修改要合并回develop分支。
使用一个专门用于发布的分支,是一个团队可以在完善发布版本的同时,另一个团队继续开发下一个版本功能。
在这里插入图片描述
3. master:生产环境的稳定分支,生产环境基于该分支构建。仅用来发布新版本,除了从pre-release或生产环境Bug修复分支进行merge,不接受任何其它修改
4. 功能(feature)分支:为了开发某个特定功能,从develop分支上面分出来的。开发完成后,要merge到develop分支。功能分支的命名,可以采用feature-*的形式命名(*为任务单号). 每个新功能位于一个自己的分支,这样可以push到中央仓库以备份和协作。但功能分支不是从master分支上拉出来的新分支,而是使用develop分支作为父分支。当新功能完成时,合并回develop分支。新功能的提交不直接与master交互。

在这里插入图片描述
5. Bug修复(fixbug)分支:为了修复某个bug,从常设分支上面分出来的。修复完成后,再merge到对应的分支。Bug修复分支的命名,可以采用fixbug-*的形式命名(*为bug单号)

维护分支(一般叫hotfix)是唯一可以直接从master分支fork出来的分支,用于线上版本bug修复。修复完成后,应马上合并回master和develop分支,同时master分支用新版本号打上Tag。

在这里插入图片描述

完整执行流程

正常开发流程

  1. 从develop分支切出一个新分支,根据是功能还是bug,命名为feature-* 或 fixbug-*。

  2. 开发者完成开发,提交分支到远程仓库。

  3. 开发者发起merge请求(可在gitlab页面“New merge request”),将新分支请求merge到develop分支,并提醒code reviewer进行review

  4. code reviewer对代码review之后,若无问题,则接受merge请求,新分支merge到develop分支,同时可删除新建分支;若有问题,则不能进行merge,可close该请求,同时通知开发者在新分支上进行相应调整。调整完后提交代码重复review流程。

  5. 转测时,直接从当前develop分支merge到pre-release分支,重新构建测试环境完成转测。

  6. 测试完成后,从pre-release分支merge到master分支,基于master分支构建生产环境完成上线。并对master分支打tag,tag名可为v1.0.0_2019032115(即版本号_上线时间)

在这里插入图片描述

并行开发测试环境Bug修复流程

并行开发(即前一个版本已经转测但未上线,后一个版本又已在开发中并部分合并到了develop分支)过程中,转测后测试环境发现的bug需要修复,但是develop分支此时又有新内容且该部分内容目前不计划转测,可以pre-release切出一个bug修复分支。完成之后需要同时merge到pre-release分支与develop分支。merge时参考“正常开发流程”。流程示意图如下

在这里插入图片描述
生产环境Bug修复流程

生产环境的Bug分两种情况:

紧急Bug:严重影响用户使用的为紧急Bug,需立即进行修复。如关键业务流程存在问题,影响用户正常的业务行为。
非紧急Bug或优化:非关键业务流程问题,仅影响用户使用体验,或出现频率较小等,为非紧急Bug,可规划到后续版本进行修复。
非紧急Bug修复参考“正常开发流程”。

紧急Bug修复,需要从master分支切出一个bug修复分支,完成之后需要同时merge到master分支与develop分支(如果需要测试介入验证,则可先merge到pre-release分支,验证通过后再merge到master分支上线)。merge时参考“正常开发流程”。流程示意图如下

在这里插入图片描述


散装知识点

HashMap以及HashTable https://blog.csdn.net/qq_41081796/article/details/104603347


SSO单点登录

在这里插入图片描述

单点登录分为两种情况 一种是自己系统进行登录另一种是第三方系统进行登录


购物车实现(有待完善)


积分系统的设计思路(有待完善)

用户通过购买商品,以及晒单评论获取积分所以需要一张积分表

积分表:

id(自增id主键)
user_id(用户id)
credit(积分)

用户使用积分兑换礼品,所以需要一张表里来记录兑换使用的积分以及兑换的商品和数量,(简化了表中字段)

积分兑换表:
id(自增id主键)
user_id(用户id)
exchanged_credit(用于兑换的积分)
product_id(兑换的商品id)

用户兑换礼品后需要调用仓储系统新增一条发货申请(这里需要调用第三方接口进行申请一个物流单号),因为需要给用户提供订单消息,物流单号(express_no字段)

发货申请表:
id(自增id主键)
type(发货类型,1:购买,2:积分兑换)
credit_exchange_id(积分兑换表的id)
product_id(要发货的商品id)
express_no(物流单号)

初步设计流程图:
在这里插入图片描述

问题提出与解决:
1.扣减积分、新增积分兑换记录、新增发货申请单,这三个步骤必须是要么一起完成,要么一起失败的。也就是说,这三个步骤必须是在一个事务里的。所以使用消息中间键RabbitMQ
解决问题:用户兑换礼品时,调用第三方物流公司的接口的时候卡住了,会导致用户界面十几秒都无法操作(现在用户点击兑换立即可以有完成操作,保证了用户体验)
在这里插入图片描述
2.若仓储服务执行新增发货申请失败怎么办?
解决问题:(重试机制的引入)引入可靠消息服务,保证仓储服务一定会完成新增发货申请
程序逻辑流程
积分服务发送消息给可靠消息服务可靠消息服务在消息表中新增记录,然后发送消息到MQ(消息中间件)
然后仓储服务消费消息新增发货申请单,如果成功就回调可靠消息服务的一个接口说自己成功了,可靠消息服务就可以更新本地消息表中的记录状态为成功
如果仓储服务长时间没通知可靠消息服务自己成功了,可靠消息服务不停的重试再次发送消息
在这里插入图片描述
3.仓储服务卡住在第三方物流系统申请物流单环节,长时间堵塞,没有回调通知给可靠消息服务,一段时间可靠消息服务进行重试,会导致仓储服务中新增了两条发货申请 为了保证新增发货申请的幂等性需要在发货申请表中添加一个字段

发货申请表:
id(自增id主键)
type(发货类型,1:购买,2:积分兑换)
credit_exchange_id(积分兑换表的id)
product_id(要发货的商品id)
express_no(物流单号)

只要在“credit_exchange_id”字段上建立一个唯一索引就可以了,保证每个积分兑换记录只能创建一条发货申请单,如果重复创建就会被唯一索引被阻止,这样就可以保证这个行为的幂等性了。


前端知识点

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值