Java高级工程师复习

  • Post author:
  • Post category:java




第一章 Java基础

1、数据类型

数值类型:整数(byte,short,int,long)、浮点数(float,double)、字符(char)

非数值类型:布尔(boolean)

引用数据类型:类(class)、接口(interface)、数组[]

2、面向对象

三大特性:继承、封装、多态

关键字的作用:

final

当用final修饰一个类时,表明这个类不能被继承。String类就是一个final类

修饰方法,不能被修改。

修饰变量,不可变。

static

1.修饰方法,称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,在静态方法中不能访问类的非静态成员变量和非静态成员方法,但是在非静态成员方法中是可以访问静态成员方法/变量的。也不能被重写

2.修饰变量,称作静态变量,静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

3.修饰代码块,用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候执行,并且只会执行一次。

4.静态导包就是Java包的静态导入,用import static代替import静态导入包是JDK1.5中的新特性。可读性差

volatile

对其他线程的可见性(采用“内存屏障”来实现),当我们使用volatile关键字去修饰变量的时候,所以线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。

3、集合框架

collection集合接口

collection是最基本的集合接口,主要的子接口有Set和List接口。注意Map不是collection的子接口。

List接口

List接口继承collection接口,是有序的,允许重复

List接口的几个实现类:LinkedList、ArrayList和Vector、Stack类。

LinkedList类

LinkedList实现了List接口,允许null元素。可以理解LinkedList就是一种双向循环链表的链式线性表,只不过存储的结构使用的是链式表而已。

LinkedList的链式线性表的特点:适合于在链表中间需要频繁进行插入和删除操作。

LinkedList的链式线性表的缺点:随机访问速度较慢。查找一个元素需要从头开始一个一个的找。

ArrayList类

ArrayList也实现了List接口,允许所有元素,包括null。可以理解ArrayList就是基于数组的一个线性表,只不过数组的长度可以动态改变而已。

ArrayList数组线性表的特点:类似数组的形式进行存储,因此它的随机访问速度极快。

ArrayList数组线性表的缺点:不适合于在线性表中间需要频繁进行插入个删除操作。因为每次插入和删除都需要移动数组中的元素。

LinkedList和ArrayList都不是线程安全的,如果多线程同时访问一个List,则必须自己实现访问同步,认为解决的办法有两个:

一、同步的方法上面加上同步关键字synchronized

二、在创建List时创建一个同步的List:List list = Collection.synchronizedList( new LinkedList( ) );

Vector类

Vector非常类似ArrayList,但是Vector是同步的。如果一定要使用多线程,Vector是ArrayList的多线程的一个替代品,描述的是一个线程安全的ArrayList。。

Stack类

Stack继承自Vector,实现一个后进先出的堆栈。Stack提供了5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

用LinkedList构造堆栈stack、队列queue。

Set接口

Set接口继承了Collection接口,与Collection接口完全一样,继承了Collection的所有方法。Set是无序的,不允许有重复的元素。

但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。

Set接口的几个实现类:HashSet、LinkedHashSet和TreeSet 类

HashSet类

HashSet继承Set接口,HashSet不允许重复元素,允许放null元素,但最多允许存放一个null元素。元素的位置是无序的,但是底层基于hash算法实现使用了HashCode,元素的位置是固定的。

LinkedHashSet类

LinkedHashSet继承Set接口,也是HashSet接口的一个子接口,底层也是基于HashSet实现的,LinkedHashSet和HashSet的主要区别在于LinkedHashSet中的存储的元素是在哈希算法的基础上增加了链式表的结构。

TreeSet类

TreeSet是一种排序二叉树。存入Set集合中的值,会按照值的大小进行相关的排序操作。底层的算法是基于红黑树来实现的。

TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key,TreeMap需要排序,所以需要一个Comparator为键值进行大小比较,也是用Comparator定位的,Comparator可以在创建TreeMap时指定,如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口。TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位。

Map接口

Map接口实现的是一组Key-Value的键值对的组合,不继承Cllection接口。

Map接口的几个实现类:HashMap、Hashtable、LinkedHashMap和TreeMap类

HashMap类

HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。

HashMap基于hash数组实现,若key的hash值相同则使用链表方式进行保存。

HashMap是非线程安全的,可以使用外部同步方法解决这个问题。

HashMap可以允许你在列表中放一个key值为null的元素,并且可以有任意多value为null

解决哈希冲突:jdk1.7,数组+链表;jdk1.8,数组+链表+红黑树,当数组长度大于64,链表长度大于8时,则将链表转为红黑树。

二叉树更加严格的平衡,添加/删除操作时,需要更高的旋转次数才能在修改时正确地重新平衡数据结构,所以红黑树比二叉树效率更高。

Hashtable类

Hashtable实现Map接口,继承自古老的Dictionary类,实现一个Key-value的键值映射表,不允许记录的键或值为null。

Hashtable是线程安全的,即一个时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时比较慢。

LinkedHashMap类

LinkedHashMap继承HashMap并且实现了Map接口,LinkedHashMap允许key和value为null。LinkedHashMap将键值对添加进链接哈希映射中,内部也采用双重链接式列表。和HashMap一样使用到了hash算法,因此不能保证映射的顺序

TreeMap类

TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排序过的。

哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中,在所有的存储结构中,array查找速度是最快的。当发生碰撞时,让array指向多个value。即,数组每个位置上又生成一个链表。

4、泛型

定义:

泛型的本质理解为:泛型是对引用类型变量的抽取(String、integer等),抽取成类型参数。

基本类型是无法抽取的,基本类型无法直接赋值给Object类型,需要用自动拆装箱特性来弥补。

泛型擦除:

Java的泛型是伪泛型,因为Java在编译时擦除了所有的泛型信息。

编译后泛型类型T还是用Object去接收各类数据,T用Object替换,底层就变为了Object或Object[]数组。

并在底层帮我们对引用类型强转,不需要我们在代码里自己强转,如User user =(User)list.get(0);所以泛型在使用的时候类型是确定的。

泛型,可以看做一种“模板代码”。所以代码模板的本质就是:用Object接收一切对象,用泛型+编译器限定特定对象,用多态支持类型强转。

Java编译器编译泛型的步骤:

1.检查泛型的类型 ,获得目标类型

2.擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)

3.调用相关函数,并将结果强制转换为目标类型。

泛型优点:

1、编译器强类型检查,把错误提前到编译器,因为运行期报错比较难找

2、避免手动强制类型转换,如String s = (String )list.get(0)。所以将list定义为List list = new ArrayList()不用自己转换

3、方便实现通用的模板,且更安全、可读性更高

利用反射绕过编译器对泛型的检查:

List list = new ArrayList<>();

list.add(“aaa”);

list.add(“bbb”);

// 编译器会阻止

// list.add(333);

// 但泛型约束只存在于编译期,底层仍是Object,所以运行期可以往List存入任何类型的元素

Method addMethod = list.getClass().getDeclaredMethod(“add”, Object.class);

addMethod.invoke(list, 333);

// 打印输出观察是否成功存入Integer(注意用Object接收)

for (Object obj : list) {


System.out.println(obj);

}

原理:反射是在运行时是才会获取类的对象和调用成员方法,所以绕过了编译期对泛型的检查。

泛型有三种使用方式:泛型类、泛型接口、泛型方法

泛型类:如public class Generic{} T为类型参数,那么在这个类里面就可以使用这个类型

泛型接口:如public interface Generator{} 泛型接口与泛型类的定义基本一致

泛型方法: 泛型方法可以使用泛型类传入的T,如public T getKey();而public T getKey(T t)第二个T是泛型方法传入的T,不是泛型类的T,注意区分。

? — 表示通配符,不确定类型

E — Element,常用在java Collection里,如:List,Iterator,Set

K,V — Key,Value,代表Map的键值对

N — Number,数字

T — Type,类型,如String,Integer等等

泛型上下边界的限制:

通配符上界:如public void tets (List<? extends Number> list),表示Number类型或者Number类型的子类都可以传入,即Number及以下

通配符下界:如public void tets (List<? super Number> list),表示Number类型或者Number类型的父类都可以传入,即Number及以上

泛型常见用途:

集合框架中使用泛型:List list1 = new ArrayList<>();

使用json工具转为具体的对象时参数传递用到了泛型 :UserDto userDto = JSON.parseObject(userStr, UserDto.class);

5、IO流

按照“流”的数据流向,分为:输入流和输出流。

按照“流”中处理数据的单位,分为:字节流和字符流。

在java中,字节是占1个Byte,即8位;而字符是占2个Byte,即16位。

字节流的抽象基类:InputStream,OutputStream

字符流的抽象基类:Reader,Writer

InputStream

InputStream是所有数据字节流的父类,它是一个抽象类。

ByteArrayInputStream、StringBufferInputStream、FileInputStream是三种基本的介质流,它们分别从Byte数组、StringBuffer、和本地文件中读取数据。

ObjectInputStream和所有FileInputStream 的子类都是装饰流(装饰器模式的主角)。

OutputStream

OutputStream是所有输出字节流的父类,它是一个抽象类。

ByteArrayOutputStream、FIleOutputStream是两种基本的介质,它们分别向Byte 数组,和本地文件中写入数据。PipedOutputStream是从与其他线程共用的管道中写入数据。

ObjectOutputStream和所有FileOutputStream的子类都是装饰流。

Reader

Reader是所有的输入字符流的父类,它是一个抽象类。

CharReader、StringReader 是两种基本的介质流,它们分别将Char数组、String中读取数据。

BuffereReader 很明显的是一个装饰器,它和其子类复制装饰其他Reader对象。

InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。

FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream转变为Reader 的方法。

Writer

Writer 是所有输出字符流的父类,它是一个抽象类。

CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。

BuffereWriter 很明显是一个装饰器,他和其子类复制装饰其他Reader对象。

OutputStreamWriter 是OutputStream 到Writer 转换到桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类

IO模型

1、阻塞BIO

2、非阻塞NIO

3、异步AIO,当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。

4、信号驱动IO,信号驱动,好了及时通知

5、IO多路复用,IO多路复用是属于阻塞IO,可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。,所以效率较阻塞IO的高。

BIO和NIO、AIO的区别

BIO是阻塞的,NIO是非阻塞的.

BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写

AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成

使用NIO实现网络通信

非阻塞IO通信图

1、通道(Channel)

Channel是一个对象,可以通过它读取和写入数据。 通常我们都是将数据写入包含一个或者多个字节的缓冲区,

然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。

2、选择器(Selector)

是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。

NIO的选择器允许一个单独的线程同时监视多个通道,可以注册多个通道到同一个选择器上,然后使用一个单独的线程来“选择”已经就绪的通道。这种“选择”机制为一个单独线程管理多个通道提供了可能。

3、缓冲区(Buffer)

Buffer 是一个缓冲数据的对象,它包含一些要写入或者刚读出的数据。

NIO零拷贝

Java NIO中提供的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法。

Netty

Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

Netty 是一个基于NIO的客户、服务器端网络通信框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的Socket服务开发。

Netty 的应用场景有哪些

典型的应用有:阿里分布式服务框架 Dubbo,默认使用 Netty 作为基础通信组件,还有 RocketMQ 也是使用 Netty 作为通讯的基础。

Netty 高性能表现在哪些方面

IO 线程模型:同步非阻塞,用最少的资源做更多的事。

内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。

内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。

串形化处理读写:避免使用锁带来的性能开销。

高性能序列化协议:支持 protobuf 等高性能序列化协议。

Netty 中有那种重要组件

1、Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。

2、EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。

3、ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。

4、ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

5、ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

6、异常处理

异常是Java在编译或运行或者运行过程中出现的错误。

Java异常机制用到的几个关键字:

try用于监听

catch捕获异常

finally总是会被执行

throw用于抛出异常

throws用于声明该方法可能抛出的异常。

异常的根接口Throwable,其下有2个子接口:

Error:指的是JVM错误,这时的程序并没有执行,无法处理。如StackOverflowError堆栈溢出、OutOfMemoryError内存溢出等。

Exception:指的是程序运行中产生的异常,用户可以使用处理格式处理。主要包含IOException和RuntimeException及其子类。

异常的分类

运行时异常(非检查性异常):

是指编译器不要求强制处置的异常,包括RuntimeException及其子类和Error。如数组索引越界ArrayIndexOutOfBoundsException、空指针NullPointerException、除0错误ArithmeticException等。

检查性异常:

在编译时不能被简单地忽略,必须进行异常处理,否则编译时会报错。如输入输出异常IOException,找不到类ClassNotFoundException、文件不存在FileNotFoundException 等。

7、反射机制

Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。

反射获取class对象方式

1、Class.forName(“全类名”):将字节码文件加载进内存,返回class对象。多用于配置文件中,将类名定义在配置文件中,读取文件并加载类。

2、类名.class:通过类名的属性class获取。多用于参数的构造。

3、对象.getClass():该方法定义在Object中,多用于对象的字节码获取。

获取成员变量常用方法

public Field getField(String name):获取指定名称的成员变量(public修饰)。

public Field[] getFields():获取全部成员变量(public修饰)

public Field getDeclaredField(String name):不考虑修饰符

public Field[] getDeclaredFields():不考虑修饰符

获取成员方法常用方法

public Method getMethod(String name,Class<?>… parameterTypes):指定参数(public修饰)

public Method[] getMethods():全部成员方法(public修饰)

public Method getDeclaredMethod(String name,Class<?>… parameterTypes):不考虑修饰符

public Method[] getDeclaredMethods():不考虑修饰符。

获取构造方法常用方法

public Constructor getConstructor(Class<?>… parameterTypes):指定class参数(public修饰)

public Constructor<?>[] getConstructors():全部构造方法(public修饰)

public Constructor getDeclaredConstructor(Class<?>… parameterTypes):不考虑修饰符

public Constructor<?>[] getDeclaredConstructors():不考虑修饰符

通过反射来创建实例:

1、使用Class对象的newInstance()方法来创建Class对象对应类的实例。

Class<?> c = String.class;

Object str = c.newInstance()

2、先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建对象,这种方法可以用指定的构造器构造类的实例。

//获取String的Class对象

Class<?> str = String.class;

//通过Class对象获取指定的Constructor构造器对象

Constructor constructor=c.getConstructor(String.class);

//根据构造器创建实例:

Object obj = constructor.newInstance(“hello reflection”);

框架中的用途:

Spring容器框架IOC实例化对象

自定义注解生效(反射+Aop)

第三方核心的框架 mybatis orm

加载数据库驱动的, Class.forName(“com.mysql.jdbc.Driver”); // 动态加载mysql驱动

8、多线程

线程的几种状态:创建状态、就绪状态、运行状态 、阻塞状态、死亡状态

创建多线程五种方式

1、继承Thread类创建线程:

1.创建一个继承Thread类的子类

2.重写Thread类的run方法

3.创建Thread子类的对象

4.通过此对象调用start方法

2、实现Runnable接口创建线程:

1.实现Runnable接口

2.实现Runnable接口中的run方法

3.创建实现类的对象,将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象Thread t1=new Thread(testThread);

3、实现callable接口和创建Future创建线程:

1.创建一个实现callable接口的实现类

2.实现call方法,将此线程需要执行的操作声明在call方法中

3.创建callable实现类的对象

4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(testThread);

5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象Thread t1 = new Thread(futureTask),

并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)

4、使用线程池用Executor框架:

1.newCachedThreadPool创建一个可缓存线程池,ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();newCachedThreadPool.execute(new tetsThread()); 同样传入实现Runnable接口的实现类

2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数

3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行

4.newSingleThreadExecutor 创建一个单线程化的线程池

5、使用@Async异步注解创建线程:

1.启动类开启异步注解@EnableAsync

2.执行方法添加@Async

3.线程池配置类实现AsyncConfigurer接口并重写getAsyncExcutor方法,并返回一个ThreadPoolTaskExevutor

4.调用异步方法,异步方法不能与被调用的异步方法在同一个类中,否则无效

6、使用lambda表达式创建线程:

1.实现run方法,返回实现类的对象(利用lambda表达式简化某些匿名内部类(Anonymous Classes)的写法)

2.将此对象作为参数传递到Thread类中的构造器中

Runnable task = () -> {


//业务代码

};

task.run();

Thread thread = new Thread(task);

thread.start();

常用方法:

start 启动当前线程,调用当前线程的重写的run方法

run 在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)

sleep 线程休眠,Thread类中的方法(不释放当前锁)

wait 阻塞当前线程,Object类的方法(释放当前锁)

stop 强制结束当前线程

notify 唤醒某个正在等待的线程

notifyAll 唤醒所有正在等等待的线程

yield 放弃当前CPU资源,将他让给其他的任务去使用

isAlive 检查线程是否处于执行状态

join 等待其他线程终止,指主线程等待子线程执行完成之后再结束,对Object提供的wait()做了一层包装而已

interrupt将线程状态置为中断状态true,它不会中断一个正在运行的线程

isInterrupted方法判断是否已经是中断状态,不会清除中断状态

interrupted方法判断是否已经是中断状态,执行后具有清除中断状态功能

setPriority设置线程优先级,线程优先级分为1~10,数值越大,优先级越高

如何优雅的停止一个线程:

1.用 volatile 标记位的停止方法(volatile修饰的标记),利用其可见性,主线程修改了,工作线程通过判断标记位来确定是否继续进行。

如果 sleep、wait等让线程进入阻塞,这种方法是没办法停止的

2.调用线程interrupt方法设置中断状态为true,通过isInterrupted方法判断是否已经是中断状态,是否继续执行

当线程处于sleep、wait等阻塞时,会响应中断抛出InterruptedException,将中断标记位设置成 false(中断信号会被清除),可在异常处理中再调用interrupt设置中断状态,让程序能后续继续进行终止操作

用户线程与守护线程的区别:

守护线程为系统中的其它对象和线程提供服务

如果JVM中所有的线程都是守护线程,那么JVM就会退出,进而守护线程也会退出

如果JVM中还存在用户线程,那么JVM就会一直存活,不会退出

守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程

通过setDaemon(true)将线程设置成守护线程

如何解决线程安全问题(如何保证线程同步):

1.Synchronized解决线程安全问题

synchronized同步代码快

synchronized 修饰实例方法

synchronized 修饰静态方法

2.Lock锁解决线程安全问题

9、并发编程

并发编程三大特性:

原子性

可见性

有序性

synchronized关键字

synchronized修饰代码块,作用的对象是调用这个代码块的对象 (对象锁)

synchronized (this) {


}

synchronized修饰普通方法,作用的对象是调用这个方法的对象 (对象锁)

synchronize修饰静态方法,作用的对象是这个类的所有对象 (类锁)

synchronize修饰类class,作用的对象是这个类的所有对象 (类锁)

synchronized(xxx.class){


}

Synchronized锁

synchronized 用的锁是存在Java对象头里的,那么什么是对象头呢?

存在锁对象的对象头的Mark Word中。

Java 对象头

对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。

其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁。

Synchronized加锁和释放锁的原理

1、synchronized修饰同步代码块有monitorenter和monitorexit指令。

当执行monitorenter指令时,线程试图获取锁也就是获取monitor的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。

相应的在执行monitorexit指令后,monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

2、synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

Synchronized可重入原理

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。即在同一锁程中,线程不需要再次获取同一把锁。

Synchronized保证可见性的原理

内存模型和happens-before规则,如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。

Synchronized与Lock区别

1、Synchronized与Lock都是可重入的互斥锁

2、Synchronized效率低,不够灵活,无法知道是否成功获得锁,自动获取锁与释放锁,JDK6开始自动锁的升级过程

3、Lock(ReenTrantLock)可以中断和设置超时,手动获取锁与释放锁,底层基于aqs锁实现需要自己手动实现锁的升级

锁的升级过程

java中对象锁有4种状态:(级别从低到高)

1.无锁状态

2.偏向锁状态

3.轻量级锁状态

4.重量级锁状态

锁升级的方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

偏向锁

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。即偏向于第一个获得它的线程

轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

重量级锁

如果锁竞争情况严重,某个达到最大自旋次数(默认10次)的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

volatile关键字

1、保证可见性

2、禁止重排序,根据内存屏障实现

3、不保证原子性,比如不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作

使用 volatile 必须具备的条件

对变量的写操作不依赖于当前值。

该变量没有包含在具有其他变量的不变式中。

只有在状态真正独立于程序内其他内容时才能使用 volatile。

ThreadLocal关键字

ThreadLocal即线程局部变量,每个线程都有自己的局部变量来确保线程安全,通过它的get或set方法访问。

ThreadLocal类型的变量一般用private static加以修饰

底层原理

每个线程都维护了一个ThreadLocalMap类型的对象,而set操作其实就是以ThreadLocal变量为key,以我们指定的值为value,最后将这个键值对封装成Entry对象放到该线程的ThreadLocalMap对象中。get()方法就是从当前线程的ThreadLocalMap对象中取出对应的ThreadLocal变量所对应的值。ThreadLocalMap底层是一个数组,数组中元素类型是Entry类型。

常见用途:隐式传递、全局存储用户信息、Spring Aop调用链传递参数

如何防御Threadlocal内存泄漏问题

1.调用remove方法

2.set方法的时候会清除之前 key为null

J.U.C并发包,即java.util.concurrent包,大致划分如下:

1、juc原子类Atomic

CAS的全称为Compare-And-Swap,就是对比交换

原理:更新一个变量的时候,一个旧值(期望操作前的值)、一个新值和内存地址中的值,在操作期间先比较旧值和内存地址当中的实际值是否相同,

如果相同,才交换成新值,发生了变化则不交换。

CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。

CAS的ABA问题

一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

JDK的Atomic包里提供了一个类AtomicStampedReference内部使用Pair(键值对)来存储元素值及其版本号来解决ABA问题。

这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,

则以原子方式将该引用和该标志的值设置为给定的更新值。

Unsafe类主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。

Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。

CAS底层通过Unsafe类实现,提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong

Atomic原子类

基础类型:AtomicBoolean,AtomicInteger,AtomicLong

数组:AtomicIntegerArray,AtomicLongArray,BooleanArray

引用:AtomicReference,AtomicMarkedReference,AtomicStampedReference

Atomic原子类是线程安全的,底层正是用到了CAS机制。

2、juc锁Lock

LockSupport 锁的基础

LockSupport是锁中的基础,是一个提供锁机制的工具类,用来创建锁和其他同步类的基本线程阻塞原语。

当调用LockSupport.park时,表示当前线程将会等待,直至获得许可,当调用LockSupport.unpark时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行。

LockSupport.park不会释放锁资源。Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。

锁核心类AQS

AQS是AbstractQueuedSynchronizer,它提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架,比如ReentrantLock。最核心的就是sync queue(同步队列)的分析。

