Redis除了做缓存,还能做什么

  1. 分布式锁 : 可以基于 Redisson 来实现分布式锁。
  2. 限流 :可以通过 Redis + Lua 脚本的方式来实现限流。
  3. 消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用,Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。
  4. 复杂业务场景 :通过 Redis 以及 Redis 扩展提供的数据结构,可以很方便地完成很多复杂的业务场景,如通过bitmap统计活跃用户。

简单介绍一下Redis

Redis是一个使用C语言开发的内存数据库,与传统数据库不同,它的数据存在内存中,读写速度非常快,被广泛应用于缓存方向。

Redis除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。

Redis提供了多种数据类型来支持不同的业务场景。此外,Redis还支持事务 、持久化、Lua 脚本和多种集群方案。

简述Redis和Memcached的异同

共同点

  1. 都是基于内存的数据库,常用于缓存。
  2. 都有过期策略。
  3. 性能都非常高。

区别

  1. Memcached只支持最简单的k/v数据类型,而Redis支持的数据类型更丰富。
  2. Redis 支持数据的持久化,而Memecached把数据全部存在内存中。
  3. Redis 有灾难恢复机制,而Memecached没有。
  4. 在服务器内存使用完之后,Redis可以将不用的数据放到磁盘上,而Memcached会直接报异常。
  5. Memcached没有原生的集群模式,而Redis原生支持cluster模式。
  6. Memcached是多线程,而Redis是单线程。
  7. Redis 支持发布订阅模型、Lua 脚本和事务等功能,而 Memcached 不支持。
  8. Memcached过期数据的删除策略只用了惰性删除,而Redis同时使用了惰性删除和定期删除。

简述drop、delete和truncate区别

  1. drop是删除表,包括表数据和表结构;truncate是清空表数据;delete是删除表数据,如果不加where子句作用和truncate类似。
  2. truncate和drop是DDL语句,操作立即生效,不能回滚,不触发trigger;而delete是DML语句,事务提交之后才生效。
  3. 执行速度:drop > truncate > delete。

什么是存储过程

可以把存储过程看成一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程在业务比较复杂的时候非常实用的,比如很多时候完成一个操作可能需要写一大串SQL语句,这时就可以写一个存储过程。存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯SQL语句执行要快,因为存储过程是预编译过的。

存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。阿里巴巴Java开发手册里要求禁止使用存储过程。

简述主键和外键的区别

主键:用于唯一标识一个元组,不能重复,不允许为空。一个表只能有一个主键。

外键 :用来和其他表建立联系,外键是另一表的主键,外键可以重复,也可以是空值。一个表可以有多个外键。

简述int和Integer的区别

int是基本数据类型,Integer是它的包装类,是引用类型,int的默认值是0,Integer的默认值是
null。

简述堆和栈的区别

  1. 栈存放基本类型变量和对象引用,当超过作用域后会被释放;堆存放new出来的对象和数组。
  2. 栈的存取速度比堆快。
  3. 栈的数据可以共享,堆不可以。
  4. 栈后进先出,堆地址是不连续的,可以随机访问。

常用的创建模式

常用的创建模式有五种,分别是单例模式、工厂方法模式、静态工厂模式、建造模式和原型模式,其中工厂方法模式是类创建模式,其余四种是对象创建模式。

描述一下jvm加载class文件的原理机制

Java中的所有类,都需要由类加载器加载到JVM中才能运行。类加载器本身也是一个类,它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式加载的,除非我们有特殊的用法,像是反射就需要显式的加载所需要的类。

类加载方式有两种 :

1.隐式加载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类加载器加载对应的类到jvm中。

2.显式加载,通过class.forname()等方法,显式加载需要的类。

Java中类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是先将保证程序运行的基础类完全加载到jvm中,至于其他类,则在需要的时候才加载。

Java自带的类加载器有三个:

BootstrapClassLoader:是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件。

ExtClassLoader:是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext下的jar包和class文件。

AppClassLoader:是自定义加载器的父类,负责加载classpath下的类文件。此外,AppClassLoader还是默认的线程上下文加载器。

TCP和UDP协议的区别

  1. TCP面向连接,UDP面向非连接。
  2. TCP可靠,UDP不可靠。
  3. TCP适合传输大量数据,UDP适合传输少量数据。
  4. TCP传输速度慢,UDP传输速度快。

CSS有哪些定位方式,说一下它们的区别

  1. 静态定位:static,默认值,元素框正常生成。
  2. 相对定位:relative,元素相对于自身偏移某个位置,原本所占的空间会被保留。
  3. 绝对定位:absolute,元素相对于最近的父级元素偏移某个位置,原本所占的空间会被删除。
  4. 固定定位:fixed,相对于可视窗口偏移某个位置。

ES6的新特性

  1. 默认参数
var style = function(height=50,color=red,width=70){
    ...
};
  1. 模板对象
var url = `www.baidu.com?username=${username}&password=${password}`
  1. 多行字符串
var ppq = `My name is Tom,
    I'm 20 years old,
    I like JavaScript very much.`
  1. 解构赋值
var {id,name} = response.data
  1. 增强的对象字面量

当属性名称和变量名称相同的时候可以只写一个,成为增强对象字面量

  1. 箭头函数
$('.btn').click((event) => {
    this.sendData();    
});
  1. Promises

Promise的构造函数接收一个参数,参数是一个是函数,并且传入两个参数:resolve和reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。

  1. 块作用域及构造let和const
    let声明的变量只在 let 命令所在的代码块内有效。
    const声明一个只读的常量,一旦声明,常量的值就不能改变。

es6可以通过class声明类

  1. 模块

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于引入其他模块提供的功能。

ES6与JS的区别与联系

ES6发布于2015年,是ECMAScript的最新标准,JS是JavaScript的简称,是ES6的一种实现。JavaScript包括了ECMAScript、DOM和BOM。

简述log4j

log4j是apache的开源项目,有三个重要组件构成:日志信息的优先级、日志信息的输出目的地和日志信息的输出格式。它定义了8个级别的log,除去off和all常用的有6种,优先级从高到低依次是:off、fatal、error、warn、info、debug、trace和all

JSP的内置对象有哪些

jsp有九种内置对象,分别是:request、response、session、application、out、page、config、exception和pageContext。

简述Servlet的生命周期


1)加载和实例化

默认情况下,servlet实例在接受到第一个请求时进行创建并且以后请求进行复用,如果有servlet实例需要进行一些复杂的操作,需要在初始化时就完成,可以配置在服务器启动时就创建实例,具体配置方法为在声明servlet标签中添加1标签。

2)初始化

一旦servlet实例被创建,将会调用servlet的init方法,同时传入ServletConfig实例,传入servlet的相关配置信息,init方法在整个servlet生命周期中只会调用一次。

3)服务

初始化后,Servlet处于能响应请求的就绪状态。当接收到客户端请求时,调用service()方法处理客户端请求,HttpServlet的service()方法会根据不同的请求,转调不同的doXxx()方法。需要注意的是要考虑线程安全问题。

4)销毁

当servlet容器将决定结束某个servlet时,将会调用destory()方法,在destory方法中进行资源释放,一旦destory方法被调用,servlet容器将不会再发送任何请求给这个实例,若servlet容器需要再次使用该servlet,需要重新实例化该实例。

简述MySql索引

MySql索引主要有两种数据结构,哈希索引和BTree索引。

哈希索引的底层数据结构就是哈希表,在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景选择BTree索引。

什么是覆盖索引

如果一个索引包含所有需要查询的字段的值,就称之为覆盖索引。由于覆盖索引不需要回表操作,查询较快。

为什么索引能提高查询速度

MySQL的基本存储结构是页,每个数据页都是是由很多记录组成单向链表,所有数据页组成一个双向链表。

每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过索引查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。而以非索引列作为搜索条件,只能从最小记录开始依次遍历单链表中的每条记录。

最左前缀原则

MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到。

由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。

注意避免冗余索引

冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。

lru算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最长时间没有被使用的数据予以淘汰。

redis内存淘汰策略

LRU:最近最少使用淘汰算法(Least Recently Used),淘汰最长时间没有被使用的数据。

LFU:最不经常使用淘汰算法(Least Frequently Used),淘汰一段时间内,使用次数最少的数据。

  • noeviction:不会驱逐任何key
  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
  • volatile-Iru:对所有设置了过期时间的key使用LRU算法进行删除
  • volatile-random:对所有设置了过期时间的key随机删除
  • volatile-ttl:删除马上要过期的key
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • allkeys-lru:对所有key使用LRU算法进行删除(工作使用)
  • allkeys-random:对所有key随机删除

redis过期键的删除策略

1.定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作 。

优点:节约内存,到时就删除,快速释放掉不必要的内存占用 。

缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量 。

2.惰性删除

数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。

优点:节约CPU性能,发现必须删除的时候才删除 。

缺点:内存压力很大,出现长期占用内存的数据 。

3.定期删除

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 。

特点:

  • CPU性能占用设置有峰值,检测频度可自定义设置 。
  • 内存压力不是很大,长期占用内存的冷数据会被持续清理 。

查看redis内存使用情况

info memory

修改redis内存

  1. 修改配置文件redis.conf的maxmemory参数
  2. 通过命令config set maxmemory 1024

一般生产上如何配置redis内存

一般设置为最大物理内存的四分之三。

redis默认内存有多大

如果不设置最大内存或者设置最大内存为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存

查看Redis最大占用内存

  1. 查看配置文件redis.conf的maxmemory参数,单位是bytes字节。
  2. 通过命令config get maxmemory查看。

Redis事务

Redis的事条是通过MULTI、EXEC、DISCARD和WATCH这四个命令来完成。

Redis的单个命令都是原子性的,所以这里确保事务的对象是命令集合。

Redis不支持回滚的操作。

命令

描述

DISCARD

取消事务,放弃执行事务块内的所有命令。

EXEC

执行所有事务块内的命令。

MULTI

标记一个事务块的开始。

UNWATCH

取消 WATCH 命令对所有 key 的监视。

WATCH key [key …]

监视一个或多个 key ,如果在事务执行之前key 被其他命令改动,那么事务将被打断。

redis数据类型

**基本类型:**string、list、set、zset(sorted set)和hash。

**其他类型:**bitmap、HyperLogLogs、GEO和Stream。

Spring是如何解决循环依赖的

spring通过3级缓存来解决循环依赖。

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。

第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,属性还未填充完,Bean的生命周期未结束。

第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂。

什么是循环依赖

多个bean之间相互依赖,形成了一个闭环。比如:A依赖于B、B依赖于C、C依赖于A。

AOP执行顺序

正常情况下:@Before前置通知----->@After后置通知----->@AfterReturning正常返回

异常情况下:@Before前置通知----->@After后置通知----->@AfterThrowing方法异常

AOP常用注解

@Before 前置通知:目标方法之前执行

@After后置通知:目标方法之后执行(始终执行)

@AfterReturning 返回后通知:执行方法结束前执行(异常不执行)

@AfterThrowing 异常通知:出现异常时候执行

@Around 环绕通知:环绕目标方法执行

AQS是什么

AbstractQueuedSynchronizer(抽象队列同步器),是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

为什么LockSupport唤醒两次后阻塞两次,但最终结果还会阻塞线程

因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

为什么LockSupport可以先唤醒线程后阻塞线程

因为unpark获得了一个凭证,之后再调用park方法有凭证消费,所以不会阻塞。

什么是LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,LockSupport类使用了一种名为Permit(许可)的概念来阻塞和唤醒线程,每个线程都有一个许可(permit),permit只有两个值0和1,默认是0。

park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。

permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

unpark(Thread thread) 唤醒处于阻塞状态的指定线程。

调用unpark(thread)方法后,会将线程的许可permit设置成1(多次调用unpark方法,不会累加,pemit值还是1)唤醒线程,即之前阻塞中的LockSupport.park()方法会立即返回。

让线程等待和唤醒的方法

1.Object类的wait()和notify()

wait和notify方法必须在同步块或同步方法里面成对出现,否则会抛出java.lang.IllegalMonitorStateException,而且要先wait后notify。

2.JUC包中Condition类的await()和signal()

await和signal方法必须成对出现,而且要先await后signal。

3.LockSupport类的park()和unpark()

park()方法会将线程的permit设置为0,unpark()方法会将线程的permit设置成1,多次调用unpark方法,permit不会累加。

可重入锁

