基础

OOM出现的几种情况

堆内存溢出

虚拟机栈和本地方法栈溢出

运行时常量池溢出

方法区溢出

微服务优缺点

  • 每个服务基本上仅关注一个业务

  • 每个服务可由不同 团队开发

  • 松耦合,可扩展

  • 可以有不同语言

  • 运维成本过高、复杂度高、服务间通信影响性能

Spirng和SpringBoot区别

SpringBoot基于Spring,是对它的扩展,消除了它的复杂性

SpringBoot是约定大于配置,Spring是配置优先

TCP和UDP的区别

区别TCPUDP
是否连接面向连接无连接
是否可靠可靠传输,使用流量控制和拥塞控制不可靠传输,不使用
连接对象个数只能一对一通信一对一、一对多、多对一、多对多
传输方式面向字节流面向报文
首部开销首部最小20字节,最大60字节首部开销小,仅8字节
场景对可靠性要求高的应用,如文件传输实时应用,视频会议、直播等

get和post区别

区别getpost
请求方式参数在url上,不安全body体内,不可见安全
限制url中的数据最大2048字节可配置无限大
影响页面后退无影响页面后退会重新提交请求
缓存可以缓存不可以缓存

http和https的区别

区别httphttps
协议运行在TCP之上,明文传输,双发无法验证身份http+ssl,ssl运行于TCP之上,是添加了加密和认证的http
端口80443
资源消耗较少加解密处理,会消耗更多的CPU和内存资源
开销无需证书需要证书,需要CA购买
加密机制共享密钥加密和公开密钥加密的混合加密机制
安全性

cookie和session的区别

区别cookiesession
数据存放客户浏览器服务器
安全不安全相对安全
限制4K
场景不重要信息登录用户信息

客户端禁止cookie,session还能用吗?

一般不能用了, 因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了 。可通过url显示传递sessionId或者持久化保存

TCP三次握手四次挥手

三次握手

  • 第一次:客户端给服务端发一个SYN报文,并指明客户端初始化序列号ISN,此时客户端处于 SYN_SEND 状态。

  • 第二次:服务器收到SYN报文, 会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

  • 第三次: 客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

四次挥手

  • 第一次:主动关闭方发送一个 FIN 报文,报文中会指定一个序列号。此时主动关闭方处于 FIN_WAIT1(终止等待1) 状态

  • 第二次:被动关闭方收到 FIN 之后,会发送 ACK 报文,且把主动关闭方的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到主动关闭方的报文了,此时被动关闭方处于 CLOSE_WAIT 状态

  • 第三次:被动关闭方也想断开连接了,发送一个FIN,指定一个序列号

  • 第四次:主动关闭方收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把被动关闭方的序列号值 +1 作为自己 ACK 报文的序列号值,此时主动关闭方处于 TIME_WAIT 状态, 需要过一阵子以确保被动关闭方收到自己的 ACK 报文之后才会进入 CLOSED 状态,被动关闭方收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

设计模式六大原则

  • 单一职责原则: 一个类只干一件事,实现类要单一

  • 里氏替换原则: 子类可以扩展父类的功能,但不能改变父类原有的功能

  • 依赖倒置原则:高层不应该依赖低层,要面向接口编程

  • 接口隔离原则:一个接口只干一件事,接口要精简单一

  • 迪米特原则:不该知道的不要知道,降低耦合度

  • 开闭原则:对扩展开放,对修改关闭

四种引用

  • 强引用:new对象 OOM的时候也不会被回收

  • 软引用: 程序内存不足时,会被回收

  • 弱引用:垃圾回收器发现了就会被回收

  • 虚引用: 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue中 。

SPI机制

如果说一句话概括的话就是:基于接口的编程+策略模式+配置文件的动态加载机制

简单来讲,就是JDK内置的一种服务提供发现机制,可以用来扩张框架和替换组件,主要是面向框架的开发人员的。比如 java.sql.Driver接口,数据库厂商可以根据自身产品的特点针对这个接口做出不同的实现,而java的SPI机制可以为某个接口寻找服务实现,核心思想就是将装配的控制权移到程序之外,达到解耦的目的。

当服务的提供者提供了一种接口的实现以后,需要在classpath下的MATE-INF/services/目录下创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包下的MATE-INF/services/中的配置文件,配置文件中有接口的具体实现类名,然后就可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的类是: java.util.ServiceLoader 。

通过查看这个类的源码便可知道,它实现了迭代器这个接口,所以具有迭代器的属性,主要就是常用道德俩 hasNext和next方法,这里主要调用的还有个被static final 修饰的常量字符串 MATE-INF/services/

应用场景

JDBC加载不同的数据库驱动, java.sql.Driver

Dubbo 基于SPI机制又做了一层封装,主要是@SPI这个注解

Slf4J 加载不同提供商的日志实现类

Spring中大量使用了SPI

​ 自动装配过程中,会加载 META-INF/spring.factories文件

​ spring-web加载不同的容器

​ 类型转换

缺点

  • 多个线程并发使用ServiceLoader类的实例不安全
  • 是通过循环加载实现类,会导致所有的实现类全部加载并实例化一遍

SPI与API的区别

  • API是调用并用于实现目标的类、接口、方法等的描述
  • SPI是扩展和实现以实现目标的类、接口、方法等的描述

创建对象的5种方式

  • 使用 new 关键字,调用了构造函数
  • 使用Class的newInstance方法,调用了构造函数
  • 使用Constructor类的newInstance方法,调用了构造函数
  • 使用clone方法,没有调用构造函数
  • 使用反序列化,没有调用构造函数

HashMap 底层原理

基础

无序且不安全的数据结构,key 唯一,可以为null,value可以重复。再次添加相同的key会覆盖之前的。

区别

jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树

链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)

hash算法简化

resize的逻辑修改(jdk7会出现死循环,jdk8不会)

dk1.7:数组 + 链表 ( 当数组下标相同,则会在该下标下使用链表) Jdk1.8:数组 + 链表(用来解决冲突) + 红黑树 (预值为8 如果链表长度>=8则会把链表&数组大小>=64变成红黑树,红黑树节点个数<6,转为链表 ) Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置 Jdk1.8中链表新元素添加到链表的尾结点 (数组通过下标索引查询,所以查询效率非常高,链表只能挨个遍历,效率非常低。jdk1.8及以 上版本引入了红黑树,以提高查询效率)

红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂要比列表强很多 ,添加红黑树其实就是为了解决hash冲突导致的链化严重的问题。

红黑树

平衡二叉树

基于二叉查找树

每个节点要么是红色,要么是黑色

根节点永黑

所有的叶子节点都是黑色

每个红色节点的两个子节点一定都是黑色

从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点

容量与扩容机制

初始值16,负载因子 0.75

阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子✖容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数,理论上来讲,负载因子越大,导致哈希冲突也就越大,负载因子越小,浪费空间越大,综合测算得出在0.75比较合理