核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。

如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,

这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS定义两种资源共享方式:

Exclusive(独占)

只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享)

多个线程可同时执行,如Semaphore、CountDownLatCh、 CyclicBarrier、ReentrantReadWriteLock

AQS底层如何实现

1.CAS保证AQS的线程安全问题

2.双向链表存放阻塞的线程

3.LockSupport阻塞和唤醒线程

ReentrantLock

ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

可重入锁ReentrantLock的底层是通过AbstractQueuedSynchronizer(AQS)实现。

ReentrantLock是如何实现公平锁的?

ReentrantLock是如何实现非公平锁的?

ReentrantLock默认实现的是公平还是非公平锁?

ReentrantLock和Synchronized的对比?

3、juc工具类Tools

CountDownLatch(计数器)

当每一个任务完成时,都会在这个锁存器上调用countDown,等待问题被解决的任务调用这个锁存器的await,将他们自己拦住,直至锁存器计数结束。

它底层是AQS是通过内部类Sync(同步)来实现的。

CyclicBarrier(可重复的屏障)

它的功能是让一组线程达到一个屏障或者公共点时被阻塞,直到最后一个线程也到达屏障时,才会打开屏障,所有被屏障拦截的线程才会继续运行。

CyclicBarrier底层是基于ReentrantLock和AbstractQueuedSynchronizer来实现的

CyclicBarrier也能实现countDownLatch的功能,并且它的计数器n是可以被重置的,也就是说n=0线程被唤醒后,n又能重新回到原有值。

和CountDonwLatch再对比

CountDownLatch减计数,CyclicBarrier加计数。

CountDownLatch是一次性的,CyclicBarrier可以重用。

CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,

但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;

而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。

Exchanger(交换机)

Exchanger用于进行两个线程之间的数据交换。

它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。

4、juc并发集合Collections

ConcurrentHashMap

JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,ConcurrentHashMap在对象中保存了一个Segment(分段)数组,put操作时首先根据hash算法定位到元素属于哪个Segment,

然后对该Segment加锁即可,Segment 通过继承 ReentrantLock 来进行加锁,ConcurrentHashMap 有 16 个 Segments

JDK1.8则使用数组+链表+红黑树数据结构,加锁则采用CAS和synchronized实现ConcurrentHashMap,index没有发生冲突使用cas锁

index发生冲突使用synchronized

loadFactor: 负载因子,默认0.75,为什么是0.75,主要是时间和内存空间取舍的问题

为什么HashTable慢

Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,

也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。

CopyOnWriteArrayList

ConcurrentLinkedQueue

BlockingQueue

5、juc线程池Executors

ThreadPoolExecutor

线程池四种创建方式

1、newCachedThreadPool(); 可缓存线程池

2、newFixedThreadPool();可定长度 限制最大线程数

3、newScheduledThreadPool() ; 可定时

4、newSingleThreadExecutor(); 单例

构造方法

ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler)

构造方法配置的参数:

1、corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。

2、maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。

3、keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,

就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。

4、unit:时间单位。为keepAliveTime指定时间单位。

5、workQueue:阻塞队列。用于保存任务的阻塞队列。比如ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue

6、threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。

7、handler:拒绝策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。

线程池工作原理:

1、如果运行的线程数量小于核心线程数corePoolSize,Executor会创建一个新的线程来执行任务,而不是添加到BlockingQueue队列中。

2、如果运行的线程数量大于核心线程数corePoolSize,Executor会将任务添加到BlockingQueue队列中而不是创建一个新的线程。

3、如果BlockingQueue队列已满,如果运行的线程小于最大线程数maximumPoolSize,将会创建一个新的线程来执行任务,否则,该任务将会被拒绝。

拒绝策略:

1、AbortPolicy 丢弃任务,抛运行时异常

2、CallerRunsPolicy 调用者所在的线程来执行任务

3、DiscardPolicy 直接丢弃任务

4、DiscardOldestPolicy 丢弃阻塞队列中靠最前的任务,并执行当前任务

5、实现RejectedExecutionHandler接口,可自定义处理器

任务的执行execute底层原理:

execute –> addWorker –>runworker (getTask)

1、execute方法调用了addWorker方法

2、addWorker方法

addWorker主要负责创建新的线程并执行任务,使用CAS更新线程池数量,在ReentrantLock锁的保证下,把Woker实例插入到HashSet集合中后,启动Woker类中的一个内部线程,调用start方法执行run方法,调用了Worker类的runWorker方法。

Worker类继承了AbstractQueuedSynchronizer(AQS),实现了Runnable接口,Worker构造方法指定了第一个要执行的任务firstTask,并通过线程池的线程工厂创建线程。

3、runWorker方法

线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行可中断;

先执行firstTask,再getTask从workerQueue中取task:

进行lock()加锁操作,保证thread不被其他线程中断(除非线程池被中断) 检查线程池状态,

倘若线程池处于中断状态,当前线程将中断。

执行beforeExecute ,

执行任务的run方法 ,

执行afterExecute方法

unlock()解锁操作

队列中的任务执行完成后,最后调用processWorkerExit方法,进入woker线程的销毁逻辑

4、getTask()方法

firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;woker线程超过空闲时间(keepAliveTime),则回收woker线程

线程池的关闭

1、shutdown方法

将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程

2、shutdownNow方法

将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程.

只要调用这两个关闭方法中的任意一个, isShutDown() 返回true。当所有任务都成功关闭了, isTerminated()返回true

如何合理配置线程池参数?

性质不同的任务可用使用不同规模的线程池分开处理:

CPU密集型: 尽可能少的线程,Ncpu+1

IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池

混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。

为什么线程池不允许使用Executors去创建?

不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

为什么要使用线程池

1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、统一管理。使用线程池可以进行统一的分配,调优和监控。

FutureTask

FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,

直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果。

因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,

或者通过cancel取消FutureTask的执行等。

Fork/Join

利用分治算法思想将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。

Fork/Join框架中提供的fork方法和join方法:

fork方法用于将新创建的子任务放入当前线程的work queue队列中

join方法用于当任务完成的时候返回计算结果。

ForkJoinWorkerThread线程执行ForkJoinTask任务,最主要的特点是每一个ForkJoinWorkerThread线程都具有一个独立的任务等待队列(work queue),这个任务队列用于存储在本线程中被拆分的若干子任务。

10、Java SPI机制

SPI

SPI全称为Service Provider Interface,是一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,调用方来制定接口规范,提供给外部来实现。

Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。SPI的作用就是为这些被扩展的API寻找服务实现。

Java SPI遵循如下约定:

1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

2、接口实现类所在的jar包放在主程序的classpath中;

3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

4、SPI的实现类必须携带一个不带参数的构造方法;

SPI机制的缺陷

1、不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。

2、如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。

3、获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。

4、多个并发多线程使用 ServiceLoader 类的实例是不安全的。

SPI应用场景

1、JDBC加载不同类型的驱动

2、SLF4J对log4j/logback的支持

3、Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

4、Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对java提供的原生SPI做了封装

5、更多应用场景需要大家一起去发现,或者自己使用SPI机制实现代码的解耦

11、Java8 新特性

1、Lambda 表达式

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),语法包含参数与方法体,方法体只有一条可以不用写return;

比如:MathOperation addition = (int a, int b) -> a + b;

2、函数式接口

函数式接口就是仅仅只包含一个抽象方法的接口。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。

java.lang.Runnable、concurrent.Callable是函数式接口最典型的例子。

采用@FunctionalInterface注解 ,确保只包含一个抽象方法,否则编译器会立刻抛出错误提示。

如定义了一个函数式接口如下:

@FunctionalInterface

interface GreetingService {


void sayMessage(String message);

}

那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):

GreetingService greetService1 = message -> System.out.println(“Hello ” + message);

3、方法引用

一般有四种不同的方法引用:

1、构造器引用。语法是ClassName::new,或者更一般的Class< T >::new,要求构造器方法是没有参数;

2、静态方法引用。语法是ClassName::static_method,要求接受一个Class类型的参数;

3、特定类的任意对象方法引用。它的语法是ClassName::method。要求方法是没有参数的;

4、特定对象的方法引用,它的语法是实例instance::method。要求方法接受一个参数,与3不同的地方在于,3是在列表元素上分别调用方法,而4是在某个对象上调用方法,将列表元素作为参数传入;

4、接口中写默认方法实现(接口定义公共方法)

默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法,default 关键字修饰实现默认方法。或者直接写静态 static方法实现也可。对于实现类,处理接口中的方法:可覆盖,或者可直接调用super.do();

5、Optional 类

Optional 类解决空指针异常。

如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

//Optional.ofNullable – 允许传递为 null 参数

Optional a = Optional.ofNullable(value1);

// Optional.of – 如果传递的参数是 null,抛出异常 NullPointerException

Optional b = Optional.of(value2);

// Optional.orElse – 如果值存在,返回它,否则返回默认值

Integer value1 = a.orElse(new Integer(0));

常用栗子:

//user为null,value也为null,搁在以前,user为null,再判断时会报错的

String value =Optional.ofNullable(user).map(User::getUserName);

6、Stream流

map(mapToInt,mapToLong,mapToDouble) 转换

flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作,将一个对象变换为多个对象

limit 限流

distint 去重

Filter 过滤

peek 挑选

skip 跳过

Sorted 排序

collect 收集Collectors类支持各种内置收集器

count 统计

noneMatch、allMatch、anyMatch 匹配

min、max 最大最小值

reduce 规约,所有的元素归约成一个,比如对所有元素求和

forEach遍历

toArray 转成数组

parallelStream并行流

7、日期时间 API

LocalDate

LocalTime

LocalDateTime

12、jvm

对象的创建、布局、定位

对象的创建

1、检查类

检查对应的类是否已被加载、解析、初始化,如果没有,那么先加载类

2、分配内存,即从堆上面划分一块内存

根据堆内存是否规整,有2种分配方法,堆内存是否规整取决于GC

①、指针碰撞,所有用过的内存在一边,空闲内存在另一边,中间通过指针作为分界点。分配内存时,将指针向空闲区域移动对象大小的距离即可

②、空闲列表,用过的内存与空闲的内存交错,通过列表记录可用内存块。分配内存时,在列表中找到足够大的内存块分配给对象,并更新列表

在分配内存时,另一个需要考虑的是并发冲突,解决方法

①、进行同步处理

②、使用TLAB,即本地线程分配缓冲区。当TLAB用完时,会分配新的TLAB,这一步需要同步处理

3、初始化对象内存空间

内存分配完成后,JVM将分配到的内存空间都初始化为零值(不包括对象头)。

4、对象头的设置

将对象的类、哈希码、对象的GC分代年龄等信息设置到对象头之中。

5、执行Java的init方法

执行init方法,按照程序员的意愿初始化,即给字段赋值

对象的内存布局

对象头

对象头包含两部分,第一部分存储自身运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。

第二部分是类型指针,即对象指向它的类元数据的指针,通过此指针来确定是哪个类的对象。

实例数据

存储对象中的各类型的字段内容。无论是从父类继承来的还是在子类中定义的。

对齐填充

并不是必然存在的,当对象实例数据部分没有对齐时,进行对齐补全。

对象的访问定位

通过栈上面的reference数据来操作堆上面的具体对象主要有2种方式:

使用句柄

如果通过句柄来访问对象,Java堆中会划出一块内存作为句柄池,reference中存储句柄地址,而句柄中包含对象的实例数据与类型数据各自的地址。

直接指针

直接指针,就是指reference中直接存储对象的地址。

java代码的编译和执行过程

类的加载机制

什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中。

将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

类的生命周期

类的生命周期包括加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程。

1、加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象

2、连接,又包含:验证、准备、初始化。

1、验证,验证Class文件格式

2、准备,为类的静态变量分配内存,并将其初始化为默认值

3、解析,把类中的符号引用转换为直接引用

3、初始化,为类的静态变量赋予正确的初始值

4、使用,new出对象程序中使用

5、卸载,执行垃圾回收

类加载器

1、启动类加载器:Bootstrap ClassLoader,加载JDK\jre\lib下的的类库

2、扩展类加载器:Extension ClassLoader,加载DK\jre\lib\ext下的类库

3、应用程序类加载器:Application ClassLoader,加载classpath下的class及jar包

4、用户自定义类加载器,通过继承ClassLoader类的方式实现,并重写findClass方法

双亲委派模型

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

双亲委派机制机制的好处

1、避免类的重复加载,保证该类在内存中的唯一性。

2、防止核心API库被随意篡改

破坏双亲委派

1、通过spi机制

SPI全称为Service Provider Interface,是一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,调用方来制定接口规范,提供给外部来实现。

SPI的作用就是为这些被扩展的API寻找服务实现。

如JDBC需要第三方提供的驱动才可以,jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是应用程序类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可以通过线程的上下文类加载器来加载。

2、在实际开发中,我们可以通过自定义类加载器继承ClassLoader类,并重写父类的loadClass方法(里面指定了双亲委派规则)和findClass方法,来打破这一机制。

jvm内存结构

1、Java堆(Heap),Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

2、方法区(Method Area),是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK1.7的时候,方法区被称作为永久代, 从JDK1.8开始,方法区称为Metaspace (元空间)。

3、程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

4、JVM栈(JVM Stacks),也是线程私有的,它的生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。静态的对象还是放在堆(不需要new,直接类调用)

5、本地方法栈(Native Method Stacks),与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;native关键字的方法是看不到的,本地方法栈中就是C和C++的代码。

栈、堆、方法区的交互关系

堆(Heap)

堆被划分成两个不同的区域:新生代、老年代,内存比例1:2。

新生代又被划分为三个区域:Eden、From Survivor、To Survivor,内存比例8:1:1。

这样划分的目的是为了使jvm能够更好的管理内存中的对象,包括内存的分配以及回收。

内存分配与回收策略

1、对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

2、大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

3、长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

4、动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

5、空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

垃圾回收机制及算法

GC是什么

GC 是垃圾收集(Gabage Collection)的意思。finalize()会在对象内存回收前被调用

对象什么时候可以被垃圾器回收

1、当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。

2、垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

对象存活判断

1、引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

2、可达性分析:从GC Roots(根)开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

GC算法

Full GC:理整个堆空间包括年轻代和老年代和永久代。

1、标记-清除算法,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点:效率不高,无法清除垃圾碎片。

2、复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3、标记-整理算法,首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。缺点:仍需要进行局部对象移动,一定程度上降低了效率

4、分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

触发Full GC执行的场景

1、调用system.gc()

2、堆中分配很大内存的对象,老年代空间不足,创建了大对象、大数组等

3、永久代空间满,在jdk1.8之前永久代空间满,且未配置CMS GC的情况下

4、CMS GC异常

5、在新生代GC后,存活的对象超过了老年代剩余空间

6、每次晋升到老年代的对象平均大小大于老年代剩余空间

7、jvm自身的固定频率full GC

垃圾收集器

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本.

Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。

Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

G1(Garbage First)收集器 ( 标记整理 + 复制算法来回收垃圾 ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

分代垃圾回收器的工作过程:

1、新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

1、把 Eden + From Survivor 存活的对象放入 To Survivor 区;

2、清空 Eden 和 From Survivor 分区;

3、From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

2、每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

3、老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

JVM调优

调优命令

JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

1、jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

2、jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

3、jmap,JVM Memory Map命令用于生成heap dump文件

4、jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看

5、jstack,用于生成java虚拟机当前时刻的线程快照。

6、jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

调优工具

1、jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控

2、jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。

3、arthas,阿里工具

常用的 JVM 调优的参数

#常用的设置

-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。

-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。

-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。

-XX:NewSize=n 设置年轻代初始化大小大小

-XX:MaxNewSize=n 设置年轻代最大值

-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4

-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8

-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。

-XX:ThreadStackSize=n 线程堆栈大小

-XX:PermSize=n 设置持久代初始值

-XX:MaxPermSize=n 设置持久代大小

-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

JVM的GC收集器设置

-XX:+UseSerialGC:设置串行收集器,年轻带收集器

-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量

-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。

-XX:+UseConcMarkSweepGC:设置年老代并发收集器

-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器



第二章 JavaWeb系列

1、spring

Spring由哪些模块组成

spring core:提供了框架的基本组成部分,包括控制反转(IOC)和依赖注入(DI)功能

spring beans:提供了BeanFactory

spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法。

spring jdbc:提供了一个JDBC的抽象层

spring aop:提供了面向切面的编程实现

spring Web:提供了针对 Web 开发的集成特性

spring test:主要为测试提供支持的

Spring两大核心技术IOC和AOP

IOC

控制翻转,它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。

Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。

Spring 中的 IOC 的实现原理就是工厂模式加反射机制。

IOC的作用

1、管理对象的创建和依赖关系的维护。

2、解耦,由容器去维护具体的对象

BeanFactory 和 ApplicationContext有什么区别

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

依赖关系

其中ApplicationContext是BeanFactory的子接口。

BeanFactory是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。

ApplicationContext还提供了更完整的框架功能:

继承MessageSource,因此支持国际化。

统一的资源文件访问方式。

提供在监听器中注册bean的事件。

同时加载多个配置文件。

载入多个(有继承关系)上下文。

加载方式

BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。

创建方式

BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

注册方式

BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

总结:

BeanFactory 可以理解为就是个 HashMap,称之为 “低级容器”。ApplicationContext称之为 “高级容器”,比 BeanFactory 多了更多的功能,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”,代表着整个大容器的所有功能。该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean。

IOC 在 Spring 里,只需要低级容器就可以实现,2 个步骤:

1、加载配置文件,解析成 BeanDefinition 放在 Map 里。

2、调用 getBean 的时候,从 BeanDefinition 所属的 Map 里拿出 Class 对象进行实例化,

同时,如果有依赖关系,将递归调用 getBean 方法完成依赖注入。

依赖注入实现方式

1、接口注入:从Spring4开始已被废弃

2、Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。(用得比较少)

3、构造器注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。(用得比较多)

AOP

面对切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,

抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,

同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。使用 @Aspect 注解的类就是切面。

Spring AOP and AspectJ AOP 有什么区别

AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类。

Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,

这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

JDK动态代理和CGLIB动态代理的区别

JDK动态代理

只提供接口的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

CGLIB动态代理

可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

Spring通知有哪些类型

1、前置通知(Before):在目标方法被调用之前调用通知功能;

2、后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;

3、返回通知(After-returning ):在目标方法成功执行之后调用通知;

4、异常通知(After-throwing):在目标方法抛出异常后调用通知;

5、环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

使用AOP的好处

1、降低模块的耦合度

2、使系统容易扩展

3、提高代码复用性

Spring 中用到的设计模式

工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;

单例模式:Bean默认为单例模式。

代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;

模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。

观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。

SpringBean

spring容器在启动的时候,先回保存所有注册进来的Bean的定义信息

1、xml注册bean:

2、注解注册Bean:@Service、@Repository、@Component、@Bean、xxx

SpringBean的生命周期

1、容器寻找Bean的定义信息并实例化,使用反射技术初始化对象调用无参构造函数

2、为Bean注入属性,Spring按照Bean定义信息使用反射调用set方法对属性实现赋值

3、若Bean实现BeanNameAware接口,调用Bean的setBeanName()方法传递Bean的ID

4、若Bean实现BeanFactoryAware接口,调用Bean的setBeanFactory()方法传入工厂自身

5、 若Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization()方法,一般用于bean实例的属性值的填充之前

6、bean的初始化

7、如果Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization的()方法,可以进行bean实例的代理封装

8、当Bean不再需要时,销毁该对象,调用销毁方法

以上是使用BeanFactory生成并管理Bean。

如果使用ApplicationContext来生成及管理Bean,在执行BeanFactoryAware的setBeanFactory()阶段后,若Bean有实现ApplicationContextAware接口,则执行其setApplicationContext()方法,接着才执行BeanPostProcessors的ProcessBeforeInitialization()及之后的流程。

Bean如何关联BeanPostProcessor接口?

容器中如果有实现BeanPostProcessors接口的实例,则任何Bean在初始化之前都会执行这个实例的processBeforeInitialization()和之后的postProcessAfterInitialization方法。

给Spring容器提供配置元数据方式

1、XML配置文件。(xml配置bean)

2、基于注解的配置。(比如用@Service等表示bean)

3、基于java的配置。(基于标记了@Bean的方法并用@Configuration注解的Java类)

SpringBean的作用域

它可以通过bean 定义中的scope属性来定义。

singleton:单例

prototype:多例

request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。

session:在一个HTTP Session中,一个bean定义对应一个实例。‘

global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。

bean的线程安全

单例bean不是线程安全的,多例bean可以保证线程安全

Spring如何处理线程并发问题

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

spring 自动装配 bean 有哪些方式

在spring中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装载模式。

在Spring框架xml配置中共有5种自动装配:

1、no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。

2、byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。

3、byType:通过参数的数据类型进行自动装配。

4、constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。

5、autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。

使用@Autowired注解自动装配的过程

使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,<context:annotation-config />。

在启动spring IOC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IOC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:

如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;

如果查询的结果不止一个,那么@Autowired会根据名称来查找;

如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。

你可以在Spring中注入一个null 和一个空字符串吗?

可以

Spring

SpringBean的注解

@Component:这将 java 类标记为 bean。

@Controller:这将一个类标记为 Spring Web MVC 控制器。

@Service:服务层类中使用

@Repository:DAO层使用

@Required:表明bean的属性必须在配置的时候设置

@AutowiredP:默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。

@Resource:默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。

@Qualifier:当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个bean时,指定应该装配哪个确切的 bean

@RequestMapping:用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。

Spring bean的循环依赖

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。

Spring中循环依赖场景有:

1、属性的循环依赖

2、构造器的循环依赖

属性的循环依赖的解决

三级缓存

一级缓存:保存实例化、注入、初始化完成的bean实例

二级缓存:保存早期只是实例化后的bean实例,避免多重循环的情况,重复创建动态代理(多重循环的时候二级缓存必要)

三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

解决A和B循环依赖过程:

1、A从一级缓存获取不到实例A,则创建实例,并添加到三级缓存中,然后依赖注入B

2、B从一级缓存获取不到实例B,则创建实例,并添加到三级缓存中,然后依赖注入A

3、A从三级缓存获取到实例B(如果实现了aop创建B动态代理,否则返回B实例),

把实例B添加到二级缓存,A从二级缓存获取B成功依赖注入,A初始化完成后添加到一级缓存中,清空A二级、三级缓存

三级缓存可以spring bean单例的循环依赖,不能解决多例的循环依赖,因为多例每次生成的不同实例,无法缓存。

构造器的循环依赖

三级缓存不能解决构造器的循环依赖,可通过@Lazy延迟加载解决构造器的循环依赖。

1、A构造器引用B的时候,会先返回一个B的cglib动态代理

2、B构造器引用B的时候,会先返回一个A的cglib动态代理

3、所以当A真正用到B的时候,才通过B的代理的增强,才会创建B的实例 (延迟加载的含义)

Spring的事务操作

事务的分类

手动事务

编程事务

事务七种传播行为

1、PROPAGATION_REQUIRED(默认传播行为)

当前线程如果存在事务,则加入当前事务;如果当前线程不存在事务则创建一个新的事务

2、PROPAGATION_SUPPORTS

当前线程如果存在事务,则加入当前事务;如果当前线程不存在事务,则以非事务

方式执行

3、PROPAGATION_MANDATORY

当前线程如果存在事务,则加入当前事务;如果当前线程存在事务则抛出异常

4、PROPAGATION_REQUIRES_NEW

当前线程如果存在事务,则会挂起当前事务,创建一个新的事务

5、PROPAGATION_NOT_SUPPORTED

总是以非事务的方式执行

6、PROPAGATION_NEVER

总是以非事务方式执行,如果当前线程存在事务则会抛出异常

7、PROPAGATION_NESTED

当前线程如果存在事务,则会嵌套一个事务

Spring中用到的设计模式

1、依赖注入DI(Dependency Inject)使用了单例模式。Spring容器里的Bean默认是单例模式

2、控制反转IoC(Inversion of Control)采用了工厂模式

3、AOP切面是基于jdk动态代理的设计模式

4、ApplicationEvent事件驱动模型使用了观察者模式

5、Controller层使用了适配器设计模式

2、springMVC

SpringMVC的执行流程:

1、 用户向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet(也叫中央控制器)。

2、DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller处理器来处理(并未调用Controller处理器,只是得知)

3、DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller处理器

4、HandlerAdapter处理器适配器去执行Controller处理器并得到ModelAndView(数据和视图),并层层返回给DispatcherServlet

5、DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图。

6、DispatcherServlet将模型数据填充到视图中

7、DispatcherServlet将结果响应给用户

3、mybatis

MyBatis

MyBatis是一个小巧、方便、高效、简单、直接、半自动化的持久层框架。

核心类

SqlSessionFactoryBuilder:创建会话工厂建造者,用来创建会话工厂SqlSessionFactory

SqlSessionFactory:创建会话工厂,用来创建 SqlSession 对象

SqlSession:会话对象,该对象中包含了执行 SQL 语句的所有方法。

Executor:Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statement,跟Simple的唯一区别就是内部缓存statement),SimpleExecutor(默认,每次都会创建新的statement)。