可重入锁又名递归锁,是指同一个线程在外层方法获的锁时,再进入该线程的的内层方法会自动获取锁(前提是锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。

Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可以在一定程度避免死锁。

可重入锁的种类:

  • 隐式锁(synchronized):同步块和同步方法
  • 显式锁(Lock)

Synchronized重入的实现机理:

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

GitHub骚操作之T搜索

在项目仓库下按键盘T,进行项目内搜索

GitHub骚操作之#L数字

一行:地址后面紧跟 #L10,如https://github.com/abc/abc/pom.xml#L13。

多行:地址后面紧跟 #Lx - #Ln,如https://github.com/moxi624/abc/abc/pom.xml#L13-L30。

GitHub骚操作之awesome搜索

公式:awesome + 关键字,一般用来收集学习、工具、书籍类相关的项目,如awesome redis

GitHub骚操作之star和fork范围搜索

公式:

  • 关键字 + stars/forks + : + >/ >=
  • 关键字 + stars/forks + : + 数字1…数字2

案例

  • 查找stars数大于等于5000的springboot项目:springboot stars:>=5000
  • 查找forks数在1000~2000之间的springboot项目:springboot forks:1000…5000

组合使用

  • 查找star大于1000,fork数在500到1000的springboot项目:springboot stars:>1000 forks:500…1000

GitHub骚操作之in限制搜索

in关键词限制搜索范围:

公式 :关键词 + in + : + name或description或readme

  • xxx in:name 项目名包含xxx的
  • xxx in:description 项目描述包含xxx的
  • xxx in:readme 项目的readme文件中包含xxx的组合使用

组合使用

  • 搜索项目名或者readme中包含秒杀的项目
  • xxx in:name,readme

GitHub骚操作之常用词

watch:会持续收到该项目的动态

fork:复制其个项目到自己的Github仓库中

star:可以理解为点赞

clone:将项目下载至本地

follow:关注你感兴趣的作者,会收到他们的动态

Linux之网络IO查看

ifstat,默认本地没有,需要下载

Linux之磁盘IO查看

iostat和pidstat

Linux之硬盘查看df

df

Linux之内存查看

free和pidstat

Linux之cpu查看

查看看所有cpu核信息

mpstat -P ALL 2

每个进程使用cpu的用量分解信息

pidstat -u 1 -p 进程编号

Linux命令之整机性能查看

top:整机性能查看

uptime:系统性能命令的精简版

GC之G1收集器

G1 (Garbage-First)收集器,是一款面向服务端应用的收集器,像CMS收集器一样,能与应用程序线程并发执行。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

1.G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。

2.G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

GC之SerialOld收集器

Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,是 Client模式下默认的年老代垃圾收集器。

在Server模式下,主要有两个用途:

  1. 在JDK1.5之前与新生代的Parallel Scavenge 收集器搭配使用。
  2. 作为老年代版中使用CMS收集器的后备垃圾收集方案。

GC之CMS收集器

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器,适合应用在互联网网站和B/S系统的服务器上,是G1出现之前大型应用的首选收集器。

收集过程:

  • 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联的对象,速度很快,需要暂停所有的工作线程。
  • 并发标记(CMS concurrent mark):进行GC Roots跟踪的过程,不需要暂停工作线程。
  • 重新标记(CMS remark):需要暂停所有的工作线程,修正并发标记期间因用户程序继续运行而导致的变动。
  • 并发清除(CMS concurrent sweep):不需要暂停用户线程,清除GCRoots不可达对象。

GC之Parallel Old收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,是JDK1.6才开始提供的吞吐量优先的垃圾收集器。

GC之Parallel收集器

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,是一个并行的多线程的垃圾收集器,即吞吐量优先收集器。

GC之ParNew收集器

ParNew收集器是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中也要暂停所有其他的工作线程。

GC之Serial收集器

单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

Server和Client模式

一般使用Server模式,Client模式基本不会使用。

  • 32位的Window操作系统,不论硬件如何都默认使用Client模式
  • 32位的其它操作系统,2G内存同时有2个以上cpu的使用Server模式,低于该配置使用Client模式
  • 64位只有Server模式

JVM默认的垃圾收集器

Java中一共有7大垃圾收集器

年轻代GC

  • UserSerialGC:串行垃圾收集器
  • UserParallelGC:并行垃圾收集器
  • UseParNewGC:年轻代的并行垃圾回收器

老年代GC

  • UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
  • UseParallelOldGC:老年代的并行垃圾回收器
  • UseConcMarkSweepGC:(CMS)并发标记清除

老嫩通吃

  • UseG1GC:G1垃圾收集器

串行、并行、并发和G1四大垃圾回收方式

串行垃级回收器(Serial):只使用一个线程进行垃圾收集,会暂停所有的用户线程,不适合服务器环境。

并行垃圾回收器(Parallel):多个垃圾收集线程并行工作,也会暂停所有的用户线程,适用于科学计算和大数据处理等弱交互场景。

并发垃圾回收器(CMS):用户线程和垃圾收集线程同时执行,不需要停顿用户线程,适用于响应时间有要求的场景。

G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

ZGC:Java 11的,了解即可

OOM

1.StackOverflowError

栈深度超过虚拟机分配给线程的栈大小,无限制的递归调用导致栈空间用尽,会抛出此异常。

2.Java heap space

创建新的对象时, 堆内存中的空间不足以存放新创建的对象,或者堆内存使用量达到最大内存限制会抛出此异常。

3.GC overhead limit exceeded

超过98%的时间用来做GC并且回收了不到2%的堆内存会抛出OutOfMemroyError。

4.Direct buffer memory

本地内存使用光了,再次尝试分配本地内存就会出现OutOfMemoryError。

5.unable to create new native thread

创建太多线程,超过了系统承载极限,会抛出此异常。

6.Metaspace

Java 8以后使用Metaspace来替代了永久代,Metaspace空间溢出会抛出此异常。

引用队列ReferenceQueue

ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。

创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于是一种通知机制。

当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVW允许我们在对象被销毁后,做一些我们自己想做的事情。

四大引用强软弱虚

1.强引用Reference

JVM宁可抛出OOM异常,也不会回收强引用关联的对象,因此强引用是造成Java内存泄漏的主要原因之一。

2.软引用SoftReference

当系统内存充足时不会回收软引用关联的对象,当系统内存不足时会回收。

3.弱引用WeakReference

不管JVM的内存是否足够,只要一进行垃圾回收,都会回收弱引用关联的对象。

4.虚引用PhantomReference

虚引用关联的对象在任何时候都可能被回收,不能单独使用,也不能通过它访问对象,必须和引用队列(ReferenceQueue)联合使用。

JVM常用基础参数

-Xss:设置单个线程栈的大小,一般默认为512k~1024K,等价于-XX:ThreadStackSize。

-XX:ThreadStackSize=size

-Xmn:设置年轻代大小

-XX:MetaspaceSize 设置元空间大小

元空间和永久代的最大的区别是元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

-XX:+PrintGCDetails 输出详细GC收集日志信息

-XX:SurvivorRatio:调节新生代中 eden 和 S0、S1的空间比例,SurvivorRatio的值就是eden区占的比例,S0和S1相同。默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1。

假如设置成 -XX:SurvivorRatio=4,则Eden:S0:S1 = 4:1:1

-XX:NewRatio设置年轻代和老年代在堆中的占比,NewRatio的值为老年代的占比,剩下的1是新生代。默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3。

-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5。

新生代特别小,会造成频繁GC。

-XX:MaxTenuringThreshold:晋升到老年代的对象年龄,值在 0~15之间,默认是15。

-XX:MaxTenuringThreshold=0

查看JVM参数

-XX:+PrintFlagsInitial查看初始默认参数值

java -XX:+PrintFlagsInitial

-XX:+PrintFlagsFinal查看修改更新参数值,=表示默认,:=表示修改过的。

java -XX:+PrintFlagsFinal

-XX:+PrintCommandLineFlags打印命令行参数

如何查看一个正在运行中的java程序的某个jvm参数是否开启,具体值是多少

  1. jps -l 查看一个正在运行中的java程序,得到Java程序号。
  2. jinfo -flag PrintGCDetails (Java程序号 )查看它的某个jvm参数是否开启。
  3. jinfo -flags (Java程序号 )查看它的所有jvm参数

142.MinorGC的回收过程

Java堆从GC的角度可以分为: 新生代和老年代,新生代又分为Eden 区、From Survivor 区和To Survivor 区。

MinorGC的过程(复制->清空->互换)

首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到Survivor From区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象会被复制到To区域,同时把这些对象的年龄+1,如果有对象的年龄已经达到了老年的标准,则复制到老年代区。

其次,清空Eden区和Survivor From区中的对象。

最后,Survivor To和Survivor From互换,原Survivor To成为下一次GC时的Survivor From,部分对象会在From和To区域中来回复制。默认情况下,如此交换15次还存活的对象,会被存入老年代。(由JVM参数MaxTenuringThreshold决定)

死锁编码及定位分析

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够碍到满足,死锁出现的可能性很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁主要原因

  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当

发生死锁的四个条件:

  1. 线程使用的资源至少有一个不能共享的。
  2. 至少有一个线程持有一个资源且正在等待获取一个当前被别的线程持有的资源。
  3. 资源不能被抢占。
  4. 循环等待。

如何解决死锁问题

破坏发生死锁的四个条件之一即可。

查看是否死锁工具

  1. jps命令定位进程号
  2. jstack找到死锁

如何合理配置线程池的线程数

  1. CPU密集型任务需要大量的运算,而没有阻塞,CPU一直全速运行,应该配置尽可能少的线程数量。参考公式:CPU核数+1
  2. IO密集型任务线程不是一直在执行任务,应配置尽可能多的线程。参考公式:CPU核数/ (1-阻塞系数),阻塞系数在0.8~0.9之间

实际项目中使用哪种线程池,为什么不使用jdk自带的线程池

项目中使用自定义的线程池。

FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,而导致 OOM。

CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,而导致 OOM。

线程池的4种拒绝策

线程池的4种拒绝策均实现了RejectedExecutionHandler接口。

  • AbortPolicy:默认,直接抛出 RejectedExecutionException异常阻止系统正常运行。
  • CallerRunsPolicy:调用者运行一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的方案。

线程池7大参数

corePoolSize:核心线程数。

maximumPoolSize:最大线程数,必须大于等于1。

keepAliveTime:多余空闲线程的存活时间。

unit:keepAliveTime的单位。

workQueue:任务队列。

threadFactory:线程工厂。

handler:拒绝策略。

常用线程池

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService和ThreadPoolExecutor这几个类。

  • Executors.newScheduledThreadPool() :周期性执行任务的线程池。
  • Executors.newWorkStealingPool(int):Java8新增的,使用目前机器上可用的处理器作为它的并行级别。
  • Executors.newSingleThreadExecutor():单线程线程池,它只会用唯一线程顺序执行,newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它底层使用的是LinkedBlockingQueue。
  • Executors.newFixedThreadPool(int):定长线程池,可控制线程最大并发数,超出会在队列中等待。 newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它底层使用的是LinkedBlockingQueue。
  • Executors.newCachedThreadPool():可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若不够,则新建线程。newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它底层使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

线程池的使用以及优势

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

线程池的优点:

  1. 降低资源消耗:通过重复利用己创建的线程,降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,可以不用的等到线程创建就能立即执行任务。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、监控和调优。

Callable接口

Callable接口线程执行完成后,能够返回结果。

Synchronized和Lock有什么区别

  1. synchronized是java的关键字,是JVM层面的;而Lock是具体类,是api层面的。
  2. synchronized不需要手动释放锁,而 Lock需要,如果不主动释放,可能出现死锁。
  3. synchronized等待不可以中断,除非抛出异常或者正常运行完成;而Lock可以中断。
  4. synchronized是非公平锁 ,而Lock默认非公平,可以在创建时传入一个boolean值,指定公平还是非公平。
  5. synchronized只能随机唤醒或者全部唤醒,而Lock可以通过绑定多个条件(Condition),实现分组唤醒或者精确唤醒。

阻塞队列

当阻塞队列为空时,从队列中获取元素的操作将会被阻塞;当阻塞队列已满时,往队列里添加元素的操作将会被阻塞。

阻塞队列的优点:

使用阻塞队列不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,BlockingQueue会自动处理。

Concurrent包发布以前,在多线程环境下,程序员需要自己控制这些细节,尤其还要兼顾效率和线程安全,这会给程序带来不小的复杂度。

阻塞队列种类:

ArrayBlockingQueue:由数组结构组成的有界阻塞队列。

LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。

PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

DelayQueue:使用优先级队列实现的延迟无界阻塞队列。

SynchronousQueue:不存储元素的阻塞队列。

LinkedTransferQueue:由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

BlockingQueue的核心方法:

方法类型

抛出异常

特殊值

阻塞

超时

插入

add(e)

offer(e)

put(e)

offer(e,time,unit)

移除

remove()

poll()

take()

poll(time,unit)

检查

element()

peek()

不可用

不可用

性质

说明

抛出异常

当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full;当阻塞队列空时:再从队列中remove移除元素,会抛出NoSuchException

特殊性

插入方法,成功true,失败false;移除方法:成功返回出队列元素,队列没有就返回空

一直阻塞

当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据或者响应中断退出。 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

超时退出

当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出

Semaphore

Semaphore信号量,主要用于两个作用,一是用于多个共享资源的互斥使用,二是用于并发线程数的控制。

Lock和synchronized在任何时刻都只允许一个任务访问一项资源,而Semaphore允许多个任务同时访问这个资源。

CyclicBarrier

CyclicBarrier可循环使用的屏障,它可以让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障才会打开,所有被屏障拦截的线程才能继续工作。

线程进入屏障通过CyclicBarrier的await方法。

CyclicBarrier与CountDownLatch的区别是CyclicBarrier可以重复多次,而CountDownLatch只能使用一次。

CountDownLatch

让一线程阻塞直到另一些线程完成一系列操作才被唤醒。

CountDownLatch主要有两个方法await()和countDown()。

当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,调用await方法被阻塞的线程会被唤醒,继续执行。

读写锁

**独占锁:**指该锁一次只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁

**共享锁:**指该锁可被多个线程所持有。

多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源可以同时进行。但是,如果有一个线程想去写共享资源,就不能再有其它线程对该资源进行读或者写了。

ReentrantReadWriteLock的读锁是共享锁,写锁是独占锁。

自旋锁

是指获取锁失败的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

可重入锁

可重入锁也叫做递归锁,是指同一个线程在获的外层方法的锁时,再进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

ReentrantLock和synchronized都是典型的可重入锁,可重入锁最大的作用是避免死锁。

公平锁和非公平锁

公平锁―是指多个线程按照申请锁的顺序来获取锁,先到后得。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

创建ReentrantLock时,可以通过指定构造函数的参数(boolean)来得到公平锁或非公平锁,默认是非公平锁。

集合类不安全之Map

java.util.ConcurrentModificationException

  1. 使用HashTable
  2. 使用Collections.synchronizedMap(new HashMap<>())
  3. 使用ConcurrentHashMap

集合类不安全之Set

java.util.ConcurrentModificationException

  1. 使用Collections.synchronizedSet(new HashSet<>())
  2. 使用CopyOnWriteArraySet

集合类不安全之ArrayList

java.util.ConcurrentModificationException

  1. 使用Vector
  2. 使用Collections.synchronizedList()
  3. 使用CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素,CopyOnWrite容器也是一种读写分离的思想。

ABA问题

CAS算法实现的一个重要前提是取出内存中某时刻的数据与当下时刻比较并替换,那么在这个时间差内可能存在数据变化。

比如线程1从内存位置V取出A,此时线程2也从内存位置V取出A,线程2进行了一些操作将值变成了B,然后又变成了A,这时线程1进行CAS操作发现内存中V位置是A,线程1操作成功。

尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。

如何解决ABA问题

使用版本号原子引用AtomicStampedReference。

UnSafe

UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

CAS

比较并交换,是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA中就是sun.misc.Unsafe类中的各个方法,调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。

CAS缺点

  1. do while循环,如果CAS失败,会一直尝试,长时间一直不成功,可能会给CPU带来很大的开销。
  2. 只能保证一个共享变量的原子操作。
  3. 存在ABA问题。

JMM

java内存模型,是Java Memory Model的简写。本身是一个抽象概念并不真实存在,它描述的是一组规范,通过这组规范定义了程序中各个变量的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷回主内存。
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存。
  3. 加锁解锁必须是同一把锁。

由于JVM运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。

Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

线程对变量的操作必须在工作内存中进行,因此首先要将变量从主内存中拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回主内存。

不同线程无法访问对方的工作内存,线程间的通信必须通过主内存来完成,其简要访问过程如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6x9EQiEt-1644861584481)(https://note.youdao.com/yws/api/personal/file/WEB69b71c5ec591fecd8f3a3b671617bec9?method=download&shareKey=a4a5b3471a488ccb63f21479c701f0b8)]

请谈谈你对volatile的理解

volatile是java虚拟机提供的轻量级的同步机制,具有以下三个特点:

  • 保证可见性。
  • 不保证原子性。
  • 禁止指令重排(保证有序性)。

**可见性:**各个线程对主内存中共享变量的操作都是在各自线程自己的工作内存操作完成后再写回主内存的,这就可能存在一个问题,线程A修改了共享变量X的值但还未写回主内存时,线程B又对主内存中共享变量X进行了操作,但此时A线程工作内存中共享变量X对线程B不可见,这种主内存与工作内存同步延迟的现象就造成了可见性问题。

**原子性:**就是完整性,即某个线程在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。

指令重排:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排,一般分以下3种:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ntdFqEza-1644861584482)(https://note.youdao.com/yws/api/personal/file/WEB5f729f78501ef281fe809649655f9695?method=download&shareKey=dcbfec3b93e24b4dc52b6f31df21a959)]

单线程环境里面能够确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

Elasticsearch和Solr的异同

**相同点:**Elasticsearch和Solr都是基于Luence搜索服务器开发的高性能的企业级的搜索服务器,都是以分词技术构建的倒排索引的方式进行查询的。

区别:

  1. 当实时建立索引的时候,solr会产生io阻塞,而es不会,es查询性能要高于solor。
  2. 在不断动态添加数据的时候,solr的检索效率会变低,而es没有什么变化。
  3. solr利用zookeeper进行分布式管理,而es自身带有分布式系统管理功能。solr一般都要部署到web服务器上,比如tomcat,启动tomcat时需要配置tomcat与solr的关联。
  4. solr支持更多的数据格式(xml、json、csv等),而es只支持json文件格式。
  5. solr是传统搜索应用的有力解决方案,但是es更适用于新兴的实时搜索应用。单纯的对已有数据进行检索时,solr的效率高于es。
  6. solr官网提供的功能更多,而es本身更注重于核心功能,高级功能多由第三方插件提供。

JVM垃圾回收机制,GC发生在JVM哪部分,有几种GC,它们的算法是什么?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-372XPG3v-1644861584483)(https://note.youdao.com/yws/api/personal/file/WEBb93794cee624b4d64e24f65abaf6fc06?method=download&shareKey=947b47ea11d325bf679614e1de8bc617)]

GC发生在堆中,分为Minor GC和Full GC。次数上频繁收集young区,较少收集old区,基本不动永久区。

GC算法有引用计数法、复制算法、标记清除、标记压缩和分代收集几种。

引用计数法(已淘汰):

引用计数法是垃圾收集器中的早期策略,每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时对象可以被回收。

**优点:**执行速度快,对程序需要不被长时间打断的实时环境比较有利。

**缺点:**无法解决循环引用的问题。

复制算法:

年轻代中使用的是Minor GC,采用的是复制算法。

从GC Root开始,从From中找到存活对象,拷贝到To中, From和To交换身份,下次内存分配从To开始。

优点:

  1. 没有标记和清除过程,效率高。
  2. 没有内存碎片,可以利用bump-the-pointer实现快速内存分配。

**缺点:**需要双倍空间。

标记清除算法:

老年代一般是标记清除或者标记清除与标记整理混合实现。

  1. 标记:从GC Root开始扫描,对存活对象进行标记。
  2. 清除:扫描整个内存空间,回收未被标记的对象。

**优点:**不需要额外空间。

缺点:

  1. 两次扫描,耗时严重。
  2. 会产生内存碎片。

标记整理:

  1. 标记:同标记清除。
  2. 压缩:再次扫描,并往一端滑动存活对象。

**优点:**没有内存碎片,可以利用bump-the-pointer。

**缺点:**移动对象比较耗费资源。

分代收集算法:

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干不同的区域。一般情况下将堆划分为老年代和新生代,在堆区之外还有一个代就是永久代。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象被回收,那么可以根据不同代的特点采取最适合的收集算法。

在新生代中,每次垃圾收集时有大批对象被收集,因此采用复制算法。而在老年代,由于对象存活率高,采用标记清除或标记整理进行回收。方法区永久代回收方法同老年代。

什么时候适合创建索引

什么是索引

索引是帮助mysql高效获取数据的数据结构。

优势:

  1. 提高数据检索的效率,降低数据库IO成本。
  2. 通过索引对数据进行排序,降低数据排序的成本,降低了CPU的消耗。

**劣势:**降低更新表的速度,对表进行增删改等操作时,不仅要保存数据还需要维护索引。此外索引还会占用一定的物理空间。

适合创建索引的条件:

  1. 主键自动建立唯一索引。
  2. 频繁作为查询条件的字段。
  3. 查询中与其他表关联的字段。
  4. 优先考虑创建组合索引。
  5. 查询中出现在order by子句中的字段。
  6. 查询中统计或者分组的字段。

不适合创建索引条件:

  1. 表记录少的。
  2. 经常增删改的表或字段。
  3. where子句中用不到的字段。
  4. 过滤性不好的字段。

git相关命令

**创建分支:**git branch + 名称

**查看分支:**git branch -v

**切换分支:**git checkout + 分支名

**创建并切换到分支:**git checkout -b + 分支名

合并分支:

  • step1,切换到主干。
    git checkout master
  • step2,合并。
    git merge + 分支名

删除分支:

  • step1,切换到主干。
    git checkout master
  • step2,删除
    git branch -D + 分支名

mybatis中当实体类的属性名和表的字段名不一致时怎么处理

  1. 写SQL时给不一致的字段设置别名。
<select id="getEmployeeById" resultType="com.atguigu.mybatis.entities.Employee">
	select id,first_name firstName,email,salary,dept_id deptID from employees where id = #{id}
</select>
  1. 在实体类上标注注解。
  2. 在mybatis的全局配置文件中开启驼峰命名规则,仅可以解决因字段名单词之间有下划线导致的不一致问题。
mapUnderscoreToCamelCase:true/false 
<!--是否启用下划线与驼峰式命名规则的映射(如first_name => firstName)-->
<configuration>  
    <settings>  
        <setting name="mapUnderscoreToCamelCase" value="true" />  
    </settings>  
</configuration>
  1. 在mapper映射文件中使用resultMap来自定义映射规则。
<select id="getEmployeeById" resultMap="myMap">
    select * from employees where id = #{id}
</select>

<!-- 自定义高级映射 -->
<resultMap type="com.atguigu.mybatis.entities.Employee" id="myMap">
    <!-- 映射主键 -->
    <id column="id" property="id"/>
    <!-- 映射其他列 -->
    <result column="last_name" property="lastName"/>
    <result column="email" property="email"/>
    <result column="salary" property="salary"/>
    <result column="dept_id" property="deptId"/>
</resultMap>

spring mvc如何解决请求中文乱码问题

post请求:在web.xml中配置字符编码过滤器CharacterEncodingFilter。

<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name> <!--强制编码-->
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

get请求:修改tomcat中server.xml文件中的URIEncoding。

<Connector URIEncoding="UTF-8" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

堆、栈和方法区

**堆(Heap):**几乎所有的对象实例和数组都在堆上分配。
**栈(Stack):**用于存放基本类型变量和对象引用,方法执行完,自动释放。
**方法区(Method Area):**用于存储己被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等数据。

方法的参数传递

public class TestDemo {
    public static void main(String[] args) {
        int i = 1;
        String str = "hello";
        Integer num = 2;
        int[] arr = {1,2,3,4,5};
        MyData md =new MyData();
        change(i, str, num, arr, md);
        System.out.println(i);
        System.out.println(str);
        System.out.println(num);
        System.out.println(Arrays.toString(arr));
        System.out.println(md.a);
    }

    public static void change(int i, String str, Integer num, int[] arr, MyData md) {
        i +=1 ;
        str += " world";
        num += 1;
        arr[0] += 1;
        md.a = 1;

    }
}

class MyData{
    int a = 10;
}

**运行结果:**1 hello 2 [2, 2, 3, 4, 5] 1

总结:

  1. 方法的传递机制,基本类型传递数据值,引用类型传递地址值。
  2. String和包装类等具有不可变性。

类和实例的初始化

public class Father {
    private int i = test();
    private static int j = method();

    static {
        System.out.println("1");
    }

    Father() {
        System.out.println("2");
    }

    {
        System.out.println("3");
    }

    public static int method() {
        System.out.println("5");
        return 1;
    }

    public int test() {
        System.out.println("4");
        return 1;
    }
}

public class Son extends Father {
    private int i = test();
    private static int j = method();

    static {
        System.out.println("6");
    }

    Son() {
        System.out.println("7");
    }

    {
        System.out.println("8");
    }

    @Override
    public int test() {
        System.out.println("9");
        return 1;
    }

    public static int method() {
        System.out.println("10");
        return 1;
    }

    public static void main(String[] args) {
        Son son1 = new Son();
        System.out.println();
        Son son2 = new Son();
    }
}

运行结果:5 1 10 6 9 3 2 9 8 7 9 3 2 9 8 7

总结:

  1. 类初始化过程
  • 一个类要创建实例需要先加载并初始化该类
  • main方法所在的类需要先加载和初始化
  • 一个子类要初始化需要先初始化父类
  • 一个类初始化就是执行方法
  • 方法由静态类变量显示赋值代码和静态代码块组成。
  • 类变量显示赋值代码和静态代码从上到下顺序执行。
  • 方法只执行一次。
  1. 实例初始化过程
  • 实例初始化就是执行方法。
  • 方法可能重载有多个,有几个构造器就有几个方法。
  • 方法由非静态实例变量显示赋值代码和非静态代码块及对应构造器代码组成。
  • 非静态变量显示赋值代码和非静态代码块从上到下顺序执行,而对应构造器的代码最后执行。
  • 每次创建实例对象,调用对应构造器,执行的就是对应的方法。
  • 方法的首行是super()或super(实参列表),对应父类的方法。
  1. 方法的重写
  • 哪些方法不能被重写
  • final方法
  • 静态方法
  • 私有方法
  • 对象的多态性
  • 子类重写了父类方法,通过子类对象直接调用的一定是子类重写的代码。
  • 非静态方法默认调用的对象是this。
  • this对象在构造器或者说方法中就是正在创建的对象。

自增变量

public static void main(String[] args) {
    int i = 1;
    i = i++;
    int j = i++;
    int k = i + ++i * i++;
    System.out.println("i: " + i);
    System.out.println("j: " + j);
    System.out.println("k: " + k);
}

**结果:**i=4 j=1 k=11

总结:

  1. 赋值=最后计算。
  2. =右边的从左到右依次压入操作数栈。
  3. 实际先算哪个,看运算符优先级。
  4. 自增和自减都是直接修改变量的值,不经过操作数栈。
  5. 最后赋值之前,临时结果存储在操作数栈中。

简述kafka的rebalance机制

**rebalance:**是consumer group中的消费者与topic下的partition重新匹配的过程。

何时会产生rebalance:

  1. consumer group中的成员个数发生变化。
  2. consumer消费超时。
  3. group订阅的topic个数发生变化。
  4. group订阅的topic的分区数发生变化。

**coordinator:**通常是partition的leader节点所在的broker,负责监控group中的consumer的存活,consumer维持到coordinator的心跳,判断consumer是否消费超时。

  1. coordinator通过心跳返回通知consumer进行rebalance。
  2. consumer请求coordinator加入组,coordinator选举产生leader consumer。
  3. leader consumer从coordinator获取所有的consumer,发送syncGroup(分配信息)到coordinator。
  4. coordinator通过心跳机制将syncGroup下发给consumer。
  5. 完成rebalance。

leader consumer监控topic的变化,触发rebalance,重新分配后,该消息会被其他消费者消费,此时之前消费完成提交offset,导致错误。

**解决方案:**coordinator每次rebalance,会标记一个Generation给consumer,每次rebalance该Generation会+1,consumer提交offset时,coordinator会比对Generation,不一致则拒绝提交。

kafka的性能好的原因

kafka是硬盘存储,不基于内存,因此消息堆积能力更强。

**顺序写:**利用磁盘的顺序访问,速度可以接近内存,kafka的消息都是append操作,partition是有序的,节省了磁盘的寻道时间,同时通过批量操作节省写入次数,partition物理上分为多个segment存储,方便删除。

传统方式:

  1. 读取磁盘文件数据到内核缓冲区。
  2. 将内核缓冲区的数据拷贝到用户缓冲区。
  3. 将用户缓冲区的数据拷贝到socket的发送缓冲区。
  4. 将socket发送缓冲区的数据发送到网卡,进行传输。

**零拷贝:**使用操作系统的指令支持,直接将内核缓冲区的数据发送到网卡传输。

kafka不太依赖jvm主要利用操作系统的pageCache,如果生产消费速率相当,则直接用pageCache交换数据,不需要经过磁盘IO。

kafka是pull模式还是push模式?分析下两种模式的优劣势。

kafka中使用的是pull模式。

pull模式:

优点:

  1. 可以控制速率,根据consumer的消费能力进行数据拉取。
  2. 可以批量拉取,也可以单条拉取。
  3. 可以设置不同的提交方式,实现不同的传输语义。

**缺点:**如果kafka没有数据,会导致consumer空循环,消耗资源。

**解决方案:**通过参数设置,consumer拉取数据为空或者没有达到一定数量时进行阻塞。

**push模式:**不会导致consumer循环等待

**缺点:**速率固定,忽略了consumer的消费能力,可能导致拒绝服务或者网络拥堵等问题。

kafka消息丢失的场景及解决方案

1. 消息发送

  • ack=0,不重试
    producer只管发送完消息,不管结果,如果消息发送失败就丢失了。
  • ack=1,leader carsh
    producer发送完消息,只等待leader写入成功就返回,虽然leader carsh了,但如果follower没来得及同步就挂掉了,消息可能会丢失。
  • unclean.leader.election.enable配置true
    允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。producer发送完异步消息,只等待leader写入成功就返回,leader crash了,如果ISR中没有follower,leader从OSR中选举,因为OSR中本来落后于Leader造成消息丢失。
    解决方案:
  1. 配置:ack=all / -1,tries > 1,unclean.leader.election.enable:false
    producer发送完消息,等待follower同步完消息再返回,如果异常则重试,不允许选举ISR以外的副本作为leader。
  2. 配置:min.insync.replicas > 1
    副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。
  3. 失败的offset单独记录
    producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行单独处理。

2. 消费:

先commit再处理消息。如果在处理消息时发生了异常,但是offset已经提交了,这条消息对于消费者来说就是丢失了,再也不会消费到了。

3. broker的刷盘:

减小刷盘间隔。

RabbitMQ死信队列和延时队列

1.消息被消费方否定确认。(使用channel.basicNack或channel.basicReject,并且此时requeue属性被设置为false)

2.消息在队列的存活时间超过设置的TTL时间。

3.消息队列的消息数量已经超过最大队列长度。

那么该消息将成为死信。死信消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置该消息将被丢弃。

为每一个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型(Direct、Fanout和Topic)

TTL:一条消息或者该队列中的所有消息的最大存活时间。

如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为死信。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。

RabbitMQ事务消息

通过对信道的设置实现

  1. channel.txSelect():通知服务器开启事务模式,服务端会返回Tx.Select-Ok。
  2. channel.basicPublish:发送消息,可以是多条,也可以是消费消息提交ack。
  3. channel.txCommit():提交事务。
  4. channel.txRollback():回滚事务。

消费者使用事务:

  1. autoAck=false,手动提交ack,以事务提交或回滚为准。
  2. autoAck=true,不支持事务,也就是说即使在接收消息之后回滚事务也是于事无补的,队列已经把消息移除了。

如果其中任意一个环节出现问题,就会抛出IOException异常,用户可以拦截异常进行事务回滚,或决定要不要重复消息。

事务消息会降低RabbitMQ的性能。

RabbitMQ如何确保消息发送和消息接收

发送方确认机制:

信道需要设置为confirm模式,则所有在信道上发布的消息都会分配一个唯一ID。

一旦消息被投递到queue(可持久化的消息需要写入硬盘),信道会发送一个确认给生产者(包含消息唯一ID)。

如果RabbitMQ发生内部错误而导致消息丢失,会发送一条nack(未确认)消息给生产者。

所有被发送的消息都将被confirm(即ack)或者nack。但是没有对消息被confirm的快慢做任何保证,并且同一条消息不会即被confirm又被nack。

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调方法会被触发。

ConfirmCallback接口:确认是否正确到达Exchange中,成功到达则回调。

ReturnCallback接口:消息失败返回时回调。

接收方消息确认模式:

消费者在声明队列时,可以指定noack参数,当noack=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或者磁盘,持久化消息)中移除消息。否则,消息被消费后会立即删除。

