【无标题】哈哈哈哈

1、MQ有哪些使用场景?(高频)

异步处理:用户注册后,发送注册邮件和注册短信。用户注册完成后,提交任务到 MQ,发送模块并行获取 MQ 中的任务。

系统解耦:比如用注册完成,再加一个发送微信通知。只需要新增发送微信消息模块,从 MQ 中读取任务,发送消息即可。无

需改动注册模块的代码,这样注册模块与发送模块通过 MQ 解耦。

流量削峰:秒杀和抢购等场景经常使用 MQ 进行流量削峰。活动开始时流量暴增,用户的请求写入MQ,超过 MQ 最大长度丢

弃请求,业务系统接收 MQ 中的消息进行处理,达到流量削峰、保证系统可用性的目的。

日志处理:日志采集方收集日志写入 kafka 的消息队列中,处理方订阅并消费 kafka 队列中的日志数据。

消息通讯:点对点或者订阅发布模式,通过消息进行通讯。如微信的消息发送与接收、聊天室等。

2、简单介绍一些Rabbitmq的架构?(高频)

架构如下所示:

消息的发送消息流程:

1、生产者和Rabbitmq服务端建立连接,然后获取通道

2、生产者发送消息发送给指定的虚拟机中的交换机

3、交换机根据消息的routingKey将消息转发给指定的队列

消费者消费消息流程:

1、消费者和Rabbitmq服务端建立连接,然后获取通道

2、消费者监听指定的队列

3、一旦队列有消息了此时就会把消息推送给指定的消费者

3、Rabbitmq中交换机的类型有哪些?(高频)

主要有以下4种:

fanout: 把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。

direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中。

topic: 匹配规则:

RoutingKey 为一个 点号'.': 分隔的字符串。比如: java.xiaoka.show

BindingKey和RoutingKey一样也是点号“.“分隔的字符串。

BindingKey可使用 * 和 # 用于做模糊匹配,*匹配一个单词,#匹配多个或者0个

headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配。性能差,基本用不到。

4、如何保证消息不被重复消费?(高频)

消息重复消费的原因:

1、生产者发送消息的时候,在指定的时间只能没有得到服务端的反馈,此时触发了重试机制,在Rabbitmq服务端就会出现重复消费,那么消费者在进行

消费的时候就出现了重复消费。

2、消费者消费完毕以后,消费方给MQ确认已消费的反馈,MQ 没有成功接受。该消息就不会从Rabbitmq删除掉,那么消费者再一次获取到了消息进行消

费。

MQ是无法保证消息不被重复消费的,只能业务系统层面考虑。不被重复消费的问题,就被转化为消息消费的幂等性的问题。幂

等性就是指一次和多次请求的结果一致,多次请求不会产生副作用。

保证消息消费的幂等性可以考虑下面的方式:

① 给消息生成全局 id,消费成功过的消息可以直接丢弃

② 消息中保存业务数据的主键字段,结合业务系统需求场景进行处理,避免多次插入、是否可以根据主键多次更新而并不影响

结果等

5、如何保证消息不丢失?(高频)

消息丢失的发送的时机:

1、生产者发送消息的时候,由于网络抖动导致消息没有发送成功

2、消息发送到Rabbitmq的以后,Rabbitmq宕机了

3、消费者获取到MQ中的消息以后,还没有及时处理,此时消费者宕机了

解决方案:

1、生产者发送消息:主流的MQ都有确认机制或事务机制,可以保证生产者将消息送达到 MQ。如 RabbitMQ 就有事务模式和 confirm模式。

2、MQ 丢失消息:开启 MQ 的持久化配置(消息、队列都需要进行持久化)。

3、消费者丢失消息:改为手动确认模式,消费者成功消费消息再确认。

6、如何保证消息的顺序性?(高频)

Rabbtimq:

1、将多个消息发送到一个队列中,队列本身就是先进先出的结构

2、避免多消费者并发消费同一个 queue 中的消息。

Kafka:

1、将多个消息发送到一个分区中,kafka可以保证一个分区中的消息的有序性

2、避免多消费者并发消费同一个分区中的消息。

7、消息大量积压怎么解决?(高频)

解决方案:

1、针对Rabbitmq可以使用惰性队列,让消息直接存储到磁盘中

2、增加消费者的数量,提升消费者的消费能力

8、导致的死信的几种原因?(高频)

1、消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。

2、消息TTL过期。

3、队列满了,无法再添加。

9、什么是延迟队列以及具体的应用场景?(高频)

概述:存储对应的延迟消息,指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

应用场景:订单超时未支付,文章的延迟发送

1 Redis基础篇

1、简单介绍一下Redis优点和缺点?

优点:

1、本质上是一个 Key-Value 类型的内存数据库,很像memcached

2、整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存

3、因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的Key-Value DB

4、Redis最大的魅力是支持保存多种数据结构(string,list,set,hash,sortedset),此外单个 value 的最大限制是 1GB,

不像memcached只能保存 1MB 的数据

5、Redis也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一个功能加强版的memcached 来用

缺点:

1、Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和

运算上。

2、系统中为什么要使用缓存?

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的

时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓

存这里而不用经过数据库。

3、常见的缓存同步方案都有哪些?(高频)

同步方案:更改业务代码,加入同步操作缓存逻辑的代码(数据库操作完毕以后,同步操作缓存)

异步方案:

1、使用消息队列进行缓存同步:更改代码加入异步操作缓存的逻辑代码(数据库操作完毕以后,将要同步的数据发送到MQ中,MQ的消费者从MQ中获取数

据,然后更新缓存)

2、使用阿里巴巴旗下的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数

据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

4、Redis常见数据结构以及使用场景有哪些?(高频)

1、 string

常见命令:set、get、decr、incr、mget等。

基本特点:string数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。

应用场景:常规计数:微博数,粉丝数等。

2、hash

常用命令: hget、hset、hgetall等。

基本特点:hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。

应用场景:存储用户信息,商品信息等。

3、list

常用命令: lpush、rpush、lpop、rpop、lrange等。

基本特点:类似于Java中的list可以存储多个数据,并且数据可以重复,而且数据是有序的。

应用场景:存储微博的关注列表,粉丝列表等。

4、set

常用命令: sadd、spop、smembers、sunion 等

基本特点:类似于Java中的set集合可以存储多个数据,数据不可以重复,使用set集合不可以保证数据的有序性。

应用场景:可以利用Redis的集合计算功能,实现微博系统中的共同粉丝、公告关注的用户列表计算。

5、sorted set

常用命令: zadd、zrange、zrem、zcard 等。

基本特点:和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

应用场景:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等。

5、Redis有哪些数据删除策略?(高频)

数据删除策略:Redis中可以对数据设置数据的有效时间,数据的有效时间到了以后,就需要将数据从内存中删除掉。而删除的时候就需要按照指定的规则

进行删除,这种删除规则就被称之为数据的删除策略。

Redis中数据的删除策略:

① 定时删除

  • 概述:在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

  • 优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。

  • 缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分CPU时间,对服务器的响应时间和吞吐量造成影响。

② 惰性删除

  • 概述:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

  • 优点:对CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。

  • 缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键

    便永远不会被删除,内存永远不会释放。

③ 定期删除

  • 概述:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期

  • 键)。

  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键

    占用的内存。

  • 缺点:难以确定删除操作执行的时长和频率。

    如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得

    到释放。

另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用定期删除函数的运行频率,

在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。

6、Redis中有哪些数据淘汰策略?(高频)

数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中

的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

常见的数据淘汰策略:

noeviction                          # 不删除任何数据,内存不足直接报错(默认策略)
volatile-lru                        # 挑选最近最久使用的数据淘汰(举例:key1是在3s之前访问的, key2是在9s之前访问的,删除的就是key2)
volatile-lfu                        # 挑选最近最少使用数据淘汰  (举例:key1最近5s访问了4次, key2最近5s访问了9次, 删除的就是key1)
volatile-ttl                        # 挑选将要过期的数据淘汰
volatile-random                     # 任意选择数据淘汰
allkeys-lru                         # 挑选最近最少使用的数据淘汰
allkeys-lfu                         # 挑选最近使用次数最少的数据淘汰
allkeys-random                      # 任意选择数据淘汰,相当于随机

注意:

1、不带allkeys字样的淘汰策略是随机从Redis中选择指定的数量的key然后按照对应的淘汰策略进行删除,带allkeys是对所有的key按照对应的淘汰策略

进行删除。

2、缓存淘汰策略常见配置项

maxmemory-policy noeviction         # 配置淘汰策略
maxmemory ?mb                       # 最大可使用内存,即占用物理内存的比例,默认值为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上。
maxmemory-samples count             # 设置redis需要检查key的个数

7、Redis中数据库默认是多少个db即作用?

Redis默认支持16个数据库,可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用select命令更

换数据库。

Redis支持多个数据库,并且每个数据库是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。

8、缓存穿透、缓存击穿、缓存雪崩解决方案?(高频)

加入缓存以后的数据查询流程:

缓存穿透

概述:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂

掉。

解决方案:

1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短

2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询

缓存击穿

概述:对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后

端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案:

1、使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,

否则重试get缓存的方法

2、永远不过期:不要对这个key设置过期时间

缓存雪崩

概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多

key,击穿是某一个key缓存。

解决方案:

将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引

发集体失效的事件。

9、什么是布隆过滤器?(高频)

概述:布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上由一个很长的二进制向量(二进制数组)和一系列随机映射函数(hash函数)。

作用:布隆过滤器可以用于检索一个元素是否在一个集合中。

添加元素:将商品的id(id1)存储到布隆过滤器

假设当前的布隆过滤器中提供了三个hash函数,此时就使用三个hash函数对id1进行哈希运算,运算结果分别为:1、4、9那么就会数组中对应的位置数

据更改为1。

判断数据是否存在:使用相同的hash函数对数据进行哈希运算,得到哈希值。然后判断该哈希值所对应的数组位置是否都为1,如果不都是则说明该数据

肯定不存在。如果是说明该数据可能存在,因为哈希运算可能就会存在重复的情况。如下图所示:

假设添加完id1和id2数据以后,布隆过滤器中数据的存储方式如上图所示,那么此时要判断id3对应的数据在布隆过滤器中是否存在,按照上述的判断规则

应该是存在,但是id3这个数据在布隆过滤器中压根就不存在,这种情况就属于误判。

误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。

删除元素:布隆过滤器不支持数据的删除操作,因为如果支持删除那么此时就会影响判断不存在的结果。

使用布隆过滤器:在谷歌的guava缓存工具中提供了布隆过滤器的实现,使用方式如下所示:

pom.xml文件

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

测试代码:

// 创建一个BloomFilter对象
// 第一个参数:布隆过滤器判断的元素的类型
// 第二个参数:布隆过滤器存储的元素个数
// 第三个参数:误判率,默认值为0.03
int size = 100_000 ;
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.03);
for(int x = 0 ; x < size ; x++) {
    bloomFilter.put("add" + x) ;
}
​
// 在向其中添加100000个数据测试误判率
int count = 0 ;     // 记录误判的数据条数
for(int x = size ; x < size * 2 ; x++) {
    if(bloomFilter.mightContain("add" + x)) {
        count++ ;
        System.out.println(count + "误判了");
    }
}
​
// 输出
System.out.println("总的误判条数为:" + count);

Redis中使用布隆过滤器防止缓存穿透流程图如下所示:

10、Redis数据持久化有哪些方式?各自有什么优缺点?(高频)

在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

RDB:定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照

优点:数据文件的大小相比于aof较小,使用rdb进行数据恢复速度较快

缺点:比较耗时,存在丢失数据的风险

AOF:将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了

优点:数据丢失的风险大大降低了

缺点:数据文件的大小相比于rdb较大,使用aof文件进行数据恢复的时候速度较慢

11、Redis都存在哪些集群方案?

在Redis中提供的集群方案总共有三种:

1、主从复制

  • 保证高可用性

  • 实现故障转移需要手动实现

  • 无法实现海量数据存储

2、哨兵模式

  • 保证高可用性

  • 可以实现自动化的故障转移

  • 无法实现海量数据存储

3、Redis分片集群

  • 保证高可用性

  • 可以实现自动化的故障转移

  • 可以实现海量数据存储

12、说说Redis哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪

个槽,集群的每个节点负责一部分 hash 槽。

13、Redis中的管道有什么用?

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务 器,而不用等待回复,最后在一个步骤中读取该答复。

14、谈谈你对Redis中事务的理解?(高频)

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis中的事务:Redis事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串

行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Reids中,单条命令式原子性执行的,但事务不保证原子性,且没有回

滚。

15、Redis事务相关的命令有哪几个?(高频)

事务相关的命令:

1、MULTI:用来组装一个事务

2、EXEC:执行一个事物

3、DISCARD:取消一个事务

4、WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行

5、UNWATCH:取消 WATCH 命令对所有key的监视

如下所示:

16、Redis如何做内存优化?

尽可能使用散列表(hash),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。

比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里

面。

17、Redis是单线的,但是为什么还那么快?(高频)

Redis总体快的原因:

1、完全基于内存的

2、采用单线程,避免不必要的上下文切换可竞争条件

3、数据简单,数据操作也相对简单

4、使用多路I/O复用模型,非阻塞IO

2 分布式锁篇

18、什么是分布式锁?

概述:在分布式系统中,多个线程访问共享数据就会出现数据安全性的问题。而由于jdk中的锁要求多个线程在同一个jvm中,因此在分布式系统中无法使

用jdk中的锁保证数据的安全性,那么此时就需要使用分布式锁。

作用:可以保证在分布式系统中多个线程访问共享数据时数据的安全性

举例:

在电商系统中,用户在进行下单操作的时候需要扣减库存。为了提高下单操作的执行效率,此时需要将库存的数据存储到Redis中。订单服务每一次生成订

单之前需要查询一下库存数据,如果存在则生成订单同时扣减库存。在高并发场景下会存在多个订单服务操作Redis,此时就会出现线程安全问题。

演示:导入基础工程(distributed-locks),演示分布式系统中的线程安全问题。

分布式锁的工作原理:

分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行

2、高可用的获取锁与释放锁

3、高性能的获取锁与释放锁

4、具备可重入特性

5、具备锁失效机制,防止死锁

可重入特性:获取到锁的线程再次调用需要锁的方法的时候,不需要再次获取锁对象。
使用场景:遍历树形菜单的时候的递归调用。

注意:锁具备可重入性的主要目的是为了防止死锁。

19、分布式锁的实现方案都有哪些?(高频)

分布式锁的实现方案:

1、数据库

2、zookeeper

3、redis

20、Redis怎么实现分布式锁思路?(高频)

Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。

127.0.0.1:6379> setnx lock value1 #在键lock不存在的情况下,将键key的值设置为value1
(integer) 1
127.0.0.1:6379> setnx lock value2 #试图覆盖lock的值,返回0表示失败
(integer) 0
127.0.0.1:6379> get lock #获取lock的值,验证没有被覆盖
"value1"
127.0.0.1:6379> del lock #删除lock的值,删除成功
(integer) 1
127.0.0.1:6379> setnx lock value2 #再使用setnx命令设置,返回0表示成功
(integer) 1
127.0.0.1:6379> get lock #获取lock的值,验证设置成功
"value2"

上面这几个命令就是最基本的用来完成分布式锁的命令。

加锁:使用setnx key value命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。

解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。

代码演示:

1、加入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

2、加锁和释放锁的工具类:

@Component
public class RedisLock {
​
    // 加锁的代码
    public boolean tryLock(Jedis jedis , String key , String requestId) {
        Long value = jedis.setnx(key, requestId);                               // 获取锁
        return value == 1 ;
    }
​
    // 释放锁
    public void releaseLock(Jedis jedis , String key , String requestId) {
        String value = jedis.get(key);
        if(requestId.equals(value)) {                                           // 同一个客户端的请求才允许进行锁的释放
            jedis.del(key) ;
        }
    }
​
}