工作原理

1、读取 MyBatis 配置文件mybatis-config.xml:配置了 MyBatis 的运行环境等信息,例如数据库连接信息。生成Configuration

2、加载映射文件Mapper.xml:该文件中配置了操作数据库的 SQL 语句。生成一个个MappedStatement对象,包括了参数映射配置、动态SQL语句、结果映射配置

3、创建会话工厂SqlSessionFactory:SqlSessionFactoryBuilder创建会话工厂

4、创建会话对象SqlSession:由会话工厂创建会话对象,该对象中包含了执行 SQL 语句的所有方法。

5、Executor执行器将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象,使用Paramterhandler填充参数,使用statementHandler绑定参数。

6、JDBC执行sql,结果处理和映射,借助MappedStatement中的结果映射关系,使用ResultSetHandler将返回结果转化成List 、Map、JavaBean等存储结构并返回。

7、关闭sqlsession会话。

Mybatis的三种Executor执行器

1、SimpleExecutor:普通的执行器。

2、ReuseExecutor:执行器会重用预处理语句(prepared statements)

3、BatchExecutor:执行器将重用语句并执行批量更新

Mybatis的延迟加载

mybatis的延迟加载就是按需查询,在需要的时候再进行查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。Mybatis仅支持一对一和一对多查询的延迟加载。

延迟加载原理

使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。

#{}和

KaTeX parse error: Expected ‘EOF’, got ‘#’ at position 7: {}的区别 #̲{}是占位符,预编译处理;

{}是拼接符,字符串替换,没有预编译处理。

Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。

#{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入

在mapper中如何传递多个参数

1、顺序传参法

#{}里面填写的数字代表传入参数的顺序。

2、@Param注解传参法

#{}里面的名称对应的是注解@Param括号里面修饰的名称

3、Map传参法

#{}里面的名称对应的是Map里面的key名称。

4、Java Bean传参法

#{}里面的名称对应的是User类里面的成员属性。

MyBatis接口绑定的实现方式

1、通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;

2、通过xml里面写SQL来绑定,要指定xml映射文件里面的namespace必须为接口的全路径名。

Dao接口的工作原理

Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

Dao接口里的方法,是不能重载的,唯一对应namespace空间下的sql的id。

Mybatis的Xml映射文件sql的id唯一性

不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。原因就是namespace+id是作为Map<String, MappedStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。

Mybatis返回结果的映射形式

1、使用标签,逐一定义列名和对象属性名之间的映射关系

2、使用sql列的别名功能,将列别名书写为对象属性名

动态sql的执行原理

Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind

原理:使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

Mybatis分页插件的原理

基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

Mybatis的一级、二级缓存

一级缓存: 是SqlSession会话级别的缓存。不同的sqlSession中的缓存是互相不能读取的。sqlSession执行commit,即增删改操作时会清空缓存。

二级缓存:是mapper级别(按namespace分)的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存。二级缓存的作用范围更大。

在setting标签中手动开启二级缓存

查询的时候先查一级缓存,没有数据再查二级缓存。



第三章 微服务系列

1、srpingboot

什么是SpringBoot

主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。作用:快速开发,快速整合,配置简化、内嵌服务容器。

SpringBoot的自动装配原理

在sprinBoot启动时由@SpringBootApplication注解读取每个starter的jar包下META-INF/spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean等配置类(被@Configuration注解),并且进行自动配置把bean注入Spring容器中,能够实现使用相应的功能。

SpringBoot的核心注解

1、@SpringBootApplication

启动类上的核心注解,标示一个声明有一个或多个的@Bean方法的Configuration类并且触发自动配置(EnableAutoConfiguration)和组建扫描(ComponentScan)。

@SpringBootApplication是一个组合注解,主要包含了以下三个注解:

@SpringBootConfiguration:声明为主配置类

@ComponentScan:开启组件扫描

@EnableAutoConfiguration:开启自动配置类,导入主配置类所在包及下面所有子包的所有组件和第三方的依赖中所有META-INF/spring.factories文件中的配置类

定义:

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan(excludeFilters = {


@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),

@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

public @interface SpringBootApplication {




}

2、@SpringBootConfiguration

表示这是一个SpringBoot的配置类,其实内部就是Spring的注解一个 @Configuration注解。

定义:

@Configuration

public @interface SpringBootConfiguration {




}

@Configuration相当于一个spring的xml文件,配合@Bean注解,可以在里面配置需要Spring容器管理的bean。

基于JavaConfig配置:

@Configuration

public class MockConfiguration{


@Bean

public MockService mockService(){


return new MockServiceImpl();

}

}

3、@ComponentScan

开启组件扫描。

定义:

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Documented

@Repeatable(ComponentScans.class)

public @interface ComponentScan {




}

基于JavaConfig配置:

@Configuration

@ComponentScan(value = “com.youzan”, excludeFilters = {


@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})

})

public class ScanConfig {

}

@ComponentScan通常与@Configuration一起配合使用,相当于xml里面的context:component-scan,用来告诉Spring需要扫描哪些包或类。

如果不设值的话默认扫描@ComponentScan注解所在类的同级类和同级目录下的所有类,

所以对于一个Spring Boot项目,一般会把入口类放在顶层目录中,这样就能够保证源码目录下的所有类都能够被扫描到。

4、@EnableAutoConfiguration

启动自动配置的功能。

包含:@AutoConfigurationPackage、@Import(EnableAutoConfigurationImportSelector.class)

两个注解。

定义:

@AutoConfigurationPackage