消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作),只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。

RabbitMQ不会为未ack的消息设置超时时间,他判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间很长,保证数据的最终一致性。

如果消费者返回ack之前断开了连接,RabbitMQ会重新分发给下一个订阅的消费者,可能存在消息重复消费的隐患,需要去重。

简述RabbitMQ的架构设计

**Broker:**RabbitMQ的服务节点。

**Queue:**队列,是RabbitMQ的内部对象,用于存储消息。RabbitMQ中消息只能存储在队列中,生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会平均的分摊(轮询)给多个消费者,而不是每个消费者都收到所有的消息进行消费。

RabbitMQ不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由key绑定多个队列,由多个消费者来订阅这些队列。

**Exchange:**交换器,生产者将消息发送到Exchange,由交换器将消息路由到一个或多个消息队列中。如果路由不到,或返回给生产者,或直接丢弃,或做其他处理。

**RoutingKey:**路由Key。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。这个路由key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时通过指定RoutingKey来决定消息流向哪里。

**Binding:**通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就可以知道如何正确的路由到队列了。

交换器和队列实际上是多对多关系,就像关系型数据库中的两张表。它们通过BindingKey做关联。在投递消息时,可以通过Exchange和RoutingKey找到相应的队列。

**信道:**信道是建立在Connection之上的虚拟连接。当应用程序与Rabbit Broker建立TCP连接的时候,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。RabbitMQ处理的每条AMQP指令都是通过信道完成的。信道就像电缆里的光纤,一条电缆内含有许多光纤,运行所有的连接通过多条光纤束进行传输和接收。