业务代码改造:

@Autowired
private RedisLock redisLock ;

@Override
public void saveOrder(String goodsId) throws InterruptedException {

    // 获取锁对象
    Jedis jedis = new Jedis("192.168.136.130" , 6379) ;
    String requestId = UUID.randomUUID().toString().replace("-" , "") ;
    boolean tryLock = redisLock.tryLock(jedis, "lock:" + goodsId, requestId);

    if(tryLock) {

        try {

            // 获取库存数据
            String stock = redisTemplate.opsForValue().get("goods_stock:" + goodsId);
            if("".equals(stock) || stock == null) {
                throw new RuntimeException("库存数据不存在");
            }

            // 判断是否存在库存,如果不存在直接抛出异常
            Integer integerStock = Integer.parseInt(stock) ;
            if(integerStock <= 0) {
                throw new RuntimeException("库存不足.....");
            }else {

                // 让线程休眠一会,其他线程获取的到CPU的执行权
                TimeUnit.SECONDS.sleep(1);

                // 如果存在生成订单,扣减库存
                redisTemplate.opsForValue().set("goods_stock:" + goodsId , String.valueOf(--integerStock));

                // 生成订单数据
                System.out.println("订单生成成功.........");
            }

        }finally {

            // 释放锁
            redisLock.releaseLock(jedis , "lock:" + goodsId, requestId);
            jedis.close();
        }

    }else {
        throw new RuntimeException("分布式锁获取失败,下单失败");
    }
}

3、Jmeter测试

|

| | ------------------------------------------------------------ |

整个加锁的逻辑如下图所示:

21、Redis实现分布式锁如何防止死锁现象?(高频)

产生死锁的原因:如果一个客户端持有锁的期间突然崩溃了,就会导致无法解锁,最后导致出现死锁的现象。

所以要有个超时的机制,在设置key的值时,需要加上有效时间,如果有效时间过期了,就会自动失效,就不会出现死锁。然后加锁的代码就会变成这样。

// 加锁的代码
// requestId描述请求的唯一性,哪一个线程加锁了在解锁的时候就需要使用哪一个线程
public static boolean tryLock(Jedis jedis , String key, String requestId , int expireTime) {    
    SetParams setParams = new SetParams();
    setParams.nx() ;
    setParams.ex(expireTime) ;
    return "OK".equalsIgnoreCase(jedis.set(key , requestId , setParams));   // 不存则保存成功返回的是OK
}

执行流程如下所示:

22、Redis实现分布式锁如何合理的控制锁的有效时长?(高频)

有效时间设置多长,假如我的业务操作比有效时间长?我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。

解决方案:

1、第一种:程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代

码的执行。

2、第二种:给锁续期。

锁续期实现思路:当加锁成功后,同时开启守护线程,默认有效期是用户所设置的,然后每隔10秒就会给锁续期到用户所设置的有效期,只要持有锁的客

户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。

上述的第二种解决方案可以使用redis官方所提供的Redisson进行实现。

Redisson是Redis官方推荐的Java版的Redis客户端。它提供的功能非常多,也非常强大分布式服务,使用Redisson可以轻松的实现分布式锁。Redisson

中进行锁续期的这种机制被称为"看门狗"机制。

redission支持4种连接redis方式,分别为单机、主从、Sentinel、Cluster 集群。

使用步骤如下:

1、加入依赖

<!-- spring boot和redisson整合的时候所对应的起步依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.11.2</version>
</dependency>

2、定义配置类

@Configuration
public class RedissionConfiguration {

    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config() ;                                              // 创建一个配置类
        config.useSingleServer().setAddress("redis://192.168.136.130:6379") ;       // 使用单节点的服务器,并设置服务器的地址信息
        return Redisson.create(config) ;                                            // 创建RedissonClient对象
    }

}

3、业务代码加入分布式锁

@Service
public class OrderServiceImpl implements OrderService  {

    @Autowired
    private RedisTemplate<String , String> redisTemplate ;

    @Autowired
    private RedissonClient redissonClient ;

    @Override
    public void saveOrder(String goodsId) throws InterruptedException {

        // 获取锁对象
        RLock lock = redissonClient.getLock("lock:" + goodsId);				// 使用hash结构存储锁数据
        boolean tryLock = lock.tryLock(3, TimeUnit.SECONDS);                // 设置尝试获取锁的最大等待时间
        if(tryLock) {

            try {

                // 获取库存数据
                String stock = redisTemplate.opsForValue().get("goods_stock:" + goodsId);
                if("".equals(stock) || stock == null) {
                    throw new RuntimeException("库存数据不存在");
                }

                // 判断是否存在库存,如果不存在直接抛出异常
                Integer integerStock = Integer.parseInt(stock) ;
                if(integerStock <= 0) {
                    throw new RuntimeException("库存不足.....");
                }else {

                    // 让线程休眠一会,其他线程获取的到CPU的执行权
                    TimeUnit.SECONDS.sleep(1);

                    // 如果存在生成订单,扣减库存
                    redisTemplate.opsForValue().set("goods_stock:" + goodsId , String.valueOf(--integerStock));

                    // 生成订单数据
                    System.out.println("订单生成成功.........");
                }

            }finally {

                // 释放锁
                lock.unlock();
            }

        }else {
            throw new RuntimeException("分布式锁获取失败,下单失败");
        }
    }

}

4、Jmeter测试

|

| | ------------------------------------------------------------ |

23、Redis实现分布式锁如何保证锁服务的高可用?(高频)

解决方案:

1、使用Redis的哨兵模式构建一个主从架构的Redis集群

2、使用Redis Cluster集群

24、当同步锁数据到从节点之前,主节点宕机了导致锁失效,那么此时其他线程就可以再次获取到锁,这个问题怎么解决?(高频)

使用Redission框架中的RedLock进行处理。

RedLock的方案基于2个前提:

1、不再需要部署从库和哨兵实例,只部署主库

2、但主库要部署多个,官方推荐至少5个实例

也就是说,想使用RedLock,你至少要部署5个Redis实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。

工作流程如下所示:

1、客户端先获取【当前时间戳T1】

2、客户端依次向这个5个Redis实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超

时,锁被其他的人持有等各种异常情况),就立即向下一个Redis实例申请加锁

3、如果客户端从 >=3 个(大多数)以上Redis实例加锁成功,则再次获取【当前时间戳T2】, 如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,

否则加锁失败

4、加锁成功,去操作共享资源

5、加锁失败,向【全部节点】发起释放锁请求

总结4个重点:

1、客户端在多个Redis实例上申请加锁

2、必须保证大多数节点加锁成功

3、大多数节点加锁的总耗时,要小于锁设置的过期时间

4、锁释放,要向全部节点发起释放锁请求

24.1 为什么要在多个实例上加锁?

本质上是为了【容错】, 部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

24.2 为什么步骤3加锁成功后,还要计算加锁的累计耗时?

因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发

生,网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,如果加锁的累计耗时已经超过了锁的过期时间,那此时有些实例上的锁可能

已经失效了,这个锁就没有意义了。

代码大致如下所示:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:5378").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.1:5379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.1:5380").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK_KEY";

RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);

// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    // isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
    }
    
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

1 基础知识篇

1、什么是微服务架构?

微服务架构是一种架构模式或者说是架构风格,它提倡将单一应用程序划分成一组小的服务。每个服务运行在其独立的自己的

进程中服务之间相互配合、相互协调,为用户提供最终价值。服务之间采用轻量级通信。每个服务都围绕具体业务进行构建,

并能够独立部署到生产环境等。

2、微服务的优缺点是什么?

优点:松耦合,聚焦单一业务功能,无关开发语言,团队规模降低。在开发中,不需要了解多于业务,只专注于当前功能,便

利集中,功能小而精。微服务一个功能受损,对其他功能影响并不是太大,可以快速定位问题。微服务只专注于当前业务逻辑

代码,不会和 html、css 或其他界面进行混合。可以灵活搭配技术,独立性比较好。每个微服务都有自己独立的数据库,

数据库的表复杂度降低

缺点:随着服务数量增加,管理复杂,部署复杂,服务器需要增多,服务通信和调用压力增大,运维工程师压力增大,人力资

源增多,系统依赖增强,数据一致性,性能监控。

3、什么是Spring Cloud?

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务

发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring

Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发

工具包。

4、Spring Boot和Spring Cloud之间关系?

Spring Boot:专注于快速方便的开发单个个体微服务(关注微观)

Spring Cloud:关注全局的微服务协调治理框架,将Spring Boot开发的一个个单体微服务组合并管理起来(关注宏观)

Spring Boot可以离开Spring Cloud独立使用,但是Spring Cloud不可以离开Spring Boot,属于依赖关系。

5、Spring Cloud和 Dubbo有哪些区别?(高频)

相同点:它们都是分布式管理框架

区别:

1、dubbo使用的是RPC通讯,占用带宽会少一点。Spring Cloud使用的是HTTP的Rest方式进行通讯,带宽会多一点,同时

使用http协议一般会使用JSON报文,消耗会更大。

2、dubbo 开发难度较大,所依赖的jar包有很多问题大型工程无法解决。Spring Cloud 对第三方的继承可以一键式生成,天

然集成。

6、什么是Eureka以及它的架构是什么样子?

介绍:eureka是Netflix开发的服务发现组件,本身是一个基于REST的服务。Spring Cloud将它集成在其子项目spring-

cloud-netflix中, 以实现Spring Cloud的服务发现功能。

架构:

Eureka是一个C/S的架构模式,包含了两部分:

1、Eureka Server:注册中心服务端,用于维护和管理注册服务的列表

2、Eureka Client:注册中心客户端,用于向Eureka Server中注册服务和从Eureka Server中拉取服务

7、简述一下Eureka的自我保护机制?

心跳检查机制:Eureka Client向Eureka Server中注册完服务信息以后,Eureka Server会通过心跳检测机制来检测当前这个客户端服务是否还存活着!

默认的检测机制是Eureka Client每隔30s向Eureka Server发送一个心跳检查包,如果Eureka Server在90s之内没有收到Eureka Client所发送的心跳检

查包,那么此时Eureka Server将该Eureka Client从服务列表中剔除掉。

自我保护机制:自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。

自我保护机制的工作机制是:如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka

Server自动进入自我保护机制,

此时会出现以下几种情况:

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

  2. Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。

  3. 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中。

因此Eureka Server可以很好的应对因网络故障导致部分节点失联的情况,而不会像ZK那样如果有一半不可用的情况会导致整个集群不可用而变成瘫痪。

自我保护的开关配置:

eureka.server.enable-self-preservation = true       # 开启自我保护机制,值设置为false关闭自我保护机制

8、什么是Ribbon以及它的工作流程?(高频)

概述:Ribbon是一个客户端的负载均衡工具

工作流程:

基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1

  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service

  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表

  • eureka返回列表,localhost:8081、localhost:8082

  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081

  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

9、在Ribbon中定义了哪些常用的负载均衡算法以及默认的负载均衡算法是哪一个?(高频)

常用的负载均衡算法:

1、RoundRobinRule:简单轮询服务列表来选择服务器

2、AvailabilityFilteringRule:对以下两种服务器进行忽略:

