Skip to content

Latest commit

 

History

History
640 lines (355 loc) · 43.3 KB

问题.md

File metadata and controls

640 lines (355 loc) · 43.3 KB

JVM

G1和CMS对比
  1. G1分Region的内存布局,只存在逻辑上的分代,CMS是老年代垃圾回收器。G1可以局部回收,CMS整个区域回收。

  2. G1可以指定最大停顿时间。G1从整体上采用标记整理的算法实现回收器,从局部上采用复制算法实现,CMS采用标记清除算法实现。使得G1不会产生大量的空间碎片。

  3. G1局部回收,CMS整体回收

JVM8为什么要增加元空间?

永久代

永久代中包含类信息、JIT编译后的代码、常量池、接口方法字段等信息。JVM运行时会用到多少永久代的空间取决于应用程序用到了多少类。如果JVM发现有的类已经不再需要了,它会去回收(卸载)这些类,将它们的空间释放出来给其它类使用。FullGC会进行持久代的回收。永久代大小MaxPermSize设置,默认64M。永久代用完会抛OutOfMemoryError"PermGen space"异常。

为什么移除持久代 ?

  1. 它的大小是在启动时固定好的--很难进行调优。-XX:MaxPermSize
  2. HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)
  3. 简化FullGC:每一个回收器有专门的元数据迭代器
  4. 可以在GC不进行暂停的情况下并发地释放类数据。

元空间

1.8 持久代的空间被彻底地删除了,被元空间所替代了。持久代删除了之后,很明显,JVM会忽略PermSize和MaxPermize这两个参数,还有就是你再也看不到java.lang.QutOfMemorvError; PermGen error的异常了。原来类的静态变量和Interned strings都被转移到了java堆区。class相关的信息被存储在元空间。HotSpot使用本地内存存储元空间数据。

元空间的特点:

  • 类及相关的元数据的生命周期与类加载器的一致

  • 每个加载器有专门的存储空间

  • 只进行线性分配

  • 不会单独回收某个类

  • 省掉了GC扫描及压缩的时间

  • 元空间里的对象的位置是固定的

  • 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉。


Synchronized 和 ReentrantLock

等待可中断:是指当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 改为处理其他事情。 可中断特性对处理执行时间非常长的同步块很有帮助。公平锁: 是指多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来依次获得锁; 而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。 synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。 在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件, 如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可。当JDK 6中加入了大量针对 synchronized锁的优化措施synchronized是在Java语法层面的同步, 足够清晰, 也足够简单。 每个Java程序员都熟悉synchronized,但J.U.C中的Lock接口则并非如此。 因此在只需要基础的同步功能时,更推荐synchronized。Lock应该确保在finally块中释放锁, 否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。尽管在JDK 5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利了。从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用JUC中的Lock的话,Java虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。

公平锁和非公平锁的优缺点

公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

偏向锁为何要延时:

偏向锁有个时延,默认为4秒。Why?因为JVM虚拟机自己有一些默认启动的线程。里面有好多sync的代码,这些代码启动的时候就知道肯定会有竞争,如果启动偏向锁,就会造成偏向锁不断的进行锁撤销和升级的过程,效率较低。

线程数量设定
  • CPU密集型程序:一个完整请求,I/O操作可以在很短时间内完成,CPU还有很多运算要处理,也就是说CPU计算的比例占很大一部分,线程等待时间接近0

    1. 单核CPU:一个完整请求,I/O操作可以在很短时间内完成,CPU还有很多运算要处理,也就是说CPU计算的比例占很大一部分,线程等待时间接近0。单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程。

    2. 多核CPU:如果是多核CPU处理CPU密集型程序,我们完全可以最大化的利用CPU 核心数,应用并发编来提高效率。CPU密集型程序的最佳线程数就是:理论上线程数量 = CPU核数(逻辑),但是实际上,数量一般会设置为CPU核数(逻辑)+ 1(经验值),CPU密集型的线程恰好在某时因为发生一个错误或者因其他原因而暂停,刚好有一个"额外"的线程,可以确保在这种情况下CPU周期不会中断工作。

  • I/O 密集型程序:与CPU密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说I/O操作占比很大部分,等待时间较长,线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程。

    1. I/O密集型程序的最佳线程数就是:最佳线程数 = CPU核心数(1/CPU利用率)= CPU核心数(1 + (I/O耗时/CPU耗时))

    2. 如果几乎全是I/O耗时,那么CPU耗时就无限趋近于0,所以纯理论你就可以说是2N (N=CPU核数),当然也有说 2N +1的,1应该是backup

    3. 一般我们说2N + 1就即可