HashMap结构内,我记得有一个记录当前数据量的字段,这个数据量字段到达扩容阈值的话,它就会触发扩容的操作。

HashMap的扩容是采用当前容量向左位移一位 newtableSize = tableSize << 1

cpu不支持乘法运算,指令层面都是转化为加法实现,位运算效率高效直接

设计模式

接口抽象类区别

编写: 抽象类abstract和接口interface

属性:抽象类可以有属性,public、protected、private及默认均可;接口只能有常量属性(public static final)

方法:抽象类可以有private 方法,可以有构造方法,抽象方法必须是public\protected;接口没有构造方法,均是public 隐式abstract, JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰 。

实例化:均不能实例化

实现: extends implements

类关系:一个类只能继承一个抽象类,一个抽象类可被多个类继承;一个类可以实现多个接口,一个接口可以被多个类实现

设计理念: is-a关系 like-a关系

适应场合:一些方法是共同的(吃,睡),无需子类分别实现,特定方法需子类分别实现(飞行动物飞,两栖动物);

特定标识如经典的序列化接口。

String\StringBuffer\StringBuilder区别

  • String类是一个不可变的类,一旦创建就不可以修改。
  • String是final类,不能被继承
  • String实现了equals()方法和hashCode()方法
  • stirng 可赋null;jdk 1.8 环境下后两者可赋null,编译时运行时均不会报错
  • StringBufferStringBuilder 继承自AbstractStringBuilder,是可变类

sbuffer线程安全,方法均用synchronized修饰了,适合多线程操作;sbuilder线程不安全,单线程操作;

在编译阶段就能够确定的字符串常量,其实没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高。 StringBuffer对象的append效率要高于String对象的"+"连接操作。

四大修饰符

public 当前类及同包、子包、其他包都可

private 只能在当前类

protected 只能在当前类、当前包及其子包下访问

default 只能在当前类及当前包下可访问

重载与重写区别

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写 发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

== 和 equals 的区别

== 的作用:   基本类型:比较值是否相等   引用类型:比较内存地址值是否相等

equals 的作用:   引用类型:默认情况下,比较内存地址值是否相等。可以按照需求逻辑,重写对象的equals方法

String str = new String(“abc“) 几个对象

两个或者一个, 一个是静态区的 ” xyz” ,一个是用 new 创建在堆上的对象 ;静态区没有就是两个,有就是一个

final

(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)

sleep和wait的区别

sleep是Thread的方法,wait是Object的方法

sleep方法没有释放锁,而wait方法释放了锁

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

notify()和 notifyAll()有什么区别

都是唤醒处于wait状态的线程,区别是一个唤醒一个,一个是唤醒所有

yield()方法

暂停当前正在执行的线程,执行其他线程,无参,不会释放锁,使当前线程重新回到可执行状态

join()方法

join() 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行

线程和进程的区别

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元

一个程序至少有一个进程,一个进程至少有一个线程

并发和并行的区别

并发其实是指单核cpu的情况下,看起来好像可以同时执行多件事,但是在操作系统层面是cpu时间片切换完成的,只是这个切换操作在用户层面来说感知不到而已。一句话概括就是一个人在一个时间段同时做多件事。

并行就是指我们现在的多核cpu,在某个时间点,每个cpu都可以执行一个进程,来完成多件事。一句话概括就是多个人在同一个时间点做多件事。

多线程

线程生命周期

New 新建

Runnable 正式启动,运行中

Blocked 阻塞,等待获取锁

Waiting 无限制等待状态

Time_waiting 有时限的等待

Terminated 执行完成,进行终止状态

线程池工作原理

线程池做的主要的工作就是控制运行的线程的数量 ,处理过程中将任务放进队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等待其他线程执行完毕,再从队列中取出任务来执行

线程复用

每一个Thread类都有一个start() 方法,当调用start()方法启动线程时,java虚拟机会调用当前类的run方法,那么该类的run()方法其实是调用了Runnable的run()方法。我们可以继承并重写Thread类,在start方法中添加不断循环调用传递过来的runnable对象。循环方法中不断获取runnbale是用Queue实现的,在获取下一个runnable之前是阻塞的。

线程池组成

线程池管理器:用于创建并管理线程

工作线程:线程池中的线程

任务接口:每个任务必须实现的接口,用于工作线程调度其运行

任务队列:用于存放待处理的任务,提供一种缓冲机制

线程池参数

  • corePoolSize:指定线程池中的线程数量
  • maximumPoolSize:指定了线程池中的最大线程数量
  • keepAliveTime:当前线程池数量超过corePoolSize,多余的空闲线程的存活时间,即多长时间内会销毁
  • unit: keepAliveTime的单位时间
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:线程工厂,用于创建线程,一般采用默认的即可
  • handle:拒绝策略,当任务太多来不及处理时,如何拒绝任务

拒绝策略

  • abortPolicy:默认拒绝处理策略,丢弃任务并抛出REE异常
  • discardPolicy:丢弃新来的任务,但是不会抛出异常
  • discardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序
  • CallRunsPolicy:由调用线程处理该任务

两种提交任务的方式

  • execute()
  • submit()

区别:

  1. execute只能执行Runnable类型的任务,submit可以执行Runnable和Callable类型任务
  2. submit方法可以返回持有计算结果的future对象,execute没有
  3. submit方便异常处理,在主线程中通过future的get方法

Fork/join框架

Synchronized

简介:一个enter两个exit实现异常自动释放锁

作用范围

作用于方法时,锁住的是对象实例(this)

作用于静态方法时,锁住的是class实例

作用于一个对象实例时,锁住的是所有以该对象为锁的代码块

实现

深入理解jvm虚拟机里说到,在jvm中一个对象分三部分组成,对象头、实例数据和对齐填充,与 synchronized有关的就是这个对象头

对象头分为两部分:

​ Mark Word(存储一些列的标记位,比如:哈希值、轻量级锁的标记位、偏向锁标记位、分代年龄等)

​ Klass Point(Class对象的类型指针)

原理

Synchronized锁住对象,其指针指向该对象的monitor,每个对象实例都会有一个monitor,其中monitor可以与对象一起创建,销毁或者当线程试图获取对象锁的自动生成。

Monitor 对象

在HotSpot虚拟机中,monitor是由C++中ObjectMonitor实现。

两个队列:_WaitSet_EntryList,用来保存 ObjectWaiter 对象列表;

_owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了 wait() 方法,此时会释放 Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒

可重入原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁 。

锁升级

synchronized 锁有四种交替升级的状态:无锁、偏向锁、轻量级锁和重量级,这几个状态随着竞争情况逐渐升级。

在锁的对象头里有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断一下threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象, 此时就会把锁从轻量级升级为重量级锁

目的:为了降低锁带来的性能消耗