(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间

就会几何级地增加。

(2)并发数过高的服务器、如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以

由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。

3、WeightedResponseTimeRule: 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,

这个权重值会影响服务器的选择。

4、ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。

而后再对Zone内的多个服务做轮询。它是Ribbon默认的负载均衡规则。

5、BestAvailableRule:忽略那些短路的服务器,并选择并发数较低的服务器。

6、RandomRule: 随机选择一个可用的服务器。

7、RetryRule:重试机制的选择逻辑。

默认的负载均衡算法:ZoneAvoidanceRule

10、什么是fegin?以及如何去使用?

概述:

1、fegin一个声明式的http的客户端工具用来简化远程调用,基于接口的注解的方式来声明一个http的客户端

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

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

使用:

1、添加pom依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、在启动类上添加@EnableFeignClients注解

3、定义一个接口,通过@FeignClient(value = "leadnews-article")指定调用的哪个服务

11、你们项目中的配置信息是如何进行管理的?

项目中的配置信息使用的是统一配置中心,常见的统一配置中心有:Spring Cloud Config和nacos。我们项目中使用的是nacos,使用nacos可以很轻松的实现配置

信息的热更新。

具体的使用方式如下所示:

1、在nacos中创建一个配置信息,指定配置信息的dataId。dataId的组成:${application.name}-${profile}.yml

2、在项目中添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

3、在项目的classpath路径下创建一个bootstrap.yml文件,文件中的内容如下所示:

server:
  port: 51803
spring:
  application:
    name: user-service                          # 指定服务名称
  cloud:
    nacos:
      config:
        server-addr: 192.168.200.130:8848       # 指定配置中心的ip地址和端口号
        file-extension: yml                     # 指定配置中心中文件的扩展名

12、什么是Spring Cloud Gateway以及在你们的项目中如何去应用该组件的?(高频)

Spring Cloud Gateway:是Spring Cloud中所提供的一个服务网关组件,是整个微服务的统一入口,在服务网关中可以实现请求路由、统一的日志记录,流量监

控、权限校验等一系列的相关功能!

项目应用:权限的校验

具体实现思路:使用Spring Cloud Gateway中的全局过滤器拦截请求(GlobalFilter、Order),从

请求头中获取token,然后解析token。如果可以进行正常解析,此时进行放行;如果解析不到直接返回。

2 服务保护篇

13、什么是雪崩效应以及常见的解决方案有哪些?(高频)

雪崩效应:一个服务的不可用导致整个系统出现不可用的现象。如下图所示:

常见的解决方案:

1、超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。

2、线程隔离:给每一个服务分配指定数量的线程,当这个服务使用完这些线程以后,该服务就不能再次被访问,而对其他服务的访问不受影响,将故障控

制到了一个小的范围。

3、熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求,并且可以给失败的调用返回一个降级方案

(兜底方案)。

4、流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。

14、常见的限流算法都有哪些?(高频)

常见的限流算法:计数器漏桶算法令牌桶算法

计数器:计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们我

们可以设置一个计数器counter,其有效时间为1分钟(即每分钟计数器会被重置为0),每当一个请求过来的时候,counter就加1,如果counter的值大

于100,就说明请求数过多;如下图所示:

这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题。

如下图所示,在1:00前一刻到达100个请求,1:00计数器被重置,1:00后一刻又到达100个请求,显然计数器不会超过100,所有请求都不会被拦截;然而

这一时间段内请求数已经达到200,远超100。

漏桶算法:漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量

是不变的,保证了整体的速率。

如下图所示:

令牌桶算法:令牌桶是一个存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃;当一个请求

达到时,会尝试从桶中获取令牌;如果有,则继续处理请求;如果没有则排队等待或者直接丢弃;可以发现,漏桶算法的流出速率恒定,而令牌桶算法的流

出速率却有可能大于r;

从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量

不做额外处理;而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输。

15、Nginx中如何实现限流?

nginx的限流主要是两种方式:限制访问频率限制并发连接数。nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会

超过设置的阈值。

nginx官方版本限制IP的连接和并发分别有两个模块:

1、limit_req_zone:用来限制单位时间内的请求数,即速率限制 , 采用的漏桶算法 "leaky bucket"。

2、limit_conn_zone:用来限制同一时间连接数,即并发限制。

limit_req_zone限流配置:

http {
    
    # 定义限流策略,$binary_remote_addr对客户端的ip进行限流,zone:定义共享内存区来存储访问信息, rateLimit:10m 表示一个大小为10M,名字为rateLimit的内    # 存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访问信息。rate定义了最大访问频率,1s最多允许1个请求访问。
    limit_req_zone $binary_remote_addr  zone=rateLimit:10m rate=1r/s ;
    
    # 搜索服务的虚拟主机
    server {
​
        location / {
            # 使用限流策略,burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区(队列)当有大量请求(爆发)过来时,
            # 超过了访问频次限制的请求可以先放到这个缓冲区内。nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所
            # 有请求会等待排队。
            limit_req zone=rateLimit burst=5 nodelay;       
            proxy_pass http://train-manager-search ;    
        }
​
    } 
​
}

limit_conn_zone限流配置:

http {
    
    # 定义限流策略,$binary_remote_addr对客户端的ip进行限流、$server_name对虚拟主机支持的最大连接数进行限流
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_conn_zone $server_name zone=perserver:10m;
    
    # 搜索服务的虚拟主机
    server {
​
        location / {
    
            # 对应的key是 $binary_remote_addr,表示限制单个IP同时最多能持有1个连接。
            limit_conn perip 1;
    
            # 对应的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。
            # 注意,只有当 request header 被后端server处理后,这个连接才进行计数。
            limit_conn perserver 10 ;
            proxy_pass http://train-manager-search ;    
        }
​
    } 
​
}

16、Sentinel中提供了哪些流控模式分别表示什么意思?(高频)

常见的流控模式:

1、直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式

2、关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流。如下流控模式:

表示的意思:当/write资源访问量触发阈值时,就会对/read资源限流,避免影响/write资源。

3、链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流

例如有两条请求链路:

  • /test1 --> /common

  • /test2 --> /common

如果只希望统计从/test2进入到/common的请求,则可以这样配置:

17、Sentinel中提供了哪些流控效果分别表示什么意思?(高频)

常见的流控效果:

1、快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。

2、warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。这种模式主要应用

于服务的冷启动,请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3。例

如,我设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10。如下图所示:

3、排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长。后来的请求必须等待前面执行完成,如果请求预期的等待时间超

出最大时长,则会被拒绝。

18、实现线程隔离有几种方式?Sentinel中使用的是哪一种方式?(高频)

线程隔离方式:

1、线程池隔离:有额外开销,但隔离控制更强

2、信号量隔离:简单,开销小

在Sentinel是通过信号量来实现线程隔离。如下图所示:

19、简述Sentinel中熔断器的工作原理?(高频)

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而

当服务恢复时,断路器会放行访问该服务的请求。断路器控制熔断和放行是通过状态机来完成的:

状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态

  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态

  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。

    • 请求成功:则切换到closed状态

    • 请求失败:则切换到open状态

3 分布式事务篇

20、什么是分布式事务?

概述:在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要

么全部成功,要么全部失败。

如下所示:

某电商系统的下单操作,需要请求三个服务来完成,这三个服务分别是:订单服务,账户服务,库存服务。当订单生成完毕以后,就需要分别请求账户服务

和库存服务进行进行账户余额的扣减和库存扣减。假设都扣减成功了,此时在执行下单的后续操作时出现了问题,那么订单数据库就进行事务回滚,订单生

成失败,而账户余额和扣减则都扣减成功了。这就出现了问题,而分布式事务就是解决上述这种不一致问题的。

21、哪些场景下都会产生分布式事务?

场景1:跨库事务

跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。如下所示:

场景二:分库分表

通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:

对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。

如,对于sql:insert into user(id,name) values (1,"tianshouzhi"),(2,"wangxiaoxiao")。这条sql是操作单库的语法,单库情况下,可以保证事务的一致

性。

但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分

库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。

场景三:跨服务事务

跨服务事务指的是,一个应用某个功能需要调用多个微服务进行实现,不同的微服务操作的是不同的数据库。如下所示:

Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。

需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。

22、什么是CAP理论?

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

1、一致性(Consistency) : 更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。

2、可用性(Availability) : 系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

3、分区容错性(Partition tolerance) : 分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发

生了故障。

如下所示:

23、为什么分布式系统中无法同时保证一致性和可用性?

首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。

如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期

间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。

如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。

24、什么是BASE理论?

CAP是分布式系统设计理论,BASE是CAP理论中AP方案的延伸,核心思想是即使无法做到强一致性(StrongConsistency,CAP的一致性就是强一致

性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。它的思想包含三方面:

1、Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。

2、Soft state(软状态):即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据

副本之间进行数据同步的过程存在延时。

3、Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统

保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

25、分布式事务的常见的解决方案有哪些?(高频)

方案一:2PC

两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议 。

中心化是指协议中有两类节点:一个是中心化协调者节点 (coordinator)和 N个参与者节点 (partcipant)。

两个阶段 :

1、第一阶段:投票阶段

2、第二阶段:提交/执行阶段。

举例订单服务A,需要调用支付服务B 去支付,支付成功则处理订单状态为待发货状态,否则就需要将购物订单处理为失败状态。 那么看2PC阶段是如何处

理的。

阶段一:

阶段一执行流程:

1、事务询问协调者向所有的参与者发送事务预处理请求,称之为Prepare,并开始等待各参与者的响应。

2、执行本地事务各个参与者节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向协调者报告说:“我这边可以处理了/我

这边不能处理”。

3、各参与者向协调者反馈事务询问的响应如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行,如果没有参与者成功执行事务,

那么就反馈给协调者 No 响应,表示事务不可以执行。

阶段二:

阶段二执行流程:

1、所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交协调者向所有参与者节点发出Commit请求

2、事务提交参与者收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。

方案二:3PC

三阶段提交又称3PC,其在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会

自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。

阶段一:

阶段一执行流程:

1、事务询问协调者向所有的参与者发送事务can commit请求,类似于2PC中的第二个阶段中的Prepare阶段,是一种事务询问操作,事务的协调者向所有

参与者询问“你们是否可以完成本次事务?”,并开始等待各参与者的响应。

2、如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。

阶段二:

阶段二的执行流程:

1、在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送

PreCommit请求。

2、参与者收到后开始执行事务操作,参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并

等待协调者的下一步指令。

3、如果阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程中超时。整个分布式事务就会中断,协调者就会向

所有的参与者发送“abort”请求。

阶段三:

阶段三执行流程:

1、在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”-》“提交状态”。然后向所有的参与者节点发

送"doCommit"请求。

2、参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。

3、相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

方案三:TCC

TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)"。

它分为三个操作:

1、Try阶段:主要是对业务系统做检测及资源预留。

2、Confirm阶段:确认执行业务操作。

3、Cancel阶段:取消执行业务操作。

如下所示:

TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这

种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 不足之处则在于对应用的侵入性

非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因

实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。

方案四:MQ分布式事务

上面的三种分布式事务的解决方案适用于对数据一致性要求很高的场景。如果数据强一致性要求没那么高,可以采用消息中间件(MQ)实现事务最终一

致。 在支付系统中,常常使用的分布式事务解决方案就是基于MQ实现的,它对数据强一致性要求没那么高,但要求数据最终一致即可。

例如:向借呗申请借钱,借呗审核通过后支付宝的余额才会增加,但借呗和支付宝有可能不是同一个系统,这时候如何实现事务呢?实现方案如下图:

执行流程如下所示:

1、找花呗借钱

2、花呗借钱审核通过,同步生成借款单

3、借款单生成后,向MQ发送消息,通知支付宝转账

4、支付宝读取MQ消息,并增加账户余额

上图最复杂的其实是如何保障2、3在同一个事务中执行(本地事务和MQ消息发送在同一个事务执行),借款结束后,借呗数据处理就完成了,接下来支付

宝才能读到消息,然后执行余额增加,这才完成整个操作。如果中途操作发生异常,例如支付宝余额增加发生问题怎么办?此时需要人工解决,没有特别好

的办法,但这种事故概率极低。

26、Seata的架构是什么?(高频)

Seata事务管理中有三个重要的角色:

1、TC (Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。

2、TM (Transaction Manager) -事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。

3、RM (Resource Manager) -资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

如下所示:

27、XA模式的工作流程是什么?(高频)

xa模式整个工作流程图如下所示:

分为两个阶段:

1、RM一阶段的工作:① 注册分支事务到TC ② 执行分支业务sql但不提交 ③ 报告执行状态到TC

2、TC二阶段的工作:TC检测各分支事务执行状态 ①如果都成功,通知所有RM提交事务 ②如果有失败,通知所有RM回滚事务

3、RM二阶段的工作:接收TC指令,提交或回滚事务

xa模式牺牲了可用性,保证了强一致性

28、AT模型的工作原理是什么?(高频)

at模式的整个工作流程图如下所示:

1、阶段一RM的工作:① 注册分支事务 ② 记录undo-log(数据快照)③ 执行业务sql并提交 ④报告事务状态

2、阶段二提交时RM的工作:删除undo-log即可

3、阶段二回滚时RM的工作:根据undo-log恢复数据到更新前

at模式牺牲了一致性,保证了可用性

29、TCC模型的工作原理是什么?(高频)

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

1、Try:资源的检测和预留;

2、Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

3、Cancel:预留资源释放,可以理解为try的反向操作。

Seata中的tcc模型的执行流程如下所示:

1、阶段一RM的工作:① 注册分支事务 ② 执行try操作预留资源 ④报告事务状态

2、阶段二提交时RM的工作:根据各分支事务的状态执行confirm或者cancel

1、Spring Boot提供了哪些核心功能?(高频)

1、jar包方式运行

通过引入spring-boot-maven-plugin插件可以将springboot项目打包成一个可以直接运行的jar包,运行方式和常规jar包一样java -jar

xxx.jar,启动后可以直接运行内嵌的web容器,根据具体引入的依赖来确定到底该启动哪种web容器。

2、使用了starter依赖

使用starter来封装依赖,简化项目引入相关依赖的复杂度

3、自动配置

springboot将spring4中的 @Condition注解发扬光大,根据特定的条件来创建相关的bean(如classpath下存在某个或者是某些类时自动创建某些

spring bean),自动完成相关框架的自动配置。

2、你如何理解Spring Boot中的starter?

starter可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring及其他技术,而不需要到处找示例代码和依赖包。

如你想使用spring data redis访问Redis,只要加入spring-boot-starter-data-redis 启动器依赖就能使用了。starter包含了许多项目中需要用到的依

赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。

3、Spring Boot常用的starter有哪些?(高频)

1、spring-boot-starter-web (嵌入tomcat和web开发需要servlet与jsp支持)

2、spring-boot-starter-data-elasticsearch (es支持)

3、spring-boot-starter-data-redis (redis支持)

4、spring-boot-starter-amqp(消息队列支持)

5、spring-boot-starter-data-mongodb (mongodb的支持)

6、mybatis-plus-boot-starter(mybatis plus的支持)

7、mybatis-spring-boot-starter(mybatis的支持)

4、Spring Boot的配置文件有哪几种格式?(高频)

Spring Boot支持两种格式的配置文件:

1、application.properties

2、application.yml

在实际的项目开发一般的都是使用配置中心管理项目中的配置信息,常见的配置中心:Spring Cloud Config、Nacos

5、如何自定义Spring Boot应用程序的端口号?

方式一:在application.properties或者application.yml文件中添加server.port配置项指定端口

方式二:在启动spring boot项目的时候通过-Dserver.port参数指定项目的端口号

方式三:通过WebServerFactoryCustomizer设置端口号,如下所示

6、Spring Boot如何定义多套不同环境配置?

提供多套配置文件,如:

applcation.properties
application-dev.properties
application-test.properties
application-prod.properties

然后在applcation.properties文件中指定当前的环境spring.profiles.active=test,这时候读取的就是application-test.properties文件。

7、Spring Boot有哪几种读取配置的方式?(高频)

Spring Boot 可以通过

1、@Value

2、Environment接口

3、@ConfigurationProperties

来绑定变量

8、如何重新加载Spring Boot上的更改,而无需重新启动服务器?(高频)

使用Spring Boot所提供的devtools工具就可以实现无需重新启动服务器而加载最新的代码。对应的工具的坐标如下所示:

如果使用的是idea开发工具,还需要做如下配置:

由于idea不会自动编译项目,所以源码修改不能被spring-boot-devtools检测到,勾选上面两个选项后idea将在窗口失去焦

点时自动编译并替换源码文件。

9、Spring Boot的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是Spring Boot的核心注解,主要组合包含了以下3个注解:

1、@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

2、@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。

如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

3、@ComponentScan:Spring组件扫描。

10、运行Spring Boot有哪几种方式?

方式一:直接通过java -jar xxx.jar的方式运行

方式二:将xxx.jar制作成Docker镜像,然后借助于Docker容器进行运行,并且可以使用docker-compose对多个容器进行统一编排

11、Spring Boot打成的jar和普通的jar有什么区别?

区别:

1、Spring Boot 项目最终打包成的jar是可执行jar,这种jar可以直接通过 java -jar xxx.jar 命令来运行,这种jar不可以作为普通的 jar 被其他项目依

赖,即使依赖了也无法使用其中的类。

2、Spring Boot 的jar无法被其他项目依赖,主要还是他和普通jar的结构不同。普通的jar包,解压后直接就是包名,包里就是我们的代码,而Spring

Boot打包成的可执行jar解压后,在 \BOOT-INF\classes目录下才是我们的代码,因此无法被直接引用。

12、Spring Boot中如何实现定时任务?

在Spring Boot中可以使用两种定时任务框架:

1、Spring Task

2、Quartz

一般在项目中使用Spring Task就可以了,因为Spring Task是Spring框架提供的可以和Spring Boot进行无缝集成。具体的使用方式如下所示:

1、在启动类上使用@EnableScheduling注解开启定时任务支持

2、在指定的方法上使用@Scheduled注解来指定定时任务的执行规则, 如下所示:

13、怎么禁用某些自动配置特性?

如果我们想禁用某些自动配置特性,可以使用 @SpringBootApplication注解的exclude属性来指明。例如,下面的代码段是使

DataSourceAutoConfiguration无效:

14、Spring Boot项目的自动化配置原理是什么?(高频)

在Spring Boot项目中有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装:@SpringBootConfiguration、

@EnableAutoConfiguration、@ComponentScan其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解的源码如下所示:

该注解通过@Import注解导入AutoConfigurationImportSelector,这个类实现了一个导入器接口ImportSelector。在该接口中存在一个方法

selectImports,如下所示:

该方法的返回值是一个数组,数组中存储的就是要被导入到spring容器中的类的全类名。在AutoConfigurationImportSelector类中重写了这个方法,

该方法内部就是读取了项目的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。

如下所示:

在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。

总结:在项目启动的时候,spring boot框架会自动读取META-INF/spring.factories配置文件中

org.springframework.boot.autoconfigure.EnableAutoConfiguration所配置的配置类,然后将其中所定义的bean根据条件注解所指定的条件来决定是

否需要将其导入到Spring容器中。

1、Spring MVC中的拦截器和Servlet中的filter有什么区别?

过滤器:依赖于servlet容器,在实现上基于函数回调,可以对几乎所有请求进行过滤

拦截器:依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的动态代理,属于面向切面编程(AOP)的一种运用。只

能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

2、Spring MVC常用的注解有哪些?(高频)

1、@RequestMapping:用于映射请求路径,可以定义在类上和方法上。用于类上,则表示类中的所有的方法都是以该地址作为父路径。

2、@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。

3、@RequestParam:指定请求参数的名称

4、@PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数

5、@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。

6、@RequestHeader:获取指定的请求头数据

3、Sping MVC中的控制器的注解一般用哪个?有没有别的注解可以替代?(高频)

一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller。

4、如果想限定发送的请求方式应该如何进行实现?

两种方式:

1、使用@RequestMapping注解的method属性,如下所示:

2、使用GetMapping

5、Spring MVC的Controller线程安全?如何解决?

Controller是默认单例模式,高并发下全局变量会出现线程安全问题!

解决方案:

1、将全局变量都变成局部变量,通过方法参数来传递。

2、将控制器的作用域从单例改为原型,如下所示:

8、Spring MVC怎么样设定重定向和转发的?(高频)

1、在返回值前面加"forward:"就可以让结果转发,譬如"forward:user.do?name=method4"

2、在返回值前面加"redirect:"就可以让返回值重定向,譬如"redirect:http://www.baidu.com"

9、Spring MVC怎么和AJAX相互调用的?

通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 :

1、导入json数据转换的核心jar包

2、在适配器中配置json数据转换的解析器(因为使用的注解驱动开发,所以不需要再进行配置)

3、Controller方法直接返回对象或者List数据,在Controller方法上使用@ResponseBody注解

10、如何解决POST请求中文乱码问题,GET的又如何处理呢?

1、解决post请求乱码问题:

2、GET请求中文参数出现乱码解决方法有两个

①修改tomcat配置文件添加编码与工程编码一致,如下:

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

②另外一种方法对参数进行重新编码:HandlerInterctpter接口

String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8")

ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。

11、使用Spring MVC如何完成文件上传?

1、文件上传对前端要求

  • 请求方式必须是POST请求

  • 表单的enctype必须是multipart/form-data

  • 表单中至少要有一个文件上传表单项<input type="file"/>

2、Spring MVC如何接收上传的文件

  • 导入commons-fileupload(spring MVC底层依赖的就是Apache的FileUpload)

  • 配置文件上传解析器

  • 在Controller方法的形参中定义MutipartFile,接收上传的文件,形要求参的名称需要和文件上传表单项名称一致

文件上传解析器的配置如下所示:

12、Spring MVC如何获得request, response, session?(高频)

1、方式一:在Controller方法的形参中可以直接定义HttpServletRequest HttpServletResponse,HttpSession

2、方式二:通过@Autowired注入HttpServletRequest,HttpServletResponse ,HttpSession

13、Spring MVC怎么处理异常?(高频)

可以直接使用Spring MVC中的全局异常处理器对异常进行统一处理,此时Contoller方法只需要编写业务逻辑代码,不用考虑异常处理代码。

开发一个全局异常处理器需要使用到两个注解:@Controlleradvice 、@ ExceptionHandler

如下所示:

14、Spring MVC执行流程是什么?(高频)

具体流程如下所示:

1、用户发送出请求到前端控制器DispatcherServlet。

2、DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。

3、HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

4、DispatcherServlet调用HandlerAdapter(处理器适配器)。

5、HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。

6、Controller执行完成返回ModelAndView对象。

7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。

9、ViewReslover解析后返回具体View(视图)。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet响应用户。

15、Spring MVC的主要组件都包含了哪些?(高频)

如下所示:

1、前端控制器 DispatcherServlet(不需要程序员开发):作用:接收请求、响应结果 相当于转发器,有了DispatcherServlet 就减少了其它组件之

间的耦合度。

2、理器映射器HandlerMapping(不需要程序员开发)作用:根据请求的URL来查找Handler

3、处理器适配器HandlerAdapter(不需要程序员开发)作用:执行处理器

4、处理器Handler(需要程序员开发)

5、视图解析器 ViewResolver(不需要程序员开发)作用:进行视图的解析 根据视图逻辑名解析成真正的视图(view)

6、视图View(需要程序员开发jsp)View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等)

1基础知识

1、说说你对Spring的理解?

1、Spring是一个开源框架,主要是为简化企业级应用开发而生。可以实现EJB可以实现的功能,Spring是一个IOC和AOP容器框架。

① 控制反转(IOC):Spring容器使用了工厂模式为我们创建了所需要的对象,我们使用时不需要自己去创建,直接调用Spring为我们提供的对象即可,

这就是控制反转的思想。

② 依赖注入(DI):Spring使用Java Bean对象的Set方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程就是依赖

注入的基本思想。

③ 面向切面编程(AOP):在面向对象编程(OOP)思想中,我们将事物纵向抽象成一个个的对象。而在面向切面编程中,我们将一个个对象某些类似的方

面横向抽象成一个切面,对这个切面进行一些如权限验证,事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。

2、在Spring中,所有管理的都是JavaBean对象,而BeanFactory和ApplicationContext就是Spring框架的那个IOC容器,现在一般使用

ApplicationContext,其不但包括了BeanFactory的作用,同时还进行了更多的扩展。

2、Spring由哪些模块组成?

截止到目前Spring框架已集成了20多个模块。这些模块主要被分如下图所示的核心容器、数据访问/集成 、Web、AOP (面向切面编程、 工具、消息和

测试模块 。

Spring常见的模块说明:

1、Spring Core(核心容器): 核心容器提供Spring框架的基本功能。Spring以bean的方式组织和管理Java应用中的各个组件及其关系。Spring使用

BeanFactory来产生和管理Bean,它是工厂模式的实现。BeanFactory使用控制反转(IoC)模式将应用的配置和依赖性规范与实际的应用程序代码分开

2、Spring Context(应用上下文): 应用上下文: 是一个配置文件,向Spring框架提供上下文信息。Spring上下文包括企业服务,如JNDI、EJB、电子邮

件、国际化、校验和调度功能

3、SpringAOP(面向切面编程):是面向对象编程的有效补充和完善,Spring的AOP是基于动态代理实现的

4、SpringDao(JDBC和Dao模块): JDBC、DAO的抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理,和不同数据库供应商所抛出的错

误信息。异常层次结构简化了错误处理,并且极大的降低了需要编写的代码数量,比如打开和关闭链接

5、Spring ORM(对象实体映射):Spring框架插入了若干个ORM框架,从而提供了ORM对象的关系工具,其中包括了Hibernate、JDO和 IBatis SQL

Map等,所有这些都遵从Spring的通用事物和DAO异常层次结构。

6、Spring Web(Web模块):Web上下文模块建立在应用程序上下文模块之上,为基于web的应用程序提供了上下文。所以Spring框架支持与Struts集

成,web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作

7、Spring Web MVC(MVC模块):MVC框架是一个全功能的构建Web应用程序的MVC实现。通过策略接口,MVC框架变成为高度可配置的。MVC容纳

了大量视图技术,其中包括JSP、POI等,模型由JavaBean构成,存放于m当中,而视图是一个接口,负责实现模型,控制器表示逻辑代码,由c的事情。

Spring框架的功能可以用在任何J2EE服务器当中,大多数功能也适用于不受管理的环境。Spring的核心要点就是支持不绑定到特定J2EE服务的可重用业

务和数据的访问的对象,毫无疑问这样的对象可以在不同的J2EE环境,独立应用程序和测试环境之间重用。

3、在Spring中有几种配置Bean的方式?(高频)

配置方式:

1、基于XML的配置

2、基于注解的配置

3、基于Java的配置

4、BeanFactory和ApplicationContext有什么区别?(高频)

1、BeanFactory:BeanFactory在启动的时候不会去实例化Bean,当从容器中拿Bean的时候才会去实例化;

2、ApplicationContext:ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例

化;

5、Spring框架中的单例bean是线程安全的吗?(高频)

肯定不是线程安全的,当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),

此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。Spring框架并没有对单例bean进行

任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Service类

和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安

全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。

6、Spring Bean有哪些作用域,它们之间有什么区别?(高频)

1、singleton :这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个bean的实例,单例的模式由bean factory自身来维

护 。

2、prototype :原形范围与单例范围相反,为每一个bean请求提供一个实例 。

3、request :在请求bean范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后, bean会失效并被垃圾回收器回收 。

4、session:与请求范围类似,确保每个session中有一个 bean 的实例,在session过期后, bean会随之失效 。

7、你用过哪些重要的Spring注解?(高频)

1、@Controller - 用于 Spring MVC 项目中的处理器类。

2、@Service - 用于服务类。

3、@RequestMapping - 用于在控制器处理程序方法中配置 URI 映射。

4、@ResponseBody - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。

5、@PathVariable - 用于将动态值从 URI 映射到处理程序方法参数。

6、@Autowired - 用于在 spring bean 中自动装配依赖项。通过类型来实现自动注入bean。和@Qualifier注解配合使用可以实现根据name注入bean。

7、@Qualifier - 和@Autowired一块使用,在同一类型的bean有多个的情况下可以实现根据name注入的需求。

8、@Scope - 用于配置 spring bean 的范围。

9、@Configuration,@ComponentScan 和 @Bean - 用于基于 java 的配置。

10、@Aspect,@Before,@After,@Around,@Pointcut - 用于切面编程(AOP)

8、请解释一下spring框架有哪些自动装配模式,它们之间有何区别?(高频)

spring的自动装配功能的定义:无须在Spring配置文件中描述javaBean之间的依赖关系(如配置<property>、<constructor-arg>)。

自动装配模式:

1、no:这是 Spring 框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在 bean 定义中用标签明确的设置依赖关系 。

2、byName:该选项可以根据bean名称设置依赖关系 。 当向一个bean中自动装配一个属性时,容器将根据bean的名称自动在在配置文件中查询一个匹

配的bean。 如果找到的话,就装配这个属性,如果没找到的话就报错 。

3、byType:该选项可以根据 bean 类型设置依赖关系 。 当向一个 bean 中自动装配一个属性时,容器将根据 bean 的类型自动在在配置文件中查询一个

匹配的 bean。 如果找到的话,就装配这个属性,如果没找到的话就报错 。

4、constructor :构造器的自动装配和byType模式类似,但是仅仅适用于与有构造器相同参数的bean,如果在容器中没有找到与构造器参数类型一致的

bean ,那么将会抛出异常 。

5、default:该模式自动探测使用构造器自动装配或者byType自动装配 。 首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如

果在bean内部没有找到相应的 构造器或者是无参构造器,容器就会自动选择 byTpe 的自动装配方式 。

比如如下注入:

可以改造为:

2 aop原理

9、spring中aop的底层是怎么实现的?(高频)

Spring中AOP底层的实现是基于动态代理进行实现的。

常见的动态代理技术有两种:JDK的动态代理和CGLIB。

两者的区别如下所示:

1、JDK动态代理只能对实现了接口的类生成代理,而不能针对类

2、Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法进行增强,但是因为采用的是继承,所以该类或方法最好不要声明为

final,对于final类或方法,是无法继承的。

Spring如何选择是用JDK还是cglib?

1、当bean实现接口时,会用JDK代理模式

2、当bean没有实现接口,会用cglib实现

3、可以强制使用cglib

10、spring aop机制都有哪些应用场景?(高频)

应用场景:

1、统一日志处理

2、统一幂等性的处理

3、spring中内置的事务处理

3 事务管理

11、spring事务的实现方式以及原理?(高频)

Spring支持编程式事务管理声明式事务管理两种方式!

编程式事务控制:需要使用TransactionTemplate来进行实现,这种方式实现对业务代码有侵入性,因此在项目中很少被使用到。

声明式事务管理:声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在

目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,

便可以将事务规则应用到业务逻辑中。

12、什么是事务的传播行为?在Spring框架中都有哪些事务的传播行为?

Spring的事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法对事务的态度。举例:methodA事务方法调用methodB事务方

法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

在Spring中提供了7种事务的传播行为:

1、REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

2、REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

3、SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

4、NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

5、MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

6、NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

7、NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

13、spring如何管理事务的?(高频)

Spring事务管理主要包括3个接口,Spring事务主要由以下三个共同完成的:

1、PlatformTransactionManager:事务管理器,主要用于平台相关事务的管理。

主要包括三个方法:

① commit:事务提交。

② rollback:事务回滚。

③ getTransaction:获取事务状态。

2、TransacitonDefinition:事务定义信息,用来定义事务相关属性,给事务管理器PlatformTransactionManager使用

主要包含的方法:

① getIsolationLevel:获取隔离级别。

② getPropagationBehavior:获取传播行为。

③ getTimeout获取超时时间。

④ isReadOnly:是否只读(保存、更新、删除时属性变为false--可读写,查询时为true--只读)

3、TransationStatus:事务具体运行状态,事务管理过程中,每个时间点事务的状态信息。

主要包含的方法:

① hasSavepoint():返回这个事务内部是否包含一个保存点。

② isCompleted():返回该事务是否已完成,也就是说,是否已经提交或回滚。

③ isNewTransaction():判断当前事务是否是一个新事务。

14、spring事务什么情况下会失效?(高频)

事务失效的常见场景:

1、数据库引擎不支持事务:这里以 MySQL为例,其MyISAM引擎是不支持事务操作的,InnoDB才是支持事务的引擎,一般要支持事务都会使用

InnoDB。

2、bean没有被Spring 管理

// @Service
public class OrderServiceImpl implements OrderService {
​
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
​
}

如果此时把 @Service注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被Spring 管理了,事务自然就失效了。

3、方法不是public的:@Transactional只能用于public的方法上,否则事务不会失效。

4、自身调用问题

public void save() {
    this.show();
}
​
@Transactional
public void show() {
​
    Account account = new Account() ;
    account.setName("itcast");
    account.setMoney(300d);
    accountDao.save(account);
​
    // 抛出异常
    int i = 1/0;            // 事务是否回滚?
}

5、数据源没有配置事务管理器

6、异常在方法内部通过try...catch处理掉了

public void save() {
    this.show();
}
​
@Transactional
public void show() {
​
    Account account = new Account() ;
    account.setName("itcast");
    account.setMoney(300d);
    accountDao.save(account);
​
    try {
​
        // 抛出异常
        int i = 1/0;            // 事务是否回滚?
​
    }catch (Exception e) {
        e.printStackTrace();
    }
​
}

7、异常类型错误:事务默认回滚的是:RuntimeException

public void save() throws Exception {
    this.show();
}
​
@Transactional
public void show() throws Exception{
​
    Account account = new Account() ;
    account.setName("itcast");
    account.setMoney(300d);
    accountDao.save(account);
​
    try {
​
        // 抛出异常
        int i = 1/0;            // 事务是否回滚?
​
    }catch (Exception e) {
        throw new Exception("保存订单数据失败") ;
    }
​
}

这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:

@Transactional(rollbackFor = Exception.class)

4 循环依赖

15、请解释一下spring bean的生命周期?

Spring Bean的生命周期如下图所示:

1、实例化Bean:

反射 BeanDefinition

  • 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用

    createBean进行实例化。

  • 对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。(BeanDefinition是Spring 中极其

    重要的一个概念,它存储了bean对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等。BeanDefinition对象的创建时通过各种

    bean的配置信息进行构建 )

2、设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着Spring根据BeanDefinition中的信息以及通过BeanWrapper提供

的设置属性的接口完成依赖注入。

3、处理Aware接口:接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:

  • 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就

    是Spring配置文件中Bean的id值。

  • 如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。

  • 如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文。

4、Bean的后置处理器(BeanPostProcessor):如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用

postProcessBeforeInitialization方法。由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

5、InitializingBean 与 init-method:如果Bean在Spring配置文件中配置了init-method 属性,则会自动调用其配置的初始化方法。

6、Bean的后置处理器(BeanPostProcessor):如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj,

String s)方法;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

