基础篇
01 == 和 equals 的区别是什么
== : 可以比较基本数据类型也可以比较引用数据类型 , 比较基本数据类型是比较值是否相等, 比较引用数据类型是比较引用地址是否相等
(基本数 据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 一般用于对象的比较 , 比较两个对象是否相等 , 会先调用对象的hashcode方法比较两个对象的hashcode是否相等 , 再调用equals()方法比较是否相等
思考 :
- 两个对象equals相等, 那么hashcode值是否可以不相等 ? 不可以
- 两个对象hashcode值相等, 那么两个对象是否一定相等 ? 不一样
- 重写一个类的equals方法有什么需要注意的 ? 先重写hashcode方法, 再重写equals
02 String、StringBuffer、StringBuilder的区别是什么
- 可变性 : String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变 的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中 也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
- 线程安全性 : String中的对象是不可变的,也就可以理解为常量,线程安全。StringBuffer对方法加了同步锁或者对调用的方法加了同 步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
- 性能 : 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对 象。StringBuffer和StringBuilder每次都会对对象本身进行操作,而不是生成新的对象并改变对象引用 , 大量字符串操作的时候, 相对而言StringBuffer和StringBuilder效率会跟高, 更节省内存资源
03 重载(Overload)和重写(Override)的区别
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与 方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于 父类,访问修饰符大于等于父类;
04 Java中常见的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等
05 说一说ArraysList和LinkedList的区别
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 ArrayList可以根据数组索引直接获取
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
06 说一说HashMap的实现原理
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表的结合体 , JDK1.8起引入了红黑树 , 数据结构就变成了数组 + 链表 + 红黑树的形式
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
相比于JDK1.7,JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
HashMap 基于 Hash 算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数 组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。
- 如果key相同,则覆盖原始值;
- 如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
07 说一说HashMap扩容机制
扩容就是HashMap底层的resize()方法 , 在两种情况下会执行 :
- 初始化HashMap集合, 第一次往里面存元素的时候会执行resize()方法 , 初始化底层的数组(默认容量是16) , 如果构造方法中传入了初始容量参数, 那么初始长度为大于传入初始容量的第一个2的n次幂
- 当HashMap集合中存入的键值对数量超过阈值(0.75)的时候会进行扩容 , 每次扩容容量为之前的2倍
在1.7 中,扩容之后需要重新去计算其Hash值,根据Hash值对节点位置进行重新分配
在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,如果为0 则留在当前位置不动 , 如果不为0 则要移动到原始位置+增加的数组大小这个位置上 , 这也是为什么HashMap扩容容量为2的N次幂的一个原因
思考 :
为什么HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模运算, 效率更高
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
为什么扩容阈值设置为0.75 ?
- 选择扩容阈值为0.75的原因是为了在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能
- 小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
08 说一说HashMap的put方法的具体流程
- 判断键值对数组table[]是否为空或为null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向 ⑥,如果table[i]不为空,转向③;
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的 是hashCode以及equals;
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值 对,否则转向⑤
- 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
思考 : HashMap在put元素的时候, 如何获取到元素存储的位置 ?
先调用key的hashcode方法 , 获取到hashcode值 , 然后再用这个hashcode值与他右移16位后的二进制进行按位异或得到最后的hash值 , 简称为二次hash
其目的是为了使HashMap集合中的key的分步更加均匀 , 避免出现某些位置元素特别多 , 某些位置元素特别少的情况 , 减少树化概率
09 你使用过的线程安全的集合有哪些
线程安全的集合有很多 , 例如 :
- Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。
- HashTable:就比HashMap多了个synchronized (线程安全),不建议使用。
- ConcurrentHashMap:高并发、高吞吐量的线程安全HashMap实现 , 推荐使用
- CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
- ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列
10 HashTable是如何实现线程安全的
HashTable使用 synchronized 来保证线程安全,效率非常低下。在HashTable内部的方法上都加上了synchronized关键字, 相当于锁的是整个HASH表结构
当一个线程 访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加 元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈 , 效率越来越低
11 说一说ConcurrentHashMap的实现原理
在JDK1.7的时候ConcurrentHashMap的底层数据结构是Segment数组 + HashEntry的形式实现
ConcurrentHashMap首先会将数据分为一段一段的存储 , 每个分段就是一个Segment,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问 , 在默认情况下 , 会将底层数据划分为16个分段 , 所以性能相对于HashTable而言提高了16倍
Segment数组中的每一个Segment元素保存的是一个HashEntry数组 , 这个HashEntry数组的结构就跟HashMap比较类似, 使用的是数组 + 链表结构
在JDK1.8的时候,放弃了Segment臃肿的设计,底层的数据结构和HashMap的数据结构相同
采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会产生并发 , 效率得到提升
12 创建线程的方式有哪些
- 继承 Thread 类;
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 实现线程池创建
- 使用匿名内部类方式
public class CreateRunnable {public static void main(String[] args) {//创建多线程创建开始Thread thread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 10; i++) {System.out.println("i:" + i);}}});thread.start();}
}
13 如何创建线程池
方式一:通过构造方法实现
根据传递的参数不同, 创建适用于不同场景的线程池
方式二:通过 Executor 框架的工具类 Executors 来实现
根据《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor() 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 , 所以我们在开发过程中创建线程池一般使用new ThreadPoolExecutor()手动设置参数的形式创建
14 线程池的核心参数有哪些
线程池的7个核心参数
- 核心线程数(corePoolSize) : 定义了最小可以同时运行的线程数量。
- 最大线程数(maximumPoolSize) : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- 工作队列(workQueue) : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
- 存活时间(keepAliveTime) : 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- 时间单位(unit) : keepAliveTime 参数的时间单位。
- 线程工厂(threadFactory) : executor 创建新线程的时候会用到。
- 拒绝策略(handler) : 关于饱和策略下面单独介绍一下
15 讲一讲线程池的任务调度流程
提交一个任务到线程池中,线程池的处理流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没 有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个 流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队 列里。如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给饱和策略来处理这个任务。
16 Java程序中怎么保证多线程的执行安全
线程的安全性问题体现在:
- 原子性:一个或者多个操作在 CPU 执行的过程中不被其他线程干扰
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
- 有序性:程序执行的顺序按照代码的先后顺序执行
解决办法:
- 使用JUC中 Atomic开头的原子类、线程安全的并发容器 , synchronized、Lock,可以解决原子性问题
- 使用 synchronized、volatile、Lock,可以解决可见性问题
17 synchronized 和 Lock 有什么区别
语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
18 说一说JVM由哪些部分组成 , 都有什么作用
JVM运行是数据区主要包括堆
虚拟机栈
方法区
本地方法栈
程序计数器
等五部分构成 , 还包括执行引擎和本地库接口
其中方法区和堆是线程共享区,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里几乎所有的对象实例都在这里分配内存
Java虚拟机栈描述的是Java方法执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务
方法区是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
程序计数器也被称为PC寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器
执行引擎就是用于执行在class中定义的指令
本地库接口用于和其他语言进行交互 , 提供和其他编程语言交互的入口
19 什么是类加载器,类加载器有哪些?
所谓类加载器就是负责将.class文件(存储的物理文件)加载到内存中的一个工具
主要有四种类加载器:
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它
- 用户自定义类加载器 (user class loader),用户通过继承 java.lang.ClassLoader类的方式自行实现的类加载器。
20 有没有了解过双亲委派模型
双亲委派是Java中类的一种加载机制 , 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
21 常用HTTP状态码有哪些 ? 代表什么意思
HTTP协议响应状态代码的第一个数字代表当前响应的类型 , 一共3位数字组成的状态代码 , 例如:
- 1xx : 消息——请求已被服务器接收,继续处理
- 2xx : 成功——请求已成功被服务器接收、理解、并接受
200 (OK): 请求处理成功
201 (Created) : 请求已被接受, 并处理成功
202 (Accepted) : 服务器已接受请求,但尚未处理
- 3xx : 重定向——需要后续操作才能完成这一请求
302 (Move Temporarily) : 重定向
304 (Not Modified) : 资源未改变 , 浏览器会从缓存获取
305 (Use Proxy) : 需要使用代理
- 4xx : 请求错误——请求含有词法错误或者无法被执行
400 (Bad Request) : 一般是请求参数错误
401 (Unauthorized) : 请求未认证
403 (Forbidden) : 请求已经被接受但是决绝执行, 一般是权限不足
404 (Not Found) : 请求资源不存在, 一般是请求路径错误
405 (Method Not Allowed) : 请求方式不被允许 , 请求方式错误
- 5xx : 服务器错误——服务器在处理某个正确请求时发生错误
500 (Internal Server Error) : 服务器内部错误, 一般是后端程序出现异常
503 (Service Unavailable) : 服务不可用, 一般出现在微服务项目, 服务调用过程中
504 (Gateway Timeout) : 网关操作, 一般网关出现问题
22 常用HTTP请求方式有哪些 ?
HTTP/1.1协议中共定义了八种请求方式来以不同方式操作指定的资源分别是
GET
,POST
,PUT
,DELETE
,TRACE
,OPTIONS
,HEAD
,CONNECT
其中前四种会用的比较多一些 , 在RestFull风格中一般
GET
用于查询请求POST
用于新增请求PUT
用于修改请求DELETE
用于删除请求
GET和POST有什么区别 ?
- GET提交的数据会放在URL之后,也就是请求行里面,以?分割URL和传输数据,参数之间以&相连,如
url?name=test1&id=123456
, POST方法是把提交的数据放在HTTP包的请求体中 - GET没有请求体 , POST有请求体
- GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制
23 JDK8和JDK11新特性有哪些
JDK8新特性
- Lambda表达式
- Stream流
- 接口默认方法
- 方法引用
- 函数式接口
- Optional
- 事件日期API
JDK11新特性
- 本地变量类型推断(Local Var):在Lambda表达式中,可以使用var关键字来标识变量,变量类型由编译器自行推断
24 Stream流中的常用方法有哪些
Stream流中的方法 :
- forEach : 用于遍历Stream中的元素。
void forEach(Consumer<? super T> action);
- map : 用于对流中的元素进行转换操作
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
- flatMap : 用于将流中的每个值都转换为另一个流,然后把所有的流都连接成一个流 , 简单来说就是流的合并操作
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
- filter : 用于对Stream中的元素进行过滤,只保留满足条件的元素。
Stream<T> filter(Predicate<? super T> predicate);
- distinct : 用于去除Stream中重复的元素。
Stream<T> distinct();
- limit : 用于限制Stream中元素的数量,截取前n个元素。
Stream<T> limit(long maxSize);
- skip:用于跳过Stream中前n个元素。
Stream<T> skip(long n);
- sorted:用于对Stream中的元素进行排序。
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
- max:用于获取Stream中最大的元素。
Optional<T> max(Comparator<? super T> comparator);
- min:用于获取Stream中最小的元素。
Optional<T> min(Comparator<? super T> comparator);
- reduce:用于对Stream中的元素进行统计,求和、求平均等操作。
T reduce(T identity, BinaryOperator<T> accumulator);
- collect:用于将Stream中的元素收集起来,转换成其他数据结构,如List、Set等。
<R, A> R collect(Collector<? super T, A, R> collector);
- anyMatch : 判断流中是否存在任意元素满足条件
boolean anyMatch(Predicate<? super T> predicate);
- allMatch : 判断流中是否所有元素满足条件
boolean allMatch(Predicate<? super T> predicate);
Collector 采集器中的方法
- Collectors.toList() : 将流中的数据转化为List集合
public static <T> Collector<T, ?, List<T>> toList()
- Collectors.toSet() : 将流中的数据转化为Set集合
public static <T> Collector<T, ?, List<T>> toSet()
- Collectors.toMap() : 将流中的数据转化为Map集合
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,Function<? super T, ? extends U> valueMapper)
- Collectors.groupingBy() : 对流中的数据进行分组
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
框架篇
01 说一说对Spring中IOC的理解
IOC即“控制反转”,不是什么技术,而是一种设计思想。IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制
谁控制谁,控制什么:传统Java程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建
为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转
02 说一说对Spring中AOP的理解
AOP就是我们常说的面向切面编程。使用AOP可以将系统中的一些公共模块抽取出来,减少公共模块和业务代码的耦合。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各 部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
像Spring中的事物控制使用的就是AOP机制实现的 , 同时也可以使用AOP做一些权限控制 , 日志记录 , 接口统一缓存等操作
Spring中的AOP底层主要是通过动态代理机制实现的 , 主要使用的是JDK动态代理和CGLIB动态代理
03 Spring中AOP的原理了解过嘛
在 Spring 中 AOP 代理使用 JDK 动态代理和 CGLIB 代理来实现,默认如果目标对象是接口,则使用 JDK 动态代理,否则使用 CGLIB 来生成代理类
JDK 动态代理是基于接口的代理 , 产生的代理对象和目标对象实现了相同的接口 , 创建代理对象的方法
Object proxy = Proxy.newProxyInstance(类加载器,目标对象接口, 代理执行器InvocationHandler)
CGLIB动态代理是基于父类的代理 , 产生的代理对象是目标对象的子类 , 创建代理对象的方法
Object proxy = Enhancer.create(目标对象类型 , 代理回调callback)
04 Spring中在什么情况下事物会失效
Spring中事物失效的场景很多 , 例如 :
- 因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 ! 如果使用原始对象事物会失效
- 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效
- 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
- @Transational中默认捕获的是RuntimeException , 如果没有指定捕获的异常类型, 并且程序抛出的是非运行时异常, 事物会失效
05 说一下Spring的事务传播行为有哪些
- PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置
- PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务
- PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
- PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行
06 简单介绍下SpringMVC执行流程
- 用户发送请求至前端控制器DispatcherServlet
- DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle
- 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器 , 组成一个处理器链 , 一并返回给DispatcherServlet
- DispatcherServlet 调用 HandlerAdapter处理器适配器;
- HandlerAdapter 经过适配调用具体处理器(Handler,也叫后端控制器)
- Handler执行完成返回ModelAndView
- HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析
- ViewResolver解析后返回具体View
- DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
- DispatcherServlet响应用户
07 Spring MVC常用的注解有哪些
- @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上
- @RequestBody:注解实现接收http请求的json数据,将json转换为java对象
- @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户
- @Controller:控制器的注解,表示是表现层,不能用用别的注解代替
- @RestController : 组合注解 @Conntroller + @ResponseBody
- @PathVariable : 接收请求路径中的参数
- @RequestParam : 接收请求参数
- @RequestHeader : 接收请求头数据
- @ExceptionHandler : 用于全局异常处理器, 指定补货具体类型的异常
- @ControllerAdvice: 主要用来处理全局数据,一般搭配@ExceptionHandler使用 , 进行异常处理
- @RestControllerAdvice : 组合注解@ControllerAdvice + @ResponseBody
- @CookieValue : 获取请求中指定名称的cookie值
- @SessionAttribute : 获取带session中属性值
- @CrossOrigin : 设置CROS跨域访问
08 #{}和${}有什么区别
- mybatis在处理
#{}
时,底层会将SQL中的#{}
替换为?
号,调用 PreparedStatement进行预编译处理 , 然后调用set方法来赋值 , 可以有效的防止SQL注入 - mybatis在处理
${}
, 底层采用的是字符串拼接的方式, 将参数直接拼接到SQL语句中 , 会有SQL 注入风险
09 讲一讲SpringBoot自动装配的原理
在SpringBoot项目的启动引导类上都有一个注解@SpringBootApplication
这个注解是一个复合注解, 其中有三个注解构成 , 分别是
- @SpringBootConfiguration : 是@Configuration的派生注解 , 标注当前类是一个SpringBoot的配置类
- @ComponentScan : 开启组件扫描, 默认扫描的是当前启动引导了所在包以及子包
- @EnableAutoConfiguration : 开启自动配置(自动配置核心注解)
在@EnableAutoConfiguration注解的内容使用@Import注解导入了一个AutoConfigurationImportSelector.class的类
在AutoConfigurationImportSelector.class中的selectImports方法内通过一系列的方法调用, 最终需要加载类加载路径下META-INF下面的spring.factories配置文件
在META-INF/spring.factories配置文件中, 定义了很多的自动配置类的完全限定路径
这些配置类都会被加载
加载配置类之后, 会配置类或者配置方法上的@ConditionalOnXxxx条件化注解是否满足条件
如果满足条件就会使用@ConfigurationProperties
注解从属性配置类中读取相关配置 , 执行配置类中的配置方法 , 完成自动配置
10 Spring Cloud 5大组件有哪些?
Spring Cloud五大组件主要是
- 注册中心组件 , 例如 : Eureka 和 Nacos
- 负载均衡组件 , 例如 : Ribbon 和 Spring Cloud LoadBalancer
- 远程调用组件 , 例如 : Feign 和 Dubbo
- 服务熔断组件 , 例如 : Hystrix 和 Sentinel
- 服务网关组件 , 例如 : Zuul 和 Spring Cloud Gateway
11 你们项目中微服务之间是如何通讯的?
服务的通讯方式主要有二种 :
1.同步通信:通过Feign发送http请求调用 或者 通过Dubbo发送RPC请求调用
2.异步通信:使用消息队列进行服务调用,如RabbitMQ、KafKa等
数据库
1 MYSQL如何实现多表查询 ?
MYSQL多表查询主要使用连接查询 , 连接查询的方式主要有 :
- 内连接
-
- 隐式内连接 : Select 字段 From 表A , 表B where 连接条件
- 显式内连接 : Select 字段 From 表A inner join 表B on 连接条件
- 外连接
-
- 左外连接 : Select 字段 From 表A left join 表B on 连接条件
- 右外连接 : Select 字段 From 表A right join 表B on 连接条件
- 子查询 : 先查询一部分数据作为临时表 , 再进行关联查询
2 内连接和外连接的区别 ?
- 语法不同 , 内连接语法为 inner join , 也可以省略写成隐式内连接 , 外连接语法是 outer join
- 结果集不同
- 内连接查询 : 内连接查询的是满足条件的二张表的数据组合
- 左外连接 : 左外连接查询左表所有数据以及满足连接条件的二张表数据组合
- 右外连接 : 右外连接右表所有数据以及满足连接条件的二张表数据组合
- 使用场景不同
- 内连接只能查询二张表交集数据, 外连接可以查询一张表的所有数据, 如果需要查询一张表所有数据, 不管满足条件与否, 就必须使用外连接
3 说一下索引失效的场景有哪些
如果索引使用不当, 在很多情况下都有可能会出现索引失效 , 常见的有如下场景 :
- 不满足左前缀法则 , 索引会失效或者部分失效
- 在索引列上使用函数和进行运算会导致索引失效
EXPLAIN select * from tb_user where phone = '17799990015'; --索引生效EXPLAIN select * from tb_user where substring(phone,10,2) = '15'; -- 索引失效
- 使用 != 或 not in等否定操作符会导致后面的索引失效
EXPLAIN SELECT * from tb_user WHERE profession = '工业' and age = 20 and gender = 'M' ;EXPLAIN SELECT * from tb_user WHERE profession = '工业' and age != 20 and gender = 'M' ;
- 连接条件
or
关键词二边 , 只要有一个条件不满足索引, 就会全表扫描 , 索引失效
EXPLAIN SELECT * from tb_user WHERE profession = '工业' or status = 2 ;
- 使用 > , < 等比较运算符号 , 比较运算符后面的条件索引会失效
EXPLAIN SELECT * from tb_user WHERE profession = '工业' and age > 20 and gender = 'M' ;
- 当查询条件左右两侧类型不匹配的时候会发生隐式转换,隐式转换带来的影响就是可能导致索引失效而进行全表扫描
EXPLAIN select * from tb_user where phone = 7553994859 ;
- like 语句的索引失效问题like 的方式进行查询,在 like “value%” 可以使用索引,但是对于 like “%value%” 这样的方式,执行全表查询
create INDEX index_name on tb_user(name) ;EXPLAIN SELECT * from tb_user WHERE name LIKE 'Shen%' ;EXPLAIN SELECT * from tb_user WHERE name LIKE '%Shen' ;
- 数据库在执行的过程中, 如果判断执行索引的效率还没有全表扫描的效率高, 也会走全表扫描
EXPLAIN select * from tb_user where email like 'jialunze@%' ;EXPLAIN select * from tb_user where email like 'j%' ;
4 一个SQL语句执行很慢, 如何分析优化
首先可以开启慢查询, 慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志 , 通过慢查询日志或者命令, 获取到执行慢的SQL语句!
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
临时配置慢查询 :
-- 查看慢查询开关
show variables like '%slow_query_log%';
-- 开启慢查询
set global slow_query_log = 1 ;
-- 设置慢查询时间
set long_query_time = 1 ;
关闭数据库连接 , 重新连接数据库, 慢查询才会生效
然后可以使用EXLPAIN命令分析SQL语句的执行过程
EXPLAIN SELECT * from tb_user WHERE profession = '工业' and age = 20 and gender = '1'
通过EXPLAIN
就能知道问题具体出现在哪里 , 进行针对性的修复就可以了 , 例如 :
- key字段为空代表没有走索引 ,可以考虑添加索引
- type字段如果出现了
index, all, index_merge
等值, 这个查询的效率可能比较低 , 是否可以调整语句结构
- index : 把索引从头到尾扫⼀遍
- all : 全表扫描数据⽂件,然后再在server层进⾏过滤返回符合要求的记录
- index_merge : 表示查询使⽤了两个以上的索引,最后取交集或者并集 , 由于要读取多个索引,性能不好 , 可以考虑使用组合索引
- Extra : 额外信息
- 如果出现
using filesort
代表排序时⽆法使⽤到索引 , 需要考虑为排序字段创建索引 - 如果出现
using join buffer
代表多表查询时被驱动的表没有索引, 考虑建立索引 - 如果出现
using where
表示存储引擎返回的记录并不是所有的都满⾜查询条件,需要在server层进⾏过滤
5 有没有做过MySQL 的性能优化👍
数据库和表设计的优化
- 选择最合适的字段属性
例如 : 定长字符串用char , 不定长用varchr状态, 性别 , 状态等有限数量值的用tinyint
- 尽量把字段设置为NOT NULL , 当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
- 尽量遵循范式设计 , 但是考虑到性能, 可以对查询比较多的字段适当设置冗余字段 , 避免连表查询
- 为合适的表和字段创建索引
- 如果一个字段只存储数字 , 尽量使用数字型字段 , 不使用字符串
索引优化
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
- 在组合/联合索引中,将有区分度高的索引放在前面
SQL语句优化
- 编写SQL尽量规避一些索引失效的情况
- 避免
select *
写法 , 因为执行 SQL 时优化器需要将 * 转成具体的列;每次查询都要回表,不能走覆盖索引。 - 避免复杂 SQL 语句 , 提升可阅读性;避免慢查询的概率;可以转换成多个短查询,用业务端处理
- 用IN来替换OR
- 避免数据类型不一致 , 数据类型不一致会发生隐式类型转化导致索引失效
- 对经常排序和分组的字段创建索引
- 尽量避免使用子查询 , 可以转化为连接查询
- 将多次插入换成批量Insert插入 , 插入的时候有主键的话, 尽量主键顺序插入
- 多表查询中用用小结果集驱动大结果集
- 对于分页查询 , 考虑到页码较大时的深度分页问题 , 提前处理
- 修改语句中经常作为条件的字段要创建索引 , 防止行锁升级为表锁, 影响执行效率
update tb_user set age = 18 where name = '张三'
-- 加行锁 , MYSQL中行锁是加在索引上的如果字段没有建立索引, 这个时候行锁就会自动升级为表锁
架构优化(选说)
- 搭建主从集群 , 主库负责写, 从库负责读 , 实现读写分离和负载均衡 , 提高MYSQL处理能力
- 对于列比较多的表进行垂直拆表 , 拆分成多张表
- 对于行比较多的表进行水平分表 , 拆分成多张表(经常按照时间拆分, 按照区域拆分, 按照数据标识拆分)
- 对于一个库中表比较多 , 数据比较多可以进行分库(微服务天然分库)
- 引入缓存对经常查询的数据进行缓存 , 减少数据库操作的次数
分布式缓存
1 你们项目中哪里用到了Redis ?
在我们的项目中很多地方都用到了Redis , Redis在我们的项目中主要有三个作用 :
- 使用Redis做热点数据缓存/接口数据缓存
- 使用Redis存储一些业务数据 , 例如 : 验证码 , 用户信息 , 用户行为数据 , 数据计算结果 , 排行榜数据等
- 使用Redis实现分布式锁 , 解决并发环境下的资源竞争问题
2 Redis的常用数据类型有哪些 ?
Redis中的数据类型有很多 , 例如 :
- string:最基本的数据类型,二进制安全的字符串,最大512M
- list:按照添加顺序保持顺序的字符串列表
- set:无序的字符串集合,不存在重复的元素
- sorted set:已排序的字符串集合
- hash:key-value对的一种集合
- bitmap:更细化的一种操作,以bit为单位
- hyperlog:基于概率的数据结构
- Geo : 地理位置类型
常用的就是string ,list , set , zset 和hash
3 Redis的数据持久化策略有哪些 ?
Redis 提供了两种方式,实现数据的持久化到硬盘。
- RDB 持久化(全量),是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
- AOF持久化(增量),以日志的形式记录服务器所处理的每一个写、删除操作
RDB和AOF一起使用, 在Redis4.0版本支持混合持久化方式 ( 设置 aof-use-rdb-preamble yes )
4 Redis和Mysql如何保证数据⼀致?
保证数据⼀致的方式有很多 , 需要根据不同的情况选择对应的解决方案, 我所了解的方案主要有三种 :
- 同步双写机制
先更新Mysql,再更新Redis,这个时候如果更新Redis失败,可能仍然不⼀致
- 删除缓存重新加载机制
先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中
这种⽅案能解决1 ⽅案的问题,但是仍然会出现数据不⼀致的问题
⽐如线程1删除了 Redis缓存数据,还没有来得及更新Mysql,此时另外⼀个查询再查询,那么就会把Mysql中⽼数据⼜查到 Redis中
- 延迟双删机制
先删除Redis缓存数据再更新Mysql,再次查询的时候在将数据添加到缓存中 , 这种方案可能仍然会有数据 , 这个时候我们可以在删除之后稍微延迟(1-2S)时间 , 再将数据删除 , 再次查询的时候进行缓存 , 这个时候就能保持一致了 , 至于延迟删除我们可以使用定时任务延迟, 也可以使用消息中间件延迟删除
- 对于一致性要求不高的场景, 也可以使用MQ异步同步, 保证数据的最终一致性 , 不需要直接删除
- 还可以使用Canal进行数据同步 , 将自己伪装成MYSQL的slave节点, 监听binlog文件的变化 , 当数据库中有数据变化, 自动获取变化的数据 , 同步到Redis中
我们项目中会根据业务情况 , 使用不同的方案来解决Redis和Mysql的一致性问题 :
- 对于一些一致性要求不高的场景 , 不做处理
例如 : 用户行为数据 , 我们没有做一致性保证 , 因为就算不一致产生的影响也很小
- 对于时效性数据 , 设置过期时间
例如 : 接口缓存数据 , 我们会设置缓存的过期时间为 60S , 那么可能会出现60S之内的数据不一致, 60S后缓存过期, 重新从数据库加载就一致了
- 对于一致性要求比较高但是时效性要求不那么高的场景 , 使用Canal或者MQ进行数据同步
例如 : 首页广告数据 , 首页推荐数据
数据库数据发生修改----> 发送消息到MQ -----> 接收消息更新缓存
- 对于一致性和时效性要求都比较高的场景 , 使用延迟双删机制
5 什么是缓存穿透 ? 怎么解决 ?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
有哪些解决办法
- 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等
- 缓存无效 key , 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间 , 尽量将无效的 key 的过期时间设置短一点比如 1 分钟
- 布隆过滤器 , 提前将数据库中存在的数据加载到布隆过滤器 , 当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程
6 什么是缓存击穿 ? 怎么解决 ?
某个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,导致数据库存在被打挂的风险
有哪些解决办法
1. 加互斥锁。当热点key过期后,大量的请求涌入时,只有第一个请求能获取锁并阻塞,此时该请求查询数据库,并将查询结果写入redis后释放锁。后续的请求直接走缓存
2. 设置缓存不过期或者后台有线程一直给热点数据续期
7 什么是缓存雪崩 ? 怎么解决
缓存雪崩是指缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
有哪些解决办法
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用
- 限流,避免同时处理大量的请求
- 设置不同的失效时间比如随机设置缓存的失效时间
- 针对热点数据设置缓存永不失效
消息中间件
1 你们项目中哪里用到了RabbitMQ ?
我们项目中很多地方都使用了RabbitMQ , RabbitMQ 是我们项目中服务通信的主要方式之一 , 我们项目中服务通信主要有二种方式实现 :
- 通过Feign实现服务调用
- 通过MQ实现服务通信
基本上除了查询请求之外, 大部分的服务调用都采用的是MQ实现的异步调用 , 例如 :
- 发布内容的异步审核
- 验证码的异步发送
- 用户行为数据的异步采集入库
- 搜索历史记录的异步保存
- 用户信息修改的异步通知(用户修改信息之后, 同步修改其他服务中冗余/缓存的用户信息)
- 静态化页面的异步生成
- MYSQL和Redis , ES之间的数据同步
- ....
2 使用RabbitMQ如何保证消息不丢失 ?
消息从发送,到消费者接收,会经理多个过程 , 其中的每一步都可能导致消息丢失
针对这些问题,RabbitMQ分别给出了解决方案:
- 消息发送到交换机丢失 : 发布者确认机制
publisher-confirm
消息发送到交换机失败会向生产者返回ACK , 生产者通过回调接收发送结果 , 如果发送失败, 重新发送, 或者记录日志人工介入
- 消息从交换机路由到队列丢失 : 发布者回执机制
publisher-return
消息从交换机路由到队列失败会向生产者返回失败原因 , 生产者通过回调接收回调结果 , 如果发送失败, 重新发送, 或者记录日志人工介入
3. 消息保存到队列中丢失 : MQ持久化(交换机持久化, 队列持久化 , 消息持久化)
- 消费者消费消息丢失 : 消费者确认机制 , 消费者失败重试机制
通过RabbitMQ本身所提供的机制基本上已经可以保证消息不丢失 , 但是因为一些特殊的原因还是会发送消息丢失问题 , 例如 : 回调丢失 , 系统宕机, 磁盘损坏等 , 这种概率很小 , 但是如果想规避这些问题 , 进一步提高消息发送的成功率, 也可以通过程序自己进行控制
设计一个消息状态表 , 主要包含 : 消息id , 消息内容 , 交换机 , 消息路由key , 发送时间, 签收状态等字段 , 发送方业务执行完毕之后 , 向消息状态表保存一条消息记录, 消息状态为未签收 , 之后再向MQ发送消息 , 消费方接收消息消费完毕之后 , 向发送方发送一条签收消息 , 发送方接收到签收消息之后 , 修改消息状态表中的消息状态为已签收 ! 之后通过定时任务扫描消息状态表中这些未签收的消息 , 重新发送消息, 直到成功为止 , 对于已经完成消费的消息定时清理即可 !
3 使用Kafka如何保证消息不丢失 ?
使用Kafka在消息的收发过程都会出现消息丢失 , Kafka分别给出了解决方案
- 生产者发送消息到Brocker丢失
设置同步发送和异步发送
- 同步发送可以通过get()获取到消息的发送结果 , 阻塞方案, 效率比较低
- 异步发送可以通过回调获取到消息的发送接口 , 非阻塞方案, 效率较高 , 可能会出现回调丢失
- 设置消息发送失败的重试次数, 设置为一个很大的值, 发送失败不断重试
- 消息在Brocker中存储丢失
Kafka提供了分区的备份机制 , 可以为每个分区设置多个副本 , 主分区服务器宕机, 副本分区还有完整数据
主分区数据同步到副本分区之前, 主分区宕机也有可能会出现消息丢失问题 , 解决方案就是设置消息确认的ACKS
确认机制 | 说明 |
acks=0 | 生产者在成功写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快 |
acks=1(默认值) | 只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应 |
acks=all | 只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应 |
- 消费者从Brocker接收消息丢失
消费者是通过offset来定位消费数据的 , 当消费者出现故障之后会触发重平衡, 会为消费者组中的消费者重新分配消费分区, 正常情况下是没有问题的 , 这也是Kafka提供的消费保障机制
但是在重平衡的过程中 , 因为Kafka默认子每隔5S自动提交偏移量 , 那么就有可能会出现消息丢失和重复消费问题
- 如果提交偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理。
- 如果提交的偏移量大于客户端的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。
解决方案有二种 :
- 设置更小的自动提交偏移量的周期 , 周期越小出现问题的概率也就越小, 对消费者性能和服务器压力的影响就越大(缓解方案,不能从根本上解决问题)
- 消费完毕手动提交偏移量
-
- 同步提交 : 会阻塞, 效率低 , 但是会重试 , 直到成功为止
- 异步提交 : 不会阻塞 , 效率高 , 但是不会重试 , 可能会出现提交失败问题
- 同步异步结合
通过Kafka本身所提供的机制基本上已经可以保证消息不丢失 , 但是因为一些特殊的原因还是会发送消息丢失问题 , 例如 : 回调丢失 , 系统宕机, 磁盘损坏等 , 这种概率很小 , 但是如果想规避这些问题 , 进一步提高消息发送的成功率, 也可以通过程序自己进行控制
设计一个消息状态表 , 主要包含 : 消息id , 消息内容 , 交换机 , 消息路由key , 发送时间, 签收状态等字段 , 发送方业务执行完毕之后 , 向消息状态表保存一条消息记录, 消息状态为未签收 , 之后再向Kafka发送消息 , 消费方接收消息消费完毕之后 , 向发送方发送一条签收消息 , 发送方接收到签收消息之后 , 修改消息状态表中的消息状态为已签收 ! 之后通过定时任务扫描消息状态表中这些未签收的消息 , 重新发送消息, 直到成功为止 , 对于已经完成消费的消息定时清理即可 !
4 使用EMQX如何保证消息不丢失 ?
在MQTT 协议中规定了消息服务质量(Quality of Service),它保证了在不同的网络环境下消息传递的可靠性 !
MQTT消息服务质量QoS等级有三个级别 :
- 0 : 消息最多传递一次, 可能会存在消息丢失
- 1 : 消息至少传递一次 , 不会出现消息丢失, 但是可能会出现消息重复
- 2 : 消息仅传递一次 , 不会出现消息丢失, 也不会出现消息重复
分布式事务
01 在你的项目中哪些模块使用了分布式事务控制 ? 能否举例说明 ?
我最近做的项目中很多地方都使用到了分布式事物 , 例如 :
功能一 :
在用户实名认证审核功能中, 用户实名认证审核通过需要做二个操作
- 调用自媒体微服务创建自媒体帐号
- 修改用户微服务实名认证审核的数据状态
这个业务中涉及到了多个服务的调用, 为了 保证数据的一致性 , 使用了分布式事物控制
功能二 :
在文章审核发布的案例中, 文章审核通过后 , 会调用文章微服务发布文章 , 同时调用自媒体微服务修改文章的发布状态 , 也涉及到了多个服务之间的调用 , 所以需要使用分布式事务
功能三 :
在自媒体用户发布文章的功能中, 文章发布成功之后需要调用阿里云进行审核, 阿里云审核通过之后需要修改自媒体服务的文章状态 , 同时需要发布延迟任务 ,为了保证服务数据的一致性 , 这里也使用了分布式事务
02 说一说Seat AT模式的工作原理?
AT模式是Seata的默认模式 , 是一种无侵入式的事物解决方案
Seata事务管理中有三个重要的角色:
- TC (Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM (Transaction Manager) -事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
- RM (Resource Manager) -资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
第一阶段 :
- TM感知到需要进行分布式事务控制的方法开始执行, 需要向TC发送指令开启全局事务
- TC会开启一个全局事务, 分配一个全局事务ID (XID)
- 之后TM会调用各个分支事务RM , RM收到指令后会向TC注册各自的分支事务 , 注册到同一个全局事务中 , 跟XID进行关联
- RM注册分支事务完毕后首先会记录业务SQL执行之前的数据快照 , 然后执行业务SQL , 再记录业务SQL执行之后的数据快照, 将 数据快照保存到一个undolog表中 , 业务SQL和undolog表的操作在同一个本地事务中 , 要保证同时成功或者同时失败 , 之后直接提交分支事物
- RM执行完毕后向TC汇报各自的执行状态
第二阶段 :
- TM感知到所有的分支事务执行完毕 , 通知TC进行期全局事务决议 , TC收到指令后 , 检查各个分支事务的状态
- 如果所有的分支事务全部执行成功 , 通知各个RM提交事务 , 如果有任何一个分支事物执行失败 , 通知各个RM回滚事务
- RM收到提交/回滚的请求后 , 利用undolog实现数据的提交/回滚
-
- 提交操作 : 删除undolog日志
- 回滚操作 : 根据全局事务ID以及分支事务ID查询 , 前后镜像数据, 生成反向补偿SQL语句 , 将前镜像数据更新回数据库即可
因为他在第一阶段业务SQL执行完毕之后直接提交, 最后通过反向补偿机制实现事务回滚, 所以AT模式是一种最终一致性的分布式事务解决方案
脏写问题 : 在AT模式中 , 因为分二阶段 ,第一阶段直接提交事物, 如果出现事务并发, 可能会出现脏写问题 , seata是通过全局锁的形式解决数据脏写问题的 ! seata的全局锁就是一张数据库表global_lock表 , 当某一个分布式事物获取了全局锁 , 会在表中记录, 其他事物再次获取锁就会失败
分布式搜索
01 说一下什么是倒排索引?
正向索引就是先获取文档内容, 再判断文档内容中是否包含所需的关键词 , 这种查询方式就是正向索引 , MYSQL的模糊查询就是正向索引
而倒排索引,是通过分词策略,形成了词条和文档的映射关系表,这种词条+映射表的结构即为倒排索引
ES的倒排索引底层使用B+树的结构进行组织 , 每一个词条即为B+树的一个节点 , 叶子节点挂的就是词条所在文档的ID , 查询的时候先根据关键词查询文档ID , 在根据文档ID获取文档详情 , 因为走索引查询所以查询效率比正向索引要高的多
02 数据同步解决方案
数据同步的方案有很多 , 常见的有如下几种 :
同步双写
在将数据写入到MySQL的时候,同时将数据写入到ES,实现数据的双写。
优点:业务逻辑简单 , 数据更新及时
缺点:
- 硬编码,需要写入MySQL的地方都需要添加写入ES的代码
- 业务耦合性高
- 存在双写失败丢失数据的风险
- 性能低,本来MySQL的性能就不算高,再写入ES系统性能必然下降
异步双写(MQ方式)
考虑到同步双写的性能和数据丢失问题,可以考虑引入MQ,从而形成异步双写的方案
在Mysql写操作执行完成之后,将插入Mysql的数据传递给MQ,由服务订阅对应的同步主题完成ES的写操作
由于MQ的性能基本比mysql高出一个数量级,所以性能可以得到显著的提高
优点:性能高;不存在丢数据问题。
缺点:
- 硬编码问题:依然存在业务强耦合
- 依然存在复杂度增加
- 系统中增加了mq的代码
- 可能存在时延问题:程序的写入性能提高了,但是由于MQ的消费可能由于网络或其它原因导致用户写入的数据不一定可以马上看到,造成延时
使用Logstash
Logstash中间件会定时获取增量数据,并将其同步到ES中
优点:
- 无需开发,仅需安装配置Logstash即可;
- 凡是SQL可以实现的Logstash均可以实现
- 支持每次全量同步或按照特定字段(如递增ID、修改时间)增量同步;
- 同步频率可控,基于cron表达式;
缺点:
- 定时爬取数据库,增加CPU压力和数据库压力
- 对实时性要求高的场景无法满足
- 不支持被物理删除的数据同步物理删除ES中的数据(可在表设计中增加逻辑删除字段is_delete标识数据删除)
使用Canal
Canal会把自己伪装成一个MYSQL的从节点 , 从主节点从获取binlog日志, 解析获取增量数据, 进行数据同步 , 是一种近实时的同步方式
优点:
- 提供了多种同步机制 , 包括TCP模式和MQ模式
- TCP模式需要自己编写一个Canal服务 , 接收到增量数据之后, 通过http请求调用下游的搜索服务完成数据同步 , 这种同步方式数据一致性比较高 , 但是实现起来会比较麻烦, 需要自己编写Canal服务
- MQ模式 , Canal接收到增量数据之后, 会直接将数据发送给MQ , 下游服务监听MQ中的变化数据完成数据同步 , 这种方式完全解除了服务之间的耦合, 但是因为是基于MQ进行的数据同步 , 时效性可能会差一些 , 下游服务处理不过来的情况下可能会出现堆积
- Canal这种方式不仅支持修改, 同样支持删除操作
具体项目中使用哪一种 , 根据情况进行选择 , 例如 : 使用logstash同步 , 可以这样回答
针对我们项目的情况(允许有一定的延迟,没有强一致要求)最终,我们采用logstash将订单表里的数据增量更新同步到ElasticSearch中。
Logstash的使用也非常简单 , 只需要简单的配置pipeline
即可 , 在pipeline
配置文件中指定数据的输入来源, 过滤方式和输出目标即可 , 我们项目中对于订单数据和售货机数据都是采用Logstash进行同步的
首先我们系统的订单和设备表里都有一个updateTime字段,这部分字段利用的是MybatisPlus的属性自动填充方式获取当前时间来进行自动设置的,我们利用logstash的增量读的方式比对表里的updateTime字段,如果表里的updateTime的值比logstash里游标的值大说明是更新的数据,会由logstash将其同步到ES中
本篇转载于语雀公开文章