集合

HashMap数组长度为啥是2的倍数?
  • hashmap通过h & (table.length -1)来定位存在数组哪个槽位,当数据长度为2的n次方时 h & (table.length -1) 运算等价于 h%length,&比%更加高效。
Jdk1.7和Jdk1.8中HashMap的区别?
  1. 1.8中如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容),链表长度小于6是退化为链表

  2. 发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入。

  3. Entry被Node替代。

HashMap为啥在Jdk1.7中用头插法在Jdk1.8中用尾插法?
  1. 1.7采用头插法因为插入一个节点如果使用尾插法,需要遍历链表到尾节点,而头插法只需要将数组的指针指向新节点,新节点指针指向原头节点。
  2. 1.8采用尾插法因为1.8中引入红黑树,链表或者红黑树不会太长,而且如果采用头插法需要重构树。
  3. 1.7中采用头插法,数组扩容过后,一个槽位链表上节点的顺序会颠倒。
  4. 1.7中头插法在多线程时链表可能会死循环。
说说你对红黑树的见解?
  • 每个节点非红即黑
  • 根节点总是黑色的
  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
  • 每个叶子节点都是黑色的空节点(NIL节点)
  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
Jdk1.8中为什么使用红黑树而不使用AVL树?
  • 性能:红黑树相比 AVL 树,最主要的优点是其插入和删除操作的时间复杂度为 O(log n),而 AVL 树的插入和删除操作的时间复杂度为 O(log log n)。在大数据量的情况下,红黑树能够提供更快的操作速度。

  • 实现的复杂性:红黑树相比 AVL 树,其平衡调整的规则更简单,因此在实现上更为简洁。这使得 HashMap 的实现更为简洁,减少了代码量,降低了维护的复杂性。

Jdk1.8中为什么使用红黑树而不使用二叉查找树?
  • 二叉查找树在特殊情况下会变成一条线性结构,降级为链表,遍历查找会非常慢。红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,红黑树属于平衡二叉树。
为什么HashMap链表长度超过8会转成树结构?
  • HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8并且map中数组长度大于64时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。红黑树中的TreeNode是链表中的Node所占空间的2倍,虽然红黑树的查找效率为O(logN),要优于链表的O(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。所以要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。之所以是8,是因为Java的源码贡献者在进行大量实验发现,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此时的链表性能已经已经很差了,操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,在这种极端的情况下才会把链表转换为红黑树,链表转换为红黑树也是需要消耗性能的,为了挽回性能,权衡之下,才使用红黑树,提高性能的,大部分情况下hashMap还是使用链表。
Jdk1.7中HashMap为啥会死循环?
ConcurrentHashMap扩容触发时机?
  • ConcurrentHashMap中的元素大于阈值(数据长度乘以负载因子)

  • 当链表达到阈值(8),但是数组长度小于64,此时会优先扩容

  • 当执行putAll方法时,会传入一个map集合,如果传入的map集合比较大,触发扩容

ConcurrentHashMap中resize过程中插入会阻塞吗?
  • jdk1.7中resize和putVal争抢的是同一把锁,所以插入同一个segment要等resize之后才会插入。
  • jdk1.8中resize是插入会判断,插入的槽位已经迁移的,则插入线程会帮助其他线程进行数据迁移,迁移完了往新的table中插入数据;如果插入的槽位没有迁移则插入,迁移和插入对一个槽位是用synchronized上锁的。
ConcurrentHashMap1.7和1.8的区别?
  1. 结构不同:1.7基于Segment+HashEntry实现,1.8基于Node实现

  2. 1.8中是以CAS+synchronized实现线程安全,没有hash冲突时,Node节点插入数组中通过CAS来保证线程安全,发生hash冲突,Node插入链表中使用synchronized链表头结点来保证线程安全。1.7中Segment继承ReentrantLock实现分段锁功能。

  3. 1.8中如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容),链表长度小于6是退化为链表。