7、DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

8、destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

示例代码:

User类:

@Component
@Slf4j
public class User implements BeanNameAware , BeanFactoryAware , ApplicationContextAware {
​
    public User() {
        System.out.println("a的构造方法执行了.........");
    }
​
    private String name ;
​
    @Value("张三")
    public void setName(String name) {
        System.out.println("setName方法执行了.........");
    }
​
    @Override
    public void setBeanName(String name) {
        System.out.println("setBeanName方法执行了.........");
    }
​
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("setBeanFactory方法执行了.........");
    }
​
    @PostConstruct
    public void init() {
        System.out.println("init方法执行了.................");
    }
​
    @PreDestroy
    public void destory() {
        System.out.println("destory方法执行了...............");
    }
​
​
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("setApplicationContext方法执行了........");
    }
​
}

Bean的后置处理器(BeanPostProcessor):

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
​
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization方法执行了.........");
        return bean;
    }
​
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInitialization方法执行了.........");
        return bean;
    }
​
}

测试类:

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = applicationContext.getBean(User.class);
System.out.println(user);
applicationContext.close();

控制台输出结果:

Generic bean: class [com.itheima.user.domain.User]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [E:\idea-edu-workplace\spring\target\classes\com\itheima\user\domain\User.class]
a的构造方法执行了.........
setName方法执行了.........
setBeanName方法执行了.........
setBeanFactory方法执行了.........
setApplicationContext方法执行了........
postProcessBeforeInitialization方法执行了.........
init方法执行了.................
postProcessAfterInitialization方法执行了.........
com.itheima.user.domain.User@4386f16
destory方法执行了...............

InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。

16、spring中的aop是在bean生命周期的哪一步实现的?

Spring的AOP是通过AspectJAwareAdvisorAutoProxyCreator来实现的, 该类的类图如下:

17、什么是Spring的循环依赖?(高频)

简单的来说就是A依赖B的同时,B依赖A。在创建A对象的同时需要使用的B对象,在创建B对象的同时需要使用到A对象。如下代码所示:

@Component
public class A {

    public A(){
        System.out.println("A的构造方法执行了...");
    }

    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
        System.out.println("给A注入B");
    }
}
@Component
public class B {

    public B(){
        System.out.println("B的构造方法执行了...");
    }

    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
        System.out.println("给B注入了A");
    }

}

18、出现循环依赖以后会有什么问题?(高频)

对象的创建过程会产生死循环,如下所示:

在spring中通过某些机制(三级缓存)帮开发者解决了部分循环依赖的问题。

19、spring如何解决循环依赖的?(高频)

生命周期回顾:

1、Spring扫描class得到BeanDefinition

2、根据得到的BeanDefinition,根据class推断构造方法, 通过反射得到一个对象(原始对象)

3、为原始对象填充属性(依赖注入)

4、如果原始对象中的某一个方法配置的有AOP,则需要针对于该原始对象生成一个代理对象

5、把最终的生成的代理对象放入单例池(singletonObjects)中, 下次getBean时直接从单例池拿即可

当然bean的整个生命周期很复杂, 还有很多的步骤, 这里就不一一列举了。

Spring解决循环依赖是通过三级缓存,对应的三级缓存如下所示:

缓存源码名称作用
一级缓存singletonObjects单例池; 缓存已经经历了完整声明周期, 已经初始化完成的bean对象
二级缓存earlySingletonObjects缓存早期的bean对象(生命周期还没有走完)
三级缓存singletonFactories缓存的是ObjectFactory, 表示对象工厂, 用来创建某个对象的

二级缓存的作用:如果要想打破上述的循环 , 就需要一个中间人的参与, 这个中间人就是缓存。

步骤如下所示:

1、实例化A得到A的原始对象

2、将A的原始对象存储到二级缓存(earlySingletonObjects)中

3、需要注入B,B对象在一级缓存中不存在,此时实例化B,得到原始对象B

4、将B的原始对象存储到二级缓存中

5、需要注入A,从二级缓存中获取A的原始对象

6、B对象创建成功

7、将B对象加入到一级缓存中

8、将B注入给A,A创建成功

9、将A对象添加到一级缓存中

三级缓存的作用:

从上面这个分析过程中可以得出,只需要一个缓存就能解决循环依赖了,那么为什么Spring中还需要singletonFactories ?

基于上面的场景想一个问题:如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的

Bean对象其实应该是AOP之后的代理对象,而B的a属性对应的并不是AOP之后的代理对象,这就产生了冲突。 也就是说, 最终单例池中存放的A对象

(代理对象)和B依赖的A对象不是同一个。

所以在该场景下, 上述提到的二级缓存就解决不了了。那这个时候Spring就利用了第三级缓存singletonFactories来解决这个问题。

singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象之后,就会构造一个ObjectFactory存入

singletonFactories中,后期其他的Bean可以通过调用该ObjectFactory对象的getObject方法获取对应的Bean。

整体的解决循环依赖问题的思路如下所示:

步骤如下所示:

1、实例化A,得到原始对象A,并且同时生成一个原始对象A对应的ObjectFactory对象

2、将ObjectFactory对象存储到三级缓存中

3、需要注入B,发现B对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在B对象所对应的ObjectFactory对象

4、实例化B,得到原始对象B,并且同时生成一个原始对象B对应的ObjectFactory对象,然后将该ObjectFactory对象也存储到三级缓存中

5、需要注入A,发现A对象在一级缓存和二级缓存都不存在,但是三级缓存中存在A对象所对应的ObjectFactory对象

6、通过A对象所对应的ObjectFactory对象创建A对象的代理对象

7、将A对象的代理对象存储到二级缓存中

8、将A对象的代理对象注入给B,B对象执行后面的生命周期阶段,最终B对象创建成功

9、将B对象存储到一级缓存中

10、将B对象注入给A,A对象执行后面的生命周期阶段,最终A对象创建成功,将二级缓存的A的代理对象存储到一级缓存中

注意:

1、后面的生命周期阶段会按照本身的逻辑进行AOP, 在进行AOP之前会判断是否已经进行了AOP,如果已经进行了AOP就不会进行AOP操作了。

2、singletonFactories : 缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前

暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后

直接把本bean放入singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到一个AOP之后的代理对

象(如果没有AOP,则直接得到一个原始对象)。

20、只有一级缓存和三级缓存是否可行?(高频)

不行,每次从三级缓存中拿到ObjectFactory对象,执行getObject()方法又会产生新的代理对象,因为A是单例的,所有这里我们要借助二级缓存来解决这

个问题,将执行了objectFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍objectFactory.getObject()方法再产

生一个新的代理对象,保证始终只有一个代理对象。

总结:所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行

objectFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象。

21、构造方法出现了循环依赖怎么解决?(高频)

Spring中大部分的循环依赖已经帮助我们解决掉了,但是有一些循环依赖还需要我们程序员自己进行解决。如下所示:

@Component
public class A {

    // B成员变量
    private B b;

    public A(B b){
        System.out.println("A的构造方法执行了...");
        this.b = b ;
    }
}

@Component
public class B {

    // A成员变量
    private A a;

    public B(A a){
        System.out.println("B的构造方法执行了...");
        this.a = a ;
    }

}

main方法程序:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

    @Autowired
    private A a ;

    @Test
    public void testTransfer() throws Exception {
        System.out.println(a);
    }

}

控制台输出:

解决方案:使用@Lazy注解

@Component
public class A {

    // B成员变量
    private B b;

    public A(@Lazy B b){
        System.out.println("A的构造方法执行了...");
        this.b = b ;
    }
}

在构造参数前面加了@Lazy注解之后, 就不会真正的注入真实对象, 该注入对象会被延迟加载 , 此时注入的是一个代理对象 。

1、什么是MyBatis?

1、Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、

创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。

2、MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结

果集。

3、通过xml文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最

后由mybatis框架执行sql并将结果映射为java对象并返回。

2、MyBatis的优点和缺点?

优点

1、基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;

提供XML标签,支持编写动态SQL语句,并可重用。

2、与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;

3、很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)

4、能够与Spring很好的集成;

5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点

1、SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。

2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

3、当实体类中的属性名和表中的字段名不一样,怎么办?

第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。

第2种: 使用resultMap来定义字段和属性的映射关系

4、xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

if

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语

句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返

回标记为 featured 的 BLOG。

where、set

如下sql语句的定义:

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

这个查询也会失败。这个问题可以使用简单地用条件元素来解决,比如在where后面添加一个恒等的条件:1 = 1。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

where元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们

去除。

set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

这个例子中,set元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以

及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符!

5、如何获取自动生成的(主)键值?(高频)

使用insert标签中的useGeneratedKeys和keyProperty 属性。使用方式如下所示:

属性说明:

1、useGeneratedKeys:是够获取自动增长的主键值。true表示获取。

2、keyProperty :指定将获取到的主键值封装到哪儿个属性里

6、使用MyBatis的代理开发有哪些要求?

使用Mapper代理方式,必须满足以下要求:

1、定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下。

2、设置SQL映射文件的namespace属性为Mapper接口全限定名

3、在Mapper接口中定义方法,方法名就是SQL映射文件中sql语句的id属性值,并保持参数类型和返回值类型一致

7、#{}和${}的区别是什么?(高频)

1、#{}是预编译处理,${}是字符串替换。

2、Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;

3、Mybatis在处理${}时,就是把${}替换成变量的值。

4、使用#{}可以有效的防止SQL注入,提高系统安全性。

#{}的日志如下所示:

${}的日志如下所示:

8、Mybatis是如何进行分页的?分页插件的原理是什么?(高频)

分页方式:

1、在执行sql语句的时候直接拼接分页参数

2、使用Mybatis的分页插件(PageHelper)进行分页

分页插件的原理:分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据

dialect方言,添加对应的物理分页语句和物理分页参数。

public class PageInterceptor implements Interceptor

举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10

9、如何执行批量插入数据?(高频)

批量插入数据:

1、mybatis的接口方法参数需要定义为集合类型List<User>

2、在映射文件中通过forEach标签遍历集合,获取每一个元素作为insert语句的参数值

10、MyBatis实现一对一有几种方式?具体怎么操作的?

两种方式:

1、联合查询

2、嵌套查询

联合查询操作:联合查询是几个表联合查询,只查询一次, 通过在resultMap使用association标签配置查询到的关联数据的映射关系。

如下所示:在查询订单的同时需要将订单所对应的用户数据也查询出来。

① 更改订单实体类

② 映射文件中定义sql语句

③ resultMap定义

④ 执行结果

只发送了一条sql语句,并且进行了数据的封装。

嵌套查询操作:嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过

select属性配置。

如下所示:在查询订单的同时需要将订单所对应的用户数据也查询出来。

① 更改订单实体类

② 映射文件中定义sql语句

③ resultMap定义

④ 执行结果

发送了两条sql语句,并且进行了数据的封装。

11、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配

置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。

默认情况下延迟加载是关闭的。

实现原理:

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用order.getUser().getUserName(),拦截器invoke()方

法发现order.getUser()是null值,那么就会单独发送事先保存好的查询关联User对象的sql,把User查询上来,然后调用order.setUser(user),于是order

的对象user属性就有值了,接着完成order.getUser().getUserName()方法的调用。

12、什么是Mybatis的一级缓存和二级缓存?(高频)

1、一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache

就将清空,默认打开一级缓存。如下所示:

使用同一个sqlSession对象获取两次UserMapper对象,进行了两次用户数据的查询。控制台的输出结果如下所示:

只执行了一次sql语句。说明第二次查询的时候使用的是缓存数据。

2、二级缓存:二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。

如下代码:

当执行完sqlSession1.close()方法时一级缓存就已经被清空掉了。再次获取了一个新的sqlSession对象,那么此时就需要再次查询数据,因此控制台的输

出如下所

示:

可以看到进行了两次查询。

默认情况下二级缓存并没有开启,要想使用二级缓存,那么就需要开启二级缓存,如下所示:

① 全局配置文件

② 映射文件

运行程序进行测试,控制台输出结果如下所示:

只进行了一次查询,那么就说明数据已经进入到了二级缓存中。

3、对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select

中的缓存将被 clear。

注意事项:

1、二级缓存需要缓存的数据实现Serializable接口

2、只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中

3、可自定义存储源,如 Ehcache。

13、请说说MyBatis的工作原理?

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

2、加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在MyBatis 配置文件 mybatis-config.xml 中加载。

mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。

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

5、Executor执行器:MyBatis底层定义了一个Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时

负责查询缓存的维护。

6、MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的

SQL 语句的 id、参数等信息。

7、输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对

preparedStatement 对象设置参数的过程。

8、输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解

析过程。

14、Mybatis都有哪些Executor执行器?它们之间的区别是什么?

Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

1、SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象,也是Mybatis默认使用的执行器。

2、ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放

置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行

