目录
1.spring的bean创建过程
创建bean的过程:
关键词:
1.PostProcessor(后置处理器、增强器),功能为提供某些额外得扩展功能
2.BeanFactoryPostProcessor,是PostProcessor提供的扩展功能,继承BeanFactory接口,通过BeanFactoryPostProcessor接口可以替换xml文件中的占位符,形成一个完整的BD对象
3.BeanDefinition,初步解析xml文件的时候,都会存储信息到BeanDefinition对象中,然后存储到BeanDefinitionMap
3.BeanFactory接口可以用来操作容器,是容器的入口
生命周期:
关键词:
1.createBeanInstance方法,该方法中完成了实例化的过程,通过反射的方式
2.populateBean方法,set方法完成自定义对象属性赋值操作
3.Aware接口,在创建容器对象时,可以通过实现Aware接口(做一个标记)然后给容器对象统一赋值,例BeanNameAware等类都是实现了Aware接口然后添加了set方法,之后在invokeAwareMethods方法中进行统一调用这些实现了Aware接口实现类的set方法
4.invokeAwareMethods方法,用于给实现了Aware类的实现类赋值(调用set方法,每个实现了Aware类的方法都有set方法)
5.BeanPostProcessor,是PostProcessor提供的扩展功能,继承BeanFactory接口,它提供了前置和后置两种实现方法,对此应用比较多的是aop,无论前置方法还是后置方法都是应用于bean对象的扩展实现(所以结论为:AOP是IOC的整体流程的一个扩展点)
6.invokeInitMethods,该方法是检测bean是否实现了invokeInitBean接口,该接口有一个afterProPertiesSet方法,afterProPertiesSet方法是用来单独设置一个bean对象的属性以及自定义一些逻辑
2.springboot自动装配原理
概述: springboot自动装配原理是通过注解去读取配置文件里提前配置好的信息;
详情附源码:首先从启动类的@SpringBootApplication注解开始,打开SpringBootApplication注解里面主要有三种注解;
@SpringBootConfiguration注解是对Configuration注解做了包装,允许上下文注册其他的bean或引用其他配置类;
@ComponentScan是扫描包下面配置@Componen(@Controller,@Service,@RestController,@Repostory)注解的类,并添加到spring容器中,可以自定义不扫描这些类,可以通过实现TypeExcludeFilter和AutoConfigurationExcludeFilter类来实现过滤不想扫描的类;
@EnableAutoConfiguration是实现自动装配的关键注解,EnableAutoConfiguration注解中有个@Import注解,里面引用了AutoConfigurationImportSelector类,AutoConfigurationImportSelector类是实现了ImportSelector接口,重写selectImports方法,并在selectImports方法中调用了getAutoConfigurationEntry方法
并且在getAutoConfigurationEntry方法中的getCandidateConfigurations方法中通过loadFactoryNames读取到springboot包中的spring.factories文件,spring.factories文件中是要自动装配的包信息,从而实现完整的自动装配流程;
3.mysql索引结构及索引优化
索引结构:mysql索引结构是用的b+树的结构,b+树结构如图所示
B+树特性:
1.B+树的结构每一个节点都是一个区域块,里面可以存放很多的索引的key
2.树结构中叶子节点指的是最后一层的节点,在B+树中非叶子节点不存具体数据,只存指向下一节点的指向,这样可以在相同的内存下存更多的节点;
3.所有节点都是按照从小打大的顺序进行排序并且叶子节点也会存储相邻节点的磁盘地址,这样区间查找更快速
mysql中InnoDB引擎和MyISAM引擎索引区别:
MyISAM:MyISAM用的是非聚集索引(也叫非聚簇索引),原因是在文件中,MyISAM引擎将索引和数据分开存储,那引用b+树的时候,叶子节点存储的其实是对应数据的磁盘地址,所以在查找到索引之后还要去数据文件再通过磁盘引擎才能查找到对应的数据;
InnoDB:InnoDB主键索引用的是聚集索引,在文件中索引和数据是存放在一起的,与MyISAM不同的是B+树结构种的叶子节点存储的是整行数据,所以从结构上来说InnoDB查询更快,但如果是非主键索引,叶子节点记录的就不是整行数据了,而是对应这条数据主键索引的key,然后还是需要通过这个主键索引的key去主键索引查询到对应的数据,所以严格意义上来说非主键索引是非聚集索引;
InnoDB为什么建议设置整型主键并且自增:从上面的描述来看,因为有了主键索引,非主键索引才能查询对应的数据,那如果不设置主键索引,mysql会根据默认rowId隐藏列作为主键索引,但是由于rowId是用户看不见的,用起来不方便,所以还是建议设置主键,至于为什么是整型主键,因为整型好比较,建议用自增是因为自增的时候插入效率更高;
联合搜索引:
联合索引结构:联合索引是按照从左到右的字段进行排序,如下图所示,当第一字段不等则按照第一字段比大小排序,当第一字段相同时,会根据第二字段进行排序,以此类推,当所有字段都相等则按照主键索引做区分;
联合索引为什么用最左原则:根据上述的结构来说,是根据从第一字段第二字段依次排序,所以只有最左原则的时候才能很好的利用联合索引结构,不是最左原则没办法利用最左原则,所以最终还是会走全表扫描;
常见索引失效原因和sql优化的技巧:
4.线程及线程池
实现多线程的三种方式:
1.继承Thread类重写run方法:
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
2.实现runnable接口,在创建Thread类的时候传入一个Runnable实现类对像作为入参
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
3.使用Callable接口和Future对象,创建Callable实现类,重写call方法,该方法可返回结果,通FutureTask(Future的实现类)的get方法可获取结果值
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i=0;i<100;i++){
System.out.println("MyCallable运行次数"+i);
}
//返回值就是表示线程运行之后的结果
return "你好";
}
public static void main(String[] args) {
//线程开启之后需要执行里面的call方法
MyCallable myCallable = new MyCallable();
//可以获取线程执行完毕之后的结果,也可以作为参数传递给Thread对象
FutureTask<String> futureTask = new FutureTask<String>(myCallable);
//创建线程对象
Thread thread = new Thread(futureTask);
//开启线程
thread.start();
try {
//获取返回结果
String s = futureTask.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
三种方式对比优缺点:继承Thread类和实现Runnable接口底层都是创建Thread类,但是由于Runnable是接口,可以多实现,Callable也是接口,所以与Runnable接口相比都有可以多实现的特性,但是从代码量来说Callable可能代码更冗余一些,不过Callable可以返回结果
常用的几种线程池核心参数:
1.corePoolSize:核心线程数,核心线程默认不会被销毁,但是ThreadPoolExecutor对象中有个参数allowCoreThreadTimeOut,当参数allowCoreThreadTimeOut设置为true的时候就和非核心线程一样在没有执行线程的一段时间之后会被销毁;
2.maximumPoolSize:最大线程数,包含非核心线程和核心线程数量,例设置最大线程数为100,核心线程数为20,则非核心线程数为80,非核心线程在没有执行任务的一段时间之后就会被销毁;
3.keepAliveTime,TimeUnit:线程(核心线程需要看allowCoreThreadTimeOut参数)不执行任务的时候的存活时间keepAliveTime是时间数量,TimeUnit是时间单位;
4.workQueue:队列,暂时存放等待执行的线程
5.threadFactory:是创建线程池的入参,如不自己定义则默认用的是DefaultThreadFactory,是用来设置线程池是怎么创建线程的
6.defaultHandler:线程池的策略拒绝策略,大体分为四种(附源码)
CallerRunsPolicy:会调用线程启动该任务
AbortPolicy:抛回异常,默认为该策略
DiscardPolicy:默认不做处理,跳过
DiscardOldestPolicy:删除队列中头部任务,然后像线程池提交任务
常用的几种线程池:(附源码)
1.newCachedThreadPool(可缓存线程池)
特点:没有核心线程,默认时间60秒
2.newFixedThreadPool(固定大小的线程池)
特点:可传入线程数量,数量为核心线程数和最大线程数
3.newSingleThreadExecutor(单个线程的线程池)
特点:核心线程和最大线程数就是1个,空闲存活时间是0,代表一旦线程执行完任务立马销毁线程;
4.newScheduledThreadPool:(可执行延时任务)
特点:可以设置核心线程数,最大线程数为默认数量,可以通过schedule方法设置延时时间
5.newWorkStealingPool:(抢占式线程池)
特点:是1.8之后新增的,用的是ForkJoinPool,原理是把一个任务分成了多个小任务,然后由不同的线程执行完合并成一个结果,之前的线程池是多个任务共用一个阻塞队列,但是newWorkStealingPool是每个线程都有自己的队列,当线程发现自己的队列没任务了,就会从别的线程获取队列任务执行,所以任务执行顺序不确定(源码没看懂 ,不加了)
5.设计模式
6.jvm
jvm内存构成:jvm是由五项内存构成,方法区,堆,虚拟机栈,程序计数器,本地方法栈构成
程序计数器:在以上流程中,程序计数器属于什么角色,首先在转换成二进制字节码时,也可以叫做jvm指令,每个指令都有一个指令地址,程序计数器就是存放了下一行要执行的指令,解释器从程序计数器获取要执行下一行的指令地址进行执行;
程序计数器特点:程序计数器在多线程的情况下,会给每个线程分配一个计数器,用来记录执行的指令,互不干扰,所以程序计数器是线程私有的,并且程序计数器是唯一一个不会存在内存溢出的内存;
虚拟机栈:上图是栈的结构,虚拟机栈它运行的是方法,那么每个方法都叫做栈帧,那么栈帧存储的就是方法里的参数,局部变量,以及方法地址,首先从结构上来看,先存入栈帧1,再存入栈帧2,那么释放的时候一定要先释放栈帧2才能释放栈帧1,通俗的来说 例如a方法调用b方法,b是栈帧2,a是栈帧1,那么一定是先执行完了b方法才能回到a方法继续执行a方法,所以这就是虚拟机栈的规律
虚拟机栈特点:由于虚拟机栈执行的方法,所以还是一样是线程私有,互相不干扰
虚拟栈内存溢出:方法递归陷入死循环可能会导致栈帧过多导致内存溢出,或者栈帧过大(这种情况比较少);
本地方法栈:结构用法与虚拟机栈类似,只是本地方法栈的方法不是java代码的,而是其他语言,例如c语言等;
堆:堆普遍存放的都是对象,数组,以及垃圾回收机制;
堆的特点:堆是线程共有的;
堆的内存溢出:堆有垃圾回收机制,但是只能回收不被使用的对象,当使用的对象达到一定大的程度的情况下依旧会内存溢出;
方法区:方法区是一种概念,而元空间和永久代是具体的实现,在1.8之前系统使用的是永久代来存储类信息以及常量池等,1.8之后是用元空间来存储类信息,用堆来存储常量池;
永久代和元空间的区别:永久代是存放在jvm内存的,元空间是存放在本地内存的;
常量池:常量池就是一个表,里面记录了要执行的方法名,类名,参数类型等基本信息,然后当代码转成jvm指令的时候,指令就是通过这个常量池来获取需要的信息;
7.HashMap底层
结构:底层是数组加链表(1.8之后达到阈值8+红黑树结构)
1.数组特性:是连续性不可分割的结构,由于有索引的存在所以查询快,而删除插入操作需要更改操作的位置及以后的所有元素的索引下标,所以慢;
2.链表特性:是不连续性可分割的结构,通过next指向下一个元素(有prev是指向上一个元素,只有next是单向链表,next和prev都有是双向链表),但必须有一个头节点和一个尾节点,所以当查询时需要找到头节点,然后通过next一个一个的找到指向下一个的元素,所以查询慢,但插入和删除的时候只需要更改next就可以,所以插入删除快;
3.红黑树结构:红黑树结构是一种非严格意义的均衡二叉树结构,大体为左中右结构,分别对应小中大,大体意思就是左边是是比节点小的值,右边是比节点大的值,常规来说就是查询某一个值,该值与节点进行比较,不相等比节点大走右边比节点小走左边,一直到能寻找到该值
put过程:
put过程:首先数组存储一个对象,对象内容大体为key,value,hash值,指向链表的下一个指针,存储值时通过哈希算法计算出应该存放的数组索引位置,然后将对象存放在索引对应的数组里,当出现索引重复,则引入链表结构,将索引重复的值存在链表中,通过指针指向下一个,后存入的数据放入链表头部,先前放入的数据按照顺序依次往后,第一个数据为尾部(1.8之后从头插法变为尾插法,避免链表闭合问题),如果链表长度超出阈值8,并且数组长度等于大于64,则引入红黑树结构;
多线程情况下使用hashMap可能导致的问题:
链表闭合问题:在1.8之前,每次链表达到一定长度则需要扩容操作,有可能这个扩容过程是需要新建一个新的哈希表,将原来的数据导入到新的哈希表中,这个过程中会重新计算当前数据存在数组的位置和链表指向下一元素的位置,当多个线程同时操作的时候一定概率下会导致例如元素a与元素b元素c哈希值一样,进而计算出的存放数组的索引位置一样,索引位置一样则需要放入链表中,但是由于是多个线程同事操作,可能会造成a指向b,b指向c,c又指向a这种链表闭环情况,从而在get的时候死循环;
size与实际长度不符:hashMap存放数据的时候,每存放一个数据size就加1,但是多线程操作可能会导致同时加入多个元素,元素加进去了 但是size加1的操作只触发了一次,所以导致size与实际长度不符合;
数据丢失:在上述put过程中有讲过链表是通过哈希算法计算出来数组的存放位置的,但是如果是多线程同时操作,例如线程1存放元素a,计算数组索引位置为1,线程2存放元素b,计算出索引位置也是1,但这种情况下就可能出现元素覆盖的情况
8.微服务架构
微服务架构概述:微服务是指将业务进行拆分,每个业务都是一个服务,那么如何使用这些微服务则涉及到的五大组件;
eureka:eureka是注册中心,目的是为了统一管理这些微服务,例如a服务调用b服务,那么a是消费者,b是提供者(生产者),b可能是多台机器,那么为了统一管理这些服务,则将所有微服务信息包括eureka自己都注册到eureka中,然后当a调用b时,a从注册中心拉取到b的注册信息,然后通过去访问b服务,至于访问的是哪个b服务,就要依靠负载均衡来分配了
9.redis
redis为什么快:
1.基于内存进行存储
2.redis使用单线程处理关键命令,所以避免了不必要的线程转换和加锁降低性能的开销
3.redis使用了非阻塞io模型,一个线程可以响应多个客户端的事件,可以针对不同的事件调用不同的函数进行处理,避免了轮询去关注客户端是否有事件发生
redis的三种集群:
主从集群:主从集群里,一个master对应多个slave,master负责读写数据,slave负责读取数据,当一个写操作执行的时候,先到master进行写操作,之后master会将数据同步给slave,这样实现一个读写分离的操作,但是缺点是没有自主修复性,当master服务挂掉之后没有机制选出新的master,为了解决修复性,有了哨兵机制;
哨兵机制:哨兵机制就是在master挂掉之后会从剩下的slave选取出一个新的master继续工作,但是这样还是有一个在线扩容的问题,所以就有了第三种集群RedisCluster;
RedisCluster:cluster通过分片的方式实现了分布式存储,并且引用了slot槽进行数据分片,每个节点都有一个slot值(0-16383)区间范围,当数据进行存储的时候,根据key可以计算出slot值,然后将这个数据存储在对应的区间范围内;
redis持久化的两种方式:
RDB:rdb是每隔一段时间就会将内存中的数据快照写入磁盘,形成一个rdb临时文件,然后写入成功之后再替换之前的文件,这个实际的操作过程是fork了一个子线程执行的,所以不会耽误redis主线程的性能;
AOF:aof是通过日志的形式进行持久化的,每个写,删除操作都会被记录到aof缓存区,再通过缓存区同步将日志记录下来,通过这样的方式达成持久化的目的,aof提供了三种同步策略,每秒同步,修改同步和不同步,由于通过日志记录,所以文件比较大,所以提供了rewrite机制来对日志文件进行压缩处理
二者对比:从安全性来说aof大于rdb,因为aof是通过追加的方式来进行持久化的,但从性能上来说rdb大于aof;
redis如何和mysql保持数据一致性:使用延时双删策略,先删除redis缓存,再读取mysql,然后延时几百毫秒之后再删除redis,这样可以保证当修改mysql数据的时候就算有其他线程读取到了老数据存在了redis中,也会被redis删掉;
redis的缓存穿透,缓存击穿和缓存雪崩:
场景描述:使用redis中,一般都是前端请求数据,先从redis读取数据,redis没有再去mysql查询,当mysql读取到数据了则把数据返回给前端,并将数据缓存在redis中;
缓存穿透:在这个redis使用场景中redis查询不到,mysql也没有,当有人恶意去用不存在的条件例如id=-1这种查询,会损耗mysql的性能乃至崩掉
穿透解决方案:
1.对参数进行合法性校验;
2.将mysql不存在的数据结果也存在redis中,不过为了避免过大的占用redis内存,可以将过期时间设置的短一些;
3.引用布隆过滤器,可以提前将数据库的id存在过滤器中,当查询id的时候过滤器会直接过滤掉不存在的id,不过布隆可能会存在一定的误判率;
缓存击穿:在这个redis使用场景中redis查询不到,mysql有,一般都是redis设置的数据过期了,然后在mysql同步数据到redis需要一定的时间,在高并发的场景中,大量数据请求同时打入到mysql中,会瞬间对mysql造成过大的压力
击穿解决方案:
1.给热门缓存设置永不过期,但这种数据也不会更新,所以需要在业务逻辑里加一个过期时间,然后通过另一个线程定时扫描这个过期时间进行更新数据
2.可以从放高并发的逻辑处理,可以在mysql同步redis的过程中加个锁;
缓存雪崩:在这个redis使用场景中redis查询不到,mysql有,这个跟击穿场景类似,区别在于雪崩是大批量缓存过期,当查询某一批数据的时候这批数据都过期了,对mysql造成过大的压力;
雪崩解决方案:
1.可以延用击穿的方案,对热点缓存设置永不过期;
2.可以通过避免大批量缓存同时过期的方式去考虑,对这些缓存设置不同的缓存时间,例如缓存时间基础时间是5秒,然后在这个5秒基础上再加一个随机数;
10.mq
三种常用的mq:
RabbitMq:rabbitMq是一种轻量级的mq,优点是简单方便,并且客户端支持的变成语言大概是最多的,但是性能应该是最差的性能,支持每秒几万到十几万的消息处理;
Kafka:kafka是分布式订阅式消息队列,需要配合zookeeper一起使用,kafka对周边生态兼容是最好的,并且性能高效,可以支持持久化,开发设计中使用了大量异步和批量的思想,所以消息的处理也是非常快的,尤其是异步收发的性能最好,直至每秒几十万的消息处理,但是由于异步的原因kafka的收发消息的延时会比较高,因为kafka在发消息的时候不会立即发送,而是攒一攒再发送,并且topic超过上百个的时候吞吐量会大幅下降;
RocketMq:rocketMq是阿里参考了kafka进行设计的,对于消息的处理可靠性跟容错性都很好,尤其是对延时响应这里的时候做了优化,可以做到毫秒级的响应,如果程序中需要要求及时响应可以使用rocketMq,每秒有几十万的消息处理量,但是由于是阿里自己开发的并且出来的比较晚,所以对生态兼容不是很好;
mq如何保证消息不丢失:
消息队列消息丢失原因:
1.生产者发送给mq消息丢失;
2.mq宕机重启导致消息丢失;
3.队列把消息发给消费者,消费者出现异常上抛异常但是消息已经接受了,但是处理失败导致丢失;
以rabbitMq为例解决丢失问题:rabbitMq从以上三方面考虑,第一种情况可以开启确认模式,生产者发送消息给rabbit之后,可以获取到发送是否成功的参数,然后进行重发或者其他操作,第二种情况可以设置消息持久化保证消息不会因为宕机丢失,第三种情况需要把确认收到消息的这个操作交给消费者自己来处理,其他mq应该都有类似的机制来处理这个问题;
mq如何保证消息不重复:
出现重复的原因:
1.生产者发送消息给mq的时候由于网络问题或者mq自身问题导致存储消息之后并没有对生产者进行一个响应告诉生产者收到消息,生产者又重新发送了消息导致消息重复;
2.消费者收到消息,由于网络或者消费端的问题导致没有告诉mq消费成功,mq又重新发送导致重复;
解决重复问题:从幂等性考虑问题,保证最终接收的消息对数据造成的影响只有一次
1.通过mvcc,也叫多版本并发控制来实现,是乐观锁的一种体现,发送消息带上当前数据版本号+1,消费者接收到消息要是版本号对不上则消费失败;
2.用去重表操作,将操作的数据例如id当作唯一标识,如果已经存在对这个数据的操作则不再执行这个操作;
11.数据库事务
1.事务四大特性:
原子性:是指事务包含的所有操作要么全部成功要么全部失败
一致性:是指事务从一个一致性状态变成另一个一致性状态,事务前后都必须保持一致性状态
隔离性:在多个用户访问数据库操作同一张表的时候,互相的事务不能发生干扰,多个并发的事务要做到互相隔离,对于任意两个并发的事务,要做到t1如果要执行当前事务,要么在t2完成后执行 要么在t2发生前之前执行
持久性:当事务一旦提交所有被改变的数据都是永久性的改变
2.事务隔离性:
说到事务隔离性要首先说一下当没有事务隔离性会发生的几种情况
脏读:就是当事务A修改数据未提交事务的时候事务B来读取了事务A修改之后但是未提交事务的数据,就会导致两个事物数据不一致,这就是脏读
不可重复读:不可重复度是当事务A读取到数据之后而事务B立马修改了该条数据,当事务A再次查询的时候发现前后数据不统一,与脏读的区别是不可重复读读取到的不是未提交事务时候的数据
幻读:幻读是事务非独立执行所发生的一种情况,当事务A做一个将全部数据某一列从1修改为2的时候,但这个时候事务B添加了一条为1的数据,当事务A修改之后会发现还有一条数据没有修改为2,但实际上这条数据是事务B添加的,就像是刚才的操作是产生了幻觉一样,所以为幻读
3.四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
12.spring知识小点
1.拦截器和过滤器的区别
过滤器和拦截器都是aop的一种实现,是解决某一类问题的工具。
出身不同:拦截器来自spring,过滤器来自servlet。
作用范围不同:过滤器依赖于tomcat容器,所以只能在web程序调用,拦截器是spring容器进行管理,所以可以用在非web容器上。
原理不同:过滤器基于过滤器链ApplicationFilterChain实现的,拦截器基于反射实现的。
一个请求是先打到servlet,再到interceptor,拦截器多了对于springmvc生态组件的一个操作能力。
2.@Autowired和@Resource的区别
@Autowired默认是根据bean的类型进行依赖注入的,当存在多个相同类型的bean的时候ioc容器会报错,可以使用@Primary注解来解决(给多个相同类型的bean的某一个声明@Primary,表示这是个主要bean,以这个bean为准),也可以使用@Qualifiler来解决(相当于加一个筛选条件,让Autowired根据bean的名称去查询bean)
@Resource支持name和type两种方式的,默认是根据name查找的,当查询name查询不到的时候会再根据type去查询,都没有的情况下会报错