JDK1.8 为什么要用synchronized代替1.7的ReentrantLock?
  • JDK1.6对synchronized进行了优化,有从无锁->偏向锁->轻量级锁->重量级锁的锁升级过程,优化了性能。

  • Segment继承ReentranLock,每个节点无论是否需要加锁都要继承,功能冗余,代码清晰度不够。

ConcurrentHashMap不支持key或者value为null 的原因?
  • 为了避免多线程下的歧义问题:一个线程去获取key时,如果返回的是null不能判定是不是这个key本身是不是null。
ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
  1. 粒度降低了;

  2. JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。

  3. 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

CopyOnWriteArrayList的缺陷和使用场景

CopyOnWriteArrayList 有几个缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc

  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用


Netty

有了nio,为何要有netty?
  1. NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
  4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。
ByteBuf和ByteBuffer的区别
  1. 可扩展到用户定义的buffer类型中
  2. 通过内置的复合buffer类型实现透明的零拷贝(zero-copy)
  3. 容量可以根据需要扩展
  4. 切换读写模式不需要调用ByteBuffer.flip()方法
  5. 读写采用不同的索引
  6. 支持方法链接调用
  7. 支持引用计数

Spring

如何实现一个IOC容器?

IOC(Inversion of Control)的意思是控制反转,不是一种技术,而是一种设计思想。IOC意味着你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

在传统的程序设计中,我们直接在对象内部通过new进行对象创建,是程序主动去创建依赖对象,而IOC是有专门的容器来进行对象的创建,即IOC容器来控制对象的创建。

在传统的应用程序中,我们是在对象中主动控制去直接获取依赖对象,这是正转,反转是由容器来帮忙创建及注入依赖对象,在这个过程中,由容器帮我们查找注入依赖对象,对象知识被动接受依赖对象。

  1. 先准备一个基本的容器对象,包含一些map结果的集合,用来方便后续过程中存储具体的对象。

  2. 进行配置文件的读取工作或者注解的工作,将需要创建的bean对象都封装成BeanDefinition对象存储在容器中。

  3. 容器将封装好的BeanDefinition对象通过反射的方式进行实例化,完成对象的实例化工作。

  4. 进行对象的初始化操作,也就是给类中的对应属性值就行设置,也就是进行依赖注入,完成整个对象的创建,变成一个完整的bean对象,存储在容器的某个map结构中。

  5. 通过容器对象来获取对象,进行对象的获取和逻辑处理工作。

  6. 提供销毁操作,当对象不用或者容器关闭的时候,将无用的对象进行销毁。

IoC和DI有什么关系呢

其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了"被注入对象依赖IoC容器配置依赖对象"。通俗来说就是IoC是设计思想,DI是实现方式

@Autowired和@Resource以及@Inject等注解注入有何区别?

1、@Autowired是Spring自带的,@Resource是JSR250规范实现的,@Inject是JSR330规范实现的

2、@Autowired、@Inject用法基本一样,不同的是@Inject没有required属性

3、@Autowired、@Inject是默认按照类型匹配的,根据类型找到多个则根据属性名字找对象,@Resource是按照名称匹配的,如果找到多个则根据类型去匹配。

4、@Autowired如果需要指定名称匹配需要和@Qualifier一起使用,@Inject和@Named一起使用,@Resource则通过name进行指定。

BeanFactory和FactoryBean有什么区别?
Bean初始化方法Constructor,@PostConstruct,InitializingBean,init-method执行顺序

Constructor > @PostConstruct > InitializingBean > init-method

a. spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用

b、实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖

c、如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。

d、@PostConstruct注解后的方法在BeanPostProcessor前置处理器中就被执行了,所以当然要先于InitializingBean和init-method执行了。

描述Spring生命周期?

spring容器帮助我们去管理对象,从对象的产生到销毁的环节都由容器来控制,其中主要包括实例化和初始化两个环节,当然在整个过程中会有一些扩展的存点,下面来详细描述下各个环节和步骤:

  1. 实例化bean对象,通过反射的方式来生产,在源码中有一个createBeanInstance的方法是专门来生产对象的。

  2. 当bean对象创建完成之后,对象的属性值都是默认值,所以要开始个bean填充属性,通过populateBean方法来完成对象属性的填充,中间会设计到循环依赖的问题。

  3. 向bean对象中设置容器属性,会调用invokeAwareMethods方法来将容器对象设置到具体的bean对象中。

  4. 调用BeanPostProcessor中的前置方法applayBeanPostProcessorBeforeInitialization来进行bean对象的扩展工作。

  5. 调用invokeInitMethods方法来完成初始化的方法调用,在此方法的过程中,需要判断当前bean是否实现了InitializingBean接口,如果实现了调用afterPropertiesSet方法,然后再调用init-method指向的方法通过invokeCustomInitMethods方法调用。

  6. 调用BeanPostProcessor中的后置方法applyBeanPostProcessorAfterInitalization完成对Bean对象的后置处理工作,aop就是在此处实现的,实现的接口实现名为AbstractAutoProxyCreator。

  7. 注册必要的Destruction相关回调接口。

  8. 获取到完整对象,通过getBean的方式来进行对象的获取和使用。

  9. 当对象使用完成之后,容器在关闭的时候,会销毁对象,首先判断时候实现了DisposableBean接口,如果实现则调用重写的destory方法,然后再去调用destroy-method指向的方法通过invokeCustomDestroyMethods方法调用。

Spring为什么不能解决构造器的循环依赖?

构造器注入形成的循环依赖: 也就是beanB需要在beanA的构造函数中完成初始化,beanA也需要在beanB的构造函数中完成初始化,这种情况的结果就是两个bean都不能完成初始化,循环依赖难以解决。

Spring解决循环依赖主要是依赖三级缓存,但是的在调用构造方法之前还未将其放入三级缓存之中,因此后续的依赖调用构造方法的时候并不能从三级缓存中获取到依赖的Bean,因此不能解决。

Spring为什么不能解决prototype作用域循环依赖?

这种循环依赖同样无法解决,因为spring不会缓存‘prototype’作用域的bean,而spring中循环依赖的解决正是通过缓存来实现的。

Spring为什么不能解决多例的循环依赖?

多实例Bean是每次调用一次getBean都会执行一次构造方法并且给属性赋值,根本没有三级缓存,因此不能解决循环依赖。


SpringBoot

1. Springboot常用注解

@SpringBootApplication

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan

2. Spring Boot 自动配置原理是什么?