CAS

compare and swag 比较并替换,是一种乐观锁。

包含3个数,内存值V 预期值A 新值B 如果V和A一致,就将V改成B。

问题:

aba问题 一个线程读取它,期望值是2,内存值也是2,想改成3,但是另外一个线程它之前将2改成了3,又改回了2.

解决办法:添加版本号,每次修改+1或者atomic包下的原子类

循环时间长开销大

只能保证一个共享变量的原子操作,多个情况需要加锁

乐观锁

一种乐观思想,认为读多写少,遇到并发的可能性低,每次去拿数据的时候都会认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(跟上一次的版本进行比较,如果一样则更新)适用于多读的场景。在java中的体现就是java.util.concurrent.atomic包下的原子类使用了乐观锁的一种实现方式CAS实现的。

悲观锁

认为写多,遇到并发的可能性高,每次去拿数据的时候都认为别人会修改,所以每次读写数据都会加锁,这样别人想要读写这个数据得先获取锁,java中的悲观锁就是sd, aqs框架下的锁则是先尝试cas乐观锁获取,获取不到,才会转为悲观锁,如RetreenLock

公平锁和非公平锁

公平锁:锁的分配机制是公平的,首先对锁提出获取请求的线程会被分配到锁

非公平锁:随机分配,就近分配的锁机制

可重入锁

同一线程,外层函数获得锁之后,内层函数仍然可以获得该锁,切不受影响

共享锁和独占锁

共享锁: 独占模式下,每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁,独占锁是一种悲观保守的加锁策略,避免了读读冲突,某个线程获取到锁,其他线程则只能等待,限制了不必要的并发性

独占锁:共享锁则允许多个线程同时获取锁,并发访问,其实也就是乐观锁

偏向锁

如果一个线程获取到锁,那么它就进入偏向模式,MarkWord也就变成了偏向结构,如果该线程再次获取锁时,无需做任何同步的操作,获取锁的过程只要检查Mark Word锁的标记位为偏向锁,并且线程id=threadid就可以了

MySQL

DML:update、insert、delete

DDL:表结构修改

DCL:权限操作

DQL:select

一条sql执行过程

服务器端收到请求,解析sql,转化成抽象语法树,执行器解析抽象语法树,生成逻辑执行计划,逻辑执行计划在存储引擎层转化为物理执行计划,然后执行。

5种类型的表(也称存储引擎)

BDB: 事务安全型

HEAP: 存在于内存中(使用散列索引),用于临时高速存储

ISAM(索引顺序访问方法): 缺省表类型,已过时

MyISAM: 默认数据表类型

Merge: 把相同结构的MyISAM数据表组织为一个逻辑单元

InnoDB: 4.0之后推出的数据表类型,支持事务处理机制,具有并发功能、崩溃恢复

索引 B+树(存储引擎层面)

经过排序的一种数据结构,类似于图书馆里的目录,通过目录可以快速查找到所需要的数据

分类:唯一索引 UNIQUE 、主键索引 primary key 、聚集索引、非聚集索引

创建方式: CREATE INDEX INDEX_NAME ON TB_NAME(NAME);

目的:加快查询速度,优化系统效率

顺序查找:复杂度O(n) 二分查找:O(log2n)

优点:建立索引,查找符合条件的索引值,通过保存在索引中的rowid(页码)快速找到记录,可以将随机IO变为顺序IO

缺点:创建与维护耗费时间,与数据量成正比;占用物理空间;对数据进行新增修改删除时,也需动态维护索引,降低了数据的维护速度;

表锁:开销小,加锁快,不会出现死锁,锁定粒度大,锁冲突概率高,并发度低

行锁:开销大,加锁慢,会出现死锁,粒度小,锁冲突概率低,并发度高

页锁:介于中间

执行过程

  1. 客户端先发送查询语句给服务器
  2. 服务器检查缓存,如果存在则返回
  3. 进行sql解析,生成解析树,再预处理,生成第二个解析树,最后再经过优化器,生成真正的执行计划
  4. 根据执行计划,调用存储引擎的API来执行查询
  5. 将结果返回给客户端

客户端到服务端之间的原理

  • 客户端和服务端之间是半双工的, 即一个通道内只能一个在发一个接收, 不能同时互相发互相接收
  • 客户端只会发送一个数据包给服务端,并不会在应用层拆成2个数据包去发(max_allowed_packet可以设置数据包最大长), 这关系到sql语句不能太长
  • 服务端返回给客户端可以有多个数据包, 但是客户端必须完整接收,不能接到一半停掉连接或用连接去做其他事(UI界面可以操作,不同的线程)
  • 例如java,如果没设置fetchSize,那么都是一次性把结果读进内存。当你使用resultSet的时候,其实已经全部进来了,而不是一条条从服务端获取。————使用fetch Size边读边处理的坏处: 服务端占用的资源时间变久了。

查询MySQL服务此时的状态

show full processlist

  • Sleep 正在等待客户端发送新的请求
  • Query 正在执行查询, 或者发结果发给客户端
  • Locked 正在等待表锁(注意表锁是服务器层的, 而行锁是存储引擎层的,行锁时状态为query)
  • Analyzing and statistics 正在生成查询的计划或者收集统计信息
  • copying to tmp table 临时表操作,一般是正在做group by等操作
  • sorting result 正在对结果集做排序
  • sending data 正在服务器线程之间传数据

隔离级别

读未提交:查询语句不加锁,可能会产生脏读

读已提交:可避免脏读的发生

可重复读(默认隔离级别):可避免脏读、不可重复读

串行化(锁表):一致性最好,性能最差,脏读、不可重读、幻读均可避免

一般不会使用读未提交和串行化

oracle 只支持串行化和读已提交,默认为读已提交

脏读:一个事务处理过程种读到另一个事务的数据

不可重复读:一个事务范围内多次查询返回不同结果,数据被另外事务修改并提交了

幻读:针对一批数据整体而言读取了另一条已提交的事务

事务四大特性

一致性:事务执行之前和之后都必须处于一致的状态,如转账,无论如何操作,ab用户总金额是不变的

隔离性:多个用户并发访问数据库,会开启多个事务,事务之间互不干扰

持久性:事务一旦被提交,改变就是永久性的,不会丢失事务提交的操作

原子性:一个事务中的操作,要么全部成功,要么全部失败

两种存储引擎的区别

myisam:不支持事务,但每次查询都是原子的,表级锁,存储表的总行数,表有三个文件(索引文件.myi、表结构文件.frm、数据文件.myd),会把自增主键的id存储到数据文件,重启不会丢失

innodb: 支持acid的事务,支持行级锁和表锁和外键约束,默认为行锁因此可以支持写并发,不存储总行数,自增主键存储到内存里, 所以重启数据库或者是对表进行OPTIMIZE 操作,都会导致最大 ID 丢失

MYSQL 支持事务吗?