Dubbo的整体架构设计及分层

五个角色:

注册中心registry:服务注册与发现

服务提供者provider:暴露服务

服务消费者consumer:调用远程服务

监控中心monitor:统计服务的调用次数和调用时间

容器container:服务允许容器

调用流程:

  1. container容器负责启动、加载和运行provider。
  2. provider在启动时,向注册中心registry注册自己提供的服务。
  3. consumer在启动时,向注册中心registry订阅自己需要的服务。
  4. registry返回服务提供者列表给consumer,如果有变更,registry将基于长连接推送变更数据给consumer。
  5. consumer调用provider服务,基于负载均衡算法进行调用。
  6. consumer调用provider的统计,基于短连接定时每分钟一次统计到monitor。

Spring Cloud核心组件及作用

Eureka:服务注册与发现

注册:每个服务都向Eureka登记自己提供服务的元数据,包括服务的IP地址、端口号、版本号和通信协议等。

Eureka将各个服务维护在一个服务清单中,同时对服务维持心跳,剔除不可用的服务。Eureka集群各节点相互注册的每个实例中都有相同的服务清单。

发现:Eureka注册的服务之间调用,不需要指定服务地址,而是通过服务名向注册中心咨询,并获取所有服务实例清单缓存到本地,然后实现服务的请求访问。

注:服务清单是一个双层Map,第一层key是服务名,第二次key是实例名,value是服务地址加端口号

**Ribbon:**服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台。Ribbon也是通过发起http请求来进行调用的,只不过是通过调用服务名的地址来实现的。虽然Ribbon不用去请求服务实例的ip地址或域名了,但是每调用一个接口都还是需要手动去发起http请求。

**Feign:**基于Feign的动态代理机制,它可以根据注解和选择的机器拼接请求url,发起请求,简化了服务间的调用。在Ribbon的基础上进行了进一步的封装,单独抽出了一个组件,就是Spring Cloud Feign。在引入Spring Cloud Feign后,只需要创建一个接口并用注解的方式来配置它,就可完成对服务提供方的接口绑定。

**Hystrix:**发起请求是通过Hystrix的线程池的,不同的服务走不同的线程池,实现了不同服务调用的隔离,通过统计接口超时次数返回默认值,实现服务熔断和降级。

**Zuul:**如果前端或移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务。通过与Eureka进行整合,将自身注册为Eureka下的应用,从Eureka下获取所有服务的实例,来进行服务的路由。Zuul还提供了一套过滤机制,开发者可以自己指定哪些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体服务实例上,否则返回错误提示。

什么是Hystrix

Hystrix是分布式容错框架:

  1. 可以阻止故障的连锁反应,实现熔断。
  2. 可以快速失败,实现优雅降级。
  3. 可以提供实时的监控和告警。

**资源隔离:**线程隔离和信号量隔离

线程隔离:Hystrix会给每一个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其他线程池造成影响。

信号量隔离:客户端需要向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求会直接被拒绝,进入fallback流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。

**熔断和降级:**调用服务失败后快速失败

熔断:防止异常扩散,保证系统的稳定性。

降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降。

Spring Cloud和Dubbo的区别

  1. Spring Cloud的底层协议基于http协议,而Dubbo的底层协议基于tcp协议,dubbo的性能相对要好一些。
  2. Spring Cloud的注册中心使用的Eureka,而dubbo的注册中心推荐使用Zookeeper。
  3. Spring Cloud将一个应用定义为一个服务,而Dubbo将一个接口定义为一个服务。
  4. Spring Cloud是一个生态,而Dubbo是Spring Cloud生态中关于服务调用的一种解决方案(服务治理)。

zk和eureka的区别

zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。

当节点crash后,需要进行Leader的选举,在这期间,zk服务是不可用的。

eureka:AP设计(高可用性),目标是一个服务注册发现系统,专门用于微服务的服务发现注册。

Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。Eureka的客户端向某个Eureka注册连接失败时,会自动切换至其他节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

同时当Eureka的服务端发现85%以上的服务都没有心跳的话,他就会认为自己的网络出了问题,就不会从服务列表中删除这些失去心跳的服务,同时Eureka的客户端也会缓存服务信息。Eureka对于服务注册发现来说是非常好的选择。

Zookeeper的watch机制

客户端可以通过在Znode上设置watch,实现实时监听Znode的变化。

Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变时,服务器将这个变化发送给设置了Watch的客户端。

  • 父节点的创建、修改和删除都会触发Watcher事件。
  • 子节点的创建和删除会触发Watcher事件。

一次性:一旦被触发就会被移除,再次使用需要重新注册,因为每次变动都需要通知所有客户端,一次性可以减轻压力,3.6.0默认持久递归,可以触发多次。

轻量:只通知发生了事件,不会告知事件内容,减轻服务器和宽带压力。

Watcher机制包括三个角色:客户端线程、客户端的WatchManager以及Zookeeper服务器。

  1. 客户端向Zookeeper服务器注册一个Watcher监听。
  2. 把这个监听信息存储到客户端的WatchManager中。
  3. 当Zookeeper中的节点发生变化时,会通知客户端,客户端会调用相应Watcher对象中的回调方法。watch回调是串行同步的。

简述zk的命名服务、配置管理和集群管理

命名服务:

通过指定的名字来获取资源或者服务地址。Zookeeper可以创建一个全局唯一的路径,这个路径可以作为一个名字。被命名的实体可以是集群中的机器,服务的地址,或者远程的对象等。一部分分布式服务框架(RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据特定的名字来获取资源的实体、服务地址和提供者信息等。

配置管理:

实际项目开发中,需要经常使用.properties或者xml配置很多信息,如数据库连接信息、fps地址端口等。程序分布式部署时,如果把程序的这些配置信息保存在zk的Znode节点下,当需要修改配置,即Znode会发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

集群管理:

集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器或加入机器。Zookeeper可以方便集群机器的管理,它可以实时监控Znode节点的变化,一旦发现有机器挂了,该机器就会与zk断开连接,对应的临时节点会被删除,其他所有机器都会收到通知。新机器加入类似。

zk的数据模型和节点类型

数据模型:树形结构

zk维护的数据主要有:客户端的会话状态和数据节点信息。

zk在内存中构造了DataThree的数据结构,维护着path到DataNode的映射以及DataNode间的树状层级关系。为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。所以,zk适用于读多写少的轻量级数据的应用场景。

数据仅存储在内存是很不安全的,zk采用事务日志文件及快照的方案来落盘数据,保障能在数据不丢失的情况下快速恢复。

树中的每个节点被称为 — Znode

Znode兼具文件和目录两种特点。可以做路径标识,也可以存储数据,并且可以具有子Znode。具有增删改查等操作。

Znode具有原子性操作,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。

Znode存储数据大小有限制。每个Znode的数据大小最多1M,常规使用中应该远小于此值。

Znode通过路径引用,如同Unix中的文件路径。路径必须是绝度路径,因此他们必须以斜杠开头。除此之外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在Zookeeper中,路由由Unicode字符串组成,并且有一些限制。字符串“/zookeeper”用以保存管理信息,比如关键配额信息。

持久节点:一旦创建,该数据节点会一直存储在zk服务器上,即使创建该节点的客户端与服务器的会话关闭了,该节点也不会被删除。

临时节点:当创建该节点的客户端会话因超时或发生异常关闭时,该节点就会被zk服务器删除。

有序节点:不是一种单独类的节点,而是在持久节点和临时节点的基础上,增加了一个节点有序的性质。

简述ZAB协议

ZAB协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议,实现分布式数据一致性。

所有客户端的请求都是写入到Leader进程中,然后,由Leader同步到Follower节点。在集群数据同步过程中,如果出现Follower节点崩溃或者Leader进程崩溃,都会通过ZAB协议来保证数据一致性。

ZAB协议包括两种基本的模式:崩溃恢复和消息广播。

消息广播:

集群中所有的事务请求都由Leader进程来处理,其他服务器为Follower,Leader将客户端的事务请求转换为事务Proposal,并且将Proposal分发给集群中其他所有的Follower。

完成广播之后,Leader等待Follower反馈,当有半数的Follower反馈信息后,Leader将再次向集群内Follower广播Commit信息,Commit信息就是确认将之前的Proposal提交。

Leader节点的写入是一个两步操作,第一步是广播事务操作,第二步是广播提交操作,其中过半数指的是返回的节点数 >= N/2+1,N是全部的Follower节点数量。

崩溃恢复:

初始化集群、Leader崩溃或Leader失去了半数的机器支持时,开启新一轮Leader选举,选举产生的Leader会与过半数的Follower进行同步,当与过半数的机器同步完成后,就退出恢复模式,然后进入消息广播模式。

整个Zookeeper集群的一致性保证就是在上面两个状态之间切换,当Leader服务正常时,就是正常的消息广播模式;当Leader不可用时,则进入崩溃恢复模式,崩溃恢复阶段会进行数据同步,完成以后,重新进入消息广播阶段。

Zxid是ZAB协议的一个事务编号,Zxid是一个64位的数字,其中低32位是一个简单的单调递增计数器,针对客户端每一个事务请求,计数器加1;而高32位则代表Leader周期年代的编号。

Leader周期(epoch),可以理解为当前集群所处的年代或者周期,每当有一个新的Leader选举出现时,就会从这个Leader服务器上取出其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1,以此作为新的周期ID。高32位代表了每代Leader的唯一性,低32位则代表了每代Leader中事务的唯一性。

Zab节点的三种状态:

following:服从leader的命令。

leading:负责协调事务。

election/looking:选举状态。

如何实现接口的幂等性

  1. 唯一id。每次操作,都根据操作和内容生成唯一的id,在执行前先判断id是否存在,如果不存在则执行后续操作,并保存到数据库或者redis中。
  2. 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token带过去,服务器判断token是否存在redis中,存在表示第一次请求,可以继续执行业务,执行完成后把redis中的token删除。
  3. 建去重表。将业务中有唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了。
  4. 版本控制。增加版本号,当版本号符合时,才更新数据。
  5. 状态控制。当为某个状态时才能进行相应操作。

分布式事务解决方案

XA规范:分布式事务规范,定义了分布式事务模型。

四个角色:事务管理器(协调者TM)、资源管理器(参与者RM)、应用程序(AP)和通信资源管理器CRM。

全局事务:一个横跨多个数据库的事务,要么全部提交,要么全部回滚。

JTA事务是Java对XA规范的实现,对应JDBC的单库事务。

两阶段协议:

第一阶段(prepare):每个参与者执行本地事务但不提交,进入ready状态,并通知协调者已经准备就绪。

第二阶段(commit):当协调者确认每个参与者都ready后,通知参与者进行commit操作,如果有参与者fail,则发送rollback命令,各参与者回滚事务。

问题:

  1. 单点故障:一旦事务管理器出现故障,整个系统不可用(参与者都会被阻塞)。
  2. 数据不一致:在阶段二,如果事务管理器只发送了部分commit消息,此时网络发生异常,那么只有部分参与者接收到commit消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  3. 响应时间较长:参与者和协调者资源都被锁住,提交或者回滚之后才能释放。
  4. 不确定性:当协调事务管理器发送commit之后,并且此时只有一个参与者接收到了commit,那么该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

**三阶段协议:**主要针对两阶段的优化,解决了两阶段单点故障的问题,但是性能问题和不一致问题仍然没有根本解决。

引入了超时机制解决参与者阻塞的问题,超时后本地提交,两阶段只有协调者有超时机制。

  • 第一阶段:CanCommit阶段,协调者询问事务参与者,是否有能力完成此次事务。
  • 如果都返回yes,则进入第二阶段。
  • 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求。
  • 第二阶段:PreCommit阶段,此时协调者会向所有的参与者发送PreCommit请求,参与者接收到后开始执行事务操作。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示已经准备好提交事务了,并等待协调者的下一步指令。
  • 第三阶段:DoCommit阶段,在阶段二中如果所有参与者都返回了Ack,那么协调者就会从预提交状态转变为提交状态。然后向所有参与者节点发送DoCommit请求,参与者节点在收到请求后就会各自执行事务提交操作,并向协调者节点反馈Ack消息,协调者接收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者未完成DoCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

TCC(补偿事务):Try、Confirm和Cancel。

针对每个操作,都要注册一个与其对应的确认和补偿操作。

Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。

TCC模型对业务的侵入性较强,改造难度较大,每个操作都需要有try、confirm和cancel三个接口实现。

confirm和cancel接口还必须实现幂等性。

消息队列的事务消息:

  • 发送prepare消息到中间件。
  • 发送成功后,执行本地事务。
  • 如果事务执行成功,则commit,消息中间件将消息下发至消费端(commit前,消息不会被消费)。
  • 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除。
  • 消费端接收到消息进行消费,如果消费失败,则不断重试。

分布式id生成方案

  1. uuid
    **结构:**当前日期和时间 + 时钟序列 + 全局唯一的IEEE机器标识号
  • 当前日期和时间:时间戳
  • 时钟序列:计数器
  • 全局唯一的IEEE机器标识号:如果有网卡,从网卡mac地址获得,如果没有网卡以其他方式获得

**优点:**代码简单,性能好(本地生成,没有网络消耗),保证唯一(相对而言,重复的概率极低,可以忽略不计)

缺点:

  1. 每次生成的id都是无序的,而且不是全数字,无法保证趋势递增。
  2. 生成的id是字符串,存储性能差,查询效率低,写的时候由于不能产生顺序的append操作,需要进行insert操作,导致频繁的页分裂,这种操作在记录占用空间比较大的情况下,性能下降严重,还会增加读取磁盘的次数。
  3. uuid长度过长,不适用于存储,耗费数据库性能。
  4. id没有业务含义,可读性差。
  5. 存在信息安全问题,可能泄露mac地址。
  1. 数据库自增序列
    1)单机模式:
    优点:
  1. 实现简单,依靠数据库即可,成本小。
  2. id数字化,单调自增,满足数据库存储和查询性能。
  3. 具有一定的业务可读性(结合业务code)。

缺点:

  1. 强依赖db,存在单点问题,如果数据库宕机,业务不可用。
  2. db生成id性能有限,单点数据库压力大,无法抗住高并发场景。
  3. 存在信息安全问题,比如暴露订单量,url查询可以通过修改id查到别人的订单。

2)数据库高可用:多主模式做负载,基于序列的起始值和步长设置,不同的初始值,相同的步长,步长大于节点数。

**优点:**解决了id生成的单点问题,同时平衡了负载。

缺点:

  1. 系统扩容困难:系统定义好步长之后,增加机器之后调整步长困难。
  2. 数据库压力大:每次获取一个id都必须读写一次数据库。
  3. 主从同步的时候,由于数据同步延迟,可能出现查不到数据的情况,可以利用加缓存和查主库的方式解决。
  1. Leaf-segment
    采用每次获取一个id区间段的方式解决,区间段用完后之后再去数据库获取新的号段,可以大大减轻数据的压力。
    **核心字段:**biz_tag、max_id和step。
    biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的id号段的最大值,step表示每次分配的号段长度。原来每次获取id都要访问数据库,现在只需要把step设置的足够合理,就可以在id用完之后再去访问数据库。
    优点:
  1. 扩张灵活,性能强能够支撑起大部分业务场景。
  2. id号码是趋势递增的,满足数据库存储和查询性能要求。
  3. 可用性高,即使id生成服务器不可用,也能够使业务在短时间内可用,为排查问题争取时间。

**缺点:**可能存在多个节点同时请求id区间的情况,依赖db。

**双buffer:**将获取一个号段的方式优化成获取两个号段,在一个号段用完之后不用立马更新号段,还有一个缓存号段备用,这样能够有效的解决冲突问题,而且采用双buffer的方式,在当前号段消耗了10%的时候就去检查下一个号段有没有准备好,如果没有准备好就去更新下一个号段,当当前号段用完了就切换到下一个已经缓存好的号段去使用,同时在该号段消耗到10%的时候,又去检查下一个号段有没有准备好,如此往复。