BFPP: BeanFactoryPostProcessor BPP: BeanPostProcessor BDRPP:BeanDefinitionRegistryPostProcessor 表达的总体思路是:总-分-总

  1. springboot自动装配是什么,解决了什么问题

  2. 自动装配实现的原理:

    1. 当启动springboot应用程序的时候,会先创建SpringApplication的对象,在对象的构造方法中会进行某些参数的初始化工作,最主要的是判断当前应用程序的类型以及初始化器和监听器,在这个过程中会加载整个应用程序中的spring.factories文件,将文件的内容放到缓存对象中,方便后续获取。

    2. SpringApplication对象创建完成之后,开始执行run方法,来完成整个启动,启动过程中最主要的有两个方法,第一个叫做prepareContext,第二个叫做refreshContext,在这两个关键步骤中完整了自动装配的核心功能,前面的处理逻辑包含了上下文对象的创建,banner的打印,异常报告期的准备等各个准备工作,方便后续来进行调用。

    3. 在prepareContext方法中主要完成的是对上下文对象的初始化操作,包括了属性值的设置,比如环境对象,在整个过程中有一个非常重要的方法,叫做load,load主要完成一件事,将当前启动类做为一个beanDefinition注册到registry中,方便后续在进行BeanFactoryPostProcessor调用执行的时候,找到对应的主类,来完成@SpringBootApplicaiton,@EnableAutoConfiguration等注解的解析工作

    4. 在refreshContext方法中会进行整个容器刷新过程,会调用中spring中的refresh方法,refresh中有13个非常关键的方法,来完成整个spring应用程序的启动,在自动装配过程中,会调用invokeBeanFactoryPostProcessor方法,在此方法中主要是对ConfigurationClassPostProcessor类的处理,这次是BFPP的子类也是BDRPP的子类,在调用的时候会先调用BDRPP中的postProcessBeanDefinitionRegistry方法,然后调用postProcessBeanFactory方法,在执行postProcessBeanDefinitionRegistry的时候回解析处理各种注解,包含@PropertySource,@ComponentScan,@ComponentScans,@Bean,@lmport等注解,最主要的是@Import注解的解析

    5. 在解析@lmport注解的时候,会有一个getlmports的方法,从主类开始递归解析注解,把所有包含@lmport的注解都解析到,然后在processlmport方法中对lmport的类进行分类,此处主要识别的时候AutoConfigurationlmportSelect归属于ImportSelect的子类,在后续过程中会调用deferredlmportSelectorHandler中的process方法,来完整EnableAutoConfiguration的加载.

    6. 上面是我对springboot自动装配的简单理解,面试官您看一下,我回答有没有问题,帮我指点一下!


Mysql

1. mysql中binlog和redolog的区别?
  • redo log是 innodb存储引擎特有的;binlog是server层,属于共有的
  • redo log是物理日志,记录该数据页更新状态内容,Binlog 是逻辑日志,记录更新的过程。
  • redo log日志是循环写的,日志的空间大小是固定的,Binlog 是追加写入,写完一个写下一个,不会覆盖使用。
  • redo log用于异常重启恢复;binlog用于备份和主从复制
2. WAL:

【存储】什么是 WAL|数据库性能_bandaoyu的博客-CSDN博客_wal

WAL(Write Ahead Log)预写日志 - 掘金

3. MySQL之Innodb引擎的4大特性:

1.插入缓冲 (Insert Buffer/Change Buffer)

2.双写机制(Double Write)

3.自适应哈希索引(Adaptive Hash Index,AHI)

4.预读 (Read Ahead)

MySQL之Innodb引擎的4大特性_oldba.cn的博客-CSDN博客_innodb引擎的4大特性

InnoDB 四大特性_盼兮猫的博客-CSDN博客_innodb四大特性

4. Explain Analyze

MySQL8 查询优化新工具 Explain Analyze - 业余砖家 - 博客园

1、什么是Explain Analyze**?**

Explain 是我们常用的查询分析工具,可以对查询语句的执行方式进行评估(并非实际的执行情况,可能与实际情况存在较大差距),给出很多有用的线索。

Explain Analyze 是 MySQL 8 中提供的查询性能分析工具,牛X之处在于可以给出**实际执行情况,**可以详细的显示出查询语句执行过程中,每一步花费了多少时间。

Explain Analyze 会做出查询计划,并且会实际执行,以测量出查询计划中各个关键点的实际指标,例如耗时、条数,最后详细的打印出来。

2、EXPLAIN的语法
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
参数解释:
    ANALYZE:执行命令并显示实际运行时间。
    VERBOSE:显示规划树完整的内部表现形式,而不仅是一个摘要。通常,这个选项只是在特殊的调试过程中有用。VERBOSE输出是否打印工整的,具体取决于配置参数 explain_pretty_print 的值。
    statement:查询执行计划的 SQL 语句,可以是任何select、insert、update、delete、values、execute、declare 语句。

3、EXPLAIN和EXPLAIN ANALYZE区别

(1)、EXPLAIN展示查询优化器对该查询计划估计的代价,但是不执行该查询。

             例如:EXPLAIN SELECT * FROM test WHERE id=2;