在缺省模式下,MYSQL 是 autocommit 模式的,所有的数据库更新操作都会即时提交,所以在缺省情况下,mysql 是不支持事务的。但是如果你的 MYSQL 表类型是使用 InnoDB Tables 或 BDB tables 的话,你的 MYSQL 就可以使用事务处理,使用 SET AUTOCOMMIT=0 就可以使 MYSQL 允许在非 autocommit 模式,使用 COMMIT 来提交更改,或者用 ROLLBACK 来回滚更改。

Mysql 如何优化 DISTINCT?

在所有列上转换为 GROUP BY,并与 ORDER BY 子句结合使用。

如何区分 FLOAT 和 DOUBLE?

浮点数以 8 位精度存储在 FLOAT 中,并且有四个字节。 浮点数存储在 DOUBLE 中,精度为 18 位,有八个字节。

CHAR_LENGTH 和 LENGTH?

CHAR_LENGTH 是字符数,而 LENGTH 是字节数。Latin 字符的这两个数据是相同的,但是对于 Unicode 和其他编码,它们是不同的。

CHAR 和 VARCHAR 的区别?

CHAR 和 VARCHAR 类型在存储和检索方面有所不同 CHAR 列长度固定为创建表时声明的长度,长度值范围是 1 到 255 当 CHAR 值被存储时,它们被用空格填充到特定长度,检索 CHAR 值时需删除尾随空格。

LIKE 声明中的%和_是什么意思?

%对应于 0 个或更多字符,_只是 LIKE 语句中的一个字符。

LIKE 和 REGEXP 操作有什么区别?

LIKE 和 REGEXP 运算符用于表示^和%。 SELECT * FROM employee WHERE emp_name REGEXP “^b”; SELECT * FROM employee WHERE emp_name LIKE “%b”;

Unix 和 Mysql 时间戳之间进行转换

UNIX_TIMESTAMP 是从 Mysql 时间戳转换为 Unix 时间戳的命令 FROM_UNIXTIME 是从 Unix 时间戳转换为 Mysql 时间戳的命令

Mysql 表中允许有多少个 TRIGGERS?

6个 (before/after) insert update delete

mysql 里记录货币用什么字段类型好

DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度 , float和double是以二进制存储的

mysql 有关权限的表都有哪几个?

通过权限表来控制用户对数据库的访问,权限表存放在 mysql 数据库里,由mysql_install_db 脚本初始化。这些权限表分别 user,db,table_priv,columns_priv 和host。

通用sql函数

CONCAT(A, B) - 拼接字符串 FORMAT(X, D)- 格式化数字 X 到 D 有效数字。 CURRDATE(), CURRTIME()- 返回当前日期或时间。 NOW() - 将当前日期和时间作为一个值返回。 MONTH(),DAY(),YEAR(),WEEK(),WEEKDAY() - 从日期值中提取给定数据。 HOUR(),MINUTE(),SECOND() - 从时间值中提取给定数据。 DATEDIFF(A,B) - 确定两个日期之间的差异,通常用于计算年龄 SUBTIMES(A,B) - 确定两次之间的差异。 FROMDAYS(INT) - 将整数天数转换为日期值。

SpringMVC

什么是Spring MVC ?简单介绍下你对springMVC的理解?

Spring MVC是一个基于Java语言实现MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。

优点

支持各种视图技术

与其他框架集成

角色分配明确:前端控制器、处理器映射器、处理器适配器、视图解析器

流程

  1. 用户发送请求到前端控制器 DispatcherServlet;
  2. DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler;
  3. 处理器映射器根据请求的url找到具体的处理器Handler,生成处理器对象及处理器拦截器,一并返回给DispatcherServlet;
  4. DispatcherServlet 调用 HandlerAdapter处理器适配器,请求执行Handler ;
  5. HandlerAdapter 经过适配调用具体处理器进行处理业务逻辑;
  6. Handler处理完返回ModelAndView返回给DispatcherServlet;
  7. DispatcherServlet 将ModelAndView传给ViewResolver视图解析器进行解析;
  8. ViewResolver解析后返回具体的View;
  9. DispatcherServlet对View进行渲染视图 ,其实就是把模型数据填充到视图中
  10. DispatcherServlet响应用户

SpringMvc用什么对象从后台向前台传递数据

通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到

把ModelMap里面的数据放入Session里面

类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key

转发、重定向

转发: 返回值前加 forward 重定向:加redirect

常用注解

Controller RestController=responsebody+controller

RequestMapping 请求url映射 用在类或方法上

RequestBody: 接收请求json数据

ResponseBody: 将返回数据转化为json数据响应给客户端

SpringMvc的控制器是不是单例模式

是单例, 多线程访问的时候有线程安全问题,解决方案是在控制器里面不能写可变状态量 , 如果需要使用这些可变状态,可以使用ThreadLocal机制解决,为每个线程单独生成一份变量副本,独立操作,互不影响。

Spring MVC的异常处理

将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器

Spring

用到的设计模式

单例:一个IOC容器中只有一个实例,如线程池、连接池、缓存等

适配器模式: SpringMVC中的适配器 HandlerAdatper ,AOP中也用到了

代理模式:AOP功能的实现

观察者模式:事件驱动机制

策略模式:Resource接口访问底层资源: UrlResourceClassPathResourceFileSystemResourceServletContextResourceInputStreamResourceByteArrayResource

装饰器模式: 类名含义Wrapper或者 Decorator,实质是动态地给一个对象添加一些额外的职责

工厂模式: Spring使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象

模板方法: jdbcTemplate、redisTemplate、restTemplate

循环依赖

多个bean之间相互依赖,形成了一个闭环,比如:a依赖b b依赖c c依赖a

如何检测是否存在循环依赖

使用一个列表来记录正在创建中的bean,bean创建之前,先去记录中看一下自己是否已经存在与列表中,如果在,则说明存在循环依赖,如果不在,则加入列表,bean创建完毕之后,从列表中删除。

org.springframework.beans.factory.support.DefaultSingletonBeanRegistry

源码方面:

创建单例bean的时候,会调用 beforeSingletonCreation 方法,有一 个Set集合存放目前正在创建中的bean名称,因为 Set里面的元素是唯一的嘛,它会去尝试添加,如果返回失败就说明里面已经存在了,抛出循环依赖的异常;

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法

prototype情况下,则是添加一步操作,判断scope是否是prototype

如何解决

创建bean主要步骤:

  1. 实例化bean,调用构造器创建bean实例
  2. 填充属性,注入依赖的bean, 通过set方式 或者注解方式
  3. 初始化,调用init方法等

以上可得知,可能发生循环依赖的就两种情况 步骤1构造器方式注入依赖和步骤2的方式

什么是早期bean:刚刚实例化好的bean,还没进行属性填充初始化等操作

内部采用3级缓存解决这个问题:

第一级是单例bean的缓存,第二级是早期暴露的bean的缓存,第三级是单例bean工厂的缓存,均是final的,map存储

先查看缓存里是否有,没有则准备创建这个bean,然后在 getSingleton 方法里,依次尝试从3级缓存中查找bean,在二级缓存中没有以及 allowEarlyReference 参数为true时,才去三级缓存中查找,三级缓存返回一个工厂,通过工厂来创建bean,然后放到二级缓存中,再从三级缓存删除,判断是否需暴露早期的bean, 通过 addSingletonFactory 用于将早期的bean暴露出去,将其放到第3级缓存中 ;

其实就是判断是否需要暴露早期的bean,然后放到三级缓存中,然后再放到二级缓存中,然后再放到一级缓存中,删除二三级自己的缓存,用的时候可以直接用,而且只有单例的bean会通过三级缓存提前暴露来解决循环依赖问题。非单例没有缓存。

只用二级缓存会出现早期暴露的bean和最终的bean不一致的问题

事件机制

事件源:事件的触发者,业务

事件:描述发生了什么事情的对象

事件监听器:其他业务,监听到事件发生的时候,做一些处理

spring中事件类作用
org.springframework.context.ApplicationEvent表示事件对象 的父类
org.springframework.context.ApplicationListener事件监听器接 口
org.springframework.context.event.ApplicationEventMulticaster事件广播器
org.springframework.context.event.SimpleApplicationEventMulticaster事件广播器的 简单实现

@EventListener 来实现一个监听器, 直接将这个注解标注在一个bean的方法上,那么这个方法就可以用来处理感兴趣的事件 也可排序,实现ordered接口 或注解

异步

监听器最终是通过 ApplicationEventMulticaster 内部的实现来调用的,所以我们关注的重点就是这个类,这个类默认有个实现类 SimpleApplicationEventMulticaster ,这个类是支持监听器异步调用的,里面有个字段: Executor

所以 我们只需要自定义一个类型为 SimpleApplicationEventMulticaster 名称为applicationEventMulticaster 的bean就可以了,顺便给 executor 设置一个值,就可以实现监听器异步执行了。

代理

jdk动态代理

只能代理接口,创建出来的代理都是java.lang.reflect.proxy的子类

主要用到java.lang.reflect包下一个接口 InvocationHandler 和一个类 Proxy

如何创建代理对象: 使用InvocationHandler接口创建代理类的处理器 , 使用Proxy类的静态方法newProxyInstance直接创建代理对象,然后就可以使用了。

cglib代理

在运行时扩展Java类和实现接口 ,本质上其实就是通过动态的生成一个子类去覆盖所要代理的类,但是final修饰的类和方法无法生成代理。然后底层使用了asm(字节码操作框架)来操作字节码生成的类。

主要用到 Enhancer 类 创建一个被代理对象的子类并且拦截所有的方法调用 ,甚至包括object下面的一些方法

区别:

  1. jdk动态代理只能代理接口,不能对普通的类进行代理,为什么呢,因为它生成的所有代理类的父类均为proxy,java类继承机制不允许多重继承,但是cglib能够代理普通类
  2. jdk动态代理使用java原生的反射api进行操作,在生成类上比较高效,cglib使用asm框架直接对字节码进行操作,执行过程会比较高效

IOC

控制反转,之前我们需要自己创建对象和组装维护,现在交给spring容器来进行创建和组装维护了,ioc是个设计思想,早在spring之前就有了,只不过spring在技术层面用java做了一个实现。主要了为了降低代码耦合,有利于系统维护和扩展

好处

最小化代码量

易于测试

影响小、侵入小、松耦合

即时实列化和延迟加载

DI

依赖注入,其实和ioc描述的是一件事,只不过角度不同,ioc站在对象的角度,di站在容器的角度。

Spring AOP 和 AspectJ AOP 有什么区别

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)

AOP

字面意思,面向切面编程,通俗的讲就是;在程序中一些具有公共特性的类或方法上进行拦截,在之前之后、或者结果返回后增加执行一些方法。主要用在 事务、日志、系统耗时等

target 目标对象即要被增强的类对象

joinpoint 连接点即程序运行的某个点,也就是一个方法的执行

proxy 代理对象,这里会加入需要增强功能

advice 通知即我们需要增强的功能,关注两点:在什么地方 执行什么操作

pointcut 切入点即把通知配置在哪里

aspect 切面即通知和切入点的组合,定义在哪些地方执行什么操作

advisor 通知器或顾问

aop 要求必须通过代理对象访问目标方法的时候,方法才会被 aop 增强。

事务

编程式事务:硬编码的方式

  1. 通过事务管理器来控制事务
  2. 通过事务模板方法方式来控制事务

声明式事务:xml或注解

spring声明式事务是通过事务拦截器TransactionInterceptor拦截目标方法,来实现事务管理的功能的 ;

七种传播行为: 通过@Transactional注解中的propagation(枚举)属性来指定事务的传播行为

事务管理器是同一个的时候才会有以下行为:

  1. REQUIRED 没有就新建,有就加入(默认)
  2. SUPPORTS 支持当前事务,没有就以非事务运行
  3. MANDATORY 使用当前事务,没有就抛出异常
  4. REQUIRES_NEW 新建个,当前存在事务则挂起,新建个事务
  5. NOT_SUPPORTED 以非事务的方式运行,当前存在把它挂起
  6. NEVER 以非事务方式执行,当前存在则抛出异常
  7. NESTED 当前存在则嵌套执行,当前没有则新建

可能失效的几种情况

  1. 未启用事务功能
  2. 方法不是public的
  3. 数据源未配置事务管理器
  4. 自身调用问题:this.方法不会启用事务,必须通过代理对象调用方法
  5. 异常类型错误: 默认情况下,RuntimeException和Error的情况下会回滚事务
  6. 异常被catch代码块吃了,却没有主动抛出异常
  7. 业务和spring事务不在一个线程内
  8. @Tansactional声明的方法调用A,调用无@Tansactional声明的方法B时,在A方法执行时,B也会有事务回滚的特性。反之则A方法无事务。其实还是this(目标对象)和代理对象调用的区别

上下文周期

MyBatis +MyBatis-plus

#{}和${}区别

  1. #{} 是预编译处理, ${}是字符串替换。
  2. Mybatis在处理 #{}时,会将sql中的 #{}替换为?号,调用PreparedStatement的set方法来赋值;
  3. Mybatis在处理 ${}时,就是把 ${}替换成变量的值。
  4. 使用 #{}可以有效的防止SQL注入,提高系统安全性。
  5. ${} 一般用的动态表名或列名,然后会在业务层进行过滤,来防止sql注入的问题

分页

使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。sql中添加分页参数或者使用分页插件都属于物理分页。 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

半自动与全自动

区别在我理解来说其实就是看是否需要编写sql语句