(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处

理相同。

1 存储引擎

1、简单描述一个Mysql的内部结构?

MySQL的基本架构示意图:

大体来说,MySQL可以分为server层存储引擎层两部分。

① server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能

② 存储引擎层:存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎

连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。

查询缓存:连接建立完成后,你就可以执行select语句了,此时会先进行查询缓存(缓存是key-value格式;key是sql语句,value是sql语句的查询结果)。

分析器

1、词法分析: MySQL需要识别出里面的字符串分别是什么,代表什么。

2、语法分析:根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。

优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器:调用存储引擎接口,执行sql语句,得到结果

2、数据库引擎有哪些?(高频)

MySQL提供了插件式的存储引擎架构。所以MySQL存在多种存储引擎,可以根据需要使用相应引擎,或者编写存储引擎。存

储引擎是基于表的,而不是基于库的。所以存储引擎也可被称为表类型。MySQL5.0支持的存储引擎包含 : InnoDB 、

MyISAM 、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED

等,其中InnoDBBDB提供事务安全表,其他存储引擎是非事务安全表。

3、InnoDB与MyISAM的区别?(高频)

1、InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和

commit之间,组成一个事务;

2、InnoDB支持外键,而MyISAM不支持。

3、InnoDB是支持表锁和行级锁,MyISAM只支持表锁

4、如何选择存储引擎?

如果没有特别的需求,使用默认的 Innodb 即可。

MyISAM:以读为主的应用程序,比如博客系统、新闻门户网站。

Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。

5、存储引擎常用命令?

show engines;  查看MySQL提供的所有存储引擎

创建新表时如果不指定存储引擎,那么系统就会使用默认的存储引擎,MySQL5.5之前的默认存储引擎是MyISAM,5.5之后就改为了InnoDB。

show variables like '%storage_engine%';  查看mysql默认的存储引擎

show table status like "table_name"\G   查看表的存储引擎

2 索引

6、什么是索引?(高频)

MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护者满足特定查找算法的

数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。如下面的示意图所示 :

左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查

找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找快速获取到相应

数据。一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。索引是数据库中用来提高性能的最常用的工

具。

导入资料中提供的sql脚本文件,已经准备了1000W条数据。

A. 根据ID查询

select * from tb_sku where id = 1999\G ;

查询速度很快, 接近0s , 主要的原因是因为id为主键, 有索引;

查看执行计划:

B. 根据 title 进行精确查询

select * from tb_sku where name = '华为Meta1999'\G ;

查询速度太慢了,几乎使用了9s才完成数据的查询。

查看执行计划:

7、如何创建索引?(高频)

为了提升上述查询效率,可以对name字段创建索引。创建索引有两种方式:

1、方式一:在创建表的时候创建索引

-- 语法结构
CREATE TABLE  表名( 属性名 数据类型[完整性约束条件], 
    属性名 数据类型[完整性约束条件], 
    ...... 
    属性名 数据类型  
    [ UNIQUE | FULLTEXT | SPATIAL ]  INDEX | KEY 
    [ 别名]  ( 属性名1  [(长度)]  [ ASC | DESC] ) 
);

示例:

-- 示例代码
CREATE TABLE `index1` ( 
  `id` int(11) DEFAULT NULL, 
  `name` varchar(20) DEFAULT NULL, 
  `sex` tinyint(1) DEFAULT NULL, 
  KEY `index1_id` (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8

2、方式二:使用create index语句进行索引创建

语法:

CREATE  [UNIQUE|FULLTEXT|SPATIAL]  INDEX index_name 
[USING  index_type]
ON tbl_name(index_col_name,...)   // 如果指定的列的名称是多个,那么这个索引我们将其称之为复合索引

示例:

create index idx_name on tb_sku(name) ;

再次进行查询:

通过explain , 查看执行计划,执行SQL时使用了刚才创建的索引

8、常见的索引约束有哪些?(高频)

1、UNIQUE:唯一索引

表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为UNIQUE。

2、FULLTEXT: 全文索引

表示全文搜索,在检索长文本的时候,效果最好,短文本建议使用普通索引,但是在检索的时候数据量比较大的时候,现将数据放入一个没有全局索引的

表中,然后在用Create Index创建的Full Text索引,要比先为一张表建立Full Text然后在写入数据要快的很多。FULLTEXT 用于搜索很长一篇文章的时

候,效果最好。用在比较短的文本,如果就一两行字的,普通的 INDEX 也可以。

3、SPATIAL: 空间索引

空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。

MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引

只能在存储引擎为MYISAM的表中创建。如果没有指定索引约束,此时创建的索引就是普通索引。而一般情况下只需要创建普通索引。

4、普通索引:如果没有指定索引约束,此时创建的索引就是普通索引。而一般情况下只需要创建普通索引。

9、常见的索引类型有哪些?(高频)

索引是在MySQL的存储引擎层中实现的,而不是在服务器层实现的。所以每种存储引擎的索引都不一定完全相同,也不是所有的存储引擎都支持所有的索

引类型的。

MySQL目前提供了以下4种索引:

  • BTREE 索引: 最常见的索引类型,大部分索引都支持 B 树索引。
  • HASH 索引:只有Memory引擎支持 , 使用场景简单 。
  • R-tree 索引(空间索引):空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少,不做特别介绍。
  • Full-text (全文索引) :全文索引也是MyISAM的一个特殊索引类型,主要用于全文索引,InnoDB从Mysql5.6版本开始支持全文索引。

各种存储引擎对索引的支持:

索引InnoDB引擎MyISAM引擎Memory引擎
BTREE索引支持支持支持
HASH 索引不支持不支持支持
R-tree 索引不支持支持不支持
Full-text5.6版本之后支持支持不支持

我们平常所说的索引,如果没有特别指明,都是指B+树(多路搜索树,并不一定是二叉的)结构组织的索引。

10、怎么看到为表格定义的所有索引?

语法:

show index  from  table_name;

示例:查看tb_sku表中的索引信息;

show index from tb_sku ;

注意:主键自动创建索引

11、唯一索引比普通索引快吗, 为什么?

唯一索引不一定比普通索引快, 还可能慢。

1、查询时, 在未使用 limit 1 的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配下一条数据, 发现不匹配后返回. 如此看来唯一索引少

了一次匹配, 但实际上这个消耗微乎其微。

2、更新时, 这个情况就比较复杂了. 普通索引将记录放到 change buffer 中语句就执行完毕了。而对唯一索引而言, 它必须要校验唯一性, 因此, 必须将数

据页读入内存确定没有冲突, 然后才能继续操作。

对于写多读少的情况 , 普通索引利用 change buffer 有效减少了对磁盘的访问次数, 因此普通索引性能要高于唯一索引.

12、索引的优缺点?

1、优点

  • 提高数据检索的效率,降低数据库的 IO 成本。

  • 通过索引列对数据进行排序,降低数据排序的成本,降低了 CPU 的消耗。

2、缺点

  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行 INSERT、UPDATE 和DELETE。因为更新表时,MySQL 不仅要保存数

    据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。

  • 实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的。

13、什么情况下设置了索引但无法使用?(高频)

环境准备

建表语句:

create table `tb_seller` (
	`sellerid` varchar (100),
	`name` varchar (100),
	`nickname` varchar (50),
	`password` varchar (60),
	`status` varchar (1),
	`address` varchar (100),
	`createtime` datetime,
    primary key(`sellerid`)
)engine=innodb default charset=utf8mb4; 

初始化数据sql:

insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('alibaba','阿里巴巴','阿里小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('baidu','百度科技有限公司','百度小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('huawei','华为科技有限公司','华为小店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itcast','传智播客教育科技有限公司','传智播客','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itheima','黑马程序员','黑马程序员','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('luoji','罗技科技有限公司','罗技小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('oppo','OPPO科技有限公司','OPPO官方旗舰店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('ourpalm','掌趣科技股份有限公司','掌趣小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('qiandu','千度科技','千度小店','e10adc3949ba59abbe56e057f20f883e','2','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('sina','新浪科技有限公司','新浪官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('yijia','宜家家居','宜家家居旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');

创建索引:

create index idx_seller_name_sta_addr on tb_seller(name,status,address);

全职匹配查询:对索引中所有列都指定具体值。该情况下,索引生效,执行效率高。

explain select * from tb_seller where name='小米科技' and status='1' and address='北京市';

违背了最左前缀法则

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。

匹配最左前缀法则,走索引:

违法最左前缀法则 , 索引失效:

如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效:

② 范围查询: 范围查询右边的列,不能使用索引

根据前面的两个字段name , status 查询是走索引的, 但是最后一个条件address 没有用到索

引。

③ 列运算:不要在索引列上进行运算操作, 索引将失效。

④ 字符串:字符串不加单引号,造成索引失效。

由于,在查询时没有对字符串加单引号,MySQL的查询优化器,会自动的进行类型转换,造成索引失效。

⑤ 模糊查询:以%开头的like模糊查询,索引失效。如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

解决方案 :

通过覆盖索引来解决

14、在建立索引的时候,都有哪些需要考虑的因素呢?

① 建立索引的时候一般要考虑到字段的使用频率,经常作为条件进行查询的字段比较适合。

② 如果需要建立联合索引的话,还需要考虑联合索引中的顺序。

③ 此外也要考虑其他方面,比如防止过多的索引对表造成太大的压力

15、创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因?

MySQL提供了explain命令来查看语句的执行计划,MySQL在执行某个语句之前,会将该语句过一遍查询优化器,之后会拿到对语句的分析,也就是执行计划,

其中包含了许多信息. 可以通过其中和索引有关的信息来分析是否命中了索引,例如possilbe_key,key,key_len等字段,分别说明了此语句可能会使用的索引,

实际使用的索引以及使用的索引长度.

3 SQL优化

16、关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?(高频)

在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要搞明白慢的原因是什么?

① 是查询条件没有命中索引?

② 是load了不需要的数据列?

③ 还是数据量太大?

所以优化也是针对这三个方向来的:

1、分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。

2、分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。

3、如果是表中的数据量是否太大导致查询慢,可以进行横向或者纵向的分表.

MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的

SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10S以上的语句。默认情况下,Mysql数据库并不启动慢查询日志,需要

我们手动来设置这个参数,当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持

将日志记录写入文件,也支持将日志记录写入数据库表。

慢查询的配置:

# 是否开启慢查询日志,1表示开启,0表示关闭
slow_query_log=1

# 旧版(5.6以下版本)MySQL数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省的文件host_name-slow.log
log_slow_queries=/var/lib/mysql/mysql_slow.log

# 新版(5.6及以上版本)MySQL数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省的文件host_name-slow.log
slow_query_log_file=/var/lib/mysql/mysql_slow.log

# 慢查询阈值,当查询时间大于设定的阈值时,记录日志。
long_query_time = 1

# 未使用索引的查询也被记录到慢查询日志中(可选项)。
log_queries_not_using_indexes=0

# 日志存储方式。log_output='FILE'表示将日志存入文件,默认值是'FILE'。log_output='TABLE'表示将日志存入数据库,这样日志信息就会被写入到mysql.slow_log表中。MySQL数据库支持同时两种日志存储方式,配置的时候以逗号隔开即可,如:log_output='FILE,TABLE'。日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件。
log_output='FILE,TABLE'

添加如上配置重启服务,产生慢查询日志:

慢查询日志文件内容:

执行如下sql语句模拟慢查询:

-- 不会记录到慢查询日志中
select sleep(0.2) ;

-- 会记录到慢查询日志中
select sleep(2) ;

17、如何优化SQL?(高频)

SQL语句中IN包含的值不应过多

MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。但是如果数值较多,产生的消耗也是比较大的。再

例如:

select id from table_name where num in(1,2,3) 对于连续的数值,能用between 就不要用in了。

SELECT语句务必指明字段名称

SELECT *增加很多不必要的消耗(cpu、io、内存、网络带宽);增加了使用覆盖索引的可能性;当表结构发生改变时,前断也需要更新。所以要求直接

在select后面接上字段名。

如果排序字段没有用到索引,就尽量少排序

如果限制条件中其他字段没有索引,尽量少用or

or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。很多时候使用 union all 或者是union(必要的

时候)的方式来代替“or”会得到更好的效果

or查询:

(1) or两边放联合索引,不触发索引(如果两边是单列索引另算)

(2) or两边是单列索引,查询走索引

(3) or两边只要有一个不是索引就不启用索引查询

单例索引演示:

复合索引演示:

(4) or两边一个是联合索引的最左索引一个是单例索引才生效,否则失效

示例:

-- 创建单列索引
 create index idx_nickname on tb_seller(nickname) ;

使用索引:

索引失效:

不建议使用%前缀模糊查询:例如LIKE “%name”或者LIKE “%name%”,这种查询会导致索引失效而进行全表扫描。但是可以使用LIKE“name%”。

18、超大分页怎么处理?(高频)

一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 1000000 , 10,此时需要MySQL排序前1000010 记

录,仅仅返回1000000 - 1000010 的记录,其他记录丢弃,查询排序的代价非常大 。

示例:

explain select * from tb_sku limit 1000000 , 10 ;

执行查询耗时:

优化思路一:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

示例:

explain select * from tb_sku s , (select id from tb_sku order by id limit 1000000 , 10 ) t where t.id = s.id ;

执行查询耗时:

优化思路二:该方案适用于主键自增的表,可以把limit 查询转换成某个位置的查询 。

示例:

 explain select * from tb_sku where id > 1000000 limit 10 ;

执行查询耗时:

19、MySQL数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?

1、设计良好的数据库结构, 允许部分数据冗余, 尽量避免join查询, 提高效率。

2、选择合适的表字段数据类型和存储引擎, 适当的添加索引。

3、MySQL 库主从读写分离。

4、找规律分表, 减少单表中的数据量 ,提高查询速度。

5、添加缓存机制, 比如 memcached, redis等。

6、不经常改动的页面, 生成静态页面。

7、书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE。

4 事务

20、什么是事务?(高频)

概述:由多个操作组成的一个逻辑单元,组成这个逻辑单元的多个操作要么都成功,要么都失败。

举例:转账

21、ACID是什么?可以详细说一下吗?(高频)

A=Atomicity原子性:就是上面说的,要么全部成功,要么全部失败,不可能只执行一部分操作。

C=Consistency一致性:系统(数据库)总是从一个一致性的状态转移到另一个一致性的状态,不会存在中间状态。

I=Isolation隔离性: 通常来说:一个事务在完全提交之前,对其他事务是不可见的.注意前面的通常来说加了红色,意味着有例外情况。

D=Durability持久性:一旦事务提交,那么就永远是这样子了,哪怕系统崩溃也不会影响到这个事务的结果。

22、并发事务带来哪些问题?(高频)

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致

以下的问题。

脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使

用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

丢失修改(Lost to modify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修

改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-

1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

不可重复读(Unrepeatableread):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的

两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称

为不可重复读。

幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的

查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

23、怎么解决这些问题呢?MySQL的默认隔离级别是?(高频)

解决方案:对事务进行隔离

MySQL的四种隔离级别如下:

未提交读(READ UNCOMMITTED):这个隔离级别下,其他事务可以看到本事务没有提交的部分修改。因此会造成脏读的问题(读取到了其他事务未提交的

部分,而之后该事务进行了回滚)。这个级别的性能没有足够大的优势,但是又有很多的问题,因此很少使用.

sql演示:

# 插入数据
insert into goods_innodb(name) values('华为');
insert into goods_innodb(name) values('小米');

# 会话一
set session transaction isolation level read uncommitted ;		# 设置事务的隔离级别为read uncommitted
start transaction ;												# 开启事务
select * from goods_innodb ;									# 查询数据

# 会话二
set session transaction isolation level read uncommitted ;		# 设置事务的隔离级别为read uncommitted
start transaction ;												# 开启事务
update goods_innodb set name = '中兴' where id = 10 ;			   # 修改数据

# 会话一
select * from goods_innodb ;									# 查询数据

已提交读(READ COMMITTED):其他事务只能读取到本事务已经提交的部分。这个隔离级别有不可重复读的问题,在同一个事务内的两次读取,拿到的结

果竟然不一样,因为另外一个事务对数据进行了修改。

sql演示:

# 会话一
set session transaction isolation level read committed ;		# 设置事务的隔离级别为read committed
start transaction ;												# 开启事务
select * from goods_innodb ;									# 查询数据

# 会话二
set session transaction isolation level read committed ;		# 设置事务的隔离级别为read committed
start transaction ;												# 开启事务
update goods_innodb set name = '中兴' where id = 1 ;			   # 修改数据

# 会话一
select * from goods_innodb ;									# 查询数据

# 会话二
commit;															# 提交事务

# 会话一
select * from goods_innodb ;									# 查询数据

REPEATABLE READ(可重复读):可重复读隔离级别解决了上面不可重复读的问题(看名字也知道),但是不能完全解决幻读。MySql默认的事务隔离级别

就是:

REPEATABLE READ

select @@tx_isolation;

sql演示(解决不可重复读):

# 会话一
start transaction ;												# 开启事务
select * from goods_innodb ;									# 查询数据

# 会话二
start transaction ;												# 开启事务
update goods_innodb set name = '荣耀' where id = 1 ;			   # 修改数据

# 会话一
select * from goods_innodb ;									# 查询数据

# 会话二
commit;															# 提交事务

# 会话一
select * from goods_innodb ;									# 查询数据

sql演示(测试不会出现幻读的情况):

# 会话一
start transaction ;												# 开启事务
select * from goods_innodb ;									# 查询数据

# 会话二
start transaction ;												# 开启事务
insert into goods_innodb(name) values('小米');			   	   # 插入数据
commit;															# 提交事务

# 会话一
select * from goods_innodb ;									# 查询数据

sql演示(测试出现幻读的情况):

# 表结构进行修改
ALTER TABLE goods_innodb ADD version int(10) NULL ;

# 会话一
start transaction ;												# 开启事务
select * from goods_innodb where version = 1;					# 查询一条不满足条件的数据

# 会话二
start transaction ;												# 开启事务
insert into goods_innodb(name, version) values('vivo', 1);	    # 插入一条满足条件的数据 
commit;															# 提交事务

# 会话一
update goods_innodb set name = '金立' where version = 1; 		   # 将version为1的数据更改为'金立'
select * from goods_innodb where version = 1;					# 查询一条不满足条件的数据

SERIALIZABLE(可串行化):这是最高的隔离级别,可以解决上面提到的所有问题,因为他强制将所以的操作串行执行,这会导致并发性能极速下降,因此也不

是很常用。

5 锁

24、MySQL中有哪几种锁?

从对数据操作的粒度分 :

1) 表锁:操作时,会锁定整个表。

2) 行锁:操作时,会锁定当前操作行。

3) 页面锁:会锁定一部分的数据

从对数据操作的类型分:

1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。

2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。

各存储引擎对锁的支持情况:

存储引擎表级锁行级锁页面锁
MyISAM支持不支持不支持
InnoDB支持支持不支持
MEMORY支持不支持不支持
BDB支持不支持支持

MySQL这2种锁的特性可大致归纳如下 :

锁类型特点
表级锁偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁开销和加锁时间界于表锁和行锁之间; 会出现死锁; 锁定粒度界于表锁和行锁之间, 并发度一般。

从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量

不同数据,同时又有并查询的应用,如一些在线事务处理(OLTP)系统。

MyISAM 在执行查询语句(SELECT)前,会自动给涉及的表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表

写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。

显示加表锁语法:

加读锁 : lock table table_name read;
加写锁 : lock table table_name write;
解锁:     unlock tables;

MyISAM锁:读锁案例

准备环境

CREATE TABLE `tb_book` (
  `id` INT(11) auto_increment,
  `name` VARCHAR(50) DEFAULT NULL,
  `publish_time` DATE DEFAULT NULL,
  `status` CHAR(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;

INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'java编程思想','2088-08-01','1');
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr编程思想','2088-08-08','0');

客户端 一 :

1)获得tb_book 表的读锁

lock table tb_book read;

2) 执行查询操作

select * from tb_book;

可以正常执行 , 查询出数据。

客户端 二 :

3) 执行查询操作

select * from tb_book;

客户端 一 :

4)查询未锁定的表

select name from tb_seller;

在一个session里面lock table锁表,你只能访问你锁的那张表;访问其他表,就报这个异常。

客户端 二 :

5)查询未锁定的表

select name from tb_seller;

可以正常查询出未锁定的表;

客户端 一 :

6) 执行插入操作