(2)、EXPLAIN ANALYZE不仅会显示查询计划,还会实际运行语句。

EXPLAIN ANALYZE会丢掉任何来自SELECT语句的输出,但是该语句中的其他操作会被执行(例如INSERT、UPDATE或者DELETE)。

要在DML语句上使用EXPLAIN ANALYZE却不让该命令影响数据,可以明确地把EXPLAIN ANALYZE用在一个事务中:
(BEGIN; EXPLAIN ANALYZE ...; ROLLBACK;)。

EXPLAIN ANALYZE运行语句后除了显示计划外,还有下列额外的信息:
1、运行该查询消耗的总时间(以毫秒计) 计划节点操作中涉及的工作者(Segment)数量
2、操作中产生最多行的Segment返回的最大行数(及其Segment ID) 操作所使用的内存
3、从产生最多行的Segment中检索到第一行所需的时间(以毫秒计),以及从该Segment中检索所有行花费的总时间。

例如:EXPLAIN ANALYZE SELECT * FROM test WHERE id=2;

4、阅读EXPLAIN 输出
查询计划类似于一棵有节点的树,执行和阅读的顺序是自底而上。计划中的每个节点表示一个操作,例如表扫描、表连接、聚集或者排序。阅读的顺序是从底向上:每个节点会把结果输出给直接在它上面的节点。一个计划中的底层节点通常是表扫描操作:顺序扫描表、通过索引或者位图索引扫描表等。如果该查询要求那些行上的连接、聚集、排序或者其他操作,就会有额外的节点在扫描节点上面负责执行这些操作。最顶层的计划节点通常是数据库的移动(MOTION)节点:重分布(REDISTRIBUTE)、广播(BROADCAST)或者收集(GATHER)节点。这些操作在查询处理时在实例节点之间移动数据。

EXPLAIN的输出对于查询计划中的每个节点都显示为一行并显示该节点类型和下面的执行的代价估计:
cost:以磁盘页面获取为单位度量。1.0等于一次顺序磁盘页面读取。第一个估计是得到第一行的启动代价,第二个估计是得到所有行的总代价。
rows:这个计划节点输出的总行数。这个数字根据条件的过滤因子会小于被该计划节点处理或者扫描的行数。最顶层节点的是估算的返回、更新或者删除的行数。
width: 这个计划节点输出的所有行的总字节数。

需要注意以下两点:
一个节点的代价包括其子节点的代价。最顶层计划节点有对于该计划估计的总执行代价。这是优化器估算出来的最小的数字。
代价只反映了在数据库中执行的时间,并没有计算在数据库执行之外的时间,例如将结果行传送到客户端花费的时间。
rows:根据统计信息估计SQL返回结果集的行数
width:返回的结果集的每一行的长度,这个长度值是根据pg_statistic表中的统计信息来计算的。

5. innodb和myisam区别
  1. innodb支持事务,myisam不支持

  2. innodb支持外键,myisam不支持

  3. innodb支持表锁和行锁,myisam只支持表锁

  4. innodb使用聚簇索引存储,myisam使用非聚簇索引存储

6. B树和B+树的区别
  1. B+tree 非叶子节点只存储键值信息, 数据记录都存放在叶子节点中。而B-tree的非叶子节点也存储数据。所以B+tree单个节点的数据量更小,在相同的磁盘I/O次数下,能查询更多的节点。

  2. B+tree 所有叶子节点之间都采用单链表连接。适合MySQL中常见的基于范围的顺序检索场景,而B-tree无法做到这一点。

7. 组合索引的优点

减少开销。建一个联合索引(a,b,c),实际相当于建了(a)、(a,b)、(a,b,c)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!

覆盖索引。对联合索引(a,b,c),如果有如下的sql: select a,b,c from student where a=1 and b=2。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。减少io操作,特别的随机io其实是dba主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。