**优点:**基于JVM存储双buffer的号段,减少了数据库查询,减少了网络依赖,效率更高。

**缺点:**segment号段长度是固定的,业务量大时可能会频繁更新号段,因为原本分配的号段消耗很快。如果号段长度设置过大,但凡缓存中有号段没有消耗完,其他节点重新获取的号段与之前相比跨度可能会很大。

  1. 基于Redis、mongodb和zk等中间件生成
  2. 雪花算法
    生成一个64bit的整型数字。
    第一位符号固定为0,41位时间戳,10位workid和12位序列号。
    位数可以有不同实现。
    优点:
  1. 每个毫秒值包含的id值很多,不够可以变动位数来增加,性能好。
  2. 时间戳在高位,中间是固定的机器码,自增序列在低位,整个id是趋势递增的。
  3. 灵活度高,能够根据业务场景数据库节点灵活的调整bit位划分。

**缺点:**强依赖于机器时钟,如果时钟回拨,会导致重复的id生成,所以一般基于此算法发现时钟回拨时,都会抛异常,阻止id生成,这可能导致服务不可用。

简述你对RPC、RMI的理解

RPC:远程方法调用,即在本地调用远程的函数,可以跨语言实现httpClient。

RMI:是RPC的java版本,java中用于实现RPC的一种机制。

如何实现RMI:

直接或间接实现接口java.rmi.Remote,成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。

远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的的这个拷贝称为存根,而服务端本身已存在的远程对象则称为骨架。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远端方法来响应客户端的请求。

分布式架构下,有哪些Session共享方案

  1. 采用无状态服务,抛弃Session。
  2. 存入Cookie,有安全风险。
  3. 服务器之间进行Session同步,这样可以保证每个服务器上都有全部的Session信息,不过当服务器数量比较多的时候,同步会有延迟甚至同步失败。
  4. 使用Nginx或其他负载均衡中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样失去了负载均衡的意义,当一台服务器挂掉的时候,会影响一批用户的使用,风险很大。
  5. 把Session放到Redis中存储,虽然架构上变得复杂了,并且需要多访问一次Redis,但是这种方案带来了很多的好处。
  • 实现了Session共享。
  • 可以水平扩展(增加Redis服务器)。
  • 服务器重启Session不会丢失。
  • 不仅仅可以跨服务器,还可以跨平台。

负载均衡算法和类型

  1. 轮询法
    将请求按顺序轮流的分配到后端服务器上,它均衡的对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
  2. 随机法
    通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
  3. 源地址哈希法
    源地址哈希的思想是根据获取客户端的ip地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客户端要访问服务器的序号。采用源地址哈希算法进行负载均衡,同一个ip地址的客户端,当后端服务器列表不变时,它每次都会影射到同一台后端服务器访问。
  4. 加权轮询法
    不同的后端服务器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请求;而给配置低、负载高的机器分配较低的权重,降低其系统负载,加权轮询能很好的处理这一问题,并将请求顺序按权重分配到后端。
  5. 加权随机法
    与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
  6. 最小连接数法
    最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对应请的处理有快有慢,它是根据后端服务器当前的连接情况,动态的选取当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后端服务的利用率,将请求合理的分流到每一台服务器。

类型:

DNS方式实现负载均衡

硬件负载均衡:F5和A10

软件负载均衡:Nginx、HAproxy和LVS。

  • Nginx:七层负载均衡,支持HTTP、E-mail协议,同时也支持四层负载均衡。
  • HAproxy:支持七层规则,性能也不错。OpenStack默认使用的负载均衡软件就是HAproxy。
  • LVS:运行在内核态,性能是软件负载均衡中最高的,严格来说工作在三层,所以更通用一些,适用于各种服务。

CAP理论和BASE理论

Consistency(一致性):

即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致。

对客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。

从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。

Availability(可用性):

即服务器一直可用,而且是正常响应时间。系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。

Partition Tolerance(分区容错性):

即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外界提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去好像是一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有体验上的影响。

CP和AP:分区容错性是必须保证的,当发生网络问题时,如果要继续服务,那么强一致性和可用性只能二选一。

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)

BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

基本可用:

  • 响应时间上的损失:正常情况下,处理用户请求需要0.5s返回结果,但是由于系统出现故障,处理用户请求的时间变为了3s。
  • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统的访问量突然剧增,系统的部分非核心功能无法正常使用。

软状态:数据同步允许一定的延迟。

最终一致性:系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时。

Redis主从复制的核心原理

通过执行slaveof命令或选项,让一个服务器复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

全量复制:

  1. 主节点通过bgsave命令fork子进程进行RDB持久化,该过程非常消耗CPU、内存和硬盘IO。
  2. 主节点通过网络将RDB文件发送给从节点,对主从节点的宽带会带来很大的消耗。
  3. 从节点清空老数据,载入新数据的过程是阻塞的,无法响应客户端的命令。如果从节点执行bgrewriteof命令,也会带来额外的消耗。

部分复制:

  1. 复制偏移量:执行复制的主从节点,分别会维护一个复制偏移量offset。
  2. 复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出的队列作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能进行全量复制。
  3. 服务器运行ID:每个Redis节点,都有其运行ID,运行ID在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就是根据运行ID来判断同步进度的。
  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会尝试使用部分复制,到底能不能进行部分复制还要看复制偏移量和复制积压缓冲区的情况。
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点不是当前的主节点,只能进行全量复制。

主从复制的过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FthMmnxf-1644861584485)(https://note.youdao.com/yws/api/personal/file/WEBbf575018c3a2876fd97ee7e14bd9d1b8?method=download&shareKey=fb817352b7599594f7b0a1b3e32104cd)]

Redis集群方案

  1. 哨兵模式
    sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下几个功能:
  1. 集群监控:负责监控redis master和slave进程是否正常工作。
  2. 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  3. 故障转移:如果master node挂掉了,会自动转移到slave node上。
  4. 配置中心:如果故障转移发生了,通知Client客户端新的master地址。

哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,相互协同工作。

  • 故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常的工作。
  • 哨兵通常需要三个实例来保证自己的健壮性。
  • 哨兵+redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性。
  • 对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
  1. 服务端分片
    Redis Cluster是一种服务端分片技术,3.0版本开始正式提供。采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接受到请求的节点会将查询请求发送到正确的节点上执行。
    方案说明:
  • 通过哈希的方式,将数据分片,每个节点均存储一定哈希槽区间的数据,默认分配了16384个槽位。
  • 每份数据分片会存储在多个互为主从的多节点上。
  • 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)。
  • 同一分片多个节点间的数据不保持强一致性。
  • 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点。
  • 扩容时需要把旧节点的数据迁移一部分到新节点。

在Redis Cluster架构下,每个redis要放开两个端口号,比如一个是6379,另一个就是+10000的端口号16379.

16379端口号是用来进行节点间通信的,也就是Cluster bus的通信,用来进行故障检测、配置更新和故障转移授权。Cluster bus用了另外一种二进制的协议(gossip协议),用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

优点:

  1. 无中心架构,支持动态扩容,对业务透明。
  2. 具备哨兵的监控和自动故障转移能力。
  3. 客户端不需要连接集群的所有节点,连接集群中任何一个可用节点即可。
  4. 高性能,客户端直连redis服务,免去了proxy代理的损耗。

缺点:

  1. 运维复杂,数据迁移需要人工干预。
  2. 只能使用0号数据库。
  3. 不支持批量操作。
  4. 分布式逻辑和存储模块耦合。
  1. 客户端分片
    Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方案。其主要思想是采样哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java Redis客户端驱动jedis,支持Redis Sharding功能。
    优点:
    非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,线性扩展非常容易,系统的灵活性很强。
    缺点:
    由于Sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
    客户端分片不支持动态增删节点。服务端Redis实力群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化。

简述Redis事务实现方式

  1. 事务开始
    MULTI命令的执行,标志着一个事务的开始。MULTI命令会将客户端状态的flags属性中的REDIS_MULTI标识打开。
  2. 命令入队
    当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、EXEC、WATCH或DISCARD,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回相应的信息。
  3. 事务执行
    客户端发送EXEC命令,服务器执行EXEC命令逻辑。
  • 如果客户端状态的flags属性不包括REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,那么直接取消事务的执行。
  • 否则客户端处于事务状态,服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端。

redis不支持事务回滚机制,但是它会检查事务中的命令是否有语法错误。

WATCH命令是一个乐观锁,可以为redis事务提供check-and-set(CAS)行为。可以监控一个或多个键,一旦其中一个键被修改或删除,之后的事务就不会执行,监控一直持续到EXEC命令。

MULTI命令用于开启一个事务,它总是返回OK。MULTI执行后,客户端可以继续向服务器发送任意多条命令,但这些命令不一定被立即执行,而是被放到一个队列中,当EXEC命令被调用时,队列中的命令才会被执行。

EXEC是执行事务块内的命令,按命令执行的先后顺序返回事务块内所有命令的返回值。当操作被打断时,返回nil。

DISCARD会清空客户端事务队列的所有命令,从事务状态中退出,放弃事务。

UNWATCH可以取消watch对所有key的监控。

简述缓存雪崩、缓存穿透和缓存击穿

**缓存雪崩:**是指缓存同一时间大面积失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  1. 缓存数据的过期时间设置随机,防止大量数据同一时间过期。
  2. 给每个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
  3. 缓存预热。
  4. 加互斥锁。

**缓存穿透:**是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  1. 接口层增加校验。
  2. 将缓存和数据库中都取不到的数据的缓存设置为key-null(有效时间设置短一点),防止攻击用户用同一id暴力攻击。
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

**缓存击穿:**是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特多,同时读缓存没读到数据,又同时去数据库取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿并发查询同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到而查数据库。

解决方案:

  1. 设置热点数据永不过期。
  2. 加互斥锁。

Redis线程模型及单线程快的原因

Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler),文件事件处理器是单线程,所以Redis是单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他线程的模块进行对接,保证了Redis内部的线程模型的简单性。

文件事件处理器的结构包含4各部分:多个Socket、IO多路复用程序、文件事件分派器和事件处理器(命令请求处理器、命令回复处理器和连接应答处理器等)。

多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。

然后一个Socket的事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。

单线程快的原因:

  1. 纯内存操作。
  2. 核心是基于非阻塞的IO多路复用机制。
  3. 单线程避免了多线程频繁上下文切换带来的性能问题。

Redis的过期键删除策略

Redis是key-value数据库,可以设置key的过期时间,Redis中同时使用了惰性过期和定期过期两种策略。

**惰性过期:**是指当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化的节省CPU资源,但是对内存非常不友好。极端情况下可能出现大量过期的key没有再次访问,从而不会被清除占用大量内存。

**定期过期:**是指每隔一定的时间,会扫描一定数量的key,并清除其中已过期的key。通过调整定时扫描的时间间隔和每次扫描的时间,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

redis持久化有几种类型及它们的区别

redis持久化有RDB和AOP两种类型。

**RDB:**在指定时间间隔内将内存中的数据集快照写入硬盘,恢复时是将快照文件直接读到内存里。

  • 备份时如何执行
    redis会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件,待持久换过程结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,确保了极高的性能。如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式要比AOF方式更加高效。缺点是数据安全性低,最后一次持久化后的数据可能丢失。
  • RDB的备份:
    先通过config get dir查询RDB文件的目录。
    将*.rdb的文件拷贝到别的地方。
  • RDB的恢复:
    关闭redis。
    先把备份的文件拷贝到工作目录下。
    启动redis,备份数据会直接加载。
  • RDB的优点:
  1. 节省磁盘空间。
  2. 恢复速度快。
  • RDB的缺点:
  1. 虽然redis在fork时使用了写时拷贝技术,但如果数据量庞大时比较消耗性能。
  2. redis一定时间间隔备份一次,如果redis意外挂掉会丢失最后一次快照后的所有修改。

**AOF:**以日志的形式记录每个写操作和删操作,将redis执行过的所有写指令和删除指令记录下来,只许追加文件不能修改,redis启动时会读取该文件重新构建数据。

  • AOF的优点:
  1. 丢失数据的概率低。
  2. 可读的日志文本,通过操作AOF文件,可以处理误操作。
  • AOF的缺点:
  1. 与RDB相比会占用更多的磁盘空间。
  2. 恢复和备份的速度慢。
  3. 每次读写都同步的话,有一定的性能压力。
  4. 存在个别bug,造成数据不能恢复。

AOF文件比RDB更新频率高,优先使用AOF还原数据。

AOF比RDB更安全,也更大。

RDB性能比AOF好。

如果AOF和RDB都配了优先加载AOF。

简述mysql中的索引类型及对数据库的影响

索引的种类:

普通索引:允许被索引的数据列包含重复值。

唯一索引:可以保证数据记录的唯一性。

主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字PRIMARY KEY来创建。

联合索引:索引可以覆盖多个数据列,如index(columnA, columnB)。

全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术。可以通过ALTER TABLE table_name ADD FULLTEXT(column)创建。

索引对数据库性能的影响:

  1. 索引可以极大的提高查询速度。
  2. 使用索引查询过程中会使用优化隐藏器,可以提高系统性能。
  3. 索引会降低插入、删除和更新的速度,因为在执行这些操作时需要操作索引文件。
  4. 索引会占用物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间。聚簇索引,需要占用很大的空间,而且聚簇索引改变非聚簇索引也都会跟着变。

MyISAM和InnoDB的区别

  1. MyISAM不支持事务,每次查询都是原子的;InnoDB支持ACID的事务,支持事务的四种隔离级别。
  2. MyISAM支持表锁,每次操作都会为整个表加锁;InnoDB支持行锁和外键约束,可以支持并发写。
  3. MyISAM存储表的总行数,InnoDB不存储。
  4. 一个MyISAM引擎有三个存储文件:索引文件、表结构文件和数据文件;一个InnoDB引擎可以存储在一个文件空间,表大小不受操作系统控制,一个表可能分布在多个文件里,也存储在多个文件空间,设置为独立表空间,表大小受操作系统文件大小限制,一般为2G。
  5. MyISAM采用非聚簇索引,索引文件的数据域存储指向数据文件的指针,辅助索引和主索引基本一致,但是辅助索引不用保证唯一性;InnoDB主键索引采用聚簇索引(索引的数据域存储数据文件本身),辅助索引的数据域存储主键的值;因此从辅助索引查找数据,需要先通过辅助索引找到主键值,再获取数据。InnoDB最好使用自增主键,防止插入数据时,为维持B+树结构文件大调整。

mysql主从同步原理

mysql的主从复制中主要有三个线程:master(binlog dump thread)、slave(I/O thread和SQL thread)。

  • 主从复制的基础是主库记录数据库的所有变更记录到binlog,binlog是数据库服务器启动的时候创建的,是一个保存所有修改数据库结构或内容的文件。
  • 当binlog有变动时,主节点log dump线程读取其内容并发给从节点。
  • 从节点I/O线程接收binlog内容,将其写入到relay log文件中。
  • 从节点的sql线程读取relay log文件内容对数据更新进行重放,最终保证主从数据库的一致性。

注意:主从节点使用binlog文件+position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机,则会自动从position的位置发起同步。

由于mysql默认的复制方式是异步的,主库把日志发送给从库后不再关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念:

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都咨询完成后才返回给客户端,这个方式很影响性能。

半同步复制

和全同步复制不同的是,半同步复制从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成了。

什么是MVCC

多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁和写锁就不会冲突了,不同的事务session会看到自己特定版本的数据。

MVCC只在READ COMMITTED和REPEATABLE READ两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务的数据行;SERIALIZABLE则会对所有读取的行都加锁。

聚簇索引记录中有两个必要的隐藏列:

trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id。

roll_pointer:每次对哪条聚簇索引记录修改的时候,都会把老版本写入undo日志中。roll_pointer存了一个指针,指向这条聚簇索引记录的上一个版本的位置,通过他来获得上一个版本的记录信息。注意插入操作的undo日志没有这个属性,因为它没有老版本。

已提交读和可重复读的区别就是它们生成ReadView的策略不同。

开始事务时创建ReadView,ReadView维护当前活动的事务id,即未提交的事务id,排序成一个数组。

访问数据时,获取数据中的事务id(获取的是事务id最大的记录),对比ReadView:

如果在ReadView的左边(比ReadView都小),可以访问(在左边意味着该事务已经提交);

如果在ReadView的右边(比ReadView都大)或者在ReadView中,不可以访问,需要获取roll_pointer,取上一个版本重新对比。(在右边意味着,该事务在ReadView生成之后出现,在ReadView中意味着该事务还未提交)

已提交读隔离级别下的事务在每次查询开始都会生成一个独立的ReadView,可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是mysql的MVCC,通过版本链实现多版本,可并发读写。通过ReadView生成策略的不同实现不同的隔离级别。

ACID靠什么保证

原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql。

一致性由其他三大特性保证,程序代码要保证业务上的一致性。

隔离性由MVCC来保证。

持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复。

如何优化慢查询

  1. 分析sql语句,检查是否查询了多余的数据或不必要的字段,进行相应优化。
  2. 分析语句的执行计划,检查索引使用情况,优化sql提高索引命中率。
  3. 分析是否是由于数据量过大导致的,如果是考虑分表。

事务的基本特性和隔离级别

事务基本特性ACID:

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

一致性:指数据库总是从一个一致性的状态转换到另一个一致性的状态。

隔离性:指一个事务在提交前对其他事务不可见。

持久性:指事务一旦提交,事务所做的修改就会永久保存到数据库中。

隔离级别:

read uncommit(读未提交):可以读到其他事务未提交的数据,也叫脏读。

read commit(读已提交):两次读取结果可能不一致,也叫不可重复读。

repeatable read(可重复读):mysql的默认隔离级别,每次读取的结果都一样,但可能产生幻读。

serializable(串行):一般不会使用,它会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

脏读:某个事务更新了一份数据,另一个事务在此时读取到了这份数据,但由于某些原因,前一个事务回滚了,则后一个事务所读取的数据就是不正确的了。

不可重复读:是指在一个事务的两次查询的数据不一致,可能原因是两次查询中间其他事务更新了相关数据。

幻读:在一个事务的两次查询中数据笔数不一致,例如一个事务查询了几列数据,而另一个事务此时插入了几列新数据,先前事务再次查询发现有几列数据是之前没有的。

mysql执行计划

执行计划就是sql的执行顺序,以及如何使用索引查询和返回的结果集的行数。

EXPLAIN SELECT * FROM a WHERE X = ? AND Y = ?

  1. id:是一个有顺序的编号,是查询的顺序号,有几个select就显示几行。id的顺序是按select出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为null最后执行。
  2. selectType:表示查询中每个select子句的类型
  • SIMPLE:表示此查询不包含UNION查询和子查询。
  • PRIMARY:表示此查询是最外层的查询(包含子查询)。
  • SUBQUERY:子查询中的第一个SELECT。
  • UNION:表示此查询是UNION的第二个或者随后的查询。
  • DEPENDENT UNION:UNION中的第二个或后面的查询语句,取决于外面的查询。
  • UNION RESULT:UNION的结果。
  • DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询。即子查询依赖于外层查询的结果。
  • DERIVED:衍生,表示导出表的SELECT(FROM子句的子查询)。
  1. table:表示该语句查询的表。
  2. type:优化sql的重要字段,也是我们判断sql性能和优化程度的重要指标。它的取值类型范围:
  • const:通过索引一次命中,匹配一行数据。
  • system:表中只有一行记录,相当于系统表。
  • eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。
  • ref:非唯一索引扫描,返回匹配某个值的所有。
  • range:只检索给定范围的行,使用一个索引来选择行,一般用于between、<、>。
  • index:只遍历索引树。
  • ALL:表示全表扫描,这个类型的查询是性能最差的查询之一。那么基本就是随着表的数量增多,执行效率越来越慢。
    执行效率:ALL < index < range < ref < eq_ref < const < system
  1. possible_keys:它表示mysql在执行该sql语句的时候,可能用到的索引信息,仅仅是可能,实际不一定会用到。
  2. key:mysql在当前查询真正使用到的索引,它是possible_keys的子集。
  3. key_len:表示查询优化器使用了索引的字节数,可以评估组合索引是否完全被使用,是优化sql时,评估索引的重要指标。
  4. rows:mysql查询优化器根据统计信息,估算该sql返回结果集需要扫描读取的行数,这个值很重要,索引优化后,扫描读取的行数越多,说明索引设置不对,或者字段传入的类型之类的问题,说明要优化空间越大。
  5. filtered:返回结果的行占需要读到的行的百分比,百分比越高说明查询到的数据越准确。
  6. extra
  • using filesort:表示mysql对结果集进行外部排序,不能通过索引顺序达到排序效果。一般有using filesort都建议优化掉,因为这种查询cpu消耗大,延时大。
  • using index:覆盖索引扫描,表示查询在索引树中就可以找到需要的数据,不用扫描数据文件,说明性能不错。
  • using temporary:查询有使用临时表,一般出现于排序,分组和多表join的情况,查询效率不高,建议优化。
  • using where:sql使用了where过滤,效率较高。

mysql有哪些类型的锁

基于锁的属性分为:共享锁和排他锁。

基于锁的粒度分为:行级锁、表级锁、页级锁、记录锁、间隙锁和临建锁。

基于锁的状态分为:意向共享锁和意向排它锁。

共享锁

共享锁又称读锁,简称S锁,当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有读锁都释放之后,其他事务才能对其加写锁。共享锁主要是为了支持并发读,读的时候不能修改,可以避免重复读问题。

排他锁

排他锁又称写锁,简称X锁,当一个事务为数据加上写锁时,其他请求不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时,不允许其他事务修改,也不允许其他事务读取。避免出现脏数据和脏读的问题。

表锁

表锁是指上锁的时候锁住整张表,必须等事务释放锁以后下一个事务才可以访问该表。

特点:颗粒大,加锁简单,容易冲突。

行锁

行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他记录可以正常访问。

特点:粒度小,加锁比表锁麻烦,但不容易冲突,相比表锁支持的并发要高。

记录锁

记录锁是行锁的一种,只不过锁的范围是表中的某一条记录,记录锁加锁后锁住的只是表的某一条记录。

精准条件命中,并且命中的条件字段是唯一索引。

加了记录锁之后可以避免数据在查询的时候被修改的重复读问题,也可以避免修改的事务未提交之前被其他事务读取的脏读问题。

页锁

页锁是mysql中锁定粒度介于行锁和表锁之间的一种锁。表锁速度快,但冲突多;行锁冲突少,但速度慢,页锁折中,一次锁定相邻的一组记录。

间隙锁

间隙锁是行锁的一种,间隙锁是锁住表记录的某一个区间,当表的相邻id之间出现空隙则会形成一个区间,遵循左开右闭原则。

临建锁

临建锁是行锁的一种,是INNODB的行锁默认算法,它会把查询出来的记录锁住,同时也会把范围查询内的所有区间锁住,除此之外它会把相邻的下一个区间也会锁住。

当事务A加锁成功之后就设置一个状态告诉后面的事务,已经有事务对表加了一个排他锁,别的事务不能对整张表加共享锁或者排他锁了。如果后面的事务需要对整张表加锁,只需要获取这个状态就可以知道是不是可以对表加锁,不用扫描整个索引树的每个节点了,这个状态就是意向锁。

意向共享锁

当一个事务试图对整个表加共享锁之前,首先需要获得这个表的意向共享锁。

意向排它锁

当一个事务试图对整个表加排他锁之前,首先需要获得这个表的意向排它锁。

索引设计的原则

  1. 经常出现在where子句或连接子句中的列适合创建索引。
  2. 数据量较小的表没必要建立索引。
  3. 尽量使用短索引,如果对长字符串列建立索引,应当指定一个前缀长度,可以节省大量索引空间。
  4. 不要过度创建索引,索引需要额外的磁盘空间,并会降低写操作的性能,索引越多维护索引的成本越高。
  5. 作为外键的列要创建索引。
  6. 频繁更新的列不适合创建索引。
  7. 区分度太低的列不适合创建索引。
  8. 尽量扩展索引,即在已有索引上创建联合索引。
  9. 定义为text、image和bit数据类型的列不适合创建索引。

mysql索引的数据结构有哪些,介绍下各自的优劣

索引的数据结构和具体存储引擎的实现有关,在mysql中使用较多的索引是hash索引和B+树索引。InnoDB存储引擎的默认索引实现为B+树,对于hash索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单表记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,选择B+树索引。

B+树:

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互连接。在B+树上的常规检索从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而基于索引顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库和文件系统等场景。

哈希索引:

哈希索引就是采用一定的哈希算法,把键值换算成哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。

如果是等值查询,那么哈希索引有绝对优势,只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不唯一,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据。

如果是范围查询,哈希索引就无用武之地了,因为原先有序的键值,经过哈希算法后,有可能变成不连续的了,没有办法再利用索引完成查询。

哈希索引不能利用索引完成排序和后导模糊查询,也不支持多列联合索引的最左侧匹配规则。

B+树索引的关键字检索效率比较平均,不像B树索引那样波动幅度大。

在有大量重复键值情况下,哈希索引的效率极低,因为存在哈希碰撞问题。

mysql聚簇索引和非聚簇索引的区别

mysql中聚簇索引和非聚簇索引都是B+树的数据结构。

**聚簇索引:**将数据存储和索引放一块,并且是按照一定的顺序组织的,找到了索引也就找到了数据,数据的物理存放顺序和索引顺序一致。即只要索引相邻,那么对应的数据一定也是相邻的。

**非聚簇索引:**叶子节点不存储数据,存储的是数据行的地址,也就是说根据索引查找到数据行的位置,再去磁盘查找数据。

聚簇索引的优点:

  1. 通过聚簇索引查询可以直接获取数据,效率更高,而非聚簇索引需要二次查询。
  2. 聚簇索引对于范围查询的效率很高,因为数据是按一定顺序排列的。
  3. 聚簇索引适合用在排序的场合,非聚簇索引不适合。

聚簇索引的缺点:

  1. 维护聚簇索引的代价很大,特别是插入新行,或者主键被更新导致要分页的时候。
  2. 由于使用uuid作为主键,数据存储稀疏,有可能聚簇索引比全表扫描更慢。
  3. 如果主键比较大,辅助索引会更大,因为辅助索引的叶子存储的是主键值,过长的主键值会导致叶子节点占用更多的物理空间。

InnoDB中一定有主键,而且主键一定是聚簇索引。不手动设置则会使用unique索引,没有unique索引则会使用数据库内部的一个行的隐藏id作为主键索引。在聚簇索引之上创建索引称为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,辅助索引叶子节点存储的不是行的物理位置,而是主键值。

MyISAM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树节点存储了主键,辅助索引B+树存储了辅助键。表数据存储在独立的地方,这两棵B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差异。由于索引树是独立的,通过辅助索引检索无需访问主键的索引树。

如果涉及到大数据量的排序、全表扫描或count之类的操作等,MyISAM更有优势,因为索引占的空间小,效率更高。

索引的基本原理

索引是用来快速查找具有特定值的记录的。如果没有索引,一般来说查询时要遍历整张表。

索引的原理就是把无序的数据查询变成有序的查询。

  1. 把创建了索引的列的内容进行排序。
  2. 对排序结果生成倒排表。
  3. 在倒排表内容上拼上数据地址链。
  4. 在查询时,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据。

简述mybatis的插件运行原理,及如何编写一个插件

mybatis只支持针对ParameterHandler、ResultSetHandler、StatementHandler和Executor这4种接口的插件,mybatis使用jdk的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke方法,拦截那些你指定需要拦截的方法。

编写插件:实现mybatis的Interceptor接口并重写intercept方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置编写的插件。

#{}和${}的区别

#{}是占位符,会进行预编译,可以有效的防止sql注入,更安全;而${}是拼接符,直接进行字符串替换。

mybatis在处理#{}时,会将sql中的#{}替换为?,调用PrepareStatement进行赋值;而处理${}时,就是把${}替换成变量的值,调用Statement来赋值。

hibernate与mybatis的区别:

  1. hibernate是一个典型的ORM框架,基本不需要写sql,偶尔写一些hql;而mybatis不是,需要自己编写sql。
  2. mybatis直接编写原生sql,可以严格控制sql执行性能;而hibernate会根据配置自动生成和执行sql,不如mybatis灵活。
  3. hibernate是面向对象的,数据库可移植性好,直接更换方言即可;而mybatis编写sql依赖于底层数据库,数据库移植性差。
  4. mybatis简单易学,但对sql功底要求较高;而hibernate相对复杂,学习成本高,但可以节省很多代码,开发效率更高。

mybatis的优缺点

优点:

  1. 基于sql语句编程,支持动态sql语句,很灵活,不会对应用程序或者数据库的现有设计造成任何影响;sql写在xml中,解除sql与程序代码的耦合,便于统一管理和复用。
  2. 与jdbc相比,减少了50%以上的代码量,消除了jdbc大量冗余的代码,不需要手动开关连接。
  3. 很好的与各种数据库兼容。
  4. 能够很好的与spring集成。
  5. 提供映射标签,支持对象与数据库的orm字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:

  1. sql语句的编写工作量较大,尤其是字段多,关联表多时,对开发人员编写sql语句的功底有一定要求。
  2. sql语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

什么是嵌入式服务器,为什么要使用嵌入式服务器

Spring Boot已经内置了tomcat.jar,运行main方法时会自动启动tomcat,并利用tomcat的spi机制加载Spring MVC。

使用嵌入式服务器不用再下载安装Tomcat,应用也不需要先打成war包再放到webapp目录下运行,只需要安装Java虚拟机就可以在上面部署应用了。

如何理解Spring Boot中的Starter

starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,Spring Boot会按照约定来加载该配置类。

开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置,就可以使用对应的功能了。

Spring Boot自动配置原理

@Import + @Configuration + Spring spi

自动配置类由各个starter提供,使用@Configuration + @Bean定义配置类,放到META-INF/spring.factories下

使用Spring spi扫描META-INF/spring.factories下的配置类