insert into tb_book values(null,'Mysql高级','2088-01-01','1');

执行插入, 直接报错 , 由于当前tb_book 获得的是 读锁, 不能执行更新操作。

客户端 二 :

7) 执行插入操作

insert into tb_book values(null,'Mysql高级','2088-01-01','1');

当在客户端一中释放锁指令 unlock tables 后 , 客户端二中的 inesrt 语句 , 立即执行 ;

写锁案例

客户端 一 :

1)获得tb_book 表的写锁

lock table tb_book write ;

2)执行查询操作

select * from tb_book ;

查询操作执行成功;

3)执行更新操作

客户端一 :

update tb_book set name = 'java编程思想(第二版)' where id = 1;

更新操作执行成功 ;

客户端 二 :

4)执行查询操作

select * from tb_book ;

当在客户端一中释放锁指令 unlock tables 后 , 客户端二中的 select 语句 , 立即执行 ;

结论:就是读锁会阻塞写,但是不会阻塞读。而写锁,则既会阻塞读,又会阻塞写。

InnoDB 的行锁模式

InnoDB 实现了以下两种类型的行锁。

  • 共享锁(S):又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

  • 排他锁(X):又称为写锁,简称X锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取

    排他锁的事务是可以对数据就行读取和修改。

对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集(行)加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;

可以通过以下语句显示给记录集加共享锁或排他锁 。

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X) :SELECT * FROM table_name WHERE ... FOR UPDATE

准备环境:

create table test_innodb_lock(
	id int(11),
	name varchar(16),
	sex varchar(1)
)engine = innodb default charset=utf8;

insert into test_innodb_lock values(1,'100','1');
insert into test_innodb_lock values(3,'3','1');
insert into test_innodb_lock values(4,'400','0');
insert into test_innodb_lock values(5,'500','1');
insert into test_innodb_lock values(6,'600','0');
insert into test_innodb_lock values(7,'700','0');
insert into test_innodb_lock values(8,'800','1');
insert into test_innodb_lock values(9,'900','1');
insert into test_innodb_lock values(1,'200','0');
Session-1Session-2

关闭自动提交功能

关闭自动提交功能

可以正常的查询出全部的数据

可以正常的查询出全部的数据

查询id 为3的数据 ;

获取id为3的数据 ;

更新id为3的数据,但是不提交;

更新id为3 的数据, 出于等待状态

通过commit, 提交事务

解除阻塞,更新正常进行
以上, 操作的都是同一行的数据,接下来,演示不同行的数据 :

更新id为3数据,正常的获取到行锁 , 执行更新 ;

由于与Session-1 操作不是同一行,获取当前行锁,执行更新;

如果按照索引列进行检索加的就是行级锁,如果没有按照索引进行检索加的就是表级锁。

1 内存结构

1、简述一下JVM的内存结构?(高频)

JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有

区和共享区。

|

| | ------------------------------------------------------------ |

线程私有区

① 程序计数器

  • 作用:是一块较小的内存空间,可以理解为是当前线程所执行程序的字节码文件的行号指示器,存储的是当前线程所执行的行号

  • 特点:线程私有 ,唯一一个不会出现内存溢出的内存空间

② 虚拟机栈

  • 作用:管理JAVA方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法中变量的变量表、操作数栈、动态链接方法、返回值、返回地址

    等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)

|

| | ------------------------------------------------------------ |

  • 特点:

    1、线程私有

    2、局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类

    型)

    3、栈太小或者方法调用过深,都将抛出StackOverflowError异常

  • 测试代码

public class StackDemo02 {
​
    // 记录调用了多少次出现了栈内存溢出
    private static int count = 0 ;
​
    // 入口方法
    public static void main(String[] args) {
​
        try {
            show() ;
        }catch (Throwable e) {
            e.printStackTrace();
        }
​
        System.out.println("show方法被调用了:" + count + "次");
​
    }
​
    // 测试方法
    public static void show() {
        count++ ;
        System.out.println("show方法执行了.....");
        show();
    }
​
}

配置虚拟机参数-Xss可以指定栈内存大小;例如:-Xss180k

栈内存的默认值问题:

The default value depends on the platform: 
* Linux/x64 (64-bit): 1024 KB 
* macOS (64-bit): 1024 KB 
* Oracle Solaris/x64 (64-bit): 1024 KB 
* Windows: The default value depends on virtual memory

③ 本地方法栈:与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。

线程共享区

① 堆内存

  • 作用:是Java内存区域中一块用来存放对象实例的区域,新创建的对象,数组都使用堆内存;【从Java7开始,常量池也会使用堆内存】

|

| | ------------------------------------------------------------ |

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

  • 特点:

    1、被线程共享,因此需要考虑线程安全问题

    2、会产生内存溢出问题

  • 测试代码:

public class HeapDemo01 {
​
    public static void main(String[] args) {
​
        // 定义一个变量
        int count = 0 ;
​
        // 创建一个ArrayList对象
        ArrayList arrayList = new ArrayList() ;
​
        try {
​
            while(true) {
                arrayList.add(new Object()) ;
                count++ ;
            }
​
        }catch (Throwable a) {
            a.printStackTrace();
            // 输出程序执行的次数
            System.out.println("总共执行了:" + count + "次");
        }
​
    }
​
}
  • 虚拟机参数:

-Xms 设置最小堆内存大小(不能小于1024K); -Xms 堆内存初始大小,可以通过jmap工具进行查看

-Xmx 设置最大堆内存大小(不能小于1024K); -Xmx 堆内存最大值,可以通过jmap工具进行查看

例如:-Xms1024K -Xmx2048K

注意:

|

| | ------------------------------------------------------------ |

② 方法区

  • 作用:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

  • 特点:

    1、方法区是一块线程共享的内存区域

    2、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误

    3、jdk1.6和jdk1.7方法区也常常被称之为永久区(永久代),大小一般都是几百兆;

    4、jdk1.8已经将方法区取消,替代的是元数据区(元空间),如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存

    5、jdk7以后就将方法区中的常量池移动至堆内存

|

| | ------------------------------------------------------------ |

变化的原因:

1、提高内存的回收效率(方法区内存的回收效率远远低于堆内存,因为方法去中存储的都是类信息,静态变量...这些信息不能被轻易回收)

2、字符串常量池在方法区,那么很容易产生内存溢出(因为方法区的垃圾回收效率比较低);

  • 测试代码

/**
    jdk1.8的元数据区可以使用参数-XX:MaxMetaspaceSzie设定大小   
 * 演示元空间内存溢出
 * -XX:-UseCompressedClassPointers -XX:MaxMetaspaceSize=10m
    UseCompressedClassPointers使用指针压缩,如果不使用这个参数可能会出现: Compressed class space内存溢出
 */
public class MaxMetaspaceDemo extends ClassLoader {             // 当前这个类就是一个类加载器
    