动态sql

查询:where/if 、where/choose/otherwise

修改:set/if

trim 格式化标签,可以完成< set > 或者是 < where > 标记的功能。4个参数: prefix:前缀, prefixOverrides:去掉第一个and或者是or, suffix:后缀, suffixOverrides:去掉最后一个逗号,也可以是其他的标记。

foreach 标签 循环

动态sql原理

简单点讲: 是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能

SqlNode接口,可以简单理解 动态SQL中、等标签信息,里面有个 apply 方法

SqlSource用于描述MyBatis中的SQL资源信息

LanguageDriver用于解析SQL配置,将SQL配置信息转换为SqlSource对象

LanguageDriver解析配置时,会把、等动态SQL标签转换为SqlNode对象,封装在SqlSource中

解析后的SqlSource对象会作为MappedStatement对象的属性保存在MappedStatement对象中

执行Mapper时,会根据传入的参数信息调用SqlSource对象的getBoundSql()方法获取BoundSql对象,这个对象完成了将SqlNode对象转换为Sql语句的过程

一级、二级缓存

一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,存储作用域为session,默认打开,当session 刷新或者关闭时失效。

二级缓存:基于 PerpetualCache 的 HashMap结构, 不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源

SpringBoot

2.X默认都使用cglib代理,官方团队解释是认为使用cglib更不容易出现错误,如需设置jdk动态代理: spring.aop.proxy-target-class=false

什么是SpringBoot

Spring变得越来越复杂,添加maven依赖,配置应用服务器,添加spring配置,springboot建立在spring之上,避免了之前繁琐的操作, 快速整合第三方框架 ,采用约定大于配置的规范,完全采用注解化,内嵌http服务器,

启动流程

  • main函数入口
  • 加载Spring上下文
  • 加载并初始化各种bean
  • 加载配置文件和环境变量
  • 内嵌tomcat的初始化和启动

什么是yaml

一种可读的数据序列化语言,比较结构化

加载yml配置文件的过程

首先在调用SpringApplication.run() 之前,会先初始化SpringApplication,在这个过程中会加载一些基础的拦截器,这些拦截器都实现了ApplicationListener接口,在这个过程中采用的是SPI机制,然后执行run()方法,绑定监听器,执行starting()方法,创建事件并广播出去,然后监听器监听到,调用自己的方法做处理,期间会创建一个Loader实例,实例化过程中会采用SPI扩展加载prop和yml文件,并调用它的load()方法,然后就是解析配置文件和数据封装,

核心注解

@SpringBootApplication 包含以下三个注解:

@SpringBootConfifiguration:组合了 @Confifiguration 注解,实现配置文件的功能。

@EnableAutoConfifiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 例 如: java 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

@ComponentScan:Spring组件扫描

支持日志框架

Log4j2, Lockback(默认), logging

SpringBoot Starter的工作原理

个人理解 SpringBoot就是由各种Starter组合起来的,我们自己也可以开发Starter ,启动时核心注解会去maven中读取每个starter中的 spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中

运行方式

jar包放在服务器运行

maven运行

启动类运行

在 Spring Boot 启动的时候运行一些特定的代码

实现接口 ApplicationRunner 或者 CommandLineRunner ,重写run方法

跨域问题解决

  1. Crossorigin注解

实现 WebMvcConfifigurer接口然后重写addCorsMappings方法解决跨域问题

配置文件加载顺序

同一目录下, bootStrap.yml 的加载顺序要高于application.yml , properties 要优先于yml,以最后加载的为准

SpringCloud

微服务架构

将单体的应用程序分成多个应用程序,多个应该程序就称为微服务, 每个微服务运行在自己的进程中,服务之间采用轻量级通信机制进行相互调用。 根据业务进行划分,可以使用不同语言,不同库, 保证最低限度的集中式管理 。

是什么

就是一系列框架的集合, 在Spring Boot的基础上巧妙地简化了分布式系统基础设施的开发,如配置中心、服务注册发现、智能路由等,把各大公司用的比较好的服务框架组合起来,通过boot的风格封装屏蔽了复杂的配置和实现原理,开发者很轻松就能上手,但也有个问题:不熟悉底层流程,出了问题不好定位。

优点

​ 低耦合,高内聚

​ 配置简单 基于注解

​ 多语言

​ 独立库

缺点

部署麻烦,多个模块都要部署

数据管理不易

性能监控及系统集成测试不易

区别

boot专注快速开发单个个体微服务

cloud 关注全局的管理及协调

cloud依赖于boot

nacos

通过spring的事件机制集成到SpringCloud中去的。

名词解释

namespace 命名空间,主要是用来对环境进行隔离,dev prod test

group 分组 一个系统归为一组

service 某个服务

dataid 一个配置文件

Namespace + Group + Service 锁定服务

Namespace + Group + DataId 锁定配置文件

注册中心 配置中心

@RefreshScope注解动态刷新参数

原理

服务实例在启动时注册到服务注册表,并在关闭时注销

服务消费者查询服务注册表,获得可用实例

服务注册中心需要调用服务实例的健康检查api来验证是否能够处理请求

gateway

请求入口,负责拦截所有请求,再分发到服务上去, 可以实现日志拦截、权限控制、解决跨域、限流、熔断、负载均衡,隐藏服务端的ip,黑名单与白名单拦截、授权等 。

Route(路由):就是转发规则

Predicate(断言或谓词):就是控制 该请求是不是走转发规则的(Route) 条件

Filter(过滤器):就是为该请求添加业务逻辑,修改请求以及响应 通过过滤器可以对请求进行一些修改和响应。

一般是实现GlobalFilter, Ordered这俩接口,重写filter方法和getOrder方法,filter就是要写的业务,order是设置执行顺序,越小越先执行

内置的 RedisRateLimiter 实现自己的RateLimiter,只需实现 RateLimiter 接口

限流的两种方式

漏桶算法: 访问速率超过响应速率,就直接拒绝

令牌桶算法: 系统按照恒定间隔向水桶里加入令牌(Token),如果桶满了的话,就不加了。每个请求来的时候,会拿走1个令牌,如果没有令牌可拿,那么就拒绝服务。

Sentinel(默认滑动窗口算法)

轻量级的流量控制、熔断降级Java库, 提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性

限流算法

计数器、令牌桶、漏斗算法,滑动窗口算法

原理

我们在访问web应用时,在web应用内部会有一个拦截器,这个拦截器会对请求的url进行拦截,拦截到请求以后,读取sentinel 控制台的流控规则,基于流控规则对流量进行限流操作。

自定义

直接或间接实现BlockExceptionHandler

与Hystrix区别

Seata

解决分布式事务问题的框架

分布式事务

分布式事务是指事务的参与者,支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部都执行成功,要么都执行失败。本质上来说,分布式事务就是为了保证不同的数据库的数据一致性。