@Import(EnableAutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {




}

@AutoConfigurationPackage

是一个自动配置包。里面是一个@Import注解:@Import({Registrar.class}),导入了一个Registrar的组件。所以,@AutoConfigurationPackage 注解就是将主配置类(@SpringBootConfiguration标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器中。

@Import(EnableAutoConfigurationImportSelector.class)

导入的是EnableAutoConfigurationImportSelector.class类,主要加载所有META-INF/spring.factories配置文件中的所有配置类加入到Spring容器中。

(org.springframework.boot.autoconfigure.EnableutoConfiguration属性对于的所有配置类)

注意:还有过滤,将满足条件(@Conditional)的自动配置类返回。

5、@Import

三种导入方式:

1、导入一个普通的 Java 类

@Import({Circle.class})

@Configuration

public class MainConfig {


}

2、导入一个ImportSelector的实现类 (可以的1的基础上)

@Import({Circle.class,MyImportSelector.class})

@Configuration

public class MainConfigTwo {


}

public class MyImportSelector implements ImportSelector {


@Override

public String[] selectImports(AnnotationMetadata annotationMetadata) {


return new String[]{“annotation.importannotation.waytwo.Triangle”};

}

}

2、导入一个ImportBeanDefinitionRegistrar的实现类,实现类中手动注入一个bean到容器中

public class Rectangle {


public void sayHi() {


System.out.println(“Rectangle sayHi()”);

}

}

@Import({Circle.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})

@Configuration

public class MainConfigThree {

}

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {


@Override

public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {


RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Rectangle.class);

// 注册一个名字叫做 rectangle 的 bean

beanDefinitionRegistry.registerBeanDefinition(“rectangle”, rootBeanDefinition);

}

}

6、@Conditional

此注解可以实现只有在特定条件满足时才启用一些配置。可以创建自定义实现类实现Conditional接口重写matches方法设置一些条件,返回true则条件成立配置生效。

其他实现:

ConditionalOnBean:容器中存在指定 Bean,则生效。

ConditionalOnMissingBean;容器中不存在指定 Bean,则生效。

ConditionalOnClass:系统中有指定的类,则生效。

ConditionalOnMissingClass:系统中没有指定的类,则生效。

ConditionalOnProperty:系统中指定的属性是否有指定的值。

ConditionalOnWebApplication:当前是web环境,则生效。

什么是 JavaConfig

它提供了配置 Spring IOC 容器的纯Java 方法。

1、面向对象的配置。

2、减少或消除 XML 配置。

3、类型安全和重构友好。

常用的Java config:

@Configuration:在类上打上写下此注解,表示这个类是配置类

@ComponentScan:在配置类上添加 @ComponentScan 注解。该注解默认会扫描该类所在的包下所有的配置类,相当于之前的 <context:component-scan >。

@Bean:bean的注入:相当于以前的< bean id=“objectMapper” class=“org.codehaus.jackson.map.ObjectMapper” />

@EnableWebMvc:相当于xml的<mvc:annotation-driven >

@ImportResource: 相当于xml的 < import resource=“applicationContext-cache.xml”>

SpringBoot 实现热部署有哪几种方式

热部署就是可以不用重新运行SpringBoot项目可以实现操作后台代码自动更新到以运行的项目中

主要有两种方式:

Spring Loaded

Spring-boot-devtools

运行 Spring Boot 有哪几种方式

1、打包用命令或者放到容器中运行

2、用 Maven/ Gradle 插件运行

3、直接执行 main 方法运行

SpringBoot事物的使用

首先使用注解EnableTransactionManagement开启事物之后,然后在Service方法上添加注解Transactional便可

Async异步调用方法

只需要在方法上使用@Async注解即可实现方法的异步调用。 注意:需要在启动类加入@EnableAsync使异步调用@Async注解生效。

Spring Boot读取配置的方式

spring Boot 可以通过 @PropertySource,@Value,@Environment, @ConfigurationPropertie注解来绑定变量

SpringBoot多数据源拆分的思路

先在properties配置文件中配置两个数据源,创建分包mapper,使用@ConfigurationProperties读取properties中的配置,使用@MapperScan注册到对应的mapper包中

SpringBoot多数据源事务如何管理

1、第一种方式是在service层的@TransactionManager中使用transactionManager指定DataSourceConfig中配置的事务

2、第二种是使用jta-atomikos实现分布式事务管理

Spring Boot中如何解决跨域问题

在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题

@Configuration

public class CorsConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/**")
              .allowedOrigins("*")
              .allowCredentials(true)
              .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
              .maxAge(3600);
  }

}

springBoot性能如何优化

1、如果项目比较大,类比较多,不使用@SpringBootApplication,采用@Compoment指定扫包范围

2、在项目启动时设置JVM初始内存和最大内存相同

3、将springboot内置服务器由tomcat设置为undertow(undertow服务器性能更搞)

Spring Boot 中如何实现定时任务

主要有两种不同的方式:

1、使用 Spring 中的 @Scheduled 注解

2、使用第三方框架 Quartz。

SpringBoot如何实现打包

进入项目目录在控制台输入mvn clean package,clean是清空已存在的项目包,package进行打包

或者点击左边选项栏中的Mavne,先点击clean在点击package。

Spring Boot 打成的 jar 和普通的 jar 的区别

Spring Boot 项目最终打包成的jar是可执行 ar ,这种jar可以直接通过java -jar xxx.jar命令来运行,

这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。

如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

SpringBoot启动过程源码解读

启动关键代码:

SpringApplication.run(Application.class, args);

启动过程

1、创建 SpringApplication 对象

构造 SpringApplication实例中:

1、推断应用类型是否是Web环境

2、设置初始化器(Initializer)

3、设置监听器(Listener) 初始化器和设置监听器都是从META-INF/spring.factorie配置文件中读取加载

4、推断应用入口类(Main)

源码解读:

//构造实例

public SpringApplication(Object… sources) {


initialize(sources);

}

private void initialize(Object[] sources) {


if (sources != null && sources.length > 0) {


this.sources.addAll(Arrays.asList(sources));

}

// 推断是否为web环境

this.webEnvironment = deduceWebEnvironment();

// 设置初始化器

setInitializers((Collection) getSpringFactoriesInstances(

ApplicationContextInitializer.class));

// 设置监听器

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

// 推断应用入口类

this.mainApplicationClass = deduceMainApplicationClass();

}

2、调用SpringApplication.run方法

run方法中:

1、获取运行监听器RunListeners(从META-INF/spring.factories 中读取)

2、准备Environment环境

3、创建Spring Context(容器)

4、容器前置处理

5、容器刷新

6、容器后置处理

最后,在容器启动成功后回调一些监听器的方法与加载项目中组件到 IOC 容器中,所有需要回调的监听器都是从类路径下的 META/INF/Spring.factories 中获取,从而达到启动前后的各种定制操作。

源码解读:

// 运行run方法

public ConfigurableApplicationContext run(String… args) {


// 此类通常用于监控开发过程中的性能,而不是生产应用程序的一部分。

StopWatch stopWatch = new StopWatch();

stopWatch.start();

ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

// 设置java.awt.headless系统属性,默认为true 
// Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。
configureHeadlessProperty();

// KEY 1 - 获取SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);

// 通知监听者,开始启动
listeners.starting();
try {
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);

    // KEY 2 - 根据SpringApplicationRunListeners以及参数来准备环境
    ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
    
    configureIgnoreBeanInfo(environment);

    // 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
    Banner printedBanner = printBanner(environment);

    // KEY 3 - 创建Spring上下文
    context = createApplicationContext();

    // 注册异常分析器
    analyzers = new FailureAnalyzers(context);

    // KEY 4 - Spring上下文前置处理
    prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);

    // KEY 5 - Spring上下文刷新
    refreshContext(context);

    // KEY 6 - Spring上下文后置处理
    afterRefresh(context, applicationArguments);

    // 发出结束执行的事件
    listeners.finished(context, null);

    stopWatch.stop();
    if (this.logStartupInfo) {
        new StartupInfoLogger(this.mainApplicationClass)
                .logStarted(getApplicationLog(), stopWatch);
    }
    return context;
}
catch (Throwable ex) {
    handleRunFailure(context, listeners, exceptionReporters, ex);
    throw new IllegalStateException(ex);
}

}

springBoot自定义starter

官方建议自定义的starter使用xxx-spring-boot-starter命名规则

1、创建两个工程 hello-spring-boot-starter和hello-spring-boot-starter-autoconfigurer

pom.xml文件中引入自动配置模块hello-spring-boot-starter-autoconfigurer依赖

com.gf

hello-spring-boot-starter-autoconfigurer

0.0.1-SNAPSHOT

2、hello-spring-boot-starter-autoconfigurer的pom.xml引入spring-boot-starter依赖

org.springframework.boot

spring-boot-starter

1.定义一个实体类映射配置信息,添加注解ConfigurationProperties(“gf.hello”),表示我们的配置文件以gf.hello开头,比如配置gf.hello.suffix=hi

@ConfigurationProperties(prefix = “gf.hello”)

public class HelloProperties {

private String prefix;
private String suffix;

public String getPrefix() {
    return prefix;
}

public void setPrefix(String prefix) {
    this.prefix = prefix;
}

public String getSuffix() {
    return suffix;
}

public void setSuffix(String suffix) {
    this.suffix = suffix;
}

}

2.定义一个Service类,里面自定义一些方法

public class HelloService {

HelloProperties helloProperties;

public HelloProperties getHelloProperties() {
    return helloProperties;
}

public void setHelloProperties(HelloProperties helloProperties) {
    this.helloProperties = helloProperties;
}

public String sayHello(String name ) {
    return helloProperties.getPrefix()+ "-" + name “-”+ helloProperties.getSuffix();
}

}

3、创建自动配置类

@Configuration

@ConditionalOnWebApplication //web应该生效

@EnableConfigurationProperties(HelloProperties.class)

public class HelloServiceAutoConfiguration {

@Autowired
HelloProperties helloProperties;

@Bean
public HelloService helloService() {
    HelloService service = new HelloService();
    service.setHelloProperties( helloProperties  );
    return service;
}

}

5.在resources下创建META-INF/spring.factories配置文件,并添加自动配置类的信息

#Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=

com.gf.service.HelloServiceAutoConfiguration

6.测试结果

1、项目引入spring-boot-starter依赖

2、application.properties文件可以添加一些配置

gf.hello.prefix = hi

gf.hello.suffix = ok

3、注入HelloService并调用sayHello(mm),查看运行结果hi- mm-ok。

总结:

启动器starter只是用来做依赖管理,需要专门写一个类似spring-boot-autoconfigure的配置模块,用的时候只需要引入启动器starter,就可以使用自动配置了

2、springcloud(第一代)

1、Eureka注册中心

Eureka

一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移,可作为注册中心。

Eureka保证可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。

Eureka自我保护机制

如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:

1、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务

2、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)

3、当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

DiscoveryClient的作用

可以从注册中心中根据服务别名获取注册的服务器信息,通过@EnableDiscoveryClient的方式进行启用。

2、Fegin客户端

Feign

Feign是一种声明式、模板化的HTTP客户端,采用基于接口的注解的声明式服务调用。

Feign优点

1、feign采用的是基于接口的注解。

2、feign整合了ribbon,具有负载均衡的能力。

3、整合了Hystrix,具有熔断的能力。

Feign的使用

1、添加pom依赖。

2、启动类添加@EnableFeignClients

3、定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务

将我们需要调用的服务方法定义成抽象方法保存在本地

SpringCloud调用接口方式

1、Feign

2、RestTemplate

Ribbon和Feign调用服务的区别

1、Ribbon需要我们自己构建Http请求,模拟Http请求然后通过RestTemplate发给其他服务,步骤相当繁琐

2、Feign则是在Ribbon的基础上进行了一次改进,采用接口的形式,将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。

OpenFeign

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。

OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,

并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

3、Ribbon负载均衡

Ribbon

Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法。

Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。

Ribbon负载均衡的注解

@LoadBalanced

Ribbon底层实现原理

Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息。

Ribbon负载均衡策略

1、RandomRule : 随机。

2、RoundRobinRule : 轮询。

3、RetryRule : 重试。

4、WeightedResponseTimeRule : 权重。

5、ClientConfigEnabledRoundRobinRule : 一般不用,通过继承该策略,默认的choose就实现了线性轮询机制。可以基于它来做扩展。

6、BestAvailableRule : 通过便利负载均衡器中维护的所有服务实例,会过滤到故障的,并选择并发请求最小的一个。

7、PredicateBasedRule : 先过滤清单,再轮询。

8、AvailabilityFilteringRule :继承了父类的先过滤清单,再轮询。调整了算法。

9、ZoneAvoidanceRule : 该类也是PredicateBasedRule的子类,它可以组合过滤条件。以ZoneAvoidancePredicate为主过滤条件,以AvailabilityPredicate为次过滤条件。

4、Hystrix熔断器

断路器

当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应。

Hystrix

Hystrix是一个熔断器,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。

Hystrix相关注解

@EnableHystrix:开启熔断

@HystrixCommand(fallbackMethod=”XXX”):声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是 0毫秒),就会执行fallback函数

hystrix熔断器的3种状态

1.第一种closed是关闭状态,服务调用方每次请求都到服务提供方;

2.第二种是open打开状态,意思是如果服务提供方的异常率或者是请求的并发量超过设置的阈值之后,就会开启熔断机制,开启熔断机制之后服务调用方所有的请求都不会在请求到服务提供方,而是直接使用本地的服务降级方法;

3.第三种是half-poen半打开状态,服务调用方所有的请求依然会请求到服务提供饭

hystrix也有自我恢复机制,意思是当服务提供方的熔断机制处于打开状态时,会在开启一个时间窗口,就是一定时间后或者是下一次请求的时间大于时间窗口的时间,hystrix就会重新将这次请求再次发送到服务提供方,如果成功就将状态改为half-open状态,如果失败就重新刷新时间窗口的时间。

熔断的条件

在一个窗口期内(关闭状态)默认5秒,若请求次数超过默认20,并且错误的数量超过默认50%错误率,那么就会触发熔断。在接下来的一个窗口期内任何请求都会降级(开启状态),直到下一个请求过去进入半开状态,进入半开状态后继续从新判定。

Hystrix有四种防雪崩方式:

服务降级:接口调用失败就调用本地的方法返回一个空

服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息

服务隔离:隔离服务之间相互影响

服务监控:在服务发生调用时,会将每秒请求数、成功请求数等运行指标记录下来。

服务降级:当客户端请求服务器端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回一个友好的提示给客户端。

服务熔断:在服务降级的基础上更直接的一种保护方式,当在一个统计时间范围内的请求失败数量达到设定值(requestVolumeThreshold)或当前的请求错误率达到设定的错误率阈值(errorThresholdPercentage)时开启断路,之后的请求直接走fallback方法,在设定时间(sleepWindowInMilliseconds)后尝试恢复。

服务隔离:就是Hystrix为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务。服务隔离有线程池和信号量两种实现方式,一般使用线程池方式。

断路器监控Hystrix Dashboard

监控主页http://localhost:9000/hystrix

监控的url:

默认的集群监控:通过URL http://项目url/turbine.stream开启,实现对默认集群的监控

服务降级底层是如何实现

Hystrix实现服务降级的功能是通过重写HystrixCommand中的getFallback()方法,当Hystrix的run方法或construct执行发生错误时转而执行getFallback()方法。

5、Zuul网关

网关

网关相当于一个网络服务架构的入口,所有网络请求必须通过网关转发到具体的服务。

网关的作用

统一管理微服务请求,权限控制、负载均衡、路由转发、监控、安全控制黑名单和白名单等

Zuul

Zuul会根据请求的路径不同,网关会定位到指定的微服务,并代理请求到不同的微服务接口,他对外隐蔽了微服务的真正接口地址。可以和Eureka,Ribbon,Hystrix等组件配合使用。

@EnableZuulProxy – 开启Zuul网关

zuul路由网关

Zuul 包含了对请求的路由和过滤两个最主要的功能:其中负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础、

Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,即以后的访问微服务都是通过Zuul跳转后获得。

动态路由表:Zuul支持Eureka路由,手动配置路由,这俩种都支持自动更新

路由定位:根据请求路径,Zuul有自己的一套定位服务规则以及路由表达式匹配

反向代理:客户端请求到路由网关,网关受理之后,在对目标发送请求,拿到响应之后在给客户端

网关配置方式有多种,默认、URL、服务名称、排除|忽略、前缀。

实现动态Zuul网关路由转发

通过path配置拦截请求,通过ServiceId(服务名称)到配置中心获取转发的服务列表,Zuul内部使用Ribbon实现本地负载均衡和转发。

Zuul的核心

过滤器

1、pre : 可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。

2、route : 在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。

3、post :在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。

4、error : 处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。

Zuul的应用场景

对外暴露,权限校验,服务聚合,日志记录等

6、Config 配置中心

Config

配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置

Config组件中的两个角色

onfig Server : 配置中心服务端,负责读取配置文件,并且暴露Http API接口。

Config Client : 配置中心客户端,通过调用Config Server的接口来读取配置文件

springcloud config实时刷新采用SpringCloud Bus消息总线。

7、Bus消息总线

Spring Cloud Bus就像一个分布式执行器,用于扩展的Spring Boot应用程序的配置文件,但也可以用作应用程序之间的通信通道。

事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可以用来动态刷新集群中的服务配置信息,可与Spring Cloud Config联合实现热部署。

简单来说就是修改了配置文件,发送一次请求,所有客户端便会重新读取配置文件。

Spring Cloud Bus 不能单独完成通信,需要利用中间插件MQ。

8、Sleuth分布式链路跟踪

Spring Cloud Stream

一种分布式追踪解决方案,轻量级事件驱动微服务框架,可以使用简单的声明式模型来发送及接收消息,主要实现为Apache Kafka及RabbitMQ。

Spring Cloud Sleuth服务链路跟踪功能就可以帮助我们快速的发现错误根源以及监控分析每条请求链路上的性能等等。

Sleuth帮助我们做了哪些工作

可以方便的了解到每个采样的请求耗时,分析出哪些服务调用比较耗时。

对于程序未捕捉的异常,可以在集成Zipkin服务页面上看到。

识别调用比较频繁的服务,从而进行优化。

9、Consul注册中心

Consul 是一种服务网格解决方案,提供具有服务发现、配置和分段功能的全功能控制平面。作为注册中心是CP模式

Consul 提供的核心功能:

1、服务发现:服务启动时将服务相关信息(地址、端口、配置、tag等)注册到 Consul 中,由它统一管理,当需要访问其他服务时向 Consul 查询所依赖服务的相关信息。查询方式支持 HTTP、RPC、DNS等。

2、健康检查:在将服务注册到 Consule 时可以配置健康检查,Consul 会对其定期进行检查,如果检查失败,就会认为该服务不可用,当有其他服务过来查询这个服务的地址时,返回时可以把不可用的地址剔除(注:同一个服务一般会有多个服务/地址)。

3、安全服务通信:Consul 可以为服务生成和分发 TLS 证书,以建立相互的 TLS 连接。

4、KV存储:Cousul 可以用来做简单的键值存储场景,比如:动态配置、功能标记、协调、领导者选举等。提供简单的 HTTP API。

5、多数据中心:Consul 支持开箱即用的多个数据中心,用以支持高可用的场景需要。

3、SpringCloudAlibaba(第二代)

1、Nacos注册中心

Nacos

Nacos可以作为注册中心和配置中心使用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。支持AP和CP模型

Nacos = Spring Cloud注册中心 + Spring Cloud配置中心。

Nacos功能特性

1、服务发现与健康监测

2、动态配置管理

3、动态DNS服务

4、服务和元数据管理(管理平台的角度,nacos也有一个ui页面,可以看到注册的服务以及实例信息(元数据信息等),动态的服务权重调整,动态服务优雅下线,都可以去做)

核心功能

1、服务注册

Nacos Client会通过发送REST请求想Nacos Server注册自己的服务,提供自身的元数据,比如IP地址,端口等信息。Nacos Server接收到注册请求后,就会把这些元数据存储到一个双层的内存Map中。

2. 服务心跳

在服务注册后,Nacos Client会维护一个定时心跳来维持统治Nacos Server,说明服务一致处于可用状态,防止被剔除,默认5s发送一次心跳。

3. 服务同步

Nacos Server集群之间会相互同步服务实例,用来保证服务信息的一致性。

4. 服务发现

服务消费者(Nacos Client)在调用服务提供的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务拉取服务最新的注册表信息更新到本地缓存。

5. 服务健康检查

Nacos Server 会开启一个定时任务来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将他的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。

动态配置服务

Nacos的动态刷新配置

我们只要修改下Nacos中的配置信息,再次调用查看配置的接口,就会发现配置已经刷新,Nacos和Consul一样都支持动态刷新配置。

Nacos数据模型

1、Namespace:命名空间,对不同的环境进行隔离,比如隔离开发环境,测试环境和生产环境

2、Group:分组,将若干个服务或者若干个配置集归为一组,通常习惯一个系统归为一个组

3、Service:某一个服务

4、DataId:配置集或者可以认为是一个配置文件

Nacos和Eureka的区别

2、Gateway服务网关

Gateway

Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。

Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。

使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。

gateway和zuul的区别

1、zuul

使用的是阻塞式的 API,不支持长连接,比如 websockets。

底层是servlet,Zuul处理的是http请求

没有提供异步支持,流控等均由hystrix支持,负载均衡需要配合ribbon。

依赖包spring-cloud-starter-netflix-zuul。

2、gateway

使用非阻塞API,支持各种长连接、websocket

Spring Boot和Spring Webflux提供的Netty底层环境,不能和传统的Servlet容器一起使用,也不能打包成一个WAR包。

提供了异步支持,提供了抽象负载均衡,提供了抽象流控,并默认实现了RedisRateLimiter。

依赖spring-boot-starter-webflux和/spring-cloud-starter-gateway

3、Seata分布式事务

Seata

Seata将为用户提供了 AT、TCC、SAGA 和 XA 事务模式。

AT模式(Auto Transaction)

AT 模式基于支持本地ACID事务的关系型数据库,开启全局事务注解 @GlobalTransactional

包括几个部分:

TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。

TM(Transaction Manager):事务管理者。控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。

AT模式将整个分布式事务分成了两个步骤(基于两阶段提交的演变):

第一阶段:业务数据和回滚日志undolog(保存业务sql的数据快照)在同一本地事务中进行提交,释放本地所和所需资源

第二阶段:如果TC决议提交:通过将提交任务放入队列中,异步删除回滚日志

如果TC决议回滚:通过一阶段的回滚日志undolog(解析里面的数据生成反向SQL)进行反向补偿,将业务数据恢复到初始状态。

AT模式分布式事务具体过程

1、服务A中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID

2、服务A中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中

3、服务A开始执行分支事务

4、服务A开始远程调用B服务,此时 XID 会根据调用链传播

5、服务B中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中

6、服务B开始执行分支事务

7、全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚

8、TC 协调其管辖之下的所有分支事务,决定是提交还是回滚

AT 模式是一种对业务无任何侵入的分布式事务解决方案。

TCC 模式

1、Try:资源的检测和预留;

2、Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;

3、Cancel:预留资源释放;

TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。

saga模式

saga模式的实现,是长事务解决方案。 Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

1、分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。

2、如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

优点:Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。

缺点:Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。

类似TCC,但是一阶段没有进行“预留”动作,而是直接提交本地事务。

XA 模式

XA 协议是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。

XA规范的基础是两阶段提交协议2PC。

在XA模式下,需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协调器,把结果给协调器。

协调器等所有分支事务操作完成、都预提交后,进行第二步;第二步:协调器通知每个数据库进行逐个commit/rollback。

其中,这个全局协调器就是XA模型中的TM角色,每个分支事务各自的数据库就是RM。

缺点:事务粒度大。高并发下,系统可用性低。因此很少使用

4、Canal分布式数据同步

canal主要是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

canal的工作原理

自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

相当于mysql的slave,和mysql的主从复制原理类似。

canal的好处

在于对业务代码没有侵入,因为是基于监听binlog日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。

配置MQ模式

配合RocketMQ或者Kafka,canal会把数据发送到MQ的topic中,然后通过消息队列的消费者进行处理。

常见用途

数据库实时备份、索引构建和实时维护(拆分异构索引、倒排索引等)、同步redis、es等。

Canal的同步过程:

4、SpringSecurity

spring security

Spring Security是Spring提供的一个安全框架,提供认证和授权功能,最主要的是它提供了简单的使用方式,同时又有很高的灵活性,简单,灵活,强大。

Security本身是一套完整的认证和授权解决方案,只是我们在做系统的时候遵循了OAuth2.0规范,引入了其实现。现在流行的前后端分离,服务端不需要保持session管理,只需要验证token合法性。使用jwt生成token,是为了直接通过解密token直接获取用户信息,简化标准OAuth2.0的操作流程,也是当前服务端架构设计的实际需要。

Authentication:用户认证,一般是通过用户名密码来确认用户是否为系统中合法主体;通过用户名和密码验证用户是否合法有效。

Authorization:用户授权,一般是给系统中合法主体授予相关资源访问权限;就是权限管理和访问控制。

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。在过滤器链中定义OAuth2.0的或者Jwt的具体实现。

spring security 的核心功能主要包括:

认证 (你是谁)

授权 (你能干什么)

攻击防护 (防止伪造身份)

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

OAuth2.0的流程

是一种授权协议,是规范,不是实现。

角色:资源所有者,客户端(第三方应用),授权服务器,资源服务器

Spring Security OAuth2:Spring 对 OAuth2 的开源实现。

具体案例如百度开发平台,微信开发平台

主要是用来获取用户的信息

这里的令牌或者token仅仅是一个标识,不包含用户信息

OAuth 认证比较常见的就是微信登录、微博登录、qq登录等。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

Jwt

就是一个包含用户信息的JSON经过加密过后的字符串。

JSON Web Token // 是一种具体的Token实现框架,是基于token的认证协议的实现

主要用来生成token,验证token合法性,是否过期,获取用户信息。

jwt包括三部分:

1、 header头部,主要是JWT的类型和加密算法

// 包括类别(typ)、加密算法(alg);

{


“alg”: “HS256”,

“typ”: “JWT”

}

jwt的头部包含两部分信息:

声明类型,这里是jwt

声明加密的算法 通常直接使用 HMAC SHA256

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2、 payload载体,主要是用户数据

// 包括需要传递的用户信息;

{ “iss”: “Online JWT Builder”,

“iat”: 1416797419,

“exp”: 1448333419,

“aud”: “www.gusibi.com”,

“sub”: “uid”,

“nickname”: “goodspeed”,

“username”: “goodspeed”,

“scopes”: [ “admin”, “user” ]

}

将上面的JSON对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0

信息会暴露:由于这里用的是可逆的base64 编码,所以第二部分的数据实际上是明文的。我们应该避免在这里存放不能公开的隐私信息。

3、signature签名,用于验证身份

// 根据alg算法与私有秘钥进行加密得到的签名字串;

// 这一段是最重要的敏感信息,只能在服务端解密;

HMACSHA256(

base64UrlEncode(header) + “.” +

base64UrlEncode(payload),

SECREATE_KEY

)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript

var encodedString = base64UrlEncode(header) + ‘.’ + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, ‘secret’); // pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0I

将这三部分用.连接成一个完整的字符串,构成了最终的jwt。

JWT是一个怎样的流程?

1、客户端使用账户密码请求登录接口

2、登录成功后返回JWT

3、客户端再次请求其他接口时带上JWT

4、服务端接收到JWT后验证签名的有效性

总结

security是基础,OAuth和Jwt是具体实现,在security上生效

OAuth2.0是规范

jwt是token的实现

如果我们的系统要给第三方做授权,就实现OAuth2.0

如果我们要做前后端分离,就实现token就可以了,jwt仅仅是token的一种实现方式

RBAC 模型的权限设计

RBAC 权限控制主要有5张表(当然可以更多,这里只列出核心内容):用户表 t_user,角色表 t_role,权限表:t_permission,用户-角色关联表:rel_user_role,角色-权限关联表:rel_role_per。

5、dubbo

Dubbo是什么

Dubbo是一款高性能、轻量级的开源 RPC 框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。

Dubbo核心功能

1、Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。

2、Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。

3、Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

Dubbo核心组件

1、Provider:暴露服务的服务提供方

2、Consumer:调用远程服务消费方

3、Registry:服务注册与发现注册中心

4、Monitor:监控中心和访问调用统计

5、Container:服务运行容器

Dubbo服务器注册与发现的流程

1、服务容器Container负责启动,加载,运行服务提供者。

2、服务提供者Provider在启动时,向注册中心注册自己提供的服务。

3、服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。

4、注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

5、服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

6、服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。

Dubbo的整体架构设分层

1、 接口服务层(Service)

该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现

2、 配置层(Config)

对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心

3、 服务代理层(Proxy)

服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory

4、 服务注册层(Registry)

封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService

5、 路由层(Cluster)

封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce

6、 监控层(Monitor)

RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService

7、 远程调用层(Protocal)

封装RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter

8、 信息交换层(Exchange)

封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer

9、 网络 传输 层(Transport)

抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec

10、数据序列化层(Serialize)

可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool

Dubbo Monitor 实现原理

Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。

1、MonitorFilter 向 DubboMonitor 发送数据

2、DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap<Statistics, AtomicReference> statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference

3、SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000)

4、SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写)

5、SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表

Dubbo和Spring Cloud区别

1、Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传输的,配合以 Hession 序列化完成 RPC 通信。

2、Spring Cloud 是基于 Http 协议 Rest 接口调用远程过程的通信,相对来说 Http 请求会有更大的报文,占的带宽也会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖

Dubbo 和 Dubbox区别

当当网基于 Dubbo 做的一个扩展项目,如加了服务可 Restful 调用,更新了开源组件等。

Dubbo 有哪些注册中心

1、Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。

2、Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。

3、Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。

4、Simple 注册中心。

推荐使用 Zookeeper 作为注册中心

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

可以通讯。启动 Dubbo 时,消费者会从 Zookeeper 拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。

Dubbo集群负载均衡策略

1、Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。

2、RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题。

3、LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求。

4、ConstantHash LoadBalance: 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。

默认为 Random 随机调用。

Dubbo的集群容错方案

1、Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。

2、Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

3、Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

4、Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

5、Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。

6、Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。

默认的容错方案是 Failover Cluster。

Dubbo配置文件是如何加载到 Spring 中的

Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的 schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象

Dubbo核心的配置

1、dubbo:service/服务配置:用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心

2、dubbo:reference/引用配置:用于创建一个远程服务代理,一个引用可以指向多个注册中心

3、dubbo:protocol/协议配置:用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受

4、dubbo:application/应用配置:用于配置当前应用信息,不管该应用是提供者还是消费者

5、dubbo:module/模块配置:用于配置当前模块信息,可选

6、dubbo:registry/注册中心配置:用于配置连接注册中心相关信息

7、dubbo:monitor/监控中心配置:用于配置连接监控中心相关信息,可选

8、dubbo:provider/提供方配置:当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选

9、dubbo:consumer/消费方配置:当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选

10、dubbo:method/方法配置:用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息

11、dubbo:argument参数配置:用于指定方法参数配置

如果是SpringBoot项目就只需要注解,或者开Application配置文件!

Dubbo 超时设置有两种方式

1、服务提供者端设置超时时间

2、服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。

dubbo 在调用服务不成功时,默认是会重试两次。

Dubbo通信协议

默认使用 Netty 作为通讯框架。

Dubbo支持哪些协议

1、Dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo推荐使用dubbo协议。

2、RMI: 采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。

3、HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。

4、Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。

5、WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。

6、Memcache:基于 Memcache实现的 RPC 协议。

7、Redis:基于 Redis 实现的RPC协议。

Dubbo 用到哪些设计模式

工厂模式

装饰器模式 (服务和消费的调用链)

观察者模式 (注册与发现)

动态代理模式 (调用实现类的获取)

Dubbo 服务降级

以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑

Dubbo SPI 和 Java SPI 区别

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

JDK SPI

JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了

Dubbo SPI

1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码

2、延迟加载,可以一次只加载自己想要加载的扩展实现。

3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。

Dubbo 支持分布式事务吗

目前暂时不支持,可与通过 tcc-transaction 框架实现

tcc-transaction 是开源的 TCC 补偿性分布式事务框架

TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。

Dubbo 支持哪些序列化方式

默认使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。

Dubbo 在安全方面有哪些措施

Dubbo 和 Zookeeper 基本都是部署在内网,不对外网开放

Zookeeper 的注册可以添加用户权限认证

Dubbo 通过 Token 令牌防止用户绕过注册中心直连

在注册中心上管理授权

增加对接口参数校验

提供IP、服务黑白名单,来控制服务所允许的调用方

服务提供者失效踢原理

出基于 zookeeper 的临时节点原理

同一个服务多个注册的情况下可以直连某一个服务吗

可以点对点直连,修改配置即可,也可以通过 telnet 直接某个服务。

Dubbo 使用过程中都遇到了些什么问题

在注册中心找不到对应的服务,检查 service 实现类是否添加了@service 注解无法连接到注册中心,检查配置文件中的对应的测试 ip 是否正确

RPC框架需要解决的问题?

1、如何确定客户端和服务端之间的通信协议?

2、如何更高效地进行网络通信?

3、服务端提供的服务如何暴露给客户端?

4、客户端如何发现这些暴露的服务?

5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作?

RPC的实现基础

1、需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架;

2、需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架;

3、可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等;

4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能;

RPC使用了哪些关键技术

1、动态代理

2、序列化和反序列化

3、NIO通信

4、服务注册中心

主流RPC框架有哪些

1、RMI

2、Hessian

3、Dubbo

4、protobuf-rpc-pro

5、Thrift

6、Avro

RPC调用服务的基本步骤

1、建立通信

2、服务寻址

3、网络传输

4、服务调用

6、zookeeper

ZooKeeper定义

ZooKeeper,它是一个开放源码的分布式协调服务,它是一个集群的管理者,它将简单易用的接口提供给用户。

zookeeper的数据模型

zookeeper的节点统一叫做znode,它是可以通过路径来标识(类似文件存放路径)

Znode包含了存储数据data、访问权限acl、子节点引用child、节点状态信息stat

data: znode存储的业务数据信息

acl: 记录客户端对znode节点的访问权限,如IP等。

child: 当前节点的子节点引用

stat: 包含Znode节点的状态信息,比如事务id、版本号、时间戳等等。

为了保证高吞吐和低延迟,以及数据的一致性,znode只适合存储非常小的数据,不能超过1M,最好都小于1K。

znode节点的4种类型:

1、持久节点

2、临时节点

3、持久顺序节点

4、临时顺序节点

Zookeeper的功能:可以基于Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Zookeeper的用途:命名服务、配置管理、集群管理、分布式锁、队列管理。

命名服务

指通过指定的名字来获取资源或者服务地址。Zookeeper可以创建一个全局唯一的路径,这个路径就可以作为一个名字。用于集群中的机器,服务的地址,或者是远程的对象等。(用作注册中心)

配置管理

把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即znode会发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

集群管理

就是监控集群机器状态,实时监控znode节点的变化,剔除机器和加入机器。

Watcher监听机制原理

Zookeeper 允许客户端向服务端的某个Znode注册一个Watcher监听,同时Watcher对象存储在客户端的WatchManager中,当服务端的一些指定事件触发了这个Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端从 WatcherManager 中取出对应的 Watcher 对象,根据Watcher通知状态和事件类型做出业务上的改变。

Zookeeper 保证了如下分布式一致性特性:

顺序一致性:从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。

原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。

单一视图:无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。

可靠性: 一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来。

实时性(最终一致性): Zookeeper 仅仅能保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。

zookeeper是如何保证事务的顺序一致性

事务ID,即zxid。zxid由Leader节点生成,有新写入事件时,Leader生成新zxid并随提案一起广播,每个结点本地都保存了当前最近一次事务的zxid,zxid是递增的,所以谁的zxid越大,就表示谁的数据是最新的。

Zookeeper 服务器角色

Leader

Leader服务器是整个ZooKeeper集群工作机制中的核心,其主要工作:

事务请求的唯一调度和处理者,保证集群事务处理的顺序性

集群内部各服务的调度者

Follower

Follower服务器是ZooKeeper集群状态的跟随者,其主要工作:

处理客户端非事务请求,转发事务请求给Leader服务器

参与事务请求Proposal的投票

参与Leader选举投票

Observer

观察者角色,观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来,其主要工作:

处理客户端的非事务请求,转发事务请求给 Leader 服务器

不参与任何形式的投票

Zookeeper下Server工作状态

1、LOOKING:寻找Leader状态。它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。

2、FOLLOWING:跟随者状态。表明当前服务器角色是Follower。

3、LEADING:领导者状态。明当前服务器角色是Leader。

4、OBSERVING:观察者状态。表明当前服务器角色是Observer。

ZooKeeper集群是一主多从的结构:

如果是写入数据,先写入主服务器(主节点),再通知从服务器。

如果是读取数据,既读主服务器的,也可以读从服务器的。

ZooKeeper如何保证主从节点数据一致性

Zookeeper是采用ZAB协议(Zookeeper Atomic Broadcast,Zookeeper原子广播协议)来保证主从节点数据一致性的,ZAB协议支持崩溃恢复和消息广播两种模式

崩溃恢复:Leader挂了,进入该模式,选一个新的leader出来

消息广播: 把更新的数据,从Leader领导者同步到所有Follower跟随着

ZooKeeper选举机制

服务器启动时选举

服务器(myid=1-n)依次启动,进行选举

1、每个服务器发出一个投票投自己,投票内容:服务器的myid和ZXID

2、接受来自各个服务器的投票

3、处理投票,优先检查ZXID。ZXID比较大的服务器优先作为leader。如果ZXID相同的话,就比较myid,myid比较大的服务器作为leader

4、统计投票,票数大于等于n/2+1(过半机制),当选为Leader

服务器运行时选举(leader挂了)

1、变更状态,跟随者变更为LOOKING寻找leader状态。

2、每个服务器发起投票,每个服务器都把票投给自己

3、接受来自各个服务器的投票

4、处理投票,优先检查ZXID(事务ID),大的优先作为Leader,如果ZXID相同的话,就比较myid,myid比较大的服务器作为leader。(myid为节点服务器自己的id数字)

5、统计投票

6、改变服务器状

启动时和运行时选举区别:

启动时和运行时选举类似,启动时选举每次启动需要发起一次选举,统计投票需要判断是否过半机制。运行时选举发起一次就可以完成选举。

ZooKeeper实现分布式锁

Zk实现分布式锁(公平锁)

1、客户端发起一个加锁请求,lock锁节点(持久节点)下创建一个临时顺序节点,顺序节点有一个递增的节点序号

2、判断是否是第一个临时顺序节点,如果是则获得锁,如果不是则加一个监听器监听它的上一个节点

3、释放锁,删除顺序节点,zk通知监听器节点删除,客户端尝试获取锁

4、zk感知到那个客户端宕机,会自动删除对应的临时顺序节点

Zk实现分布式锁(非公平锁)

多个客户端获取锁,是在一个节点下争抢创建一个同名的一个节点,比如lock,谁创建成功谁就获取

zk和redis分布式锁区别

1、redis分布式锁的设计并不是强一致性(AP模式),即便使用redlock实现,也无法保证其实现100%没有问题。要不断去尝试获取锁,比较消耗性能,redis获取锁的那个客户端者挂了,那么只能等待超时时间之后才能释放锁。

2、zk分布式锁是强一致性(CP模式),获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小;zk创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁。

zookeeper和eureka 注册中心的区别

1、zookeeper CP模式(强一致性)

zookeeper在选举leader时,会停止服务,直到选举成功之后才会再次对外提供服务,这个时候就说明了服务不可用,保证了一致性和分区容错性。

2、eureka AP模式(可用性)

Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务,保证了可用性和分区容错性,实现最终一致性。

dubbo为什么选择Zookeeper作为注册中心

1、命名服务,服务提供者向Zookeeper指定节点写入url,完成服务发布。

2、负载均衡,注册中心的承载能力有限,而Zookeeper集群配合web应用很容易达到负载均衡。

3、zk支持监听事件,特别适合发布/订阅的场景,dubbo的生产者和消费者就类似这场景。

4、数据模型简单,数据存在内存,可谓高性能



第四章 数据库MySQL

事务的基本要素(ACID)

1、 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做

原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏

3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。隔离性由MVCC多版本并发控制来保证

4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚

事务的并发问题

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:A在操作数据的时候,这时B新增或删除数据,就好像发生了幻觉一样,这就叫幻读。

注意:不可重复读侧重于修改,幻读侧重于新增或删除,解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

MySQL的四种事务隔离级别

1、读未提交(read-uncommitted),会出现脏读、不可重复读、幻读问题。

2、不可重复读(read-committed),会出现不可重复读、幻读问题。

3、可重复读(repeatable-read),会出现幻读问题。

4、串行化(serializable)

mysql默认的事务隔离级别为可重复读(repeatable-read)

mysql存储引擎

1、innodb

从Mysql5.5版本开始,InnoDB是默认的表存储引擎

支持事物、行级锁、外键,不支持全文索引,通过MVCC来支持高并发,索引和数据存储在一起(聚簇索引)

2、myisam

不支持事务和行级锁(是表锁)、外键,支持全文检索,索引和数据是分开存储的

mysql的索引

聚簇索引

即将数据存入索引叶子页面上,数据和索引在一起存储的索引方式。一张表上最多只能创建一个聚集索引

非聚簇索引

叶子页面上存储索引列和数据存储的地址信息

InnoDB默认对主键建立聚簇索引, 但是InnoDB引擎上表其他字段加索引(二级索引),那么这个索引叶子页面则只会存储主键id,属于非聚簇索引。myisam使用非聚集索引,叶子页面存储数据地址。

索引按照数据结构来说:

B+树

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接,是有序的。B+树使用聚簇索引,节点只包含id索引列,而叶子节点包含索引列和数据,一个叶子节点可以存储多个索引列和数据。

B+树比B树更适合做索引

1、B+树的查询更加稳定,因为非终端结点仅仅是作为叶子结点中关键字的索引,并不存储数据,所有的关键字的查找都会走一条从根到叶子结点的路径。

2、B+树的磁盘读写代价更低,B+树叶结点增加的链指针,只需要去遍历叶子节点就可以实现整棵树的遍历,加强了区间访问性,可使用在区间查询的场景;而使用B树则无法进行区间查找

Hash索引

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可,是无序的。

没有大量重复键值时,等值查询,哈希索引效率更高,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。

等值查询:select id, name from table where name=‘李明’;

哈希索引不适用的场景:

不支持范围查询

不支持索引完成排序

不支持联合索引的最左前缀匹配规则

联合索引的最左前缀原则:

以index (a,b,c)为例建立这样的联合索引相当于建立了索引a、ab、abc三个索引。最左优先,先优先匹配最左边的索引。

常用的 InnoDB 引擎中默认使用的是B+树索引,它会实时监控表上索引的使用情况

如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引(在InnoDB中默认开启自适应哈希索引)。

like操作和%的通配符操作也不适用于自适应哈希索引,可能要关闭自适应哈希索引。

回表查询

先通过二级索引(非聚簇索引)的值定位到聚簇索引值,在通过聚簇索引的值定位到行记录数据,要通过扫描两次索引B+树,它的性能较扫描一次较低。

索引覆盖

查询的列数据都能被索引覆盖,无需回表,速度更快。(查询的字段都建立了索引)

锁的类型

按锁级别分类分,共享锁和排他锁,也叫做读锁和写锁。

从颗粒度来区分,可以分为表锁和行锁两种。

锁又可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。

MVCC

MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。

MVCC实现原理

1、隐式字段

每一行记录有隐藏字段,DB_TRX_ID事务ID,DB_ROW_ID隐含的自增ID,B_ROLL_PTR回滚指针,用于配合undo日志,指向上一个旧版本。

每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。

2、undolog日志

当我们对数据进行操作的时候,就会产生undo log记录,记录了老版本的数据。从而能够读取之前版本的数据,MVCC就是基于Undo log实现的。

INSERT没有undo log记录,UPDATE和DELETE操作产生的undo log

MVCC解决读写冲突,乐观锁解决写写冲突,提高了数据库并发读写的性能

3、Read View读视图

事务进行快照读操作的时候,数据库为该行数据生成一个Read View读视图,会生成数据库系统当前的一个快照,分配一个ID

在RR级别(可重复读epeatable-read)下,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。

在RC级别(不可重复读read-committed)下的,事务中,每次快照读都会新生成一个快照,并获取最新的Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

分库分表

垂直分库: 比如用户、订单、商品、库存等

垂直分表:订单分为基础信息、订单拓展、收获地址

水平分表

首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),

数据支持某段时间内的查询,这段时间内的数据分为1024张表,可以用user_id作为sharding_key,比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。

使用MyCat实现水平分片策略

1、求模算法

2、分片枚举

3、日期指定

主从复制

原理:

1、master提交完事务后,写入binlog

2、slave连接到master

3、master创建dump线程,推送binglog到slave

4、slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中

5、slave再开启一个sql线程读取relay log事件并在slave执行,完成同步

6、slave记录自己的binglog

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

Mysql读写分离

通过Mycat,即中间件会分析对应的SQL,写操作时会连接主数据库,读操作时连接从数据库。

Mycat 原理主要是通过对SQL的拦截,然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等,然后将SQL发给后端真实的数据库,并将返回的结果做适当处理返回给客户端。

explain

可以使用explain+SQL语句来模拟优化器执行SQL查询语句,从而知道mysql是如何处理sql语句的。

explain简单示例

mysql>explain select * from t_user;

explain执行计划中包含的信息如下:

id: 查询序列号

select_type: 查询类型

table: 表名或者别名

partitions: 匹配的分区

type: 访问类型, 如果有类型是 all 时,表示预计会进行全表扫描

possible_keys: 可能用到的索引, possible_keys列有结果,而 后面的key列显示 null 的情况,这是因为此时表中数据不多,优化器认为查询索引对查询帮助不大,所以没有走索引查询而是进行了全表扫描。

key: 实际用到的索引, 如果没有使用索引,则该列是 null。

key_len: 索引长度

ref: 与索引比较的列

rows: 估算的行数

filtered: 按表条件筛选的行百分比

Extra: 额外信息

Mysql性能优化

1、选择合适的存储引擎

2、合理使用索引

3、优化SQL语句

4、配置mysql性能优化参数(最大连接数,等待空闲时间,引擎,缓冲区大小等等)。

5、优化数据表结构、字段类型、字段索引、分表,分库、读写分离等等。

6、使用缓存Redis和NoSQL数据库方式存储,来缓解高并发下数据库查询的压力。

7、减少数据库操作次数,尽量使用数据库访问驱动的批处理方法。

8、通过explain查看SQL的执行计划



第五章 redis

Redis数据结构

1、String

常用命令: set,get,decr,incr,mget 等

常规key-value缓存应用; 常规计数:微博数,粉丝数等。

2、List有序列表

常用命令: lpush,rpush,lpop,rpop,lrange等

可用作消息队列、微博的关注列表,粉丝列表

3、Set无序集合

常用命令: sadd,spop,smembers,sunion 等

可以基于 set 轻易实现交集、并集、差集的操作。如共同关注、共同粉丝、共同喜好等功能

4、Sorted Set(ZSet) 有序集合

常用命令: zadd,zrange,zrem,zcard等

和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,消息排行榜等,适合使用 Redis 中的 SortedSet 结构进行存储。

跳跃表(skiplist)是一种有序数据结构, 它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。它其实就是在链表的基础上,增加多级索引,以提高查找效率。

4、Hash

常用命令: hget,hset,hgetall 等。

Hash 是一个 string 类型的 field 和 value 的映射表,适合用于存储对象,来存储用户信息,商品信息等等。

Redis为什么这么快

1、数据存在内存中,完全基于内存操作

2、高效的数据结构

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程之间的切换消耗资源,不用去考虑各种锁的问题

4、使用多路 I/O 复用模型,非阻塞 IO

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制

Redis的应用场景

1、计数器

2、缓存

3、会话缓存

4、消息队列

5、分布式锁实现

生成业务唯一code的时候会用到redis队列,先生成一些code再放到list队列里面,业务系统再从list里面获取,保证并发安全。

Redis的优缺点

优点

1、读写性能优异

2、支持数据持久化,支持AOF和RDB两种持久化方式

3、支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。

4、数据结构丰富

5、支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

1、数据库容量受到物理内存的限制,不能用作海量数据的高性能读写

2、Redis 不具备自动容错和恢复功能

3、Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂

Redis的内存淘汰策略

全局的键空间:

1、noeviction:新写入操作会报错

2、allkeys-lru:移除最近最少使用的key(这个是最常用的)

3、allkeys-random:随机移除某个key

设置过期时间的键空间:

4、volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。

5、volatile-random:在设置了过期时间的键空间中,随机移除某个key。

6、volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis的持久化

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

1、RDB 快照持久化

快照持久化是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的RDB数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

在redis.conf配置文件中默认有此下配置:

save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

2、AOF 只追加文件

AOF持久化,则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。Redis的配置文件,默认的文件名是appendonly.aof,存在三种不同的 AOF 持久化方式:

appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度

appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘

appendfsync no #让操作系统决定何时进行同步

区别:

1、AOF文件比RDB更新频率高,优先使用AOF还原数据。

2、AOF比RDB更安全,文件也更大

3、RDB性能比AOF好

4、如果两个都配了优先加载AOF

缓存雪崩

缓存同一时间大面积的失效,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

2、使用快速失败的熔断策略,减少数据库瞬间压力。

3、利用 redis 持久化机制保存的数据尽快恢复缓存

4、使用主从模式和集群模式来尽量保证缓存服务的高可用。

缓存击穿

并发查同一条数据,缓存中没有但数据库中有的数据,并发用户特别多,造成数据库压力瞬间增大。

解决方案

1、设置热点数据永远不过期。

2、加互斥锁,保证同一个进程中针对同一个数据不会并发请求到 DB

缓存穿透

请求缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

1、采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。

2、从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒。

3、接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截

Redis的主从复制

1、Redis的从库启动后,会向主库发送 sync 命令给主库以保持通讯。

2、主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来

3、当快照完成后,主库会将快照文件和所有缓存的写命令发送给从库

4、从库收到后,加载快照文件并执行收到缓存的命令。与MySQL主从复制原理类似。

与数据库双写的一致性

1、延时双删,先删除redis,再更新数据库,再延时删除redis (不能保证实时)

2、使用阿里中间件canal实时同步数据库的数据

Redis哨兵模式(Sentinel)

Redis的哨兵 (sentinel) 系统用于管理多个 Redis 服务器,该系统执行以下三个任务:

1、监控(Monitoring):哨兵(sentinel) 会不断地检查你的 Master 和 Slave 是否运作正常。

2、提醒(Notification):当被监控的某个 Redis 出现问题时,哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。

3、自动故障转移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效 Master 的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master;当客户端试图连接失效的 Master时,集群也会向客户端返回新Master的地址,使得集群可以使用新的 Master 代替已失效 Master。

哨兵模式原理

1、通过发送命令,让Redis服务器返回监控其运行状态,以确认对方是否存活,包括主服务器和从服务器。

2、当多数哨兵监测到master宕机,通过一定的选举算法,选举一个slave自动切换成master,

然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

redis分布式锁

setnx实现分布式锁

1、如果setnx返回1,说明该进程获得锁,setnx将key的值设置为锁的超时时间(当前时间 + 锁的有效时间),同时设置一个key的失效时间。

2、如果setnx返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试setnx 操作,以获得锁。

问题:

1、如果一个进程获得锁之后,断开了与redis的连接,那么锁一直的不断释放,其他的进程就一直获取不到锁,就出现了死锁,设置一个key的失效时间避免死锁。

2、如果检测到锁已超时,即当前的时间大于key的值,由于多个进程同时去获得锁的问题。

执行getset操作设置key的新值,如果返回null,说明锁被释放可以获得锁,如果返回key的旧值,小于当前时间可以获得锁,大于当前时间,说明其他线程已经获得了锁。

redisson现分布式锁

代码示例:RLock lock = redisson.getLock(“myLock”);

lock .lock();

lock.unlock();

1、加锁机制,客户端根据hash算法选择一个redis节点,发送一段lua脚本到redis上执行(原子性),判断是否可以加锁。

lua脚本的判断逻辑:

1、判断key“myLock”是否存在

2、如果不存在,则在其下设置一个字段为“8743c9c0-0795-4907-87fd-6c719a6b4586:1”,值为“1”的键值对 ,并设置它的过期时间

3、如果存在,则进一步判断“8743c9c0-0795-4907-87fd-6c719a6b4586:1”是否存在,若存在,则其值加1,并重新设置过期时间

4、返回“myLock”的生存时间(毫秒)

KEYS[1]代表加锁的key“myLock”

ARGV[1]代表的就是锁key的默认生存时间,默认30秒

ARGV[2]代表的是加锁的客户端的ID为uuid+线程id,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1

加锁成功后,为一个hash数据结构,加锁就是用“hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1”命令

myLock:

{


“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:1 //后面的1为加锁的次数(实现可重入锁)

}

2、watch dog自动延期机制,锁key默认生存时间只有30秒,一旦加锁成功,就会启动一个后台线程watch dog看门狗,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,看门狗的默认续期时间是30s

3、解锁机制,同样执行一段lua脚本,解锁成功后,向Channel中广播一条消息0,通知那些等待获取锁的线程现在可以获得锁

master宕机故障转移问题

在redis master实例完成加锁,异步复制给对应的slave实例宕机,slave变为了新的master,其他客户端在新的master上完成枷锁,可能导致多个客户端同时完成加锁。

如果能够过生存时间再升级为主master(延迟升级)后,或者立刻升级为主master但是过生存的时间后再执行获取锁的任务(延迟重启) ,就能成功产生互斥效果

RedLock现分布式锁

假设有N个完全独立的redis主服务器,TTL为锁key的生存时间

1、获取当前时间戳

2、client尝试按照顺序使用相同的key和value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。

比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁

3、client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有N/2+ 1(过半机制)个redis实例成功获取锁,才算真正的获取锁成功

4、如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);

5、如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于N/2+ 1个锁,必须释放,否则影响其他client获取锁

RedLock性能及崩溃恢复的相关解决方法

1、如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性

2、当我们重启redis后,由于AOF持久化方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用RDB持久化 (每一个写命令都同步到硬盘)造成性能急剧下降

有效解决:redis同步到磁盘方式保持默认的每秒,停掉后要等待TTL生存时间后再重启(延迟重启)



第六章 消息中间件MQ

MQ的优点

异步处理 – 相比于传统的串行、并行方式,提高了系统吞吐量。

应用解耦 – 系统间通过消息通信,不用关心其他系统的处理。

流量削锋 – 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。

日志处理 – 解决大量日志传输。

消息通讯 – 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

ActiveMQ

什么是activemq

activeMQ是一种开源的,实现了JMS1.1规范的,面向消息(MOM)的中间件,为应用程序提供高效的、可扩展的、稳定的和安全的企业级消息通信。

JMS 规范

JMS是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。

activemq基本组件

1、Broker,消息代理,表示消息队列服务器实体,接受客户端连接,提供消息通信的核心服务。

2、Producer,消息生产者,业务的发起方,负责生产消息并传输给 Broker 。

3、Consumer,消息消费者,业务的处理方,负责从 Broker 获取消息并进行业务逻辑处理。

4、Topic,主题,发布订阅模式下的消息统一汇集地,不同生产者向 Topic 发送消息,由 Broker 分发到不同的订阅者,实现消息的广播。

5、Queue,队列,点对点模式下特定生产者向特定队列发送消息,消费者订阅特定队列接收消息并进行业务逻辑处理。

6、Message,消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务 数据,实现消息的传输。

消息模型

1、点对点(P2P)

在点对点模型中,一般消息由发送者将消息发送到消息队列中,然后接收者从消息队列中消费消息,消息被消费者消费之后,消息就不存在了。

如果消息发送不成功此消息默认会保存到 activemq 服务端直到有消费者将其消费, 所以此时消息是不会丢失的。

2、发布订阅模型(Pub/Sub)

在发布订阅模型中,发布者通常将消息发布到主题(topic)中,然后,订阅者通过订阅主题来消费消息,发布订阅模型的消息是可以被多次消费的!默认情况下只通知一次, 如果接收不到此消息就没有了。 这种场景只适用于对消息送达率要求不高的情况。 如果要求消息必须送达不可以丢失的话, 需要配置持久化订阅,消息会持久化到服务端(就是硬盘上)。

作为subscriber ,在接收消息时有两种方法,destination的receive方法,和实现message listener 接口的onMessage 方法。

两种模式的区别

1、P2P在发送者和接收者之间没有时间上的依赖性,也就是说发送者发送了消息之后,不管接收者有没有运行,不会影响消息发送到队列,而Pub/Sub模式有时间上的依赖性,消费者必须先订阅主题,才能够消费消息。

2、P2P模式的每个消息只能有一个消费者,消费完了消息就不存在了,Pub/Sub模式可以有多个消费者。

ActiveMQ服务器宕机怎么办?

非持久化消息是存储在内存中的,持久化消息是存储在文件中的。但是,在非持久化消息堆积到一定程度,内存告急的时候,ActiveMQ会将内存中的非持久化消息写入临时文件中,以腾出内存。虽然都保存到了文件里,但它和持久化消息的区别是,重启后持久化消息会从文件中恢复,非持久化的临时文件会直接删除。

解决方案:使用持久化消息,尽量不要用非持久化消息,非要用的话,将临时文件限制尽可能的调大。

丢消息怎么办?

解决方案:用持久化消息,或者非持久化消息及时处理不要堆积,或者启动事务,启动事务后,commit。方法会负责任的等待服务器的返回,也就不会关闭连接导致消息丢失了。

持久化消息非常慢

默认的情况下,非持久化的消息是异步发送的,持久化的消息是同步发送的。遇到慢一点的硬盘,发送消息的速度是无法忍受的。但是在开启事务的情况下,消息都是异步发送的,效率会有2个数量级的提升。所以在发送持久化消息时,请务必开启事务模式。其实发送非持久化消息时也建议开启事务,因为根本不会影响性能。

消息的不均匀消费

有时在发送一些消息之后,开启2个消费者去处理消息。会发现一个消费者处理了所有的消息,另一个消费者根本没收到消息。原因在于ActiveMQ的prefetch机制。当消费者去获取消息时,不会一条一条去获取,而是一次性获取一批,默认是1000条。如果某一个消费者全部获取完,但是消费者既不消费确认,又不崩溃,那这些消息就永远躺在消费者的缓存区里无法处理。

解决方案:将prefetch设为1,每次处理1条消息,处理完再去取。

死信队列

如果你想在消息处理失败后,不被服务器删除,还能被其他消费者处理或重试,可以关闭AUTO_ACKNOWLEDGE,将ack交由程序自己处理。那如果使用了AUTO_ACKNOWLEDGE,如果一条消息不能被处理,会被退回服务器重新分配,在重试6次后,将会把消息丢到死信队列里。

重发时间间隔和重发次数

initialRedeliveryDelay 默认为1秒,重发时间间隔

lmaximumRedeliveries默认值6,最大重传次数,达到最大重连次数后拋出异常。为-1时不限制次数,为0时表示不进行重传。

RabbitMQ

什么是RabbitMQ

RabbitMQ是一款开源的,Erlang编写的,消息中间件; 最大的特点就是消费并不需要确保提供方存在,实现了服务之间的高度解耦 可以用它来:解耦、异步、削峰。

rabbitmq 的使用场景

1、服务间异步通信

2、顺序消费

3、定时任务

4、请求削峰

RabbitMQ基本概念

Broker: 简单来说就是消息队列服务器实体

Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列

Queue: 消息队列载体,每个消息都会被投入到一个或多个队列

Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来

Routing Key: 路由关键字,exchange根据这个关键字进行消息投递

VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。

Producer: 消息生产者,就是投递消息的程序

Consumer: 消息消费者,就是接受消息的程序

Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。

使用@RabbitListener来消费消息。

RabbitMQ的工作模式

1、 fanout

把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。

2、 direct

把消息路由到BindingKey和RoutingKey完全匹配的队列中。

3、topic

匹配规则:

RoutingKey 为一个 点号’.’: 分隔的字符串。 比如: java.xiaoka.show

BindingKey和RoutingKey一样也是点号“.“分隔的字符串。

BindingKey可使用 * 和 # 用于做模糊匹配,*匹配一个单词,#匹配多个或者0个

4、headers

不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配。性能差,基本用不到。

生产者发送消息过程

1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。

2.Producer声明一个交换器并设置好相关属性。

3.Producer声明一个队列并设置好相关属性。

4.Producer通过路由键将交换器和队列绑定起来。

5.Producer发送消息到Broker,其中包含路由键、交换器等信息。

6.相应的交换器根据接收到的路由键查找匹配的队列。

7.如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。

8.关闭信道。

9.管理连接。

消费者接收消息过程

1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。

2.向Broker请求消费响应的队列中消息,可能会设置响应的回调函数。

3.等待Broker回应并投递相应队列中的消息,接收消息。

4.消费者确认收到的消息,ack。

5.RabbitMq从队列中删除已经确定的消息。

6.关闭信道。

7.关闭连接。

死信队列

DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。

导致的死信的几种原因:

消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。

消息TTL过期。

队列满了,无法再添加。

优先级队列

存储对应的延迟消息,指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

优先级高的队列会先被消费。

可以通过x-max-priority参数来实现。

当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。

RabbitMQ中消息的几种状态

alpha: 消息内容(包括消息体、属性和 headers) 和消息索引都存储在内存中 。

beta: 消息内容保存在磁盘中,消息索引保存在内存中。

gamma: 消息内容保存在磁盘中,消息索引在磁盘和内存中都有 。

delta: 消息内容和索引都在磁盘中 。

消息基于什么传输

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。channel信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

如何保证RabbitMQ消息的顺序性

1、拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;

2、或者就一个 queue (消息队列)但是对应多个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 工作线程worker来处理。

如何保证消息不被重复消费

原因:

先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;

但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。

解决:

针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;

1、在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;

2、在业务系统做消息的等幂性处理

如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?

发送方确认模式

将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

接收方确认机制

消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性

特殊情况

如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)

如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。

如何保证RabbitMQ消息的可靠传输?

丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;

1、生产者丢失消息:从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息;

transaction机制就是说:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降;

confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后;

rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

2、消息队列丢数据:消息持久化。

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。

这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢?

1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列

2. 发送消息的时候将deliveryMode=2

这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据

3、消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;

解决方案:处理消息成功后,手动回复确认消息。

如何处理消息堆积

1、修复Consumer不消费问题,比如:一条一条消息消费处理改用批量处理

2、考虑水平扩容,增加临时队列,增加Topic的队列数和消费者数量

如何保证高可用的?

RabbitMQ 的集群

RocketMQ

RocketMq消息模型(专业术语)

Message

就是要传输的消息,一个消息必须有一个主题,一条消息也可以有一个可选的Tag(标签)和额外的键值对,可以用来设置一个业务的key,便于开发中在broker服务端查找消息。

Topic

主题,是消息的第一级类型,每条消息都有一个主题

Tag

标签,是消息的第二级类型,可以作为某一类业务下面的二级业务区分,它的主要用途是在消费端的消息过滤。

Group

组,可分为ProducerGroup生产者组和ConsumerGroup消费者组,一个组可以订阅多个Topic。一般来说,某一类相同业务的生产者和消费者放在一个组里。

Message Queue

消息队列,一个Topic可以划分成多个消息队列。Topic只是个逻辑上的概念,消息队列是消息的物理管理单位,当发送消息的时候,Broker会轮询包含该Topic的所有消息队列,然后将消息发出去。有了消息队列,可以使得消息的存储可以分布式集群化,具有了水平的扩展能力。

offset

是指消息队列中的offset,可以认为就是下标,消息队列可看做数组。

顺序消息

RocketMQ 通过自己的方式解决了消息顺序性的问题:

RocketMQ通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。根据不同业务,可以将业务ID作为计算队列,让业务ID相同的消息先后发送到同一个队列中,在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个OrderId获取到的肯定是同一个队列。

RocketMQ给我们提供了MessageQueueSelector接口,可以重写里面的接口,实现自己的算法,比如判断i%2==0,那就发送消息到queue1否则发送到queue2。

消息重复

1、消费端处理消息的业务逻辑保持幂等性

2、利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。

业务端去重:

1、记录下每个消息的msgID

2、新消息来的时候,查看该消息的msgID是否已记录,是则抛弃,否则消费 然后将msgID 存入缓存中,如果当再有此消息发送过来,查看redis中是否存在键值,如存在,则对此消息不进行处理。

事务消息

适用场景

交易系统下单之后,发送一条交易下单的消息到消息队列 RocketMQ,购物车系统订阅消息队列 RocketMQ 的交易下单消息,做相应的业务处理,更新购物车数据。

消息队列 RocketMQ 事务消息交互流程如下所示: 事务消息

1、发送方向消息队列 RocketMQ 服务端发送消息。

2、服务端将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。

3、发送方开始执行本地事务逻辑。

4、发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。

5、在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。

6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

7、发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半消息进行操作。

说明:事务消息发送对应步骤 1、2、3、4,事务消息回查对应步骤 5、6、7。

注意事项

1、事务消息的 Group ID 不能与其他类型消息的 Group ID 共用。与其他类型的消息不同,事务消息有回查机制,回查时消息队列 RocketMQ 服务端会根据 Group ID 去查询客户端。

2、通过 ONSFactory.createTransactionProducer 创建事务消息的 Producer 时必须指定 LocalTransactionChecker 的实现类,处理异常情况下事务消息的回查。

3、事务消息发送完成本地事务后,可在 execute 方法中返回以下三种状态:

TransactionStatus.CommitTransaction 提交事务,允许订阅方消费该消息。

TransactionStatus.RollbackTransaction 回滚事务,消息将被丢弃不允许消费。

TransactionStatus.Unknow 暂时无法判断状态,期待固定时间以后消息队列 RocketMQ 服务端向发送方进行消息回查。

4、可通过以下方式给每条消息设定第一次消息回查的最快时间:

Message message = new Message();

// 在消息属性中添加第一次消息回查的最快时间,单位秒。例如,以下设置实际第一次回查时间为 120 秒 ~ 125 秒之间

message.putUserProperties(PropertyKeyConst.CheckImmunityTimeInSeconds,“120”);

// 以上方式只确定事务消息的第一次回查的最快时间,实际回查时间向后浮动0~5秒;如第一次回查后事务仍未提交,后续每隔5秒回查一次。

Producer如何发送消息

1、 可靠同步发送

同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。

2、可靠异步发送

异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。

3、单向(Oneway)发送

单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。

消息实时性

RocketMQ采取长轮询+PULL模式保证消息的持久性

消息订阅

1、集群订阅

同一个 Group ID 所标识的所有 Consumer 平均分摊消费消息。 例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在集群消费模式下每个实例平均分摊,只消费其中的 3 条消息。

2、广播订阅

同一个 Group ID 所标识的所有 Consumer 都会各自消费某条消息一次。 例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在广播消费模式下每个实例都会各自消费 9 条消息。

主从同步策略

1、多 Master 多 Slave 模式,异步复制

每个 Master 配置一个 Slave,有多对Master-Slave,HA(主从)采用异步复制方式,主备有短暂消息延迟,毫秒级。

2、多 Master 多 Slave 模式,同步双写

每个 Master 配置一个 Slave,有多对Master-Slave,HA(主从)采用同步双写方式,主备都写成功,向应用返回成功。

定时消息和延时消息

定时消息:Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。

延时消息:Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

消息的刷盘策略

1、异步刷盘(ASYNC_FLUSH)

返回成功状态时,消息只是被写入内存 pagecache(磁盘高速缓存),写操作返回快,吞吐量达,当内存里的消息积累到一定程度时,统一出发写磁盘动作,快速写入。

2、同步刷盘(SYNC_FLUSH)

消息写入内存 pagecache(磁盘高速缓存)后,立即通知刷盘线程,刷盘完成后,返回消息写成功的状态。

消息持久性

RocketMQ收到消息后,会将消息持久化到文件,并利用Linux文件系统内存来提高性能

消息存储

RocketMQ存储模型

ocketMQ的消息的存储是由ConsumeQueue和CommitLog 配合来完成的,ConsumeQueue中只存储很少的数据,消息主体都是通过CommitLog来进行读写。

CommitLog:

是消息主体以及元数据的存储主体,对CommitLog建立一个ConsumeQueue,每个ConsumeQueue对应一个(概念模型中的)MessageQueue,所以只要有Commit Log在,Consume Queue即使数据丢失,仍然可以恢复出来。

Consume Queue:

是一个消息的逻辑队列,存储了这个Queue在CommitLog中的起始offset,log大小和MessageTag的hashCode。每个Topic下的每个Queue都有一个对应的ConsumerQueue文件,例如Topic中有三个队列,每个队列中的消息索引都会有一个编号,编号从0开始,往上递增。

RocketMQ存储特点:

零拷贝原理:Consumer 消费消息过程,使用了零拷贝,零拷贝包含以下两种方式:

1、使用 mmap + write 方式

优点:即使频繁调用,使用小块文件传输,效率也很高

缺点:不能很好的利用 DMA 方式,会比 sendfile 多消耗CPU,内存安全性控制复杂,需要避免 JVM Crash 问题。

2、使用 sendfile 方式

优点:可以利用 DMA 方式,消耗 CPU 较少,大块文件传输效率高,无内存安全新问题。

缺点:小块文件效率低亍 mmap 方式,只能是 BIO 方式传输,不能使用 NIO。

RocketMQ 选择了第一种方式,mmap+write 方式,因为有小块数据传输的需求,效果会比 sendfile 更好。

RocketMQ 数据存储结构:

消息可靠性

生产者的可靠性保证:生产者发送消息后返回SendResult,如果isSuccess返回true,则表示消息已经确认发送到服务器并被服务器接收保存。整个发送过程是一个同步过程。

服务器的可靠性:消息生产者发送的消息,RocketMQ服务收到后在做必要的校验和检查之后马上保存到磁盘,写入成功后返回给生产者。因此可以确认每条发送结果为成功的消息都会被消息服务器写入磁盘。

消费者的可靠性:消费者是一条一条顺序消费的,之后在成功消费一条后才会消费吓一跳。如果在消费某一条消息时失败则会重试消费这条消息,默认为5次,如果超过最大次数仍然无法消费,则将消息保存到本地,后台线程继续重试消费,主线程则会继续往后走,消费队列后面的消息。

消息过滤

Consumer 可以根据消息标签(Tag)对消息进行过滤,确保 Consumer 最终只接收被过滤后的消息类型。消息过滤在消息队列 RocketMQ 的服务端完成。

Kafka

Kafka核心概念

生产者:Producer 往Kafka集群生成数据

消费者:Consumer 往Kafka里面去获取数据,处理数据、消费数据Kafka的数据是由消费者自己去拉去Kafka里面的数据

主题:topic,每个主题在创建时会要求制定它的副本数(默认1)。

分区:partition 默认一个topic有一个分区(partition),自己可设置多个分区(分区分散存储在服务器不同节点上)

kafka特性

消息持久化

高吞吐量

扩展性

多客户端支持

Kafka Streams

安全机制

数据备份

轻量级

消息压缩

Kafka的集群架构

Kafka集群中,一个kafka服务器就是一个broker,Topic只是逻辑上的概念,partition在磁盘上就体现为一个目录。

Consumer Group:消费组 消费数据的时候,都必须指定一个group id,指定一个组的id假定程序A和程序B指定的group id号一样,那么两个程序就属于同一个消费组。

Controller:Kafka节点里面的一个主节点,借助zookeeper。

Kafka二分查找定位数据

Kafka里面每一条消息,都有自己的offset(相对偏移量),存在物理磁盘上面,在position Position:物理位置(磁盘上面哪个地方)也就是说一条消息就有两个位置:offset:相对偏移量(相对位置)position:磁盘物理位置稀疏索引: Kafka中采用了稀疏索引的方式读取索引,kafka每当写入了4k大小的日志(.log),就往index里写入一个记录索引。其中会采用二分查找。

Kafka磁盘顺序写保证写数据性能

kafka写数据:顺序写,往磁盘上写数据时,就是追加数据,没有随机写的操作。生产者生产消息,经过kafka服务先写到os cache 内存中,然后经过sync顺序写到磁盘上。

Kafka零拷贝机制保证读数据高性能

消费者读取数据流程:

1、消费者发送请求给kafka服务

2、kafka服务去os cache缓存读取数据(缓存没有就去磁盘读取数据)

3、从磁盘读取了数据到os cache缓存中

4、os cache复制数据到kafka应用程序中

5、kafka将数据(复制)发送到socket cache中

6、socket cache通过网卡传输给消费者

Kafka日志分段保存

一个分区下面默认有n多个日志文件(分段存储),一个日志文件默认1G。

Kafka冗余副本保证高可用

在kafka里面分区是有副本的。创建主题时,可以指定分区,也可以指定副本个数。

优秀架构思考-总结

Kafka — 高并发、高可用、高性能

高可用:多副本机制

高并发:网络架构设计 三层架构:多selector -> 多线程 -> 队列的设计(NIO)

高性能:

写数据:

把数据先写入到OS Cache;写到磁盘上面是顺序写,性能很高

读数据:

根据稀疏索引,快速定位到要消费的数据;零拷贝机制 减少数据的拷贝 减少了应用程序与操作系统上下文切换

Kafka 为何如此之快

Kafka 实现了零拷贝原理来快速移动数据,避免了内核之间的切换。Kafka 可以将数据记录分批发送,从生产者到文件系统(Kafka 主题日志)到消费者,可以端到端的查看这些批次的数据。批处理能够进行更有效的数据压缩并减少 I/O 延迟,Kafka 采取顺序写入磁盘的方式,避免了随机磁盘寻址的浪费。

1、顺序读写

2、零拷贝

3、消息压缩

4、分批发送

kafka适合哪些场景

日志收集、消息系统、活动追踪、运营指标、流式处理、时间源等。

MQ的区别

ActiveMQ、RabbitMQ、RocketMQ、Kafka区别



第七章 es搜索引擎

Elasticsearch

Elasticsearch 是一个实时的分布式存储、搜索、分析的引擎。

为什么要用Elasticsearch

相对于数据库,Elasticsearch的强大之处就是可以模糊查询。(搜索速度很快)

首先我们得知道为什么Elasticsearch为什么可以实现快速的“模糊匹配”/“相关性查询”,实际上是你写入数据到Elasticsearch的时候会进行分词。

倒排索引是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。

这根据某个词(不完整的条件)再查找对应记录。

那Elasticsearch怎么切分这些词呢?,Elasticsearch内置了一些分词器:

Standard Analyzer 。按词切分,将词小写

Simple Analyzer。按非字母过滤(符号被过滤掉),将词小写

WhitespaceAnalyzer。按照空格切分,不转小写等等

Elasticsearch分词器主要由三部分组成:

Character Filters(文本过滤器,去除HTML)

Tokenizer(按照规则切分,比如空格)

TokenFilter(将切分后的词进行处理,比如转成小写)

内置的分词器都是英文类的,而我们用户搜索的时候往往搜的是中文,现在中文分词器用得最多的就是IK。

Elasticsearch的数据结构

我们输入一段文字,Elasticsearch会根据分词器对我们的那段文字进行分词(也就是图上所看到的Ada/Allen/Sara…),

这些分词汇总起来我们叫做Term Dictionary(词典),而我们需要通过分词找到对应的记录,这些文档ID保存在PostingList(倒排列表)

在Term Dictionary中的词由于是非常非常多的,所以我们会为其进行排序,等要查找的时候就可以通过二分来查,不需要遍历整个Term Dictionary

由于Term Dictionary的词实在太多了,不可能把Term Dictionary所有的词都放在内存中,于是Elasticsearch还抽了一层叫做Term Index(词语索引),这层只存储 部分词的前缀,Term Index会存在内存中(检索会特别快)

Term Index在内存中是以FST(Finite State Transducers)的形式保存的,其特点是非常节省内存。FST有两个优点:

1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;

2)查询速度快。O(len(str))的查询时间复杂度。

PostingList里边存的是文档ID,我们查的时候往往需要对这些文档ID做交集和并集的操作(比如在多条件查询时),PostingList使用Roaring Bitmaps来对文档ID进行交并集操作。

使用Roaring Bitmaps的好处就是可以节省空间和快速得出交并集的结果。

所以到这里我们总结一下Elasticsearch的数据结构有什么特点:

Elasticsearch的术语和架构

Index:Elasticsearch的Index相当于数据库的Table

Type:这个在新的Elasticsearch版本已经废除(在以前的Elasticsearch版本,一个Index下支持多个Type–有点类似于消息队列一个topic下多个group的概念)

Document:Document相当于数据库的一行记录

Field:相当于数据库的Column的概念

Mapping:相当于数据库的Schema的概念

DSL:相当于数据库的SQL(给我们读取Elasticsearch数据的API)

Elasticsearch的架构

从上面我们也已经得知,Elasticsearch最外层的是Index(相当于数据库 表的概念);一个Index的数据我们可以分发到不同的Node上进行存储,这个操作就叫做分片。

为什么要分片?原因也很简单:

如果一个Index的数据量太大,只有一个分片,那只会在一个节点上存储,随着数据量的增长,一个节点未必能把一个Index存储下来。

多个分片,在写入或查询的时候就可以并行操作(从各个节点中读写数据,提高吞吐量)。

分片会有主分片和副本分片之分(为了实现高可用),数据写入的时候是写到主分片,副本分片会复制主分片的数据,读取的时候主分片和副本分片都可以读。

简单总结一下Elasticsearch的架构:

Elasticsearch 写入的流程

户端写入一条数据,到Elasticsearch集群里边就是由节点来处理这次请求:

集群上的每个节点都是coordinating node(协调节点),协调节点表明这个节点可以做路由。比如节点1接收到了请求,但发现这个请求的数据应该是由节点2处理(因为主分片在节点2上),所以会把请求转发到节点2上。

coodinate(协调)节点通过hash算法可以计算出是在哪个主分片上,然后路由到对应的节点

shard = hash(document_id) % (num_of_primary_shards)

路由到对应的节点以及对应的主分片时,会做以下的事:

1、将数据写到内存缓存区

2、然后将数据写到translog缓存区

3、每隔1s数据从buffer中refresh到FileSystemCache中,生成segment文件,一旦生成segment文件,就能通过索引查询到了

4、refresh完,memory buffer就清空了。

5、每隔5s中,translog 从buffer flush到磁盘中

6、定期/定量从FileSystemCache中,结合translog内容flush index到磁盘中。

解释一下:

1、Elasticsearch会把数据先写入内存缓冲区,然后每隔1s刷新到文件系统缓存区(当数据被刷新到文件系统缓冲区以后,数据才可以被检索到)。所以:Elasticsearch写入的数据需要1s才能查询到

2、为了防止节点宕机,内存中的数据丢失,Elasticsearch会另写一份数据到日志文件上,但最开始的还是写到内存缓冲区,每隔5s才会将缓冲区的刷到磁盘中。所以:Elasticsearch某个节点如果挂了,可能会造成有5s的数据丢失。

3、等到磁盘上的translog文件大到一定程度或者超过了30分钟,会触发commit操作,将内存中的segement文件异步刷到磁盘中,完成持久化操作。

说白了就是:写内存缓冲区(定时去生成segement,生成translog),能够让数据能被索引、被持久化。最后通过commit完成一次的持久化。

等主分片写完了以后,会将数据并行发送到副本集节点上,等到所有的节点写入成功就返回ack给协调节点,协调节点返回ack给客户端,完成一次的写入。

Elasticsearch更新和删除

Elasticsearch的更新和删除操作流程:

给对应的doc记录打上.del标识,如果是删除操作就打上delete状态,如果是更新操作就把原来的doc标志为delete,然后重新新写入一条数据

前面提到了,每隔1s会生成一个segement 文件,那segement文件会越来越多越来越多。Elasticsearch会有一个merge任务,会将多个segement文件合并成一个segement文件。

在合并的过程中,会把带有delete状态的doc给物理删除掉。

Elasticsearch查询

查询我们最简单的方式可以分为两种:

1、根据ID查询doc

2、根据query(搜索词)去查询匹配的doc

public TopDocs search(Query query, int n);

public Document doc(int docID);

根据ID去查询具体的doc的流程是:

1、检索内存的Translog文件

2、检索硬盘的Translog文件

3、检索硬盘的Segement文件

根据query去匹配doc的流程是:

同时去查询内存和硬盘的Segement文件

从上面所讲的写入流程,我们就可以知道:Get(通过ID去查Doc是实时的),Query(通过query去匹配Doc是近实时的),因为segement文件是每隔一秒才生成一次的

Elasticsearch查询又分可以为三个阶段:

1、QUERY_AND_FETCH(查询完就返回整个Doc内容)

2、QUERY_THEN_FETCH(先查询出对应的Doc id ,然后再根据Doc id 匹配去对应的文档)

3、DFS_QUERY_THEN_FETCH(先算分,再查询)

「这里的分指的是 词频率和文档的频率(Term Frequency、Document Frequency)众所周知,出现频率越高,相关性就更强」

一般我们用得最多的就是QUERY_THEN_FETCH,第一种查询完就返回整个Doc内容(QUERY_AND_FETCH)只适合于只需要查一个分片的请求。

QUERY_THEN_FETCH总体的流程流程大概是:

1、客户端请求发送到集群的某个节点上。集群上的每个节点都是coordinate node(协调节点)

2、然后协调节点将搜索的请求转发到所有分片上(主分片和副本分片都行)

3、每个分片将自己搜索出的结果(doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

4、接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。

Query Phase阶段时节点做的事:

协调节点向目标分片发送查询的命令(转发请求到主分片或者副本分片上)

数据节点(在每个分片内做过滤、排序等等操作),返回doc id给协调节点

Fetch Phase阶段时节点做的是:

协调节点得到数据节点返回的doc id,对这些doc id做聚合,然后将目标数据分片发送抓取命令(希望拿到整个Doc记录)

数据节点按协调节点发送的doc id,拉取实际需要的数据返回给协调节点

主流程我相信大家也不会太难理解,说白了就是:由于Elasticsearch是分布式的,所以需要从各个节点都拉取对应的数据,然后最终统一合成给客户端

es检索过程:

1、输入关键词

2、进行分词

3、利用倒排索引,全局的映射文档建立好索引

4、数据存在es不同的分片,有master节点和data节点

5、用词去匹配属于哪个分片

6、查询结果有记录的总条数、命中数、热度等信息

master挂了怎么选举?

ES面试题

https://juejin.cn/post/6895195188063371272#heading-26

ELK收集日志

1、集群中的每台微服务的节点都安装 Logstash 日志收集插件。

2、Logstash的input插件负责收集微服务的日志文件。

3、Logstash 的filter插件将日志格式化为 JSON 格式,根据每天创建不同的索引,output插件再输出到 ElasticSearch 中。

4、浏览器使用安装 Kibana 查询日志信息。

ELK+Kafka收集日志

使用AOP技术环绕和异常通知拦截日志内容缓存到BlockingQueue中,单独线程从BlockingQueue中取出msg转化成json格式投递到Kafka主题中,Logstash订阅Kafka主题实时将日志信息输出到ES中,最后使用kibana调用ES接口以图形报表信息查询日志。



第八章 分布式文件存储FastDFS



第九章 分布式任务调度XXL-JOB



第十章 23种设计模式

什么是设计模式

设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

设计模式的六大原则

1、开放封闭原则

原则思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化

2、里氏代换原则

原则思想:使用的基类可以在任何地方使用继承的子类,完美的替换基类。

3、依赖倒转原则

原则思想:依赖倒置原则的核心思想是面向接口编程。

4、接口隔离原则

原则思想:使用多个隔离的接口,比使用单个接口要好。

5、迪米特法则(最少知道原则)

原则思想:一个对象应当对其他对象有尽可能少地了解,简称类间解耦

6、单一职责原则

原则思想:一个方法只负责一件事情。

设计模式分类

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、责任链模式、迭代子模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

1、 单例模式

保证一个类只有一个实例,并且提供一个访问该全局访问点。

1、饿汉式

类初始化时,会立即加载该对象,线程天生安全,调用效率高。

//饿汉式

public class Demo1 {


// 类初始化时,会立即加载该对象,线程安全,调用效率高

private static Demo1 demo1 = new Demo1();

private Demo1() {
    System.out.println("私有Demo1构造参数初始化");
}
public static Demo1 getInstance() {
    return demo1;
}
public static void main(String[] args) {
    Demo1 s1 = Demo1.getInstance();
    Demo1 s2 = Demo1.getInstance();
    System.out.println(s1 == s2);
}

}

2、懒汉式

类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

//懒汉式

public class Demo2 {

//类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
private static Demo2 demo2;
private Demo2() {
    System.out.println("私有Demo2构造参数初始化");
}
public synchronized static Demo2 getInstance() {
    if (demo2 == null) {
        demo2 = new Demo2();
    }
    return demo2;
}
public static void main(String[] args) {
    Demo2 s1 = Demo2.getInstance();
    Demo2 s2 = Demo2.getInstance();
    System.out.println(s1 == s2);
}

}

3、静态内部类

结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

// 静态内部类方式

public class Demo3 {

private Demo3() {
    System.out.println("私有Demo3构造参数初始化");
}
public static class SingletonClassInstance {
    private static final Demo3 DEMO_3 = new Demo3();
}
// 方法没有同步
public static Demo3 getInstance() {
    return SingletonClassInstance.DEMO_3;
}
public static void main(String[] args) {
    Demo3 s1 = Demo3.getInstance();
    Demo3 s2 = Demo3.getInstance();
    System.out.println(s1 == s2);
}

}

4、枚举单例式

使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障,避免通过反射和反序列化的漏洞, 缺点没有延迟加载。

//使用枚举实现单例模式

public class Demo4 {

public static Demo4 getInstance() {
    return Demo.INSTANCE.getInstance();
}
public static void main(String[] args) {
    Demo4 s1 = Demo4.getInstance();
    Demo4 s2 = Demo4.getInstance();
    System.out.println(s1 == s2);
}
//定义枚举
private static enum Demo {
	INSTANCE;
	// 枚举元素为单例
	private Demo4 demo4;
	private Demo() {
		System.out.println("枚举Demo私有构造参数");
		demo4 = new Demo4();
	}
	public Demo4 getInstance() {
		return demo4;
	}
}

}

5、双重检测锁方式

双锁机制的出现是为了解决前面同步问题和性能问题

public class Singleton {


private volatile static Singleton uniqueInstance;

private Singleton() {}

public static Singleton getUniqueInstance() {


//先判断对象是否已经实例过,没有实例化过才进入加锁代码

if (uniqueInstance == null) {


//类对象加锁

synchronized (Singleton.class) {


if (uniqueInstance == null) {


uniqueInstance = new Singleton();

}

}

}

return uniqueInstance;

}

}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的

因为uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

1、为 uniqueInstance 分配内存空间

2、初始化 uniqueInstance

3、将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,避免其他线程获取到未初始化的uniqueInstance,保证在多线程环境下也能正常运行。

2、 工厂模式

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式。

Spring开发中的工厂设计模式

在Spring IOC容器创建bean的过程是使用了工厂设计模式,当容器拿到了beanName和class类型后,动态的通过反射创建具体的某个对象,最后将创建的对象放到Map中。

简单工厂模式

1、创建一个接口

public interface Car {


public void run();

}

2、创建实现接口的实现类

public class Bmw implements Car {


public void run() {


System.out.println(“我是宝马汽车…”);

}

}

3、创建一个工厂类,生成基于给定信息的实现类的对象

public class CarFactory {


public static Car createCar(String name) {


if(name.equals(“宝马”)){


return new Bmw();

}

return null;

}

}

5、使用该工厂,通过传递类型信息来获取实现类的对象。

public class Client01 {


public static void main(String[] args) {


Car bmw =CarFactory.createCar(“宝马”);

bmw.run();

}

}

工厂方法模式

在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。

1、创建一个接口

2、创建实现接口的实现类

3、创建工厂方法接口

4、创建工厂方法接口的实现类,用于创建实现类的对象 (可以有多个不同的实现类)

5、使用该工厂,通过使用不同的工厂方法实现类来获取实体类的对象。

抽象工厂模式

抽象工厂简单地说是工厂的工厂,抽象工厂可以创建具体工厂,由具体工厂来产生具体产品。

1、创建子接口A,及实现类

2、创建子接口B,及实现类

3、创建一个总工厂接口,及一个实现类(该实现类包含创建子接口A、B对象的方法)

4、使用总工厂的实现类,由总工厂的实现类决定调用哪个工厂的哪个实例。

3、 代理模式

通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。

代理模式应用场景

Spring AOP、日志打印、异常处理、事务控制、权限控制等

代理的分类

1、静态代理:简单代理模式,是动态代理的理论基础。

2、jdk动态代理:使用反射完成代理。需要有顶层接口才能使用,常见是mybatis的mapper文件是代理。

3、cglib动态代理:也是使用反射完成代理,可以直接代理类(jdk动态代理不行),使用字节码技术,不能对 final类进行继承。

静态代理

通过继承目标类的方式完成静态代理

例如:在不修改UserDao接口类的情况下开事务和关闭事务

//目标类

public class UserDao{


public void save() {


System.out.println(“保存数据方法”);

}

}

修改代码,添加代理类

//代理类

public class UserDaoProxy extends UserDao {


private UserDao userDao;

public UserDaoProxy(UserDao userDao) {
	this.userDao = userDao;
}

public void save() {
	System.out.println("开启事物...");
	userDao.save();
	System.out.println("关闭事物...");
}

}

/添加完静态代理的测试类

public class Test{


public static void main(String[] args) {


UserDao userDao = new UserDao();

UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);

userDaoProxy.save();

}

}

JDK动态代理

利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就叫JDK动态代理。

//接口

public interface UserDao {


void save();

}

//接口实现类

public class UserDaoImpl implements UserDao {


public void save() {


System.out.println(“保存数据方法”);

}

}

//下面是代理类,可重复使用,不像静态代理那样要自己重复编写代理

// 每次生成动态代理类对象时,实现了InvocationHandler接口的调用处理器对象

public class InvocationHandlerImpl implements InvocationHandler {

// 目标对象,用来调用具体的业务方法
private Object target;
// 通过构造函数传入目标对象
public InvocationHandlerImpl(Object target) {
    this.target = target;
}

//动态代理实际运行的代理方法,
//proxy:代理对象,newProxyInstance方法的返回对象。method:调用的方法。args: 方法中的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("调用开始处理");
    //调用invoke()方法
	Object result = method.invoke(target, args);
    System.out.println("调用结束处理");
    return result;
}

}

public class Test {


public static void main(String[] args) {


// 被代理对象

UserDao userDaoImpl = new UserDaoImpl();

InvocationHandlerImpl invocationHandlerImpl = new InvocationHandlerImpl(userDaoImpl);

    //类加载器
    ClassLoader loader = userDaoImpl.getClass().getClassLoader();
    Class<?>[] interfaces = userDaoImpl.getClass().getInterfaces();

    // 参数:类加载器、目标类的所有接口及调用处理器实例
    UserDao newProxyInstance = (UserDao) Proxy.newProxyInstance(loader, interfaces, invocationHandlerImpl);
    newProxyInstance.save();//代理对象调用save方法时,自动执行invocationhandler中的invoke方法。
}

}

CGLIB动态代理

利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

动态生成一个被代理类的子类,子类重写被代理的类的所有不是final的方法。

CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类

(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。

//接口

public interface UserDao {


void save();

}

//接口实现类

public class UserDaoImpl implements UserDao {


public void save() {


System.out.println(“保存数据方法”);

}

}

//代理主要类

public class CglibProxy implements MethodInterceptor {


private Object targetObject;

// 这里的目标类型为Object,则可以接受任意一种参数作为被代理类,实现了动态代理

public Object getInstance(Object target) {


this.targetObject = target;

Enhancer enhancer = new Enhancer();//字节码增强器

enhancer.setSuperclass(target.getClass());//设置目标类

enhancer.setCallback(this);//回调

return enhancer.create();//创建代理类

}

//代理实际方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
	System.out.println("开启事物");
	Object result = proxy.invoke(targetObject, args);
	System.out.println("关闭事物");
	// 返回代理对象
	return result;
}

}

//测试CGLIB动态代理

public class Test {


public static void main(String[] args) {


CglibProxy cglibProxy = new CglibProxy();

UserDao userDao = (UserDao) cglibProxy.getInstance(new UserDaoImpl());

userDao.save(); //在调用代理类中方法时会被我们实现的方法拦截器进行拦截

}

}

4、 模板模式

定义一个操作中的算法骨架(父类),而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构来重定义该算法的。即抽象父类中有固定的算法结构(逻辑),子类对特定的步骤方法可以重写。

比如

1、对数据库的连接,已经封装好的固定步骤模板。

2、支付流程大致相同,使用模板封装主要支付流程,不同的支付子类可以重写一些请求参数校验之类的方法。

5、 策略模式

策略模式

定义了一系列的算法 或 逻辑,并将每一个算法、逻辑封装起来,而且使它们还可以相互替换。为了 简化 if…else 所带来的复杂和难以维护。

例如:1、有一个支付模块,有微信支付、支付宝支付、银联支付等,不同的支付封装成一个个单独的算法或逻辑

2、库存的入库逻辑,不同类型的库存封装不用的逻辑,根据入库类型判断使用哪个入库策略逻辑

6、 责任链模式

责任链模式

为请求创建了一个接收者对象的链。通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

7、 观察者模式

又叫发布-订阅模式,他定义对象之间一种一对多的依赖关系,使得当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。

实现有两种方式:

1、推:每次都会把通知以广播的方式发送给所有观察者,所有的观察者只能被动接收。

2、拉:观察者只要知道有情况即可,至于什么时候获取内容,获取什么内容,都可以自主决定。

观察者模式应用场景

1、spring的事件监听ApplicationEvent

2、跨系统的消息交换场景,如消息队列、事件总线的处理机制。

步骤:

1、定义抽象观察者,每一个实现该接口的实现类都是具体观察者。

//观察者的接口,用来存放观察者共有方法

public interface Observer {


// 观察者方法

void update(int state);

}

2、定义具体观察者

// 具体观察者

public class ObserverImpl implements Observer {


// 具体观察者的属性

private int myState;

public void update(int state) {


myState=state;

System.out.println(“收到消息,myState值改为:”+state);

}

public int getMyState() {


return myState;

}

}

3、定义主题类。主题定义观察者数组list,并实现增、删及通知观察者

public class Subjecct {


//观察者的存储集合,不推荐ArrayList,线程不安全,

private Vector list = new Vector<>();

// 注册观察者方法

public void registerObserver(Observer obs) {


list.add(obs);

}

// 删除观察者方法

public void removeObserver(Observer obs) {


list.remove(obs);

}

// 通知所有的观察者更新

public void notifyAllObserver(int state) {


for (Observer observer : list) {


observer.update(state);

}

}

}

4、定义具体的类,继承主题类,在这里实现具体业务

//具体主题

public class RealObserver extends Subjecct {


//被观察对象的属性

private int state;

public int getState(){


return state;

}

public void setState(int state){


this.state=state;

//主题对象(目标对象)值发生改变

this.notifyAllObserver(state);

}

}

5、运行测试

public class Client {


public static void main(String[] args) {


// 目标对象

RealObserver subject = new RealObserver();

// 创建多个观察者

ObserverImpl obs1 = new ObserverImpl();

ObserverImpl obs2 = new ObserverImpl();

// 注册到观察队列中

subject.registerObserver(obs1);

subject.registerObserver(obs2);

// 改变State状态

subject.setState(300);

System.out.println(“obs1观察者的MyState状态值为:”+obs1.getMyState());

System.out.println(“obs2观察者的MyState状态值为:”+obs2.getMyState());

}

}

8、 建造者模式

将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

使用多个简单的对象一步一步构建成一个复杂的对象。

1、建立一个对象,有不同的方法和属性

2、创建Builder接口(给出一个抽象接口,以规范产品对象的各个组成成分的建造,这个接口只是规范)

3、创建Builder实现类(这个类主要实现复杂对象创建的哪些部分需要什么属性)

4、Director(调用具体建造者来创建复杂对象的各个部分,只负责保证对象各部分完整创建或按某种顺序创建)

8、外观模式

也叫门面模式,隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。

它向现有的系统添加一个接口,用这一个接口来隐藏实际的系统的复杂性。

9、原型模式

原型设计模式简单来说就是克隆。

原型模式的使用方式

1、实现Cloneable接口。

2、重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法。

原型模式分为浅复制和深复制

1、浅复制:只是拷贝了基本类型的数据,而引用类型数据(对象类型),只是拷贝了一份引用地址。

2、深复制:在计算机中开辟了一块新的内存地址用于存放复制的对象。

public class User implements Cloneable {


private String name;

private String password;

private ArrayList phones;

//重写clone()

protected User clone() {


try {


User user = (User) super.clone();

//重点,如果要连带引用类型一起复制,需要添加底下一条代码,如果不加就对于是复制了引用地址

user.phones = (ArrayList) this.phones.clone();//设置深复制

return user;

} catch (CloneNotSupportedException e) {


e.printStackTrace();

}

return null;

}

}



第十一章 版本控制Git

git

git add 添加文件到暂存区

git commit 提交暂存的更改

git pull 从远程仓库拉取代码并合并到本地

git push 上传本地指定分支到远程仓库

git fetch 与 git pull 不同的是 git fetch 操作仅仅只会拉取远程的更改,不会自动进行 merge 操作。

git branch 新建本地分支

git merge 把一个分支的修改合并到当前分支上

git rebase 叫变基,作用和 merge 很相似,用于把一个分支的修改合并到当前分支上。

merge和rebase区别

处理冲突的方式

1、使用merge命令合并分支,解决完冲突,执行git add .和git commit -m’fix conflict’。这个时候会产生一个commit。

merge确实只需要解决一遍冲突,比较简单粗暴

2、使用rebase命令合并分支,解决完冲突,执行git add .和git rebase –continue,不会产生额外的commit。

用rebase如果合并的分支中存在多个commit,需要重复处理多次冲突。

Maven

常用命令

mvn clean 清理,这个命令可以用来清理已经编译好的文件

mvn compile 编译,将 Java 代码编译成 Class 文件

mvn test 测试,项目测试

mvn package 打包,根据用户的配置,将项目打成 jar 包或者 war 包

mvn install 安装,手动向本地仓库安装一个 jar

mvn deploy 上传,将 jar 上传到私服

gradle



第十二章 自动化部署

jenkins

Jenkins是一个开源的、提供友好操作界面的持续集成(CI)工具,要用于持续、自动的构建/测试软件项目、监控外部任务的运行。

通常与版本管理工具(SCM)、构建工具结合使用。常用的版本控制工具有SVN、GIT,构建工具有Maven、Ant、Gradle。

CI/CD是什么

CI(Continuous integration)是持续集成,持续集成强调开发人员提交了新代码之后,立刻进行构建、(单元)测试。根据测试结果,我们可以确定新代码和原有代码能否正确地集成在一起。

CD(Continuous Delivery)是持续交付,是在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境(类生产环境)中。比如,我们完成单元测试后,可以把代码部署到连接数据库的Staging环境中更多的测试。如果代码没有问题,可以继续手动部署到生产环境。

CD(Continuous Deployment)是持续部署。

Jenkins 持续交付工作流程

  1. 开发者向 GitLab 或者 Gitee 或者 GitHub 提交代码。
  2. Git服务器使用 WebHook(钩子) 通知 Jenkins 有代码更新。
  3. Jenkins 从节点拉取代码,并打包生成镜像。
  4. Jenkins生成镜像后自动运行测试用例。
  5. 如果测试通过,将镜像推送到镜像仓库中。
  6. Jenkins 在应用服务器上更新部署。
  7. Jenkins 将构建过程的报告以邮件的方式通知相关人员。

Docker

Docker是容器技术的一种实现,也是操作系统层面的一种虚拟化,与虚拟机通过一套硬件再安装操作系统完全不同。

Docker的核心组成

镜像(Image)

Docker镜像是一个特殊的文件系统,提供容器运行时所需的程序、库、资源、配置等文件,另外还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。Docker镜像就是一个Root文件系统

#列出所有镜像

docker image ls

#从仓库拉取镜像

docker pull

#运行镜像

docker run -it 镜像 /bin/bash

#删除镜像

#image_name表示镜像名,image_id表示镜像id

dockere image rm image_name/image_id

容器(Container)

Docker的镜像是用于生成容器的模板,镜像分层的,镜像与容器的关系,就是面向对象编程中类与对象的关系,我们定好每一个类,然后使用类创建对象,对应到Docker的使用上,则是构建好每一个镜像,然后使用镜像创建我们需要的容器。

#启动容器,container_name表示容器名称

docker run

#container_id表示容器的id

docker start container_id

#停止容器,container_id表示容器的id

docker stop container_id

#查看所有容器

docker container ls

#删除容器,container_id表示容器id,通过docker ps可以看到容器id

docker rm container_id

#进入容器,container_id表示容器的id,command表示linux命令,如/bin/bash

docker exec -it container_id command

仓库(Repository)

Docker Hub就是Docker提供用于存储和分布镜像的官方Docker Registry,也是默认的Registry,其网址为https://hub.docker.com,前面我们使用docker pull命令便从Docker Hub上拉取镜像。

Docker怎么用

Docker File

在运行容器前需要编写Docker File,通过 dockerFile 生成镜像,然后才能运行 Docker 容器。Docker File 定义了运行镜像(image)所需的所有内容,包括操作系统和软件安装位置。

Docker Swarm

是 Docker 自家针对集群化部署管理的解决方案。

Docker-Compose

可以同时进行多容器的构建以及部署运行。

kubernetes

k8s是基于容器的集群编排引擎,具备扩展集群、滚动升级回滚、弹性伸缩、自动治愈、服务发现等多种特性能力。

k8s核心组件

Controller Manager,即控制平面,用于调度程序以及节点状态检测。

Nodes,构成了Kubernetes集群的集体计算能力,实际部署容器运行的地方。

Pods,Kubernetes集群中资源的最小单位。

k8s提供如下功能:

1、快速部署功能:定义对应的charts,可以方便把大型的应用部署上去。

2、智能的缩扩容机制:部署时候会自动去考虑容器应该部署在哪个服务器上,以及副本的数量可以自定义。

3、自愈功能:某个节点的服务崩溃了,可以自动迁移到另外一个服务器节点来恢复来实现高可用。

4、智能的负载均衡:利用Ingress,可以实现流量通过域名访问进来时候,进行流量的分流到不同服务器上。

5、智能的滚动升降级:升级或者降级时候,会逐个替换,当自定义数量的服务升级OK后,才会进行其他的升级以及真正销毁旧的服务。



第十三章 服务器Linux

Linux 的体系结构

1、用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。

2、内核空间(Kernel Space) :内核空间又包括系统调用接口(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture-Dependent Kernel Code) 。

Linux 基本命令

cd 切换目录

pwd 显示当前工作目录的绝对路径

ls 查看当前目录下的所有文件夹

ll 查看当前目录下的所有详细信息和文件夹

touch 创建文件

mkdir 创建目录

cat 查看文件命令(可以快捷查看当前文件的内容)

more 分页查看文件命令(不能快速定位到最后一页)

less 分页查看文件命令(可以快速定位到最后一页)

tail 查看文件命令(看最后多少行)

cp 复制功能

mv 移动功能

rm 删除文件,或文件夹

find 查找指定文件或目录

vi 文本编辑器 类似win的记事本

vim 改进版文本编辑器

| 管道命令(把多个命令组合起来使用)

grep 正则表达式,用于字符串的搜索工作(模糊查询)

yum install -y lrzsz 命令(实现win到Linux文件互相简单上传文件)

tar -zxvf 解压命令

tar -zcvf 压缩命令

ps 进程状态

clear 清屏命令

ifconfig 用于查看和更改网络接口的地址和参数

ping 用于检测与目标的连通性,语法:ping ip地址

free 显示系统内存

top 显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等

netstat 用于显示网络状态。

file 可查看文件类型

du 显示磁盘空间的使用情况,用于查看当前目录的总大小。

df 以磁盘分区为单位查看文件系统,可以获取硬盘被占用了多少空间,目前还剩下多少空间等信息。

reboot 重启命令

halt 关机命令

Linux零拷贝技术

磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。

零拷贝技术实现的方式通常有 2 种:

mmap + write

Read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。

buf = mmap(file, len);

write(sockfd, buf, len);

mmap就是将一个文件或者其它对象映射进内存。mmap() 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

sendfile

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下: #include <sys/socket.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

磁盘调度算法

常用的磁盘调度算法有以下四种

1、先来先服务算法

FCFS算法根据进程请求访问磁盘的先后顺序进行调度,这是一种最简单的调度算法

2、最短寻找时间优先算法

SSTF算法选择调度处理的磁道是与当前磁头所在磁道距离最近的磁道,以使每次的寻找时间最短。

3、扫描算法(又称电梯算法)

SCAN算法在磁头当前移动方向上选择与当前磁头所在磁道距离最近的请求作为下一次服务的对象。

4、循环扫描算法

在扫描算法的基础上规定磁头单向移动来提供服务,回返时直接快速移动至起始端而不服务任何请求。

Nginx

Nginx主要功能:1、反向代理 2、负载均衡 3、HTTP服务器(包含动静分离) 4、正向代理

反向代理

server {


listen 80;

server_name localhost;

client_max_body_size 1024M;

  location / {
      proxy_pass http://localhost:8080;
      proxy_set_header Host $host:$server_port;
  }

}

负载均衡

1、轮询(默认)

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

简单配置:

upstream test {


server localhost:8080;

server localhost:8081;

}

server {


listen 81;

server_name localhost;

client_max_body_size 1024M;

  location / {
      proxy_pass http://test;
      proxy_set_header Host $host:$server_port;
  }

}

配置了2台服务器,当然实际上是一台,只是端口不一样而已,而8081的服务器是不存在的,也就是说访问不到,但是我们访问 http://localhost 的时候,也不会有问题,会默认跳转到http://localhost:8080

2、权重

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 例如

upstream test {


server localhost:8080 weight=9;

server localhost:8081 weight=1;

}

3、ip_hash

每个请求按访问ip的hash结果分配

upstream test {


ip_hash;

server localhost:8080;

server localhost:8081;

}

4、url_hash

按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器

upstream backend {


hash $request_uri;

hash_method crc32;

server localhost:8080;

server localhost:8081;

}



第十四章 分布式事务解决方案

ACID理论

Atomicity(原子性)

Consistency(一致性)

Isolation(隔离性)

Durability(持久性)

CAP原理

CAP理论作为分布式系统的基础理论,它描述的是一个分布式系统在以下三个特性中:

1、一致性(Consistency)

2、可用性(Availability)

3、分区容错性(Partition tolerance)

最多满足其中的两个特性。也就是下图所描述的。分布式系统要么满足CA,要么CP,要么AP,无法同时满足CAP。

BASE理论

基本可用(Basically Available):保证系统的基本可用

软状态(Soft state):允许系统中的数据暂时存在中间状态

最终一致性():数据最终能够一致性

BASE是对CAP中一致性和可用性权衡的结果,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

2PC(两阶段提交)

引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备和提交两个阶段。

准备阶段

1、协调者询问参与者事务是否执行成功,参与者发回事务执行结果。准备阶段,参与者执行了事务,但是还未提交。

提交阶段

2、如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。

两段提交(2PC)的缺点:

2PC是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

3PC(三两阶段提交)

新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。

同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题,但3PC还是没能从根本上解决数据一致性的问题。

1、准备阶段CanCommit

协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。

2、预提交阶段PreCommit

协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作,参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦参与者中有向协调者发送了No响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送abort请求,参与者接受abort命令执行事务的中断。

3、提交阶段DoCommit

在前两个阶段中所有参与者的响应反馈均是YES后,协调者向参与者发送DoCommit命令正式提交事务,如协调者没有接收到参与者发送的ACK响应,会向所有参与者发送abort请求命令,执行事务的中断。

三段提交(3PC)的缺点:

还是会存在数据不一致问题

TCC

2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务

1、Try 指的是预留,即资源的预留和锁定。

2、Confirm 指的是确认操作,这一步其实就是真正的执行了。

3、Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。

TCC的缺点:

应用侵入性强:TCC由于基于在业务层面,至使每个操作都需要有try、confirm、cancel三个接口。 开发难度大:代码开发量很大,要保证数据一致性confirm和cancel接口还必须实现幂等性。

TCC框架:tcc-transaction、Seata

本地消息表

本地消息表实现的是最终一致性

事务上游处理逻辑

事务上游在执行本地业务的同时,将消息持久化到业务数据源中,持久化流程与业务操作处于同一个事务中,保证了业务与消息同时成功持久化。

到这里,事务上游的业务就执行结束了,通过后台线程异步地轮询消息表,将待发送的消息投递到消息中间件的事务执行队列(我们将该队列的Topic称为事务执行Topic)中。投递成功则更新消息状态为 [已投递]。

事务下游处理逻辑

事务下游拉取消息进行消费,在业务消费之前,首先将消息持久化到本地,持久化成功后执行消费逻辑。

下游通过重试最大限度地保证业务消费逻辑执行成功,如果达到某个设定的消费次数阈值仍旧消费失败,那么我们认为事务下游没办法将事务处理成功,此时事务下游拷贝之前持久化的消息,标记为回滚状态消息,通过后台线程扫描后投递到事务回滚队列(我们将该队列的Topic称为事务回滚Topic)中。投递成功则更新消息状态为 [已投递]。

事务上游需要实现回滚逻辑,接收到事务下游投递的回滚消息后,执行回滚逻辑对业务进行回滚操作。该流程通过消费重试实现,如果达到最大消费次数仍旧不能回滚,则该回滚消息会进入消息中间件的死信队列。此时需要人工干预,取出死信消息进行手工回滚操作,保证业务的正常运行。

MQ消息事务

RocketMQ 就很好的支持了消息事务

第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。

并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。

如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。

如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。

如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。

最大努力通知

尽力最大的努力想达成事务的最终一致。本地消息表和MQ消息事务也算最大努力通知。



第十五章 网络通信

计算机网络体系结构

OSI参考模型

OSI定义了网络互连的七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP/IP参考模型

TCP/IP四层协议:数据链路层、网络层、传输层、应用层

TCP / UDP

1、TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成对的两个之间必须建 立连接),TCP提供可靠的服务,TCP只支持点对点通信。

2、UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可靠性的协议,UDP支持一对一、一对多、多对一、多对多通信。

ARP协议 (Address Resolution Protocol)

ARP协议完成了IP地址与物理地址的映射。每一个主机都设有一个 ARP 高速缓存,里面有所在的局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。

NAT (Network Address Translation, 网络地址转换)

用于解决内网中的主机要和因特网上的主机通信。由NAT路由器将主机的本地IP地址转换为全球IP地址,分为静态转换(转换得到的全球IP地址固定不变)和动态NAT转换。

TCP的三次握手

在网络数据传输中,传输层协议TCP是要建立连接的可靠传输,TCP建立连接的过程,我们称为三次握手。

大概逻辑:

客户端向服务端发送同步SYN -> 服务端返回同步SYN,确认ACK -> 客户端发送确认ACK

第一次握手:客户端向服务端发送同步信号SYN=1和一个随机初始序列号seq,进入请求连接状态SYN_SENT

第二次握手:服务端收到之后,向客户端发送同步信号SYN=1和确认信号ACK=1,产生一个确认序号ack = 序列号seq + 1,并随机产生一个自己的初始序列号,进入同步收到状态SYN_RCVD;

第三次握手:客户端检查ack是否为序列号seq+1,将自己的ACK置为1,产生一个确认序号ack=服务器发的序列号+1,发送给服务器,进入已确认状态ESTABLISHED;服务器检查ACK为1和ack为序列号+1之后,也进入已确认状态ESTABLISHED;完成三次握手,连接建立。

TCP的四次挥手

大概逻辑:

客户端向服务端发送终止FIN -> 服务端返回确认ACK(仍可发送数据) -> 服务端发送终止FIN -> 客户端向服务端发送确认ACK

TCP的四次挥手

第一次挥手:客户端向服务端发送终止信号FIN=1和一个序列号seq,进入终止等待1状态FIN_WAIT_1。

第二次挥手:服务端收到终止之后,发送确认信号ACK=1,ack=序列号seq+1;进入关闭等待状态CLOSE_WAIT。客户端收到服务器的确认请求后,客户端就进入终止等待2状态FIN_WAIT_2,但仍可以接受服务器发来的数据。

第三次挥手:服务端向客户端发送终止信号FIN=1和一个服务端序列号;进入最后确认状态LAST_ACK。

第四次挥手:客户端收到服务端的终止信号FIN=1,进入时间等待状态TIME_WAIT,然后发送确认信号ACK=1和ack = 服务端序列号+1 服务器;服务器收到确认信号ACK=1和ack后,变为关闭状态CLOSED,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入关闭状态CLOSED。完成四次挥手。

Socket

网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。

Socket通常用来实现客户方和服务方的连接。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。socket连接就是所谓的长连接,客户端和服务器需要互相连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉的,但是有时候网络波动还是有可能。Socket偏向于底层。一般很少直接使用Socket来编程,框架底层使用Socket比较多。

socket属于网络的哪个层面

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

Socket通讯的过程

基于TCP

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

基于UDP

UDP 协议是用户数据报协议的简称,也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议,但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 就会表现出更大的优势。客户端只需要发送,服务端能不能接收的到不管

TCP协议Socket代码示例:

服务端

//TCP协议Socket使用BIO进行通行:服务端

public class BIOServer {


// 在main线程中执行下面这些代码

public static void main(String[] args) {


//1单线程服务

ServerSocket server = null;

Socket socket = null;

InputStream in = null;

OutputStream out = null;

try {


server = new ServerSocket(8000);

System.out.println(“服务端启动成功,监听端口为8000,等待客户端连接…”);

while (true){


socket = server.accept(); //等待客户端连接

System.out.println(“客户连接成功,客户信息为:” + socket.getRemoteSocketAddress());

in = socket.getInputStream();

byte[] buffer = new byte[1024];

int len = 0;

//读取客户端的数据

while ((len = in.read(buffer)) > 0) {


System.out.println(new String(buffer, 0, len));

}

//向客户端写数据

out = socket.getOutputStream();

out.write(“hello!”.getBytes());

}

} catch (IOException e) {


e.printStackTrace();

}

}

}

客户端

//TCP协议Socket:客户端

public class Client01 {


public static void main(String[] args) throws IOException {


//创建套接字对象socket并封装ip与port

Socket socket = new Socket(“127.0.0.1”, 8000);

//根据创建的socket对象获得一个输出流

OutputStream outputStream = socket.getOutputStream();

//控制台输入以IO的形式发送到服务器

System.out.println(“TCP连接成功 \n请输入:”);

while(true){


byte[] car = new Scanner(System.in).nextLine().getBytes();

outputStream.write(car);

System.out.println(“TCP协议的Socket发送成功”);

//刷新缓冲区

outputStream.flush();

}

}

}

UDP协议Socket代码示例

服务端

//UDP协议Socket:服务端

public class Server1 {


public static void main(String[] args) {


try {


//DatagramSocket代表声明一个UDP协议的Socket

DatagramSocket socket = new DatagramSocket(8888);

//byte数组用于数据存储。

byte[] car = new byte[1024];

//DatagramPacket 类用来表示数据报包DatagramPacket

DatagramPacket packet = new DatagramPacket(car, car.length);

// //创建DatagramPacket的receive()方法来进行数据的接收,等待接收一个socket请求后才执行后续操作;

System.out.println(“等待UDP协议传输数据”);

socket.receive(packet);

//packet.getLength返回将要发送或者接收的数据的长度。

int length = packet.getLength();

System.out.println(“啥东西来了:” + new String(car, 0, length));

socket.close();

System.out.println(“UDP协议Socket接受成功”);

} catch (IOException e) {


e.printStackTrace();

}

}

}

客户端

//UDP协议Socket:客户端

public class Client1 {


public static void main(String[] args) {


try {


//DatagramSocket代表声明一个UDP协议的Socket

DatagramSocket socket = new DatagramSocket(2468);

//字符串存储人Byte数组

byte[] car = “UDP协议的Socket请求,有可能失败哟”.getBytes();

//InetSocketAddress类主要作用是封装端口

InetSocketAddress address = new InetSocketAddress(“127.0.0.1”, 8888);

//DatagramPacket 类用来表示数据报包DatagramPacket

DatagramPacket packet = new DatagramPacket(car, car.length, address);

//send() 方法发送数据包。

socket.send(packet);

System.out.println(“UDP协议的Socket发送成功”);

socket.close();

} catch (Exception e) {


e.printStackTrace();

}

}

}

http

Http协议

Http协议是对客户端和服务器端之间数据之间实现可靠性的传输文字、图片、音频、视频等超文本数据的规范,格式简称为“超文本传输协议”

Http协议属于应用层,及用户访问的第一层就是http。

Socket和http的区别和应用场景

Socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;

Socket适用场景:网络游戏,银行持续交互,直播,在线视屏等。

http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断开等待下次连接

http适用场景:公司OA服务,互联网服务,电商,办公,网站等等

http的请求体

HTTP请求体由:请求行 、请求头、请求数据组成的

注意:Get请求是没有请求体的

http的响应报文

响应报文包含三部分 状态状态码、响应首部字段、响应内容实体实现

常见的状态码

200:请求被正常处理

403:请求的对应资源禁止被访问

404:服务器无法找到对应资源

500:服务器内部错误

503:服务器正忙

http和https的区别

1、https需要拿到ca证书,需要钱的

2、端口不一样,http是80,https443

3、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

4、http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)

HTTPS加密工作原理

1、首先HTTP请求服务端生成ca证书,客户端对证书的有效期、合法性、域名是否与请求的域名一致、证书中的服务端公钥等进行校验;

2、客户端如果校验通过后,生成随机数,使用服务端公钥进行非对称加密(RSA加密);

3、发送给服务端,服务端用自己的私钥解密。

4、解密得到的随机数作为密钥,后续全都使用AES对称加密进行通信。

HTTPS使用的是非对称加密和对称加密的组合:先用非对称性加密使双方得到通讯的密钥,后续再使用对称性加密该密钥进行通讯。

注:ca认证机构就是数字证书认证的机构,其颁发的证书就是我们说的ca证书。

关于ca证书的证书签名,证书分发以及证书验证的过程:(验证服务端发来的公钥是合法性)

1、服务端使用RSA算法生成公钥和私钥。服务端将公钥进行分发证书之前需要向ca机构申请给将要分发的公钥进行数字签名。

2、生成数字签名的公钥证书:对于ca机构来说,它自己也有两个密钥,公钥和私钥。ca机构将服务端的公钥生成Hash值。使用CA私钥将这个Hash值进行加密处理,最后服务端的公钥+CA私钥加密的Hash值一起,成为了数字签名。

3、服务器获取到这个已经含有数字签名并带有公钥的证书,将该证书发送给客户端。当客户端收到该公钥数字证书后,会验证其有效性。大部分客户端都会预装CA机构的公钥,也就是CA公钥。客户端使用对数字证书上的签名进行验证,使用CA公钥对CA私钥加密的内容进行解密得到的hash值和服务端的公钥生成的Hash值匹配。如果匹配成功,则说明该证书就是相应的服务端发过来的。否则就是非法证书。

浏览器一次完整的http请求过程

1、完成域名解析,浏览器查询DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件和向本地DNS服务器进行查询等。

2、浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;

3、TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求;

4、服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

5、浏览器解析htm代码,并请求html代码中的资源(如js、css、图片等);

6、浏览器向服务器发起四次挥手,断开TCP连接;

7、浏览器对页面进行渲染,最终向用户呈现一个完整的页面。

常见加密算法

对称加密

对称加密:加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。如AES 算法

非对称加密

它有两个密钥,公钥和叫私钥,公钥是公开的,而私钥是保密的,公钥加密后只能用私钥解密,反过来私钥加密后也只能用公钥解密。如RSA算法

如果用于加密一般私钥加密,公钥解密;如果用于数字签名,一般是私钥加密,公钥解密来验签。

摘要算法

保证数据完整性的手段,也就是常说的散列函数、哈希函数。所以一般是生成固定长度的不可逆的算法。比如 MD5、SHA-1算法

数字签名

数字签名就是经过非对称加密私钥加密后的摘要。验签的时候就是用公钥来解密后,再使用相同的摘要算法来验签是否匹配。

数字证书

数字证书就是可信任的权威机构(CA)颁发的服务器相关信息和其公钥。服务器通过向客户端提供证书来证明自己确实是某某网站。CA的公钥是公开的,客户端拿到证书后用CA公钥解开就可以拿到服务器公钥了。



第十六章 数据结构与算法

数据结构

常见排序算法



第十七章 源码解读

集合源码

线程池源码

Spring源码

Mybatis源码

Springcloud源码

Dubbo源码



第十八章 其他常见内容

单点登录的实现

1、使用cookie+redis实现

解决Cookie跨域问题:token(session ID)不保存服务端,而已存储在redis中,

第一次登录成功后,将token返回客户端写入Cookie,并创建各个应用系统隐藏的ifram方式,调用各应用系统接口将token返回各自得客户端写入Cookie,实现了同一份 Token 被多个域所共享。Cookie就是一次用户会话,关闭浏览器后,Cookie即失效,存放数据较小。

2、独立的认证中心(CAS)

认证中心就是一个专门负责处理登录请求的独立的 Web 服务。

户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的。)应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。

3、LocalStorage 跨域

我们可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中。前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。LocalStorage 是永久保存的,要主动清理才会失效,存放数据较大。

防止重复提交

使用AOP自定义切入+Redis实现(支持分布式)

request进来,没有就先存在Redis缓存中,继续操作业务,最后删除缓存或者缓存设置生命周期。如果存在,就直接对request url进行验证,就不能继续操作业务。自定义一个注解作用于controller,通过AOP 切面对所有标记了该注解的方法拦截,获取当前用户的request url作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁) ,进行验证对比。业务方法执行后,释放锁。

token (不支持分布式)

访问请求到达服务器,服务器端生成token,分别保存在客户端和服务器session。提交请求到达服务器,服务器端校验客户端带来的token与此时保存在服务器的token是否一致,如果一致,就继续操作,删除服务器的token。如果不一致,就不能继续操作,即这个请求是重复请求。

Redis的计数器

Redis的计数器是原子操作,不存储请求,又能提升QPS的峰值。每次request请求,若相同请求,计数器+1,否则新建id为key的计数器。如果>1,不能获取锁;如果=1,获取锁,操作,最后删除计数器(删除锁)。

拦截器、过滤器和监听器

拦截器和过滤器的区别

1、拦截器是基于java的反射机制的,而过滤器是基于函数回调。

2、拦截器不依赖与servlet容器,过滤器依赖与servlet容器。

3、拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。

4、拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。

5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。

6、拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

监听器

通过listener可以监听web服务器中某一执行动作,并根据其要求作出相应的响应。常用的监听器 servletContextListener、httpSessionListener、servletRequestListener、ContextLoaderListener

有1亿个数,快速找出其中最大的前100个?

1、局部淘汰法

用一个容器保存100个数,然后将剩余的所有数字与容器内的最小数字相比,如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完所有数。得到的结果容器中的100个数。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即100。

2、分治法

将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的100个。最后在剩下的100*100个数据里面找出最大的100个。100万个数据里面查找最大的100个数据的方法如下:用快速排序的方法,将数据分为2堆,堆个数N大于100个,继续对大堆快速排序一次分成2堆

3、Hash法

如果这1亿个数里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的100个数。

4、采用最小堆法

首先读入前100个数来创建大小为100的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为100),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有100个数字。该算法的时间复杂度为O(nmlogm)

生产CPU飙升100%,如何排查问题?

Top命令

1、top,通过top命令找到,找到最耗CPU的进程PID1

2、top -Hp PID1,查看这个进程下的线程的运行列表,找到占用cpu最高的那个线程的pid2

3、printf “%x\n” pid2,打印pid2的16进制结果

4、jstack -l PID1 > PID1.log,导出进程PID1的堆栈信息

5、通过pid2的16进制数在日志文件都查询错误信息

如何设计一个秒杀系统?

秒杀架构设计理念

1、限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。

2、削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

3、异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

4、内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。

前端方案

1、页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

2、禁止重复提交:没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。用户提交之后按钮置灰,禁止重复提交。

3、用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流

后端方案

利用redis缓存实现简单的秒杀系统(两种方案)

1、采用Redis队列插入秒杀请求

采用Redis队列数据结构,用一个原子类型的变量值(AtomicInteger)作为key,把用户id作为value,库存数量便是原子变量的最大值。对于每个用户的秒杀,我们使用 RPUSH key value插入秒杀请求, 当插入的秒杀请求数达到上限时,停止所有后续插入。然后我们可以在后台启动多个工作线程,使用 LPOP key 读取秒杀成功者的用id发到RabbitMQ消息队列异步处理下单和下游的通知系统订阅(通知用户秒杀成功),然后再操作数据库做最终的下订单减库存操作。

2、库存数量提前存入redis

采用Redis加入库存数量,利用Lua脚本的原子性操作扣减库存,扣减库存成功,用户请求信息放入RabbitMQ消息队列异步处理下单和下游的通知系统订阅(通知用户秒杀成功),然后再操作数据库做最终的下订单减库存操作。

如何防止商品被超卖?

1、加分布式锁,再扣减库存(压力大、效率低)

2、把库存数据放入到redis缓存中,利用redis原子性操作(比如执行lua脚本)保证同时只有一个线程操作库存。

3、利用redis队列,插入秒杀请求,即使有很多用户同时到达,也是依次执行,达到最大库存数后,再读取redis队列作扣减库存操作

订单未支付30分钟自动取消是如何实现?

1、定时任务轮询扫描订单表

2、利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素

3、使用Redis,设置过期时间,监听过期事件。

4、使用RabbitMQ的过期队列与死信队列,设置消息的存活时间,在设置的时间内未被消费,即会投递到死信队列,我们监听死信队列即可,再做最后取消订单的逻辑处理。

1、TTL过期时间+死信对队列

2、延迟队列

重复支付,如何保证订单的幂等性?

  1. 根据唯一订单号先查询一下订单,订单的状态是否已经支付,如果已经支付则返回,如果未支付,则修改为已支付(还无法保证并发的情况,会有ABA问题)

    2、使用乐观锁,数据库表增加版本号version来做乐观锁,更新数据时每次version+1

    公司项目的介绍和整体架构

开发过程中遇到的问题及解决方案



版权声明:本文为qq_16304475原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。