效率高。索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下sql:select from table where a=1 and b=2 and c=3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W10%=100w条数据,然后再回表从100w条数据中找到符合b=2 and c= 3的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w10% 10% *10%=1w,效率提升可想而知!

缺点。联合索引越多,索引列越多,则创建的索引越多,索引都是存储在磁盘里的,通过索引算法来查找数据,的确可以极大的提高查询效率,但是与此同时增删改的同时,需要更新索引,同样是需要花时间的,并且索引所占的磁盘空间也不小。

建议。单表尽可能不要超过一个联合索引,单个联合索引不超过3个列;

8. 索引失效的场景?

遵循最左前缀匹配原则

不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换)

存储引擎不能使用索引中范围条件右边的列(范围以后全失效)

like可能导致索引失效

is null和is not null导致索引失效

排序优化

9. 慢查询日志

MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,这些SQL会被记录到慢查询日志中。默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置参数开启。当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。下面时查看和开启慢查询日志的方式:

利用日志可以提取出慢sql,可以再结合explain来分析慢的原因,进行相应的调节。

10 使用mysql索引的原则?

减少回表,索引覆盖,最左匹配,索引下推

11. 索引下推的缺点:

需要在磁盘上多做数据筛选,原来的筛选是放在内存中的,现在放在磁盘上查找数据的环节,这样看起来成本比较高,但是别玩了,数据是有序的,所有的数据是聚簇存放,所以性能不会有影响,而且整的的io会大大减少,反而会提升性能。

12. MRR mutl_range_read

回表时先进行排序再进行范围查询,加快了速度,不用一个一个匹配。

13. FIC fast index create

老方式:插入和删除数据:

1、先创建临时表,将数据导入到临时表

2、把原始表删除

3、修改临时表的名字

fic方式:

给当前表添加一个share锁,不会有临时创建临时文件的资源消耗,还是在源文件中但是此时如果有人发起DML操作,很明显数据会不一致,所以添加share锁,读取是没问题的,但是DML会有问题。

14. 优化小细节
  1. 当使用索引列进行查询的时候尽量不要使用表达式,把计算放到业务层而不是数据层。

  2. 尽量使用主键查询,而不是其他索引,因此主键查询不会触发回表操作。

  3. 使用前缀索引

  4. 使用索引扫描来排序

  5. union all, in, or 都能使用索引,但是推荐使用in,单列索引or走索引,组合索引or不走索引

  6. 范围列可以用到索引:范围条件是:<,<=,>,>=,between;范围列可以使用索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列。

  7. 强制类型转换会全表扫描。

  8. 更新十分频繁,数据区分度不高的字段不宜建立索引。

  9. 创建索引的列,不允许为null,可能会得到不符合预期的结果

  10. 当需要进去表连接的时候,最好不要超过三张表,应为需要join的字段,数据类型必须一致

  11. 能使用limit的时候尽量使用limit

  12. 单表索引建议控制在5个以内(现在没有太多的限制)

  13. 单索引字段数不允许超过5个


Redis

1. Redis在进行快照操作的这段时间,如果发生服务崩溃怎么办?

在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

2. Redis可以每秒做一次快照吗?

虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销

  • 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
  • 另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。
3. Redis调优

Redis进阶 - 性能调优:Redis性能调优详解 | Java 全栈知识体系

4. Redis实现分布式锁实现? 什么是 RedLock

Kafka

kafka为啥快

多个partition采取分治思想,集群

发送是批量发 batch 默认大小16k获取每个1ms发送一次

broker在收到消息调用write函数将数据写入pagecache中则认为写入成功

操作系统从pagecache中刷新到磁盘中的时候采用的是顺序写,避免了寻址时间

发送采用零拷贝技术,结尾.index的offset文件采用mmap+write 结尾.log的数据文件采用 sendfile来完成。


Nginx

Nginx的5种负载均衡算法

轮询法(Round Robin)(默认)

加权轮询法(Weight Round Robin)- weight