使用MQ解决分布式事务(消息最终一致性)

使用MQ,发送异步消息,把分布式事务拆分称本地事务,只要两边事务确保执行成功,就没有问题。生产者确保消息发送成功,消费者确保消息消费成功,如果失败,则进行重试,如果一直重试失败,则是代码上或者业务上的处理出了问题,需要人工介入。

AT 模式

通过一张undo_log表来记录回滚日志信息,在执行更新的sql语句之前,先通过这个sql语句生产一条查询语句,查询出前镜像,再执行更新的sql,执行完之后,通过前镜像的id查询出后镜像,把前后镜像加上业务逻辑的sql生产一条回滚日志信息的记录,插入到回滚日志信息表。将来需要回滚数据,就是通过这张表的数据来进行回滚,通过这个回滚日志信息,生产一个update语句,把数据更新回来,这样就完成了回滚的操作。 回滚的方式是反向补偿。

XA 模式

利用本地事务的机制,回滚是本地事务回滚,提交是本地事务提交。由TC通知统一进行回滚,由TC通知统一进行提交。是真正的数据库回滚操作。

TCC 模式

把自定义分支事务纳入到全局事务中来管理,如果当前系统不支持本地事务,可用采用TCC模式 ;

AT和TCC的区别:

AT 需要支持本地事务,提交和回滚的操作不需要自己操作

  • 一阶段 prepare 行为:本地事务中,一并提交业务数据更新和相应的回滚日志记录
  • 二阶段 commit 行为:马上成功结束,异步批量删除回滚记录,(自动)
  • 二阶段 rollback 行为:通过回滚日志记录,生成补偿策略,(自动)

TCC 不需要支持本地事务,提交和回滚的操作需要自己实现

  • 一阶段 prepare 行为:调用自定义的 prepare 的逻辑 (手动)
  • 二阶段 commit 行为:调用自定义的 commit 的逻辑 (手动)
  • 二阶段 rollback 行为:调用自定义的 rollback 的逻辑(手动)

CAP定理

Consistency 一致性: 一定能够读到最新的数据,意思是写之后的读操作,必须返回该写入的值

Availability 可用性:一个请求必须返回一个响应

Partition tolerance 分区容错性:一个挂了不会影响其他的服务

实现原理?

三部分组成:

事务协调器TC:协调者, 维护全局和分支事务的状态,驱动全局事务提交或回滚。

事务管理器TM:发起方, 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

资源管理器RM:参与方

  1. 发起方会向协调者申请一个全局事务id,并保存到ThreadLocal中(弱引用,线程之间不会发生数据冲突)
  2. seata数据源代理发起方和参与方的数据源,将前置镜像和后置镜像写入到 undo_log表中,方便后期回滚使用
  3. 发起方获取全局事务id,通过改写Feign客户端请求头传入全局事务id
  4. 参与方从请求头中获取全局事务id保存到ThreadLocal中,并把该分支注册到SeataServer中
  5. 如果没有出现异常,发起方会通知协调者,协调者通知所有分支,通过全局事务id和本地事务id删除undo_log数据,如果出现异常,通过undo_log逆向生成sql语句并执行,然后删除undo_log语句。如果处理业务逻辑代码超时,也会回滚。

Ribbon和Feign调用服务的区别

ribbon 需自己构建http请求,通过RestT emplate进行调用

OpenFeign

Feign最终发送Request请求以及接收Response响应都是由Client组件来完成,Client在Feign源码中是一个接口,默认情况下Client的实现类是 Client.Default 。它是由 HttpURLConnection来实现网络请求的。另外Client还支持 HttpClient和OkHttp来进行网络请求 。

添加这两个依赖

配置

feign:
  httpclient:
    enabled: false
  okhttp:
    enabled: true

使用步骤:

  1. 引入依赖 spring-cloud-starter-openfeign

  2. 定义一个openfeign接口,接口上添加FeignClient注解,指定服务名,也可以指定ip和端口

  3. 启动类添加EnableFeignClients注解

  4. 接口里写需要远程调用的方法

  5. 引入添加了feign注解的服务,使用它的方法

核心流程:

  1. Spring启动阶段调用方OpenFeign框架会主动扫包
  2. 指定目录下扫描并加载被feignclient注解 ,转换成bean,交给容器管理
  3. 经过 MVC Contract 协议解析,放到方法元数据中
  4. 生成代理对象,添加到容器里,注入到对应的服务中
  5. 调用接口,准备发起远程调用
  6. 从代理对象中找到一个methodhandler实例,生成request,包含有服务请求的url
  7. 经过负载均衡算法找到一个服务的ip地址,拼接出请求的url
  8. 被调用方处理请求,将结果返回给调用方

Redis

每一条命令都会调用call函数

持久化机制 RDB和AOF

客户端执行info命令会显示服务端记录的相关持久化状态信息:是否正在加载RDB文件内容、是否开启AOF功能等

RDB:保存某一个时间点之前的数据

记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

两种触发方式:

	1. 配置参数: save 60 1000   60秒内有1000个key发生变化,则会触发一次RDB快照的执行,save会阻塞服务器进程
2. 客户端执行bgsave命令,redis会调用bgsaveCommand函数,该函数会fork一个子进程执行rdbSave函数进行实际的快照工作,父进程继续处理相关请求,子进程结束后父进程调用回调函数进行后续处理

AOF

简单来说, AOF就是将Redis服务端执行过的每一条命令都保存到一个文件, 这样当Redis重启时只要按顺序回放这些命令就会恢复到原始状态。

RDB保存的是一个时间点的快照, 那么如果Redis出现了故障, 丢失的就是从最后一次RDB执行的时间点到故障发生的时间间隔之内产生的数据。 如果Redis数据量很大, QPS很高, 那么执行一次RDB需要的时间会相应增加, 发生故障时丢失的数据也会增多 。

RDB保存的是最终的数据, 是一个最终状态, 而AOF保存的是达到这个最终状态的过程。 如果Redis有大量的修改操作, RDB中一个数据的最终态可能会需要大量的命令才能达到, 这会造成AOF文件过大并且加载时速度过慢(Redis提供了一种AOF重写的策略来解决上述问题)

而AOF保存的是一条条命令, 理论上可以做到发生故障时只丢失一条命令。 但由于操作系统中执行写文件操作代价很大, Redis提供了配置参数, 通过对安全性和性能的折中, 我们可以设置不同的策略 。

事务

显示开启和提交事务:multi exec

放弃事务: discard

watch 提供了一种乐观锁机制:监听多个key,未被修改时才会执行事务

unwatch:取消监听

过期键的删除策略

定时删除: 在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时,立即执行对键的删除操作

惰性删除: 放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键

定期删除: 每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定

JVM

什么时候可以进行垃圾回收

  • 对象没有任何引用
  • 程序发生意外被终止
  • 堆无法扩展并且无法为对象分配足够的内存空间时
  • 应用程序空闲时

