0.String字符串的比较,new string()和 String str = “”区别,intern()在jdk不同版本中的差异?
1. 谈谈final, finally, finalize的区别
性质不同
- final为关键字;
- finalize()为方法;
- finally为区块标志,用于try语句中;
作用
- final为用于标识常量的关键字,final标识的关键字存储在常量池中(在这里final常量的具体用法将在下面进行介绍);
- finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/0操作);
- finally{}用于标识代码块,与try{}进行配合,不论try中的代码执行完或没有执行完(这里指有异常),该代码块之中的程序必定会进行;
final:关键字主要⽤在三个地⽅:变量、⽅法、类。
final修饰变量:
如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。
final修饰方法:
把⽅法锁定,以防任何继承类修改它的含义(重写);
final修饰类:
final 修饰类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。一个类不能被继承,除了final关键字之外,还有可以私有化构造器。(内部类无效)
finally—再异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)
finalize—方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的
2. java中创建线程的方法有几种,都有哪些?怎么创建?
有四种方法,
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池
3. sleep()和wait()的区别?
相同点:
一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1.两个方法的声明位置不同,Thread类中声明sleep(),Object类中声明wait()
2.调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块和同步方法中使用。
3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放锁,wait()会释放锁。
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。
sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。
CPU及资源锁释放
sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。
sleep和wait的区别:
1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
3、它们都可以被interrupted方法中断。
4. 面试题:对比String、StringBuffer、StringBuilder
String(JDK1.0):不可变字符序列
StringBuffer(JDK1.0):可变字符序列、效率低、线程安全
StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全
注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值。
5.反向代理与正向代理的区别详解与nginx的负载均衡
正向代理:顾名思义 客户端发出请求 找到代理服务器 由代理服务器发出真正的请求给真正的服务器 获得响应后 再把数据返回给客户端。
用同一个通俗的例子来解释 就是A想找C借钱 但是C嫌弃A人品不行 于是A找到人品较好的B 由B发出请求找C借钱,借钱给B,B拿到钱之后再把钱转交给A。从始至终C都不知道要把钱给A(因为他要是知道是A借的就不会借了)
反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器 IP 地址。
反向代理:最经典的例子就是nginx 客户端之间找到nginx服务器 由nginx访问真正的服务器 nginx拿到数据之后再把结果返回给客户端。
再举一个栗子
大家访问百度这个域名 www.baidu.com 百度真正的服务器地址我们不需要关心 因为他是由反向代理得来的 我们只需要知道百度这个域名 他就可以自动帮我们转接到真正的服务器 自始至终我们都不需要知道百度服务器真正地址的存在 。
你去超市买东西,超时就是代理服务器,你只和超市有联系,但是超市实际上是从各个供应商拿的商品,但你和供应商没关系。
nginx还有一个重要作用就是负载均衡 但很多客户端访问地址的时候可以减缓服务器的压力 当三个人访问都是www.baoidu.com这个域名 nginx可以把这三个人的访问请求量分摊到百度的三个服务器上 不用一个服务器处理三个请求 而是三个服务器一个服务器处理一个请求 。
6. 什么是MVCC?
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读 。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
MVCC的实现依赖于:隐藏字段、Undo Log版本链,ReadView实现。
ReadView的设计思路
ReadView的规则
总结:
详细介绍MVCC文章
7.聚簇索引(B+Tree)
聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。
聚簇索引的特点,优点和缺点,以及限制如下:
8. 二级索引(辅助索引、非聚簇索引)
二级索引的叶子节点存放的是主键值或指向数据行的指针。
二级索引和聚簇索引主要区别就是叶子结点存放是否一个完整的一行数据,二级索引存放是指向聚簇索引的指针。
什么是回表?
我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树!
9.聚簇索引和二级索引的区别
聚簇索引的叶子节点存储的是每一个行的数据。
二级索引的叶子节点存储的是每个聚簇索引的主键和构成二级索引的key。
10.B+与B树的区别
下面是B树的图:
B+ 树和 B 树的差异:
- 有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。
- 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
- 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中, 非叶子节点既保存索引,也保存数据记录 。
- 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
B+树的中间节点并不直接存储数据,这样的好处都有什么呢?
首先,B+树查询效率更稳定。因为B+树每次只有访问到叶子节点才能找到对应的数据,而在B树中,非叶子节点也会存储数据,这样就会造成查询效率不稳定的情况,有时候访问到了非叶子节点就可以找到关键字,而有时需要访问到叶子节点才能找到关键字。
其次,B+树的查询效率更高。这是因为通常B+树比B树更矮胖(阶数更大,深度更低),查询所需要的磁盘I/0也会更少。同样的磁盘页大小,B+树可以存储更多的节点关键字。
**不仅是对单个关键字的查询上,在查询范围上,B+树的效率也比B树高。**这是因为所有关键字都出现在B+树的叶子节点中,叶子节点之间会有指针,数据又是递增的,这使得我们范围查找可以通过指针连接查找。而在B树中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
11.B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘I/O?
B+树索引一个索引页可以存储多少个索引:以bigint建索引的话,一个bigint为8字节,光存索引值还不行
还得存一个指针找到下一个索引页或数据页,InnoDB中指针大小为8个字节。
存储索引:16kb/(8b+8b) = 1k 约等于1000
12.Hash 索引与 B+ 树索引的区别?
13.数据库优化的步骤
整个流程划分成了 观察(Show status) 和 行动(Action) 两个部分。字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。
-
我们先观察服务器的状态,看是否周期性波动(比如双十一),我们可以使用缓存技术。
-
如果加上缓存任然是延迟卡顿,我可以开启SQL慢查询日志,修改long_query_time阈值(默认是10秒),找到慢查询日志文件,使用慢查询日志分析工具mysqldumpslow进行分析,找到执行较慢的SQL语句。
-
找到执行比较慢的SQL语句,如果是SQL等待的时间比较长,调优服务器参数,如果是SQL执行时间长,使用分析查询语句EXPLAIN,进行索引优化,数据设计优化等等。
EXPLAIN 语句输出的各个列的作用如下:
- 如果是SQL语句查询达到瓶颈,我们就需要考虑,主从框架,读写分离,分库分表等等。
14.什么是微服务?微服务架构的优缺点、应用?
微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作(和出现故障)时不会相互影响。
优点:
1.易于开发和维护:一个服务只关注一个特定的业务功能,所以它业务清晰,代码量少。开发和维护单个微服务相当简单。而整个应用是若干个微服务构建而成的,所以整个应用在被维持在一个可控的状态;
2.局部修改易部署:单个应用只要有修改,就得重新部署整个应用,微服务解决了这个问题。一般来说,对某个微服务进行修改,只需要重新部署这个服务即可;
3.技术栈不受限:在微服务架构中,可以结合业务和团队的特点,合理选用技术栈。例如有些服务可以使用关系型数据库Mysql,有的服务可以使用非关系型数据库redis。甚至可根据需求,部分服务使用JAVA开发,部分微服务使用Node.js开发
4.按需收缩:可根据需求,实现细粒度的扩展。例如,系统中的某个微服务遇到了瓶颈,可以结合微服务的特点,增加内存,升级CPU或增加节点。
缺点:
1.微服务过多,治理成本高,不利于维护系统
2.分布式系统开发的成本高(容错,分布式事务等)对团队挑战大
3.性能降低,网络延迟。
4.接口调整成本高:微服务之间通过接口进行通信
15.缓存穿透?缓存击穿?缓存雪崩?解决方案?
缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案:
(1) 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2) 设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:
- 让热点的数据的缓存永不过期。
- 采用分布式锁,缓存失效后只有一个线程更新并写入。
缓存雪崩
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
正常访问缓存失效瞬间
解决方案:
(1) 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2) 使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3) 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4) 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
16.synchronized 和 Lock 有什么区别?
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
17.spring事务的传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。
Spring定义了7种类传播行为:
18.谈谈动态代理模式
什么是动态代理?
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
19.谈谈ThreadLocal理解
20.spring的循环依赖
spring内部通过3级缓存来解决循环依赖 - DefaultSingletonBeanRegistry
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。
第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完。
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂。
举例说明:
A / B两对象在三级缓存中的迁移说明
- A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B。
- B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
- B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
详细过程参考这个链接
21.Redis加锁的演变过程(从单机到分布式)
下面我们以某个商品超卖现象进行加锁解决的过程:
- 在单机模式下,单机版的锁synchronized和ReentraLock锁,(synchronized和ReentraLock锁的区别)可以解决超卖问题。
synchronized和ReentraLock锁的区别,在锁的方法,synchronized为死等,而ReentraLock可以控制等待的时间,如使用tryLock()方法,可以控制时间。 - 有分布式以后,有出现了超卖现象,我们使用JMeter工具进行测试
解决方案:使用setnx加上try/catch/finally可以解决,但是又出现了新的问题
如果部署微服务jar包的机器宕机了,代码层次没有运行到finally这块,没有解锁。 - 如果出现上面异常的话,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。
- 设置key+过期时间分开了,必须要合并成一行具备原子性。比如stringRedisTemplate.opsForValue() .setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
另一个新问题:张冠李戴,删除了别人的锁,如果进程A,设置了加锁10秒钟,而且进程A的业务逻辑代码比较耗时,超过了10秒钟,结果过了时间自动删除了锁,有进来一个进程B,拿到锁进行业务代码,此时进程A 进行解锁,就把进程B的锁给删了。 - 解决上面的方法,只能自己删除自己的,不许动别人的。我们可以在finally块的判断 + del删除操作,同样finally块的判断 + del删除操作要是原子操作。可以使用用lua脚本或者用redis自身的事务。
- 还有一个锁的续期问题?确保RedisLock过期时间大于业务执行时间的问题。锁的续期我们使用Redisson技术进行解决,(同样可以解决上面的所有的问题)。
- 同样为了让代码变得更加严谨,我们在释放锁的时候加一些判断
public static final String REDIS_LOCK = "REDIS_LOCK";
@Autowired
private Redisson redisson;
@GetMapping("/doSomething")
public String doSomething(){
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
//doSomething
}finally {
//添加后,更保险
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
lock.isLocked():判断要解锁的key是否已被锁定。
lock.isHeldByCurrentThread():判断要解锁的key是否被当前线程持有。
主要是为了避免一下异常的出现:
IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0
22. Redis内存淘汰策略
redis过期键的删除策略
请问一下如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?
不是马上删除的,那过期后到底什么时候被删除呢??是个什么操作?(接下里听我细细道来)
三种不同的删除策略
- 定时删除 - 总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
- 惰性删除 - 总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
- 定期删除 - 上面两种方案都走极端 - 定期删除 - 定期抽样key,判断是否过期(存在漏网之鱼)定期删除策略是前两种策略的折中。
定时删除
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
优点:节约内存,到时就删除,快速释放掉不必要的内存占用
缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量
总结:用处理器性能换取存储空间 (拿时间换空间)
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。
优点:节约CPU性能,发现必须删除的时候才删除
缺点:内存压力很大,出现长期占用内存的数据
总结:用存储空间换取处理器性能(拿空间换时间)
定期删除
定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询Redis库中的时效性数据,来用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间(随机抽查,重点抽查)
经过上述步骤删除策略的任意一种,还有漏洞吗?(肯定有,不然不问)
有什么漏洞呢,如下所示:
- 定期删除时,从来没有被抽查到
- 惰性删除时,也从来没有被点中使用过
上述2步骤====>大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽(这个时候就必须有一个解决的方案,要么报异常,要么找个兜底的方法)
必须要有一个更好的兜底方案:
内存淘汰策略登场(Redis 6.0.8版本)
- noeviction:不会驱逐任何key
- volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
- volatile-Iru:对所有设置了过期时间的key使用LRU算法进行删除
- volatile-random:对所有设置了过期时间的key随机删除
- volatile-ttl:删除马上要过期的key
- allkeys-lfu:对所有key使用LFU算法进行删除
- allkeys-Iru:对所有key使用LRU算法进行删除
- allkeys-random:对所有key随机删除
如何配置,修改
命令
config set maxmemory-policy noeviction
config get maxmemory
配置文件 - 配置文件redis.conf的maxmemory-policy参数
23.Spring怎么解决循环依赖?
spring内部通过3级缓存来解决循环依赖 - DefaultSingletonBeanRegistry
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
- 第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。
- 第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完。
- 第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂。
package org.springframework.beans.factory.support;
...
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
...
}
A / B两对象在三级缓存中的迁移说明
- A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B。
- B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
- B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
为什么要有二级缓存?二级缓存具体解决什么问题?
二级缓存是为了解决,在多线程环境中,获取不完整Bean实例。如果只有一级缓存的话,在多线程环境下,我们使用加锁,并发下降,所以放到二级缓存中加锁,解决并发性能,又可以解决循环依赖。增加性能。(注意在二级缓存加锁代码中需要双重检查,解决并发获取的不完整的Bean)
为什么要有三级缓存?三级缓存具体解决什么问题?(接着吹)
如果只是死循环出现的栈溢出,一级缓存就可以解决,三级缓存的出现,就是为了解决循环依赖中的动态代理,只有二级缓存,其实也是可以解决的,但是(最害怕出现但是)如果出现多次循环依赖,会创建多次的动态代理,所以就有三级缓存。
(说明一下多次循环依赖,就是A依赖B,B依赖A,A依赖C,C依赖A,这种场景)
这里有一个视频可以帮助我们更好的理解上面的问题和spring解决循环依赖的原理。
视频参考链接
24.spring中bean的生命周期
spring中bean的生命周期 参考这个链接
bean的更完整的生命周期分为以下七步:
(1)通过构造器创建bean实例(调用无参的构造函数)
(2)为bean的属性设置值和对其它bean引用(调用set方法)
(3)把bean实例传递到bean后置处理器的方法
(4)调用bean的初始化方法(初始化方法需要配置)
(5)把bean实例传递到bean后置处理器的方法
(6)bean的使用(获取到对象)
(7)容器关闭,调用bean的销毁的方法(销毁方法需要进行配置)
如果加上一些实现的接口,spring中bean的生命周期大致可以分为这十个步骤:
25.主流对象复制框架使用与比较(对象克隆使用的框架比较)
在 Java 生态中,有很多第三方框架可以进行对象复制,分别是Apache的BeanUtils和PropertyUtils、Spring的BeanUtils、Cglib的BeanCopier等,下面我们一起来了解一下吧!
其中 apache 提供了2个类来实现对象复制,分别是BeanUtils和PropertyUtils,这两者的区别在于BeanUtils提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换,而PropertyUtils不支持这个功能!
而spring的BeanUtils和cglib的BeanCopier,因为属性对应的类型不一致,直接跳过复制!
总结
- 在低频次下,apache的PropertyUtils效率最高,在高频次下,cglib BeanCopier效率最高;
- apache的BeanUtils,在四个框架中效率最低;
- 在高频次下,spring的BeanUtils的效率仅次于cglib BeanCopier;
- 因为apache的BeanUtils在进行复制的时候,在支持的数据类型范围内进行转换,所有性能会有些损失,对于数据结构严谨的复制,不建议采用!
Spring BeanUtils
特点:
- 字段名不一致,属性无法复制;
- 类型不一致,属性无法复制(直接跳过)。但是注意,如果类型为基本类型以及基本类型的包装类,这种可以转化;
- 浅拷贝
Apache BeanUtils
特点:
- 字段名不一致的,属性无法被复制;
- 类型不一致的字段,将会进行默认类型转化;
- 浅拷贝。
Apache PropertyUtils
特点:
- 字段名不一致的,属性无法被复制;
- 类型不一致的字段,不支持数据类型转换;
- 浅拷贝。
cglib BeanCopier
特点:
- 字段名不一致,属性无法复制
- 类型不一致,属性无法复制(直接跳过)。如果类型为基本类型/基本类型的包装类型,这两者也无法被拷贝。但可自定义转换器实现不同类型的拷贝。
- 浅拷贝
26.Spring IOC和AOP的原理以及实际应用
Spring IOC和AOP的原理以及实际应用:参考链接
27.TCP 三次握手和四次挥手的流程
TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。
在建立TCP连接时,需要通过三次握手来建立,过程是:
- 客户端向服务端发送一个SYN
- 服务端接收到SYN后,给客户端发送一个SYN_ACK
- 客户端接收到SYN_ACKJ后,再给服务端发送一个ACK
在断开TCP连接时,需要通过四次挥手来断开,过程是:
- 客户端向服务端发送FN
- 服务端接收FN后,向客户端发送CK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还
有数据正在处理 - 服务端处理完所有数据后,向客户端发送FN,表示服务端现在可以断开连接
- 客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了
TCP 三次握手和四次挥手的流程
28.MySQL原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
- 事务的隔离性由 锁机制 实现。
- 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。
29.redo日志和undo日志的理解和深刻认识
30.死锁产生的原因,怎么避免死锁?
死锁是指两个或多个以上的进程在执行过程中,因争夺资源而造成一种互相等待的现象,若无外力干涉那他们都将无法推进下去。这些一直处于相互等待资源的线程就称为死锁线程。
导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁。
- 互斥条件,共享资源 X 和 Y 只能被一个线程占用;
- 请求和保持条件,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待条件,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏的,因为这是互斥锁的基本约束,其他三方条件都有办法来破坏:
- 对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
- 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
- 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
31.浏览器发出一个请求到收到响应经历了哪些步骤?
- 浏览器解析用户输入的URL,生成一个HTTP格式的请求
- 先根据URL域名从本地hosts.文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
- 浏览器通过操作系统将请求通过四层网络协议发送出去
- 途中可能会经过各种路由器、交换机,最终到达服务器
- 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了
- tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
- 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,.那么则会找到对应的Controller中的方法,并执行该方法得到结果
- Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
- 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负贵解析并渲染
32.谈谈equlas()和hashcode()方法的之间的关系(为什么重写equlas方法必须要重写hashcode方法)?
hashCode()方法介绍
在Object类中的一个本地方法,作用是获取哈希码(散列码),它是一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置。
为什么要有 hashCode()方法?
以“HashSet 如何检查重复”为例子说明。
当把对象添加到HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值做比较,如果没有相同的hashCode,则认为没有重复的对象。如果有相同的hashCode(发生碰撞),则会调用equals()方法来判断对象是否相同。如果相同,则不会让其加入成功。如果不同,则重新散列到其他位置。这样就减少使用equals的次数,提高了执行速度。
重写equals方法,必须重写hashCode方法
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()方法,则两个对象是永远不相等的(即使有同样的数据)。
如果两个对象相等(equals返回true),则它们的hashCode也一定相等。
如果两个对象的hashCode相等,但它们不一定相等(equals不一定返回true)。
详细点击这里
33.常见的MySQL的SQL优化注意事项
- 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
- 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0 - 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
- 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20 - in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3 - 下面的查询也将导致全表扫描:
select id from t where name like ‘%abc%’ Like查询(非左开头) - 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2 - 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)=‘abc’ // oracle总有的是substr函数。
select id from t where datediff(day,createdate,‘2005-11-30’)=0 //查过了确实没有datediff函数。
应改为:
select id from t where name like ‘abc%’ - 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
- 很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
(等等,如果嫌少自行百度)
适合创建索引条件
1、主键自动建立唯一索引
2、频繁作为查询条件的字段应该建立索引
3、查询中与其他表关联的字段,外键关系建立索引
4、单键/组合索引的选择问题,组合索引性价比更高
5、查询中排序的字段,排序字段若通过索引去访问将大大提高排序效率
6、查询中统计或者分组字段
不适合创建索引条件
1、表记录少的
2、经常增删改的表或者字段
3、where条件里用不到的字段不创建索引
4、过滤性不好的不适合建索引
34.我们为什么会限流?限流算法有几种?
随着用户的流量突增,后端服务的处理能力是有限的,如果不能处理好突发流量,后端服务很容易就被打垮,导致整个系统崩溃!
限流算法
- 计数限流
- 固定窗口限流算法
- 滑动窗口限流
- 漏桶算法
- 令牌桶算法
详细限流算法参考
35. Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别?
- ZooKeeper中的节点服务挂了就要选举,在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的,选举就是改微服务做了集群,必须有一台主其他的都是从
- Eureka各个节点是平等关系,服务器挂了没关系,只要有一台Eureka就可以保证服务可用,数据都是最新的。如果查询到的数据并不是最新的,就是因为Eureka的自我保护模式导致的
- Eureka本质上是一个工程,而ZooKeeper只是一个进程
- Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper 一样使得整个注册系统瘫痪
- ZooKeeper保证的是CP,Eureka保证的是AP
36.说一下HTTP和RPC的区别
RPC(即Remote Procedure Call,远程过程调用)和HTTP(HyperText Transfer Protocol,超文本传输协议),两者前者是一种方法,后者则是一种协议。两者都常用于实现服务,在这个层面最本质的区别是RPC服务主要工作在TCP协议之上(也可以在HTTP协议),而HTTP服务工作在HTTP协议之上。由于HTTP协议基于TCP协议,所以RPC服务天然比HTTP更轻量,效率更胜一筹。
RPC服务
RPC服务基本架构包含了四个核心的组件,分别是Client,Server,Clent Stub以及Server Stub。
Client (客户端):服务调用方。
Server(服务端):服务提供方。
Client Stub(客户端存根):存放服务端的地址消息,负责将客户端的请求参数打包成网络消息,然后通过网络发送给服务提供方。
Server Stub(服务端存根):接收客户端发送的消息,再将客户端请求参数打包成网络消息,然后通过网络远程发送给服务方。
RPC效率优势明显,在实际开发中,客户端和服务端在技术方案中约定客户端的调用参数和服务端的返回参数之后就可以各自开发,任何客户端只要按照接口定义的规范发送入参都可以调用该RPC服务,服务端也能按接口定义的规范出参返回计算结果。这样既实现了客户端和服务端之间的解耦,也使得RPC接口可以在多个项目中重复利用。
RPC调用分为同步方式和异步方式。
什么是同步调用?什么是异步调用?同步调用就是客户端等待调用执行完成并返回结果。异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。如果客户端并不关心结果,则可以变成一个单向的调用。这个过程有点类似于Java中的callable和runnable接口,我们进行异步执行的时候,如果需要知道执行的结果,就可以使用callable接口,并且可以通过Future类获取到异步执行的结果信息。如果不关心执行的结果,直接使用runnable接口就可以了,因为它不返回结果,当然啦,callable也是可以的,我们不去获取Future就可以了。
HTTP服务
HTTP服务开发即开发ERESTful风格的服务接口。在接口不多、系统之间交互较少的情况下,是一种信息传递的常用通信手段。HTTP接口的优点是简单、直接、开发方便,利用现成的HTTP协议进行传输。在服务开发的时候,约定一个接口文档,严格定义输入和输出,明确每一个接口的请求方法和需要的请求参数及其格式。
在内部子系统较多、接口较多的情况下,RPC框架的好处就凸显出现了,首先是长连接,不必每次通信都要像HTTP那样三次握手,减少了网络开销;其次是RPC框架一般都有注册中心,有丰富的监控发布方法;RPC接口的发布、下线、动态扩展等对调用方是无感知的、统一化的操作。
RPC接口和HTTP接口的区别与联系
传输协议
- RPC:可以基于TCP协议,也可以基于HTTP协议。
- HTTP:基于HTTP协议。
传输效率
- RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2.0协议,也可以很好地减少报文体积,提高传输效率。
- HTTP:如果时基于HTTP1.1的协议,请求中会包含很多无用的内容;如果是基于HTTP2.0,那么简单地封装一下还是可以作为一个RPC使用的,这时标准RPC框架更多是服务治理。
性能消耗
- RPC:可以基于thrift实现高效的二进制传输
- HTTP:大部分是通过json实现的,字节大小和序列化耗时都比thrift要更消耗性能
负载均衡
- RPC:基本都自带了负载均衡策略
- HTTP:需要配置Nginx,HAProxy实现
服务治理(下游服务新增,重启,下线时如何不影响上游调用者)
- RPC:能做到自动通知,不影响上游
- HTTP:需要事先通知,修改Nginx/HAProxy配置
RPC主要用于公司内部服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器调用,APP接口调用,第三方接口调用等等。
RPC和HTTP都可以用于实现远程过程调用,如何选择
- 从速度上看,RPC比HTTP更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩
- 从难度上看,RPC实现较为复杂,http相对简单
- 从灵活性上看,HTTP更胜一筹,因为它不关心实现细节,跨平台,跨语言
两者有不同的使用场景:
- 如果对效率要求更高,并且开发过程使用统一的技术栈,那么RPC还是不错的
- 如果需要更加灵活,跨语言、跨平台,显然HTTP更合适
37.4核8g服务器应用jvm参数如何设置
1、堆内存:
首先是堆内存大小的设置。当我们的机器只有4核8G的时候,堆内存的大小肯定不能太大,一般不建议设置的太大,因为我们需要给机器上的其他应用预留出一部分内容。所以,我们一般建议都是把JVM的堆内存设置成操作系统内存的一半,也就是4G。至于初始内存和最大内存,我们这场景中建议设置成一样的。这样可以避免 JVM 在运行过程中频繁进行内存扩容和收缩操作,提高应用程序的性能和稳定性。
-Xms4096M
-Xmx4096M
2、垃圾收集器选择
在设置了堆空间的总大小之后,我们需要考虑用那种垃圾收集器。另外,我们前面分析过,这个业务中会频繁在新生代创建并销毁对象,那么,就意味着新生代的GC会比较频繁。所以我们需要选择一种在GC过程中STW时间短的,并且在年轻代的回收中也能发挥效果的。
从使用门槛上来说,G1是可以用的,因为一般来说,内存要大于等于4G的话,才适合使用G1进行GC。
所以,我们采用G1作为垃圾收集器:
-XX:+UseG1GC
在使用了G1之后,其实他自己是有一套自动的预测和调优机制的。我们只需要通过参数来设置最大停顿时间就行了。一般建议设置到100-200之间,一般这个时长对用户来说基本无感知:
-XX:MaxGCPauseMillis=200
其次,我们还可以自己调节一些G1的配置,比如设置他的GC线程数,可以先配置4个线程数进行GC,后续根据实际情况再做调整:
-XX:ParallelGCThreads=4 // 设置并行 GC 线程数为 4
-XX:ConcGCThreads=2 // 设置并发 GC 线程数为 2
38.MySQL的组合索引失效和不失效的情况
我们创建一个组合索引(A,B,C)当我使用where后面的查询条件,不论是使用(A)、(A,B) 还是 (A,B,C),在查询时都使用到了联合索引。
那我们打乱查询的顺序,又会是什么效果呢?
即便我们打乱了查询顺序,仍然可以使用到索引,这是因为MySQL中有查询优化器explain,所以sql语句中字段的顺序不需要和联合索引定义的字段顺序相同,查询优化器会判断纠正这条SQL语句以什么
创建索引idx_user_nameAgeSex索引,接下来我们再来看因为不满足最左原则导致的索引失效场景:
由上图可知,因为不满足最左原则,本来要以user_name排序开始,现在user_name断层了,没办法使用后面的索引了,故变成全表扫描了。
那我们有什么办法可以解决这种失效问题呢?
由上图可知,我们查询的不是全表字段,而是索引字段,通过观察发现上面key字段在搜索中也使用了idx_user_nameAgeSex索引,可能许多同学就会疑惑它并没有遵守最左匹配原则,按道理会索引失效,为什么也使用到了联合索引?因为没有从age始匹配,且age单独来说是无序的,所以它确实不遵循最左匹配原则,然而从type字段可知,它虽然使用了联合索引,但是它是对整个索引树进行了扫描,正好匹配到该索引,与最左匹配原则无关,一般只要是某联合索引的一部分,但又不遵循最左匹配原则时,都可能会采用index类型的方式扫描,但它的效率远不如最做匹配原则的查询效率高,index类型类型的扫描方式是从索引第一个字段一个一个的查找,直到找到符合的某个索引,与all不同的是,index是对所有索引树进行扫描,而all是对整个磁盘的数据进行全表扫描。
39.MySQL组合索引如何在实际项目中创建?
联合索引最左匹配原则
联合索引在使用的时候一定要注意顺序,一定要注意符合最左匹配原则。
最左匹配原则:在通过联合索引检索数据时,从索引中最左边的列开始,一直向右匹配,如果遇到范围查询(>、<、between、like等),就停止后边的匹配。
这个定义不太好理解,我解释一下:
假如对字段 (a, b, c) 建立联合索引,现在有这样一条查询语句:
where a = xxx and c = xxx 可以用到 a 列的索引,用不到 c 列索引。
where a like 'xxx%' and b = xxx 可以用到 a 列的索引,用不到 b 列的索引。
where a > xxx and b = xxx 可以用到 a 列的索引,用不到 b 列的索引。
where b = xxx 不使用索引
如何选择合适的联合索引
1.where a = xxx and b = xxx and c = xxx 如果我们的查询是这样的,建索引时,就可以考虑将选择性高的列放在索引的最前列,选择性低的放后边。
如何判断每一个列的选择高低:
(这里计算每一个字段不重复的字段数量和总字段数量的比值)
(意味着字段里边重复的值相对来说会少一些),根据此字段更容易锁定一行,查询效率要更高一些。
2.如果是 where a > xxx and b = xxx 或 where a like ‘xxx%’ and b = xxx 这样的语句,可以对 (b, a) 建立索引。
3.如果是 where a = xxx order by b 这样的语句,可以对 (a, b) 建立索引。
40.索引覆盖和索引下推
索引覆盖是指在一个查询语句中,某个索引已经 “覆盖了” 需要被查询出来的列,此时就不需要进行回表查询了,这就叫做索引覆盖。
比如:创建的索引(ABC),执行sql语句为:
select A,B,C for 表名 where A = “1” and B = “2” and C = “3”;
不需要进行二次回表查询。
索引下推是指在查询非聚簇索时,拿到了叶子结点的聚簇索引,然后对聚簇索引中包含的字段先做判断,直接过滤掉不满足条件的记录,从而减少回表次数,这就是索引下推!!(索引下推是在 MySQL 5.6 之后才引入的,它属于非聚簇索引中功能)
以 user 表中的联合索引(name,age)为例:
select * from user where name='张%' and age='10';
// 表中有四条数据
// 1 张三 10
// 2 张四 11
// 3 张五 12
// 4 老六 13
① 在非聚簇索引中根据 name=‘张%’ 查到聚簇索引中匹配的 id
② 使用匹配的 id 进行回表查询
此时会进行三次回表操作,而联合索引中的 age 字段就没用上。
MySQl 5.6 之后引入索引下推,它会根据 name=‘张%’ 和 age 一起过滤数据:
【好处】:它的第二步操作就可以节省回表的次数
② 使用匹配的 id 进行回表查询
引入索引下推后,只执行了一次回表查询,这就是索引下推的好处。