使用@Import导入自动配置类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0wwMNgq-1644861584487)(https://note.youdao.com/yws/api/personal/file/WEB09af759b091f01e99c0ca2919863d6f7?method=download&shareKey=9a0ce2692849ace8dc6e1da74be7454b)]

Spring MVC的主要组件

Handler:处理器,它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成一个Handler,只要可以实际处理请求就可以是Handler。

  1. HandlerMapping
    处理器映射器,在Spring MVC中会有很多请求,每个请求都需要一个Handler处理,具体收到一个请求之后使用哪个Handler处理,由HandlerMapping决定。
  2. HandlerAdapter
    适配器,因为Spring MVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法,HandlerAdapter可以让固定的Servlet处理方法调用灵活的Handler。
  3. HandlerExceptionResolver
    异常情况处理,根据异常设置ModelAndView,再交给render方法进行渲染。
  4. ViewResolver
    视图解析器,用来将String类型的视图名和Locale解析为View类型的视图。
  5. RequestToViewNameTranslator
    ViewReslover是根据ViewName查找View,但是有的Handler处理完后并没有设置View也没有设置ViewName,需要从request中获取ViewName,这时就需要用到RequestToViewNameTranslator。在Spring MVC容器中只能配置一个RequestToViewNameTranslator,所以request到ViewName的转换规则都需要在一个Translator里面全部实现。
  6. LocaleResolver
    LocaleResolver用于从request解析出Locale,在视图解析或者国际化时会用到。
  7. ThemeResolver
    用于解析主题。
  8. MultiPartResolver
    用于处理上传请求。
  9. FlashMapManager
    用来管理FlashMap,FlashMap主要用在redirect中传递参数。

Spring MVC工作流

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  3. 处理器映射器找到具体的处理器,生成处理器及处理器拦截器一并返回给DispatcherServlet。
  4. DispatcherServlet调用HandlerAdapter处理器适配器。
  5. HandlerAdapter经过适配器调用具体的处理器。
  6. Controller执行完成返回ModelAndView。
  7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
  9. ViewReslover解析后返回具体View。
  10. DispatcherServlet根据View渲染视图。
  11. DispatcherServlet响应用户。

Spring、Spring MVC和Spring Boot的区别

Spring是一个IOC容器,用来管理bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更加方便的将不同类不同方法中的共同处理抽取成切面,自动注入给方法执行,比如日志和异常等。

Spring MVC是Spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。

Spring Boot是Spring提供的一个快速开发工具包,让程序员更方便,更快速的开发Spring和Spring MVC应用,简化了配置(约定了默认配置),整合了一系列的解决方案,可以开箱即用。

什么是bean的自动装配,有哪些方式

开启自动装配,只需要在xml配置文件的中定义“autowire”属性。

<bean id="customer" class="com.xxx.xxx.Customer" autowire=""/>

autowire属性有五种装配方式:

  • 缺省,自动装配通过“ref”属性手动设定。
  • byName,根据bean的属性名称进行自动装配。
<!-- Customer的属性名称是person,spring会将bean id为person的bean通过setter方法进行自动装配。 -->
<bean id="customer" class="com.xxx.xxx.Customer" autowire="byName"/>
<bean id="person" class="com.xxx.xxx.Person"/>
  • byType,根据bean的类型进行自动装配。
<!-- Customer的属性person的类型为Person,spring会将Person类型通过setter方法进行自动装配。 -->
<bean id="customer" class="com.xxx.xxx.Customer" autowire="byType"/>
<bean id="person" class="com.xxx.xxx.Person"/>
  • constructor,类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型相同,则进行自动装配,否则抛出异常。
<!-- Customer构造器的参数person的类型为Person,spring会将Person类型通过构造方法进行自动装配。 -->
<bean id="customer" class="com.xxx.xxx.Customer" autowire="constructor"/>
<bean id="person" class="com.xxx.xxx.Person"/>
  • autodetect,如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。

@Autowire自动装配bean,可以在字段、setter方法和构造函数上使用。

spring事务什么时候会失效

spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了。常见的情况有以下几种:

  1. 发生自调用,即类里面使用this调用本类方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身。
    解决方法是让那个this变成UserService的代理类。
  2. 方法不是public的。
    @Transactional只能用于public的方法上,否则事务不会生效,如果要用在非public方法上,可以开启AspectJ代理模式。
  3. 数据库不支持事务。
  4. 没有被spring管理。
  5. 异常被catch掉了,或者抛出的异常没有被定义。

spring支持的常用数据库事务传播属性

propagation:设置事务的传播行为

  1. 什么是事务的传播行为?
    一个方法运行在一个开启了事务的方法中时,当前方式是使用原来的事务还是开启一个新事务。
  2. 事务的传播属性
    REQUIRED:如果有事务在运行,当前的方法就在这个事务内运行,否则开启一个新的事务,并在自己的事务内运行。
    REQUIRED_NEW:当前方法必须启动新事务,并在自己的事务内运行,如果有事务正在运行将它挂起。
    SUPPORTS:如果有事务正在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中。
    NOT_SUPPORTED:当前方法不应该运行在事务中,如果有运行的事务,就将它挂起。
    MANDATORY:当前方法必须运行在事务内部,如果没有正在运行的事务就抛出异常。
    NEVER:当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。
    NEATED:如果有事务在运行,当前方法就应该在这个事务的嵌套事务内运行,否则启动一个新事务,并在它自己的事务内运行。

isolation事务的隔离级别:

  • isolation.REPEATABLE-READ:可重复读,MySQL的默认事务隔离级别。
  • isolation.READ_COMMITED:读已提交,Oracle默认的事务隔离级别,开发中最常用。

数据库事务的并发问题:

  • 脏读:读取了别的事务更新但未提交的数据。
  • 不可重复读:两次读取的数据内容不一致。
  • 幻读:两次读取的行数不一致。

隔离级别:

READ UNCOMMITTED:读未提交,可能出现脏读、幻读和不可重复读的问题。

READ COMMITTED:读已提交,可以避免脏读,存在不可重复读和幻读的问题。

REPEATABLE READ:可重复读,可以避免脏读和不可重复读,不能避免幻读。

SERIALIZABLE:串行化:可以避免脏读、幻读和不可重复读,但效率最低。

nested和requires_new的区别:

requires_new是新建一个事务并且新开启的这个事务与原事务无关,而nested是当前存在事务时会开启一个嵌套事务。在nested情况下父事务回滚时,子事务也会回滚,而且requires_new情况下,原事务回滚,不会影响新开启的事务。

nested和required的区别:

required情况下,调用方存在事务时,则被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch异常,事务都会回滚;而在nested情况下,被调用方发生异常时,调用方可以catch异常,这样只有子事务回滚,父事务不受影响。

Spring事务的实现方式和原理以及隔离级别?

在使用Spring框架时,可以有两种事务的方式,一种是编程式,一种是声明式,@Transactional注解就是声明式。

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能够让程序员操作事务更加方便的方式。

比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功或失败。

在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。

当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。

spring事务隔离级别就是数据库的隔离级别:外加一个默认级别

  • read uncommitted(未提交读)
  • read committed(提交读、不可重复读)
  • repeatable read(可重复度)
  • serializable(可串行化)

数据库配置的隔离级别和spring中不一致时,以spring中的事务隔离级别为准,除非spring中设置的事务隔离级别数据库不支持。

Spring框架中都用到了哪些设计模式

**简单工厂模式:**BeanFactory就是简单工厂模式,根据传入的唯一标识来获得Bean对象。

**工厂方法模式:**实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()获得该bean时,自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getObject()方法的返回值。

**单例模式:**spring中的单例模式,提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,因为spring要管理任意的java对象。

**适配器模式:**Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就可以完成SpringMVC的扩展了。

**装饰器模式:**Spring中用到的装饰器模式在类名上有两种表现:一种类名中包含有Wrapper,另一种类名中含有Decorator。

**动态代理模式:**切面在应用运行时被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建,动态的创建一个代理对象。SpringAOP就是以这种方式植入切面的。织入是把切面应用到目标对象并创建新的代理对象的过程。

**观察者模式:**spring的事件驱动模式使用的是观察者模式,spring中的Observer模式常用的地方是listener的实现。

解释下spring支持的几种bean的作用域

  1. singleton:默认,每个容器只有一个bean的实例,单例模式由BeanFactory自身来维护。该对象的生命周期与Spring IOC容器一致,在第一次被注入时创建。
  2. prototype:为每一个bean请求提供一个实例,每次注入时都会创建一个新的对象。
  3. request:为每个Http请求中创建一个单例对象,也就是说在单个请求中都会复用这个单例对象。
  4. session:和request类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。
  5. application:在ServletContext的生命周期中复用一个单例对象。
  6. websocket:在websocket的生命周期中复用一个单例对象。
  7. global-session:全局作用域,和Portlet应用相关。当应用部署在Portlet容器中工作时,它包含很多portlet。如果想要声明让所有portlet共用的对象,需要存储在global-session中。全局作用域和Servlet中的session作用域效果相同。

描述一下Spring Bean的生命周期

  1. 解析类得到BeanDefinition。
  2. 如果有多个构造方法,推断要使用的构造方法。
  3. 确认好构造方法后进行实例化,得到一个对象。
  4. 如果对象有@Atuowired注解,进行属性填充。
  5. 回调Aware方法,比如BeanNameAware、BeanFactoryAware。
  6. 调用BeanPostProcessor的初始化前的方法。
  7. 调用初始化方法。
  8. 调用BeanPostProcessor的初始化后的方法,这里会进行AOP。
  9. 如果创建的bean是单例的,把bean放入单例池。
  10. 使用bean。
  11. 容器关闭时调用DisposableBean中的destory方法,销毁bean。

BeanFactory和ApplicationContext的区别

ApplicationContext是BeanFactory的子接口,ApplicationContext提供了更完整的功能:

  1. 继承了MessageSource,支持国际化。
  2. 统一的资源文件访问方式。
  3. 提供在监听器中注册bean的事件。
  4. 可以同时加载多个配置文件。
  5. 可以载入多个上下文,使得每一个上下文都专注于一个特定的层次。
  • BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean的时候(调用getBean方法),才会对Bean进行加载实例化。这样,我们就不能提前发现一些存在的spring的配置问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直到第一次调用getBean方法才会抛出异常。
  • ApplicationContext是在容器启动时,一次性创建所有的Bean。这样,这容器启动时,我们就可以发现spring中存在的配置错误,有利于检查所依赖的属性是否注入。ApplicationContext启动会预载入所有的单例Bean,通过预载入单例Bean,确保需要的时候不用等待。
  • 相对于BeanFactory,ApplicationContext唯一的不足是占用内存空间。当应用程序配置的Bean较多时,程序启动较慢。
  • BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,例如使用ContextLoader。

谈谈你对IOC的理解

IOC容器:

实际上IOC容器就是一个map,里面存的是各种对象,在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名通过反射创建对象放到map中,扫描到标有注解的类也是通过反射创建对象放到map中。

map里面存放了各种对象,用到的时候通过DI注入。

控制反转:

没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,必须自己主动去创建对象B或者使用已创建的对象B。无论是创建还是使用已有的,控制权都在自己手里。

引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

综上所述可以发现,对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是控制反转。

依赖注入:

获得对象的过程被反装了,控制被反装之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态的将某种依赖关系注入到对象之中。

谈谈你对AOP的理解

系统是由许多不同的组件组成的,每一个组件各自负责一块特定的功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。因为OOP适用于定义从上到下的关系,而不适用于定义从左到右的关系。

例如日志功能,日志代码往往水平的散布在所有对象层次中,而与它所散布到对象的核心功能毫无关系。

在OOP设计中,它导致了大量的代码重复,而不利与各个模块的重用。

AOP将程序中的交叉业务逻辑,封装成一个切面,然后注入到目标对象中去。AOP可以对某个对象或某些对象的功能进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情。

spring是什么

spring是一个轻量级的控制反转和面向切面的容器框架,具有以下几个主要特征:

  1. 从大小和开销两方面而言spring都是轻量级的。
  2. spring通过控制反转达到了松耦合的目的。
  3. spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑和系统服务进行内聚性的开发。
  4. spring是一个容器,包含并管理应用对象的配置和生命周期。
  5. spring是一个框架,将简单的组件配置,组合成为复杂的应用。

线程池中线程复用的原理

线程池将线程和任务进行了解耦,摆脱了之前通过Thread创建线程时一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断的获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都调用线程的start方法来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有就直接执行,也就是调用任务中的run方法,将run方法当成一个普通方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来了。

达到最大核心数后为什么是先添加任务队列而不是先创建最大线程

因为创建新线程时,需要获取全局锁,影响整体效率,而往任务队列中添加任务代价比较小。

线程池中为什么使用阻塞队列而不是普通队列

  1. 普通队列只能保证作为一个有效长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,而阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
  2. 阻塞队列自带阻塞和唤醒功能,不需要额外处理,没有任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一致占用CPU资源。

简述线程池处理流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xwveapyh-1644861584487)(https://note.youdao.com/yws/api/personal/file/WEB28172a027d798418817d687747d09c32?method=download&shareKey=9bc59992c9dcbcccd35ac67d0a0b9535)]

为什么使用线程池,解释下线程池参数

为什么使用线程池:

  1. 提高线程利用率,减少创建和销毁线程的资源消耗;
  2. 提高响应速度,任务来了,直接有线程可用,不用先去创建线程;
  3. 提高线程的可管理性,线程是稀缺资源,使用线程池可以统一管理。

线程池参数:

  • corePoolSize:核心线程数,线程池维持的常驻线程数量。
  • maxinumPoolSize:最大线程数,线程池允许创建的最大线程数量。
  • keepAliveTime:空闲存活时间,线程池中的线程数超过核心线程数后,线程空闲时间超过keepAliveTime就会被销毁。
  • unit:空闲存活时间的单位。
  • workQueue:待执行任务队列,线程池中的线程数量超过核心线程数后,再有任务进来就会被放入到任务队列中,任务队列放满之后,会创建新的线程,但线程池中的总线程数量不会超过最大线程数。
  • ThreadFactory:线程工厂,用来生产线程执行任务。默认的线程工厂产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。可以根据业务需求自定义线程工厂。
  • Handler:拒绝策略,分两种情况:一是调用shutdown方法关闭线程池后,即使线程池中还有任务在执行,再向线程池提交任务也会遭到拒绝;二是线程池中的线程数已达到最大线程数,无法接收新的任务时,再有任务时也会有任务被拒绝。

并发的三大特性

并发的三大特性是:原子性、可见性和有序性。

原子性:是指在一个操作过程中,CPU不可以中途暂停然后再调度,即不可以被中断,要么都执行完,要么都不执行。

可见性:是指多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

有序性:虚拟机在进行代码编译时,会对那些逻辑上没有依赖关系的代码进行优化重排,这有可能会导致线程安全问题。

串行、并行和并发的区别

串行在时间上不可能发生重叠,前一个任务没执行完,下一个任务只能等待。

并行在时间是重叠的,两个任务在同一时刻互不干扰的同时执行。

并发两个任务交替执行,但同一时刻只有一个任务执行。

Threadlocal内存泄漏原因及如何避免

不再被使用的对象或者变量占用的内存不能被回收就是内存泄漏,一次内存泄漏的危害可以忽略,但是内存泄漏堆积的后果很严重,无论多少内存,迟早会溢出。

强引用:使用最普遍的引用,一个对象具有强引用,就不会被垃圾回收器回收。当内存不足时,Java虚拟机宁愿抛出内存溢出错误,终止程序也不会回收强引用的对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示,可以在缓存中使用弱引用。

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用,key势必会被GC回收,这样就导致ThreadLocalMap中的key为null,而value还存在着强引用,只有线程退出以后,value的强引用链条才会断掉,如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链。

若ThreadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

若ThreadLocalMap的key为弱引用,回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null时,在下一次ThreadLocalMap调用get、set或remove方法的时候会清除value值。

因此,ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的key就会导致内存泄漏,而不是因为使用弱引用。

ThreadLocal正确的使用方法:

  1. 每次使用完ThreadLocal都调用它的remove方法清除数据。
  2. 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都通过ThreadLocal的弱引用访问到Entry的value值,防止被清除掉。

ThreadLocal的原理和使用场景

每一个Thread对象都含有一个ThreadLocalMap类型的成员变量threadLocals,它存储线程中所有ThreadLocal对象及其对应的值。ThreadLocalMap由一个个Entry对象构成,Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和Object构成。由此可见,Entry的key是ThreadLocal对象,而且是一个弱引用。当没指向key的强引用后,该key就会被垃圾回收器回收。

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程与set类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而无需使用同步机制来保证多线程访问容器的互斥性。

使用场景:

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据间隔。
  3. 进行事务操作,用于存储线程事务信息。
  4. 数据库连接,Session管理。

对守护线程的理解

守护线程是为所有非守护线程服务的,相当于JVM中所有非守护线程的保姆。守护线程的生死无所紧要,当没有其他线程运行时,守护线程就会被中断。需要注意的是守护线程的生死是自身无法控制的,因此不要把IO、File等重要逻辑分配给它。

守护线程的作用:

举个例子,GC就是一个经典的守护线程,当我们的程序中不再有任何运行的线程时,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当JVM中的线程只剩下GC时,GC线程就会被终止。GC始终在一个低级别的运行状态中运行,用于实时监控和管理系统可回收的资源。

守护线程的应用场景:

  1. 为其他线程提供服务支持的线程;
  2. 在任何情况下,程序结束时,都必须立刻关闭的线程。

注意:

  1. 不能把正在运行的线程设置为守护线程,thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
  2. 在守护线程中产生的新线程也是守护线程。
  3. 守护线程不能用于访问固有资源,比如读写操作和计算逻辑,因为它随时可能被中断。
  4. Java中自带的多线程框架,如ExecutorService会把守护线程转换为用户线程,因此若要使用守护线程就不能使用自带的的线程池。

Thread和Runnable的区别

Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作,就继承Thread;如果只是简单的执行一个任务,就实现Runnable。

对线程安全的理解

当多个线程同时访问某一个对象时,如果不需要进行额外的同步控制每次运行的结果和单个线程运行时的一致,那么就是线程安全的。

堆是进程和线程共有的空间,分为全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所在的内存区域的唯一目的就是存放实例对象,几乎所有的实例对象和数组都在这里分配内存。

栈是每个线程独有的,保存其运行状态和局部变量。栈在线程开始的时候初始化,每个线程的栈相互独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。

sleep()、wait()、join()和yield()的区别

锁池:所有需要竞争同步锁的线程都会被放入到锁池中,例如当前对象的锁已经被其中一个线程得到,则其他线程就需要在这个锁池中等待,当前面的线程释放锁后,锁池中的线程就会去竞争锁,得到锁的线程会进入就绪状态等待CPU资源分配。

等待池:当我们调用了wait方法后,线程会被放入到等待池,等待池的线程不会去竞争同步锁。只有调用notify或者notifyAll方法后,等待池中的线程才会开始去竞争锁。notify方法随机从等待池中唤醒一个线程放到锁池,notifyAll方法将等待池中的所有线程全部唤醒放到锁池中竞争锁。

sleep()和wait()的区别:

  1. sleep()是Thread类的静态本地方法,wait()是Object类的本地方法。
  2. sleep()不会释放锁,而wait会释放锁并进入等待池。
    sleep方法就是把CPU的执行资格和执行权释放出去,不再运行此线程,当等待时间结束后再取回CPU资源,参与CPU的调度,获取到CPU资源后就可以继续运行了。但如果sleep时该线程已经有锁,sleep不会释放这个锁,而是把锁带入冻结状态,其他需要这个锁的线程不可能获取到这个锁,因此程序无法执行。如果在睡眠期间其他线程调用了这个线程的interrupt方法,这个线程就会抛出interrupteexception异常返回。
  3. sleep方法不依赖于同步器synchronized,而wait需要依赖synchronized关键字。
  4. sleep不需要被唤醒,而wait需要。
  5. sleep一般用于当前线程休眠,或者轮循暂停操作,而wait多用于线程之间的通信。
  6. sleep会让出cpu执行时间且强制切换上下文,而wait不一定,可能立刻被唤醒并竞争到锁继续执行。

yield()执行后线程直接进入就绪状态,虽然释放了cpu执行权,但保留了cpu执行资格,所以有可能立马又获取到了cpu执行权,进而继续执行。

join()执行后线程进入阻塞状态,直到被调用join方法的线程执行完毕或者中断,才进入就绪状态。

线程的生命周期

线程通常有5种状态:创建、就绪、运行、阻塞和死亡。

创建:是新建了一个线程对象。

就绪:是线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,等待获取cpu的使用权。

运行:是就绪状态的线程获取cpu后,执行程序代码。

阻塞:是线程由于某种原因放弃cpu使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞有3种情况:

  • 等待阻塞:运行线程执行了wait方法,该线程会释放占用的所有资源,JVM将该线程放入等待池中。进入这个状态后,线程不能自动唤醒,必须依靠其他线程调用notify或者notifyAll方法才能被唤醒,wait是Object类的方法。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM会把线程放入锁池中。
  • 其他阻塞:运行的线程执行了sleep方法或者join方法,或者发出来I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时、join等待线程终止或超时,或者I/O处理完毕时,线程会重新进入就绪状态。sleep是Thread类的方法。

死亡:是线程执行完毕或者因异常退出了run方法,该线程的生命周期结束。

GC如何判断对象是否可以被回收

常用的算法有两种:引用计数法和可达性分析法。

引用计数法:每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时对象可以被回收,缺点是无法解决循环引用的问题。

可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,这个对象就是可以被回收的。

GC Roots对象有:

  • 虚拟机栈中引用的对象
  • 方法区静态属性引用的对象
  • 方法区常量引用的对象
  • 本地方法栈中引用的对象

可达性算法中不可达对象并不是立即死亡的,对象有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程,第一次是经过可达性分析发现没有与GC Roots相连的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。

当对象变成不可达时,GC会判断该对象是否重写了finalize()方法,若未覆盖,则直接将其回收。否则,若对象未执行finalize方法,将其放到F-Queue队列,由一低优先级线程执行该对象的finalize方法。finalize方法执行完毕后,GC再次判断该对象是否可达,若不可达,则进行回收,否则对象复活。

每个对象finalize方法只会执行一次。

由于finalize方法运行代价高昂,且不确定性大,无法保证各个对象的调用顺序,不推荐使用。

Java中的异常体系

Java中所有异常都继承自顶级父类Throwable,Throwable下有两个子类Exception和Error。

Error是程序无法处理的错误,一旦出现,程序将被迫停止运行。

Exception不会导致程序停止,又分为两种RunTimeException和CheckedException。

RunTimeException发生在程序运行过程中,又称运行时异常,会导致程序当前线程执行失败。

CheckedException发生在编译过程中,会导致程序编译失败,又称为受检异常。

双亲委托模型

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1x7QtP3j-1644861584488)(https://note.youdao.com/yws/api/personal/file/WEBe2d62e51d1d526eda14b6cc6f5da2223?method=download&shareKey=f561c7ad674a0a3542e8de3427f178bb)]

双亲委托模型的好处:

  1. 安全可以保证核心类不被篡改。
  2. 高效可以避免重复加载。

Java的类加载器

JDK自带了三个类加载器:BootstrapClassLoader、ExtClassLoader和AppClassLoader。

BootstrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件。

ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext下的jar包和class文件。

AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件。此外,AppClassLoader还是默认的线程上下文加载器。

自定义类加载器需要继承ClassLoader。

什么是字节码,使用字节码的好处是什么

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译器一个共同的接口。

编译器只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码,它不面向任何特定的处理器,只面向虚拟机。

每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后生成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。

采用字节码的好处:

Java采用字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时也是Java可以跨平台的基础。

如何实现一个IOC容器

  1. 配置文件中指定需要扫描的包路径;
  2. 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解和获取配置文件注解等;
  3. 从配置文件中获取需要扫描的包路径,获取该路径下的文件信息及文件夹信息,将该路径下所有以.class结尾的文件添加到一个Set集合中进行存储;
  4. 遍历这个Set集合,获取在类上有指定注解的类,并交给IOC容器,定义一个安全的Map来存储这些对象;
  5. 遍历这个IOC容器,获取到每个类的实例,判断里面是否有依赖其他类的实例,然后进行递归注入。

ConcurrentHashMap的原理,以及在java7和java8中的区别

java7:

数据结构:ReentrantLock + Segment + HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。

元素查询:两次hash,第一次hash定位到segment,第二次hash定位到元素所在的链表的头部。

锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容也不会影响其他的Segment。

get方法无需加锁,通过volatile保证线程安全。

java8:

数据结构:synchronized + CAS + Node + 红黑树,Node的val和next都用volatile修饰,保证可见性。

查找、替换和赋值操作都使用CAS。

锁:锁链表的head节点,不影响其他元素的读写,锁精度更细,效率更高,扩容时,阻塞所有读写操作,并发扩容。

读操作无锁,Node的val和next使用volatile修饰,读写线程对该变量相互可见;数组也用volatile修饰,保证扩容时被读线程感知。

HashMap和HashTable的区别,以及它们的底层实现

HashMap和HashTable的区别:

  1. HashMap没有排序允许一个null键和多个null值,HashTable不允许;
  2. HashMap把HashTable的contains方法去掉了,改成了containsKey和containsValue;
  3. HashTable继承自Dictionary类,而HashMap是Map接口的实现;
  4. HashTable线程安全,HashMap线程不安全。

HashMap的底层实现:数组+链表

HashMap是基于哈希表的Map接口的非同步实现,它的底层是一个数组,而数组的每一项又是一个链表。java8开始,链表高度达到8,且数组长度超过64时,链表将转变为红黑树,元素以内部类Node节点的形式存在,而当链表高度低于6时将红黑树转回链表。

当我们往HashMap中put元素时,先根据key的hashCode的计算hash值,再根据hash值得到这个元素在数组中的位置,如果数组中该位置已经存放了其他元素,那么在该位置上的元素将以链表的形式存放,新加元素放在链头,最先加的元素放在链尾。如果数组上的该位置没有元素,就直接将元素放在数组的该位置上。当key为null时,会放在下标为0的位置。

当我们从HashMap中get元素时,首先计算key的hashCode,找到数组中对应的位置,如果数组中的该位置上只有一个元素直接返回,否则通过key的equals方法找到相应的元素返回。

构造HashMap时,如果不指明初始大小,默认为16,随着HashMap中的元素越来越多,hashCode冲突的几率越来越多,为了提高效率,当存放元素的数量超过数组长度的0.75时,就需要对HashMap进行扩容,一般扩为原来的两倍。扩容很耗时,初始化时指定合理的大小可以节省性能。

ArrayList和LinkedList的区别

ArrayList和LinkedList都实现了List接口,它们的主要区别是:

ArrayList的底层是动态数组,需要连续内存存储,可以根据下标随机访问,查询速度快。但数组初始化时要设置长度,当存储的元素超过数组长度时,需要对数组进行扩容,而且如果不是在尾部插入或删除元素,还会涉及到元素的移动,比较耗费性能。

LinkedList的底层是双向链表,可以存储在分散的内存中,它的每一个元素都和前后元素链接在一起,查找某个元素的时间复杂度是O(n)。但插入和删除时,不需要像数组那样重新计算大小和更新索引,速度较快。

综上,使用ArrayList时,采取尾插法并在初始化时指定合理的初始容量可以极大地提高性能。遍历LinkedList时,要使用迭代器而不要使用for循环,因为每次for循环体内通过get(i)取得某一元素时,都需要对list重新进行遍历,性能消耗极大。另外也不要试图使用indexOf返回LinkedList元素的索引,并利用其进行遍历,因为indexOf对list进行了遍历,当结果为空时会遍历整个列表。

hashCode和equals的关系

hashCode方法是定义在Object类中的,任何对象都拥有此方法,它的作用是获取哈希码,也称为散列码,根据哈希码可以快速确定对象在哈希表中的位置。

两个对象相等,hashCode一定相同,调用equals方法返回结果一定为true;但两个拥有相同哈希值的对象不一定相等。

重写equals方法时,需要重写hashCode方法,否则就位违反hashCode方法的通用约定,导致这个类无法与基于散列值的集合类一起使用。

List和Set的区别

List有序可重复,是按对象进入的顺序保存对象的,允许多个null元素对象。可以使用迭代器取出所有元素,再逐一遍历,也可以使用下标获取指定下标的元素。

Set无序不可重复,最多只能有一个null元素对象,取元素时只能使用迭代器取出所有元素,再逐一遍历各个元素。

接口和抽象类的区别

从设计角度上来说,接口是对类的行为进行约束,它提供了一种机制,强制要求不同的类具有相同的行为,但它只是约束了行为的有无,而不对行为的实现进行约束;而抽象类的设计目的是代码复用,当不同的类具有某些相同的行为,且其中一部分行为的实现方式一致时,可以让这些类都派生于一个抽象类,在抽象类中实现公共部分。抽象类体现的是一种继承关系,可以形象的用is a表示,一个类只能继承一次;而接口体现的是一种契约关系,可以形象的用like a表示,一个类可以实现多个接口。

从语法角度上来说,java8以前,接口中的所有方法都是抽象的,方法不能有默认行为,而抽象类中的方法可以是抽象的,也可以是非抽象的。java8以后,接口中可以定义默认方法和静态方法。此外,抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是常量(public static final)。

重载和重写的区别

重载是在同一个类中,方法名必须相同,参数类型、个数或顺序不同,返回值和访问修饰符可以相同也可以不同。

重写是在父子类中,方法名和参数列表必须相同,子类的返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法的访问修饰符为private,则子类不能重写该方法。

String、StringBuffer和StringBuilder的区别

String是final修饰的,不能改变,每次操作都会产生新的对象,而StringBuffer和StringBuilder都是在原对象上操作。StringBuffer和StringBuilder的区别是StringBuffer的方法都是synchronized修饰的,线程安全。

性能:StringBuilder > StringBuffer > String

使用场景:经常修改的字符串使用StringBuffer或StringBuilder,优先使用StringBuilder,多线程使用共享变量时用StringBuffer。

为什么局部内部类和匿名内部类只能访问final修饰的局部变量

因为内部类和外部类是同一级别的,它不会因为定义在方法中就随着方法执行完而销毁,为了保证方法执行完,它仍然可以继续访问到局部变量,它会将局部变量复制一份作为自己的成员变量。只有通过final修饰才能保证局部变量和内部类建立的副本始终保持一致,因此局部内部类和匿名内部类只能访问final修饰的局部变量。

final、finally和finalize的区别

final表示最终的,可以用来修饰类、方法或者变量,final修饰的类不能被继承,修饰的方法不能被重写(可以被重载),修饰的变量不能被修改。

如果final修饰的是引用类型变量,初始化后不能再指向其他对象,但引用的值是可以修改的。

finally用于异常处理,修饰的代码块无论正常执行还是抛出异常都会被执行。

finalize是Object类一个方法,在垃圾回收器将对象清除之前会调用对象的此方法,因此可以通过复写此方法进行一些必要的清理工作。

==和equals的区别

当比较的是基本类型时,==比较的是变量值;

当比较的是引用类型时,==比较的是对象的地址。

equals未被重写前跟==一样,但可以通过重写比较二者的内容是否相同。

JDK、JRE和JVM的关系?

JDK:java开发工具包,是整个java的核心,包括java运行环境、java工具和java基础类库。

JRE:java运行环境,是运行java程序所必须的环境集合,包括JVM标准实现和java核心类库。

JVM:java虚拟机,是一个虚构出来的计算机,通过在实际计算机上仿真模拟各种计算机功能实现的。JVM屏蔽了与具体操作系统相关的信息,使得java程序只需生成字节码就可以在多种平台上不加修改的运行,是java跨平台的基础。

什么是面向对象

面向对象是利用类和对象编程的一种思想,它更注重事情有哪些参与者及各自需要做什么,与面向过程相比它更易于复用、扩展和维护。

java是一种面向对象的编程语言,在java中万物可归类,一切皆对象。

类是对事物的高度抽象,对象是通过类创建的具体实例。

面向对象具有三大特征:封装、继承和多态。

封装:

封装是指将一类事物抽象成一个类,隐藏其内部实现机制,对外提供访问它的方法。

封装可以明确标识出允许外部使用的成员属性和方法,外部不用关注类的内部实现,根据协议调用其对外提供的方法即可。

封装提高数据隐秘性的同时,使代码模块化,提高了代码的可复用性。

继承:

继承是指从已有类中派生新类,新类接收基类属性和行为的同时,可以扩展自己的的个性化能力。

多态:

多态是指允许不同类的对象对同一消息做出不同的响应,具体表现在方法的重载和重写上。