简述Java垃圾回收机制

在Java中,我们是不需要显示的去释放一个对象的内存的,这个操作由虚拟机自己执行。在JVM中,有一个垃圾回收线程,它的优先级较低,只有在虚拟机空闲或者堆内存不足时,才会触发执行,扫描那些没有被引用的对象,并添加到要回收的集合中,进行回收。

组成部分及作用

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

过程

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

深拷贝和浅拷贝

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

堆栈的区别

物理地址

堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)

栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

内存分别

堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。

栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

存放的内容

堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

PS:

  1. 静态变量放在方法区
  2. 静态的对象还是放在堆。

程序的可见度

堆对于整个应用程序都是共享、可见的。

栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

队列和栈是什么?有什么区别?

队列和栈都是被用来预存储数据的。

  • 操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
  • 可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
  • 操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。

Java 中都有哪些引用类型?

  • 强引用:发生 gc 的时候不会被回收。
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

垃圾回收算法

标记-清除算法

标记无用对象,然后进行清除回收。

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。

复制算法

为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。

标记-整理算法

在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。

分代收集算法

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代老年代永久代

垃圾回收器

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

说一下类装载的执行过程?

类装载分为以下 5 个步骤:

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

什么是双亲委派模型?

在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。

类加载器分类:

  • 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
  • 其他类加载器:
  • 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
  • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

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

JVM调优

说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

常用的 JVM 调优的参数都有哪些?

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

Dubbo

SPI机制

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类 。

基于@SPI注解

ES

倒排索引

通过分词策略,形成一个映射关系表,这种词典+映射表即为倒排索引。查询时间复杂度为O(1)。

底层实现是基于FST数据结构,什么是FST呢,就是一种有限状态转移机,它空间占用小,查询速度快。因为es它基于Lucene嘛,而Lucene从4.0以后开始很多地方都在使用这个FST。

索引调优

动态索引层面

存储层面

冷热分离存储,最近几天的为热数据,其余为冷数据,冷数据基本上不会再写入新数据,可以进行一个压缩操作。

部署层面

索引文档过程

官方文档中有一个图,一共分3步

  1. 客户端发送请求,往集群中的某个节点写入数据
  2. 某个节点接收到请求,然后使用文档_id来确定文档属于哪个分片,借助的是路由算法,这个请求就会被转移到分片所在的那个节点。(路由算法是根据路由和文档id计算目标的分片id的过程)
  3. 然后在对应的节点上执行写操作,成功则将请求并行转发到其他节点的副本分片上,等待结果返回。等所有的副本分片都返回成功了,然后这个当前节点会向第一次接收到请求的那个节点返回成功,然后告诉客户端写入成功。

搜索过程

​ query then fetch 查找并获取

query阶段:

​ 假设一个索引数据有N个分片,一次请求会命中一个,然后每个分片在本地查询,结果返回到本地队列中,然后把结果发给其他的协调节点,协调节点产生一个全局的排序列表。

fetch阶段:路由节点获取所有文档,返回给客户端。

SpringSecurity

安全措施

身份认证(你是谁)

权限校验(你能做什么)

攻击防护(防止伪造身份)

流程

  1. 用户登录,过滤器获取,Security会把用户名密码封装到 UsernamePasswordAuthenticationToken 这个类中
  2. 然后就是 AuthenticationManager 身份管理器验证这个类对象
  3. 认证成功以后,

核心组件

AuthenticationManagerBuilder

SecurityContextHolder

Authentication

MQ

优点

异步处理-相比于传统的串行、并行方式,提高了系统的吞吐量

应用解耦-系统间通过消息通信,不用关心其他系统怎么处理

流量削峰-可以通过消息队列长度控制请求量,可以缓解短时间内高并发请求

日志处理-解决大量日志处理

消息通讯-消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

缺点

系统可用性降低

系统复杂度提高

消息基于什么传输

使用信道的方式来传输数据,信道是建立在真实的TCP连接内的虚拟连接,每条TCP连接上的信道数量没限制

常见问题

  1. 消息的顺序消费问题,保证生产者-MQServer-消费者是一对一的关系
  2. 消息的重复问题,保持幂等性,每条消息有个唯一id,利用日志表来存储已经处理成功的id,如果新到的消息id已存在则不处理

工作模式

简单模式:队列中有消息就消费调=掉,可能出现消费者未消费成功消息就 没了。应用场景:聊天
工作模式:资源竞争,多个消费者共同争抢队列里的内容,场景:发红包,大项目中的资源调度
发布订阅模式:共享资源,场景:邮件群发、群聊、广播
路由模式:消费者根据路由key进行消费,场景:error错误、客户通知等,开发者可以自定义消费者,实时接收错误或者通知等
topic模式:路由模式的一种,*代表多个,#代表一个, 就是routing查询的一种模糊匹配,就类似sql的模糊查询方式

交换机类型

fanout:如果交换机收到消息,会广播到所有绑定的队列上

direct:路由键完全匹配,消息就会被投递到对应的队列上

topic:通配符,可以接收不同的源头的消息到达同一个队列

确保发送方发送接收方消费

将信道设置成confirm模式(发送方确认模式),那么所有信道上发布的消息都会被指派一个id,如果消息被投递到队列后,或者被持久化,信道会发送一个确认给生产者(包含消息唯一id),如果内部发生错误导致消息丢失,会发送一条nack消息。这种模式是异步的,生产者可继续发送消息,当收到确认消息,生产者的回调方法会被触发

消费者接收每一条消息后必须进行确认,消息接收和消息确认是两件事,通过消费者的连接中断来确认是否需要重新发送消息。

消息持久化

消息持久化的前提是队列持久化,会把消息写入一个持久化日志文件,消息消费以后,会在日志文件中标记为等待垃圾回收,如果重启,会自动重建交换机和队列,并将持久化日志文件重新发布到合适的队列

Vue

vue-router有哪几种导航钩子

三种,一种是全局导航钩子:router.beforeEach(to,from,next),作用:跳转前进行判断拦截。第二种:组件内的钩子;第三种:单独路由独享组件

指令

v-if:判断是否隐藏;v-for:数据循环出来;v-bind:class:绑定一个属性;v-model:实现双向绑定

vue-router

vue用来写路由一个插件。router-link、router-view

导航钩子

a/全局钩子和组件内独享的钩子。

b/beforeRouteEnter、afterEnter、beforeRouterUpdate、beforeRouteLeave

参数:有to(去的那个路由)、from(离开的路由)、next(一定要用这个函数才能去到下一个路由,如果不用就拦截)

Vue的双向数据绑定原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

具体步骤:

第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: 1、在自身实例化时往属性订阅器(dep)里面添加自己 2、自身必须有一个update()方法 3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

- END -

更多推荐

Java 面试题分享