一.MySql篇
1优化相关问题
1.1.MySql中如何定位慢查询?
慢查询的概念:在MySQL中,慢查询是指执行时间超过一定阈值的SQL语句。这个阈值是由
long_query_time
参数设定的,它的默认值是10秒1。也就是说,如果一条SQL语句的执行时间超过了long_query_time
所设定的时间,那么这条SQL语句就被认为是慢查询。现在的问题就是:出现页面加载过慢,响应时间过长。我怎么确定就是我sql的问题呢?就算是sql的问题,我应该怎么找出执行慢的那条sql语句呢?
方案一:使用开发工具
以Skywalking为例。这个工具,可以直观看到每个接口的执行时间。还可以追踪进接口,详细的看耗时情况,其中也包含了sql的耗时。
方案二:使用MySql自带的慢日志
简单来说,这个日志记录了执行时间较长的sql。
总结:
1.2.索引相关概念
基础概念:
在MySQL中,索引是一种数据结构,用于加快数据库查询的速度和性能。索引可以帮助MySQL快速定位和访问表中的特定数据,就像书籍的索引一样,通过存储指向数据行的指针,可以快速查找到需要的页面。
底层数据结构 (B+树):
1.磁盘读写代价更低:非叶子节点不存放数据,只存储指针,相对来说存储压力低。数据只存储在叶子节点,在查询比对的时候,就不会把非叶子节点上的数据也加载出来了。
2.查询效率稳定:因为数据都存储在非叶子节点,在查询时都要从根节点开始对比。最终到叶子节点获取数据。
3.便于扫库和区间查询:叶子节点之间采用双向指针。在范围查询时更加方便。比如说:我们现在要查找索引为6~34区间的数据。先从根节点出发找到比38小的16,再从16往左找到叶子节点6,由于叶子节点之间有双向指针,因此6~34区间的数据都能获取!(这个区间内的数据不需要再从根节点再次查找)
总结:
1.什么是索引:
索引在项目中还是比较常见的,它是帮助MySQL 高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO 成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU 的消耗。2.索引底层的数据结构:MySQL的默认的存储引擎 InnoDB 采用的 B+ 树的数据结构来存储索引,选择B+ 树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+ 树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是 B+ 树便于扫库和区间查询,叶子节点是一个双向链表。3.B树和B+树的区别:第一:在B 树中,非叶子节点和叶子节点都会存放数据,而 B+ 树的所有的数据都会出现在叶子节点,在查询的时候,B+ 树查找效率更加稳定。第二:在进行范围查询的时候, B+ 树效率更高,因为 B+ 树都在叶子节点存储,并且叶子节点是一个双向链表。
1.3.聚簇索引和非聚簇索引
这里的主键id就是聚集索引,每个叶子节点存放的数据,就是当前叶子节点主键id所对应的一行数据!
假如我们这里把name字段也添加了索引,它就是非聚集索引 (二级索引)。它的叶子节点所存储的数据,就是当前name对应的主键id。比如说叶子节点Arm,存储的数据就是:10。
1.4回表查询
现在我们有一条sql语句:select * from user where name = 'Arm'
由于我们给name字段添加了索引。这里的执行顺序就是:
1.先去二级索引里面找Arm,先跟根节点Lee比对,A在L的左边,找到Geek,A在G的左边。找到叶子节点Arm,该叶子节点存储的数据是Arm的主键id:10。
2.因为这里我要获取Arm的所有信息 * 。因此,二级索引无法提供完整信息,我还要根据找到的主键值10,去聚集索引里面查询。
3.重复类似过程一,从聚集索引里面找到主键id = 10的叶子节点。该叶子节点存储的数据,就是id = 10 的所有信息!
这整个过程就是回表查询
总结:
1.聚集索引和非聚集索引
聚簇索引主要是指数据与索引放到一块,B+ 树的叶子节点保存了整 行数据,有且只有一个,一般情况下主键在作为聚簇索引的。非聚簇索引值的是数据与索引分开存储,B+ 树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引2.回表查询回表查询跟聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表。
1.5.覆盖索引
MySql超大分页处理问题:
解决方案:
先根据id排序并从9000000条数据里面获取10条。这里根据id排序并返回id走的是覆盖索引。
再跟之前的表做关联,做一个等价查询。这样性能就有所提升了。
第一条sql是把9000010条数据全部提交给server层,然后再根据limit的第一个参数丢弃前9000000条数据,只要10条数据。
而第二条sql,是把9000010条id给server操作,相比来说就优化了。
总结:
1.覆盖索引:
覆盖索引是指select 查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id 查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select * ,尽量在返回的列中都包含添加索引的字段。2.MySql超大分页处理超大分页一般都是在数据量比较大时,我们使用了limit 分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决先分页查询数据的 id 字段,确定了 id 之后,再用子查询来过滤,只查询这个id列表中的数据就可以了因为查询id 的时候,走的覆盖索引,所以效率可以提升很多。
1.6.索引创建的原则
总结:
一般表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql 的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。
1.7.索引失效的情况
这是正常查询情况,满足最左前缀,先查有先度高的索引。
1.
注意这里最后一种情况,这里和上面只查询 name = '小米科技' 的命中情况一样。说明索引部分丢失!
2.
这里第二条sql中的,status > '1' 就是范围查询
3.
这里sql中对name索引进行了截取,导致索引失效。
4.
发送类型转换,导致索引失效。
5.
1.8.sql的优化经验
这里解释一下SQL语句的优化的原理
1.指明字段名称,可以尽量使用覆盖索引,避免回表查询,因此可以提高效率
2.字面意思,无需过多赘述。索引就是为了提高查询效率的。
3.图中两条sql直接可以使用union all 或者 union连接。使用union all 的话,就会把两条sql所有符合条件的查询结果直接合并。使用union的话,就会多一步过滤,过滤掉二者的重复项。效率相对较低。
4.避免在where子句使用对字段进行表达式操作,这也是因为可能会造成索引失效。如:1.7中索引失效的第三种情况
5.如下图,这里有两个循环。循环三次的可以看作小表,循环1000次的看作大表。
小表放外面,MySql只需要连接三次数据库,然后在里面执行1000次操作。
而把大表放外面,就相当于要连接1000次数据库!每次执行三次操作。
而使用内连接,就会优先把小表放外面,大表放里面。而左右连接并不会改变顺序(因为编写sql的时候就已经确定了两个表的位置了)。因此优先使用内连接。
当有写操作进来的时候,就会走Master数据库,进行写操作,然后Master数据库再把数据同步到Slave数据库。
当有读操作进来的时候,就直接走Slave进行查询。
从而实现读写分离。
二.框架篇
1.Spring框架
1.1.Spring框架中的bean默认情况下是单例的吗?
Spring框架中的bean默认情况下是单例的。Spring中有一个注解@Scope,这是用来设置bean的情况。默认情况下设置为singleton(单例的)
1.2.Spring框架中的单例bean是线程安全的吗?
简单来说就是:判断当前成员变量能不能修改。如果是int这种可修改的类型,线程就不是安全的。
如果是,Service、DAO这些无状态类 (没有什么可变状态)就算是线程安全的。
总结:
不是线程安全的,是这样的:当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。Spring框架并没有对单例bean 进行任何多线程的封装处理。关于单例 bean的线程安全和并发问题需要开发者自行去搞定。比如:我们通常在项目中使用的Spring bean都是不可可变的状态 (比如Service类和DAO 类 ) ,所以在某种程度上说 Spring 的单例 bean是线程安全的。如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由 “ singleton ”变更为“prototype”。
1.3.什么是AOP?项目中有没有使用AOP?
AOP的概念及常见使用场景:总结:aop是面向切面编程,在 spring 中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等。实际项目中使用AOP的例子:在后台管理系统中,就是使用 aop 来记录了系统的操作日志 主要思路是这样的,使用 aop 中的环绕通知 + 切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库。
1.4.Spring中事务是如何实现的?
这里的"保存用户"方法,就是我们要加事务的方法。在该方法上添加@Transactional注解。
总结:
声明式事务管理实际上是基于Spring的AOP(面向切面编程)机制实现的。当你在方法上使用@Transactional注解时,Spring会在运行时创建一个代理对象,这个代理对象会在方法调用前后插入事务管理的代码,从而实现事务的自动管理。
具体来说,当Spring容器在运行时遇到带有@Transactional注解的方法时,它会创建一个代理对象来拦截这个方法。在代理对象拦截方法调用时,Spring会在方法调用前后分别插入开始事务和结束事务的代码。如果方法执行过程中抛出异常,Spring会根据异常类型决定是否回滚事务。
因此,虽然在使用声明式事务管理时,开发者不需要自己编写AOP代码,但是Spring仍然使用了AOP技术来实现事务管理。这也是为什么有时候我们会说声明式事务管理是基于AOP的。
1.5.Spring中事务失效的场景
这种情况下,自己把异常处理了,导致@Transactional没有发现异常,事务没有回滚。
Spring默认只会回滚非检查异常也就是runtime异常。而这里的FileNotFound是一个检查异常,并不能被捕获。
Spring为方法创建代理,添加事务通知,前提条件都是该方法是public修饰的!
1.6.Bean的生命周期
1.BeanDefinition
2.构造函数
在此期间,调用Bean的构造函数,实例化对象 (但是还未赋值!)
3.依赖注入
Spring容器会将Bean的属性设置为Bean定义中指定的值。这个过程也被称为依赖注入,因为Spring容器会自动寻找并注入Bean所依赖的其他Bean。
4.Aware接口
用于增强Bean的功能
如果Bean实现了以Aware结尾的接口,就要重写里面的方法。图中三个接口,就是分别用于在Bean的初始化过程中,用于获取:Bean的名字、Bean的工厂、容器的上下文对象(Spring容器本身)。
5.BeanPostProcessor#before
bean的后置处理器,在初始化方法调用之前执行。
6.初始化方法
1.InitalizingBean接口,实现了该接口,就要实现里面的方法。而在这一步就会执行重写方法。
2.自定义init:在bean中某个方法上标注了@PostConstruct注解。就会在这一步执行这个方法。
7.BeanPostProcessor#before
bean的后置处理器,在初始化方法调用之后执行。
当一个类的功能被增强了使用到了AOP,大概率就是使用后置处理器被增强的。
8.销毁Bean
如果在哪个方法上标注了@PreDestroy方法,Spring容器在关闭前就会调用该方法。
注:
1.Bean的创建和初始化是分开的,第2步是创建bean,3-7是初始化赋值
2.第5步和第7步的两个后置处理器。都是某个类实现了BeanPostProcessor接口,所重写的两个方法。分别在初始化方法前后执行。
测试代码:
第一个类User类,作为我们测试的Bean
import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @Component public class User implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean { public User() { System.out.println("1.User的构造方法执行了........."); } private String name; @Value("张三") public void setName(String name) { System.out.println("2.依赖注入执行了.........."); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("3.BeanNameAware的实现方法执行了......."); } @Override public void setBeanName(String s) { System.out.println("3.BeanNameAware的实现方法执行了......."); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("3.ApplicationContextAware的实现方法执行了......."); } @PostConstruct public void init() { System.out.println("5.自定义初始化方法执行了.........."); } @Override public void afterPropertiesSet() throws Exception { System.out.println("5.InitializingBean的实现方法执行了.........."); } @PreDestroy public void destroy(){ System.out.println("7.destroy方法执行了........."); } }
第二个类:实现了BeanPostProcessor重写了其中方法的类。里面是两个后置处理器。
import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("user")) { System.out.println("4.postProcessBeforeInitialization方法执行了 -> user对象初始化方法之前执行"); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("user")) { System.out.println("6.postProcessAfterInitialization方法执行了 -> user对象初始化方法之后执行"); } return bean; } }
执行结果
destroy方法要在容器关闭时执行
总结:
1.首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息, 这里面就封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息。
2.在创建bean的时候,第一步是调用构造函数实例化bean
3.第二步是bean的依赖注入,比如一些set方法注入,像平时开发用的@Autowire都是这一步完成。
4.第三步是处理Aware接口,如果某一个bean实现了Aware接口就会重写方法执行。
5.第四步是bean的后置处理器BeanPostProcessor,这个是前置处理器。
6.第五步是初始化方法,比如实现了接口InitializingBean或者自定义了方init-method标签或@PostContruct。
7.第六步是执行了bean的后置处理器BeanPostProcessor,主要是对bean进行增强,有可能在这里产生代理对象。
8.最后一步是销毁bean。
1.7.Spring中bean的循环引用
在Spring中bean的生命周期中的构造方法执行时,只是把bean对象创建出来了。但还未进行初始化。初始化的第一步就是进行依赖注入,对当前bean所需要的bean进行注入。这个时候,如果出现:A依赖注入B,B就会去创建并初始化。但如果此时出现了:B初始化时,也需要A的实例。两个半成品Bean,就会死循环,谁都创建不出来!
解决循环依赖的方法:
注:这里的一级缓存中,存放的是单例的Bean。对于多例Bean,则是在获取Bean的时候才会被创建,并不会提前被创建存放在一级缓存中。
三级缓存解决循环依赖图解:
注意:这里三级缓存相较于二级缓存的有点就是:三级缓存中的ObjectFactory既可以生成A(原始对象)也可以生成A的代理对象。而二级缓存是没法把代理对象放入二级缓存中。
而在这个过程中:我们ObjectFactory产生的B的半成品Bean和A的代理对象半成品Bean都会放在二级缓存中,之后往A中注入B从而产生完整的A,就是从二级缓存中拿B的半成品Bean。因为,如果多次调用ObjectFactory,就可能产生多例的情况!
总的来说:要解决循环依赖问题,1、2、3级缓存需要相互配合,都必不可少!
三级缓存能解决的是出现在Bean初始化时的循环依赖问题,但如果是在构造方法时出现的循环依赖,就要我们手动解决了。如:
当Bean被注入到另一个Bean中时,Spring并不是直接将这个Bean实例化并注入到另一个Bean中,而是使用一个代理对象来代替实际的Bean。在这里就是:创建A实例的时候,Spring先创建一个B的代理对象用于A的构造方法初始化。从而解决了循环依赖问题。
总结:
1.循环依赖:循环依赖其实就是循环引用 , 也就是两个或两个以上的 bean 互相持有对方 , 最终形成闭环。比如 A 依赖于 B,B 依赖于 A循环依赖在spring 中是允许存在, spring 框架依据三级缓存已经解决了大部分的循环依赖① 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean 对象② 二级缓存:缓存早期的 bean 对象(生命周期还没走完)③ 三级缓存:缓存的是 ObjectFactory ,表示对象工厂,用来创建某个对象的2.三级缓存解决循环依赖的具体流程:第一,先实例 A 对象,同时会创建 ObjectFactory 对象存入三级缓存singletonFactories第二, A 在初始化的时候需要 B 对象,这个走 B 的创建的逻辑第三, B 实例化完成,也会创建 ObjectFactory 对象存入三级缓存singletonFactories第四, B 需要注入 A ,通过三级缓存中获取 ObjectFactory 来生成一个 A 的对象同时存入二级缓存,这个是有两种情况,一个是可能是A 的普通对象,另外一个是A 的代理对象,都可以让 ObjectFactory 来生产对应的对象,这也是三级缓存的关键第五, B 通过从通过二级缓存 earlySingletonObjects 获得到 A 的对象后可以正常注入,B 创建成功,存入一级缓存 singletonObjects第六,回到 A 对象初始化,因为 B 对象已经创建完成,则可以直接注入 B , A创建成功存入一次缓存singletonObjects第七,二级缓存中的临时对象 A 清除
1.8.SpringMVC的执行流程(介绍核心组件)
SpringMVC核心组件的功能:
- DispatcherServlet:前端控制器 (调度中心),用于接收HTTP请求并将其分派给相应的处理器。DispatcherServlet是整个请求处理的流程控制中心,它降低了组件之间的耦合性。
- HandlerMapping:用于确定HTTP请求需要被哪个处理器处理。HandlerMapping根据请求URL查找相应的处理器(就是Controller层对应路径的方法)。这个save方法就是一个处理器。
- HandlerAdapter:1.用于调用处理器(Handler比如说这里的save方法)并处理HTTP请求。2.处理Handler中的参数和返回值。HandlerAdapter会调用处理器,并返回一个ModelAndView对象,其中model是一个Map结构,存放了返回的所有数据,view是逻辑视图名,即ViewName。
- ViewResolver:用于确定响应结果的视图。ViewResolver根据ModelAndView对象中的逻辑视图名解析出具体的视图文件,再生成对应的View视图对象。
如果是基于接口开发,很多返回值是json格式,并没有ModelAndView对象。
处理器中某个方法添加了@ResponseBody注解,这个注解会把当前方法的返回值转化为json格式并响应给前端。