upstream bakend {  
  server 192.168.0.14 weight=10;  
  server 192.168.0.15 weight=10;  

源地址哈希法(Hash)- ip_hash

upstream bakend {  
  ip_hash;  
  server 192.168.0.14:88;  
  server 192.168.0.15:80;  
}

fair(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

upstream backend {  
  server server1;  
  server server2;  
  fair;  
}

url_hash(第三方)


Tomcat

1. 单台tomcat最大并发量是多少?

Tomcat 默认配置的最大请求数是 150,也就是说同时支持 150 个并发,当然了,也可以将其改大。当某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群。具体能承载多少并发,需要看硬件的配置,CPU 越多性能越高,分配给 JVM 的内存越多性能也就越高,但也会加重 GC 的负担。 操作系统对于进程中的线程数有一定的限制:Windows 每个进程中的线程数不允许超过 2000;Linux 每个进程中的线程数不允许超过 1000。另外,在 Java 中每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用。

Tomcat的最大并发数是可以配置的,实际运用中,最大并发数与硬件性能和CPU数量都有很大关系的。更好的硬件,更多的处理器都会使Tomcat支持更多的并发。Tomcat 默认的 HTTP 实现是采用阻塞式的 Socket 通信,每个请求都需要创建一个线程处理。这种模式下的并发量受到线程数的限制,但对于 Tomcat 来说几乎没有 BUG 存在了。
Tomcat 还可以配置 NIO 方式的 Socket 通信,在性能上高于阻塞式的,每个请求也不需要创建一个线程进行处理,并发能力比前者高。但没有阻塞式的成熟。
这个并发能力还与应用的逻辑密切相关,如果逻辑很复杂需要大量的计算,那并发能力势必会下降。如果每个请求都含有很多的数据库操作,那么对于数据库的性能也是非常高的。
对于单台数据库服务器来说,允许客户端的连接数量是有限制的。
并发能力问题涉及整个系统架构和业务逻辑。
系统环境不同,Tomcat版本不同、JDK版本不同、以及修改的设定参数不同。并发量的差异还是蛮大的。


分布式

幂等有哪些技术解决方案?

幂等是指就是针对一个操作,不管做多少次,产生效果或返回的结果都是一样的。

从增删改查和业务的角度去分析:

  1. 查询操作在数据不变的前提下,无论查询几次,结果都是一样的。天然的支持幂等。

  2. 删除操作基本也是幂等的,在数据不变的前提下,删除几次都是一样的,返回的结果可能会不一样(数据不存在返回0,数据存放返回删除的个数)。

  3. 唯一索引:防止新增脏数据,给新增的表的业务唯一ID设置为主键或者唯一索引,多次重复的插入会报错,再查询一次,如果存在则返回结果。

  4. 全局流水号(乐观锁):如果是更新操作,发起方请求全局流水号,各个节点操作完则插入本地流水表中,每次操作先查询如果本地事务表中存在该流水则不做操作。注意所有操作(查询全局流水,更新业务数据,插入全局流水)放入同一事务中。

从调用双方场景分析:

  1. token机制:防止页面重复提交。采用token + redis。数据提交前申请token返回给页面并存入redis,提交时校验token并删除redis中的token。重复提交时校验token丢失则放弃提交。

  2. Kafka支持启动幂等性,enable.idompotence = true;Producer在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。


分布式事务

1. TCC空回滚解决问题

    在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么此时Cancel就不能正常执行,因为Try没有对数据进行修改,如果Cancel进行了对数据的修改,那就会导致数据不一致。     解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚,如果没执行,那就是空回滚。建议TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入条记录,表示Try阶段执行了。Cancel接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在则是空回滚。

2. 如何解决TCC中幂等问题

    为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段Confirm和Cancel接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。解决思路在上述分支事务记录中增加执行状态,每次执行前都查询该状态。在每次查询该状态的之前先获取分布式锁,更新完状态再释放锁,防止在获取状态之后其他线程修改状态。

3. 如何解决TCC中悬挂问题

    悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。     出现原因是在调用分支事务Try时,由于网络发生拥堵,造成了超时,TM就会通知RM回滚该分布式事务,可能回滚完成后,Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后无法继续处理。     解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try。