    public static void main(String[] args) {
        
        // 定义变量,记录程序产生类的个数
        int j = 0;
        
        try {
            
            MaxMetaspaceDemo test = new MaxMetaspaceDemo();
            
            for (int i = 0; i < 10000; i++, j++) {
                
                // 字节码写入器
                ClassWriter cw = new ClassWriter(0);
                
                // 定义一个类版本为Opcodes.V1_1,它的访问域为public,名称为Class{i},父类为java.lang.Object,不实现任何接口
                cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = cw.toByteArray();
                
                // 加载该类
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

2、堆和栈的区别?(高频)

① 功能不同:栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储

在堆内存中。

② 共享性不同:栈内存是线程私有的。堆内存是所有线程共有的。

③ 异常错误不同:如果栈内存或者堆内存不足都会抛出异常。栈空间不足:java.lang.StackOverFlowError。堆空间不足:

java.lang.OutOfMemoryError。

④ 空间大小:栈的空间大小远远小于堆的。

3、怎么获取Java程序使用的内存?堆使用的百分比?

可以通过java.lang.Runtime类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余

空间。

1、Runtime.freeMemory() 方法返回剩余空间的字节数

2、Runtime.totalMemory()方法总内存的字节数

4、栈帧都有哪些数据?

栈帧包含:局部变量表、操作数栈、动态连接、返回值、返回地址等。

5、如何启动系统的时候设置jvm的启动参数?

其实都很简单,比如说采用"java -jar"的方式启动一个jar包里面的系统,那么就可以才用类似下面的格式:

|

| | ------------------------------------------------------------ |

2 垃圾回收

6、如何判断一个对象是否为垃圾?(高频)

两种算法:

① 引用计数法:堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被

赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,

对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。

特点:简单、无法解决循环引用问题

定义学生类:

public class Student {
​
    // 定义成员变量
    public Object instance ;
​
}

编写测试类:

/*
    jvm参数:-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
    -verbose:gc -XX:+PrintGCDetails:打印gc日志信息
    -XX:+PrintGCTimeStamps: 打印gc日志的时间戳
*/
public class ReferenceCountGcDemo {
​
    public static void main(String[] args) {
​
        // 创建Student对象
        Student a = new Student() ;
        Student b = new Student() ;
​
        // 进行循环引用
        a.instance = b ;
        b.instance = a ;
​
        // 将a对象和b对象设置为null
        a = null ;
        b = null ;
​
        // 调用System.gc进行垃圾回收
        System.gc();                    // 如果没有触发垃圾回收说明Hotspot的jvm使用的就是引用计数法来判断对象是否为垃圾
​
    }
​
}

控制台输出gc日志:

0.076: [GC (System.gc()) [PSYoungGen: 7802K->856K(151552K)] 7802K->864K(498688K), 0.0008493 secs] [Times: user=0.17 sys=0.02, real=0.00 secs] 
0.077: [Full GC (System.gc()) [PSYoungGen: 856K->0K(151552K)] [ParOldGen: 8K->620K(347136K)] 864K->620K(498688K), [Metaspace: 3356K->3356K(1056768K)], 0.0044768 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 151552K, used 3901K [0x0000000716c00000, 0x0000000721500000, 0x00000007c0000000)
  eden space 130048K, 3% used [0x0000000716c00000,0x0000000716fcf748,0x000000071eb00000)
  from space 21504K, 0% used [0x000000071eb00000,0x000000071eb00000,0x0000000720000000)
  to   space 21504K, 0% used [0x0000000720000000,0x0000000720000000,0x0000000721500000)
 ParOldGen       total 347136K, used 620K [0x00000005c4400000, 0x00000005d9700000, 0x0000000716c00000)
  object space 347136K, 0% used [0x00000005c4400000,0x00000005c449b318,0x00000005d9700000)
 Metaspace       used 3365K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 370K, capacity 388K, committed 512K, reserved 1048576K

① 0.076: 代表gc发生的时间,从jvm启动以来经过的秒数
② [GC和[Full Gc: 说明这次垃圾收集器的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有"Full",说明此次GC发生了stop-the-world。System.gc()是说明显示的调用了                 System.gc方法进行垃圾回收
③ [PSYoungGen:表示GC发生的区域, 不同的垃圾收集器展示的区域名称不一样,PSYoungGen表示的是新生代,这里默认使用的是Parallel Scavenge收集器 (-XX:+UseSerialGC)
④ 7802K->856K(151552K):GC前该区域已使用容量 -> GC后该区域已使用容量(该区域的总容量)
⑤ 7802K->864K(498688K):GC前Java堆已使用容量 -> GC后Java堆已使用容量(Java堆总容量)
⑥ 0.0008493 secs:该区域GC所占用的时间
⑦ [Times: user=0.17 sys=0.02, real=0.00 secs]: 分别表示用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间(墙钟时间包括非运算的等待耗时)。多线程操作会叠加这些CPU时间,所以user、sys时间超过real时间是完全正常的。  

② 可达性分析算法 : 可达性分析算法又叫做跟搜索法,就是通过一系列的称之为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的

路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

(类似于葡萄串);

|

| | ------------------------------------------------------------ |

7、可达性算法中,哪些对象可作为GC Roots对象?(高频)

可以作为GC ROOTS对象的情况:

1、虚拟机栈中引用的对象

2、方法区静态成员引用的对象

3、方法区常量引用对象

4、本地方法栈引用的对象

8、Java中都有哪些引用类型?(高频)

① 强引用

Java中默认声明的就是强引用,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引

用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

示例:

/**
 * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
 */
public class StrongReferenceDemo01 {

    private static List<Object> list = new ArrayList<Object>() ;

    public static void main(String[] args) {

        // 创建对象
        for(int x = 0 ;  x < 10 ; x++) {
            byte[] buff = new byte[1024 * 1024 * 1];
            list.add(buff);
        }


    }

}

② 软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回

收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

示例代码:

/**
 * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
 */
public class SoftReferenceDemo01 {

    private static List<Object> list = new ArrayList<>();

    public static void main(String[] args) {

        // 创建数组对象
        for(int x = 0 ; x < 10 ; x++) {
            SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024 * 1024 * 1]) ;
            list.add(softReference) ;
        }

        System.gc();  // 主动通知垃圾回收器进行垃圾回收
        
        for(int i=0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
        
    }

}

我们发现无论循环创建多少个软引用对象,打印结果总是有一些为null,这里就说明了在内存不足的情况下,软引用将会被自动回收。

③ 弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2之后,用

java.lang.ref.WeakReference来表示弱引用。

示例代码:

/**
 * JVM参数:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
 */
public class WeakReferenceDemo01 {

    private static List<Object> list = new ArrayList<>();

    public static void main(String[] args) {

        // 创建数组对象
        for(int x = 0 ; x < 10 ; x++) {
            WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024 * 1024 * 1]) ;
            list.add(weakReference) ;
        }

        System.gc();  // 主动通知垃圾回收器进行垃圾回收

        for(int i=0; i < list.size(); i++){
            Object obj = ((WeakReference) list.get(i)).get();
            System.out.println(obj);
        }
        
    }

}

④ 虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用

PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是

说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

public class PhantomReference<T> extends Reference<T> {

    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }

    /**
     * Creates a new phantom reference that refers to the given object and
     * is registered with the given queue.
     *
     * <p> It is possible to create a phantom reference with a <tt>null</tt>
     * queue, but such a reference is completely useless: Its <tt>get</tt>
     * method will always return null and, since it does not have a queue, it
     * will never be enqueued.
     *
     * @param referent the object the new phantom reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

特点:

1、每次垃圾回收时都会被回收,主要用于监测对象是否已经从内存中删除

2、虚引用必须和引用队列关联使用, 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中

3、程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那

么就可以在所引用的对象的内存被回收之前采取必要的行动

示例代码:

public class PhantomReferenceDemo {

    public static void main(String[] args) throws InterruptedException {

        // 创建一个引用队列
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
        
        // 创建一个虚引用,指向一个Object对象
        PhantomReference<Object> phantomReference = new PhantomReference<Object>(new Object(), referenceQueue);
        
        // 主动通知垃圾回收器进行垃圾回收
        System.gc();
        
        // 从引用队列中获取元素, 该方法是阻塞方法
        System.out.println(referenceQueue.remove()); 

    }
}

9、常见的垃圾回收算法都有哪些?(高频)

① 标记清除

执行过程:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

|

| | ------------------------------------------------------------ |

优点:速度比较快

缺点:会产生内存碎片,碎片过多,仍会使得连续空间少

② 标记整理

执行过程:首先标记出所有需要回收的对象,在标记完成后统一进行整理,整理是指存活对象向一端移动来减少内存碎片,相对效率较低

|

| | ------------------------------------------------------------ |

优点:无内存碎片

缺点:效率较低

③ 复制算法

执行过程:开辟两份大小相等空间,一份空间始终空着,垃圾回收时,将存活对象拷贝进入空闲空间;

|

| | ------------------------------------------------------------ |

优点:无内存碎片

缺点:占用空间多

注意:如果有很多对象的存活率较高,这时我们采用复制算法,那么效率就比较低;

④ 分代回收

概述:根据对象存活周期的不同,将对象划分为几块,比如Java的堆内存,分为新生代和老年代,然后根据各个年代的特点采用最合适的算法;

新生代对象的存活的时间都比较短,因此使用的是【复制算法】;而老年代对象存活的时间比较长那么采用的就是【标记清除】或者【标记整理】;

10、简述Java垃圾回收机制?有什么办法主动通知虚拟机进行垃圾回收?

在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下

是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回

收。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

3 对象分配

11、对象在内存中是如何进行分配的?(高频)

① 对象优先在Eden分配:对象优先在『伊甸园』分配,当『伊甸园』没有足够的空间时,触发 'Minor GC'(小范围的GC)

情况一:伊甸园的内存空间足够,不会发生'Minor GC'

情况二:伊甸园的空间不够了

垃圾回收线程启动,进行垃圾回收,此时会触发"stop the world"(停止所有用户线程),

Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄最多到一定值(最大值是15,对

象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁)(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有

达到阈值的对象会被复制到“To”区域。

"From"和"To"会交换他们的角色,下一次垃圾回收的时候也是从Eden将存活的对象复制到TO区

Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

案例演示:

jvm参数设置:

-XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gc.log -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

-XX:+UseSerialGC 是指使用 Serial + SerialOld 回收器组合
-XX:+PrintGCDetails -verbose:gc 是指打印 GC 详细信息
-XX:+PrintGCTimeStamps 打印gc日志的时间戳
-Xloggc:./gc.log 将gc日志输出到一个日志文件中
-Xms20M -Xmx20M -Xmn10M 是指分配给JVM的最小,最大以及新生代内存
-XX:SurvivorRatio=8 是指『伊甸园』与『幸存区 From』和『幸存区 To』比例为 8:1:1

定义内存大小变量

private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _4MB = 4 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;

案例1:没有创建数组对象,看参数运行情况

案例2:创建一个4M的数组,查看内存分配情况

// 创建一个4M大小的数组
byte[] bytes = new byte[_4MB] ;
Heap
 def new generation   total 9216K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K, 100% used [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff400000)  // 在伊甸园中创建对象
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

没有触发GC操作,对象直接在Eden分配;

案例3:创建一个7M的数组,查看内存分配情况

// 创建一个7M大小的数组
byte[] bytes1 = new byte[_7MB] ;
-- 触发垃圾回收
[GC (Allocation Failure) [DefNew: 2004K->647K(9216K), 0.0023439 secs] 2004K->647K(19456K), 0.0024142 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 7897K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  88% used [0x00000000fec00000, 0x00000000ff314930, 0x00000000ff400000)
  from space 1024K,  63% used [0x00000000ff500000, 0x00000000ff5a1e58, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3446K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

由于程序在启动的时候jdk内部还会存在一些对象的创建,因此当我们分配了一个7M的内存空间,eden内存不足,因此发生了一次Minor GC!并且将存

活下的对象最终存储到from区中。

案例4: 在案例3的基础上,在分配一个512KB的数组内存空间

byte[] bytes1 = new byte[_7MB] ;
byte[] bytes2 = new byte[_512KB] ;
[GC (Allocation Failure) [DefNew: 2005K->623K(9216K), 0.0015235 secs] 2005K->623K(19456K), 0.0015799 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8713K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  98% used [0x00000000fec00000, 0x00000000ff3e6820, 0x00000000ff400000)
  from space 1024K,  60% used [0x00000000ff500000, 0x00000000ff59bdb8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

触发一次GC操作!并且将存活下的对象最终存储到from区中,第二次分配_512KB大小的内存空间的时候,直接在伊甸园分配即可。

案例5: 在4的基础上在分配一个512KB的数组内存空间

byte[] bytes1 = new byte[_7MB] ;
byte[] bytes2 = new byte[_512KB] ;
byte[] bytes3 = new byte[_512KB] ;
[GC (Allocation Failure) [DefNew: 2004K->620K(9216K), 0.0018706 secs] 2004K->620K(19456K), 0.0019275 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8628K->539K(9216K), 0.0063389 secs] 8628K->8323K(19456K), 0.0063773 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1133K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   7% used [0x00000000fec00000, 0x00000000fec94930, 0x00000000ff400000)
  from space 1024K,  52% used [0x00000000ff400000, 0x00000000ff486de0, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7784K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  76% used [0x00000000ff600000, 0x00000000ffd9a040, 0x00000000ffd9a200, 0x0000000100000000)
 Metaspace       used 3443K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

触发了2次垃圾回收!并且将from区中存活的对象存储到老年代!

② 大对象直接晋升至老年代

当对象太大,伊甸园包括幸存区都存放不下时,这时候老年代的连续空间足够,此对象会直接晋升至老年代,不会发生 GC

结果

案例演示:

案例1:直接分配一个8M的内存空间

byte[] bytes1 = new byte[_8MB] ;

伊甸园总大小只有 8 MB,但新分配的对象大小已经是 8MB,而幸存区都仅有 1MB,也无法容纳这个对象

Heap
 def new generation   total 9216K, used 2169K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee1e560, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 3443K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

可以看到结果并没有发生 GC,大对象直接被放入了老年代「tenured generation total 10240K, used 8192K」

案例演示2:老年代连续空间不足,触发 Full GC

byte[] bytes1 = new byte[_8MB] ;
byte[] bytes2 = new byte[_8MB] ;

第一个 8MB 直接进入老年代,第二个 8MB 对象在分配时发现老年代空间不足,只好尝试先进行一次 Minor GC,结果发现新生代没有连续空间,只好

触发一次 Full GC,最后发现老年代也没有连续空间,这时出现 OutOfMemoryError

[GC (Allocation Failure) [DefNew: 2004K->647K(9216K), 0.0022693 secs][Tenured: 8192K->8838K(10240K), 0.0452151 secs] 10197K->8838K(19456K), [Metaspace: 3438K->3438K(1056768K)], 0.0504669 secs] [Times: user=0.00 sys=0.00, real=0.05 secs] 
[Full GC (Allocation Failure) [TenuredException in thread "main" : 8838K->8820K(10240K), 0.0027463 secs] 8838K->8820K(19456K), [Metaspace: 3438K->3438K(1056768K)], 0.0027877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
	at com.itheima.jvm.gc.ObjectMemoryDemo.main(ObjectMemoryDemo.java:14)
Heap
 def new generation   total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 8820K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  86% used [0x00000000ff600000, 0x00000000ffe9d220, 0x00000000ffe9d400, 0x0000000100000000)
 Metaspace       used 3470K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 379K, capacity 388K, committed 512K, reserved 1048576K

12、对象是怎么从年轻代进入老年代的?

存在3种情况:

1、如果对象够老,会通过提升(Promotion)进入老年代,这一般是根据对象的年龄进行判断的。

2、动态对象年龄判定。有的垃圾回收算法,比如G1,并不要求age必须达到15才能晋升到老年代,它会使用一些动态的计算方法。

3、超出某个大小的对象将直接在老年代分配。不过这个值默认为0,意思是全部首选Eden区进行分配。

13、简单描述一下(分代)垃圾回收的过程?(高频)

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是2/3。

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

当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC)。具体过程如下:

1、在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Survivor分区(以下简称to)

2、From区中的对象根据对象的年龄值决定去向,达到阈值15移动到老年代,没有达到复制到to区域(复制算法)

3、在把Eden和to区中的对象清空掉

14、JVM的永久代中会发生垃圾回收么?

永久代会触发垃圾回收的,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。

注:Java 8 中已经移除了永久代,新加了一个叫做元数据区(Metaspace)的内存区。

4 垃圾收集器

15、常见的垃圾收集器都有哪些?(高频)

常见的垃圾收集器如下所示:

不同的垃圾收集器,作用的堆内存空间是不一样的;上面的 serial , parnew , Paraller Scavenge 是新生代的垃圾回收

器;CMS , Serial Old ,

Paralle Old是老年代的垃圾收集器 , G1垃圾收集器可以作用于新生代和老年代; 连线表示垃圾收集器可以搭配使用;

① Serial

特点:

  1. Serial是一个单线程的垃圾收集器

  2. "Stop The World",它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉。

应用场景:

  1. 使用场景:多用于桌面应用,Client端的垃圾回收器

  2. 桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接受

Serial Old收集器是Serial的老年代版本和Serial一样是单线程,使用的算法是"标记-整理"

② ParNew

概述: ParNew 收集器其实就是 Serial 收集器的多线程版本

特点:

1、会触发stop the world

2、多线程方式进行垃圾回收

应用场景:它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器

注意:如果是单核cpu即使使用该垃圾回收器也无法提高执行效率

③ Parallel Scavenge

概述:Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

特点:由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为“吞吐量优先”收集器

所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟

机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%

应用场景: 高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel old收集器Parallel Scavenge收集器的老年代版本,使用多线程+标记整理算法

④ CMS(重点)

概述:CMS (Concurrent Mark Sweep)收集器是-种以获取最短回收停顿时间为目标的收集器。

特点:

  1. CMS 收集器是基于“标记-清除”算法实现的

  2. 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好

    的体验。

步骤流程:

  • 初始标记(CMS initial mark) -------- 标记一下 GC Roots 能直接关联到的对象,速度很快(stop the world)

  • 并发标记(CMS concurrent mark) -------- 对初始标记标记过的对象,进行trace(进行追踪,得到所有关联的对象,进行标记)

  • 重新标记(CMS remark) -------- 为了修正并发标记期间因用户程序导致标记产生变动的标记记录(stop the world)

  • 并发清除(CMS concurrent sweep)

缺点:会产生垃圾碎片

⑤ G1

概述: G1是一个分代的,并行与并发的"标记-整理"垃圾回收器。 它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂

停时间(pause time),同时兼顾良好的吞吐量。

相比于CMS:

  1. G1垃圾回收器使用的是"标记-整理",因此其回收得到的空间是连续的。

  2. G1回收器的内存与CMS回收器要求的内存模型有极大的不同。G1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内

    存的回收是以region作为基本单位的;

16、你都用过G1垃圾回收器的哪几个重要参数?

① -XX:MaxGCPauseMillis

暂停时间,默认值200ms。这是一个软性目标,G1会尽量达成,如果达不成,会逐渐做自我调整。

② -XX:G1HeapRegionSize

Region大小,若未指定则默认最多生成2048块,每块的大小需要为2的幂次方,如1,2,4,8,16,32,最大值为32M。

③ -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent

新生代比例有两个数值指定,下限:-XX:G1NewSizePercent,默认值5%,上限:-XX:G1MaxNewSizePercent,默认值60%。

17、串行(serial)收集器和吞吐量(throughput)收集器的应用场景?

吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。 而串行收集器对大多数的小应用(在现代处理器上需要大概

100M 左右的内存)就足够了。

18、生产上如何配置垃圾收集器的?

|

| | ------------------------------------------------------------ |

1、首先是内存大小问题,基本上每一个内存区域我都会设置一个上限,来避免溢出问题,比如元空间。通常,堆空间我会设置成操作系统的2/3(这是想

给其他进程和操作系统预留一些时间),超过8GB的堆优先选用G1。

2、接下来,我会对JVM进行初步优化。比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。

3、再接下来,就是专项优化,主要判断的依据就是系统容量、访问延迟、吞吐量等。我们的服务是高并发的,所以对STW的时间非常敏感。我会通过记录

详细的GC日志,来找到这个瓶颈点,借用gceasy(重点)Universal JVM GC analyzer - Java Garbage collection log analysis made easy这样的日志分析工具,很容易定位到问题。之所以选择采用工具,是因

为gc日志看起来实在是太麻烦了,gceasy号称是AI学习分析问题,可视化做的较好。

5 类加载器

19、什么是类加载器,类加载器有哪些?(高频)

类加载器的作用:负载将的class文件加载到java虚拟机中,并为之创建一个Class对象

从Java虚拟机的角度来讲,只存在如下两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader), 这个类加载器使用C++语言实现,是虚拟机自身的一部分

  2. 其他类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部都继承自抽象类(java.lang.ClassLoader)

从Java开发人员的角度来讲,类加载器还可以划分的更细致一下,绝大部分Java程序都会使用到以下3种系统提供的类加载器:

  1. 启动类加载器(Bootstrap class loader):它是虚拟机的内置类加载器,通过表示为null

  2. 平台类加载器(Platform class loader) :它是平台类加载器; 负责加载JDK中一些特殊的模块;

  3. 系统类加载器(System class loader) :它也被称为应用程序类加载器, 它负责加载用户类路径上所指定的类库,一般情况下这个就是程序中默

    认的类加载器

20、Java的双亲委托机制是什么?(高频)

概述

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自定义的类加载器。这些类加载器之间的层次关系一般会如下图所

示:

上图所展示的类加载器之间的这种层次关系,就称之为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该

有自己的父类加载器。这里的类加载器的父子关系不是真正物理意义上的继承,而是逻辑上的继承。

工作过程

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一个

层次的类加载器都是如此,因此所有的加载请求最终都应该传说到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(它的搜索

返回中没有找到所需的类)时,子类加载器才会尝试自己去加载。

6 性能调优

21、调优命令有哪些?

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

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

行数据。

查询帮助文档:jstat -options

|

| | ------------------------------------------------------------ |

|

| | ------------------------------------------------------------ |

3、jmap,JVM Memory Map命令用于查看堆内存的分配情况以及生成heap dump文件

查询帮助文档:jmap -h

示例1:jmap -heap 33193 查询堆内存的分配情况

示例2:jmap -dump:format=b,file=thread-cup.log 33193

4、jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的

分析结果后,可以在浏览器中查看

查询帮助文档:

jhat -h

示例:jhat -J-Xmx512M thread-cup.log

|

| | ------------------------------------------------------------ |

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

查看帮助文档:jstack -h

示例:jstack -l 33193

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

查看帮助文档:jinfo -h

示例:jinfo -flags 33193

|

| | ------------------------------------------------------------ |

22、你知道哪些JVM性能调优参数?(高频)

1、设定堆内存大小:

-Xms 设置最小堆内存大小(不能小于1024K); -Xms 堆内存初始大小,可以通过jmap工具进行查看

-Xmx 设置最大堆内存大小(不能小于1024K); -Xmx 堆内存最大值,可以通过jmap工具进行查看

2、设定新生代大小:

-XX:NewSize:新生代大小

-XX:NewRatio 新生代和老生代占比

3、-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

4、设定垃圾回收器

年轻代用 -XX:+UseParNewGC

年老代用-XX:+UseConcMarkSweepGC

23、你用过哪些性能调优工具?(高频)

常用调优工具分为两类

1、jdk自带监控工具

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

    等的监控

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

2、第三方

  • MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和

    减少内存消耗

  • GChisto,一款专业分析gc日志的工具

24、你都有哪些手段用来排查内存溢出?(高频)

内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动后,使用jstat命令,发现Old区在一直增长。我使用

jmap命令,导出了一份线上堆栈,然后使用MAT进行分析。通过对GC Roots的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学做缓

存用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 Guava Cache,并设置了弱引用,故障就消失了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值