该面试题的答案不保证一定对
一、Java基础
1、多线程开发的时候,有哪些并发的容器?或者说哪些是线程安全的?
StringBuffer、List中的Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、hashtable、Atomicxxx-包装类的线程安全类、Concurrentxxx
2、hashmap的原理
通过 HashSet 存储对象,会根据对象的 hashCode() 生成 hash 值,根据 hash 生成索引,根据索引找到hash 表中的位置;判断位置上是否有对象,没有对象直接插入,有对象则通过 equals() 判断内容是否相同,如果相同则不插入,不同则根据链表往下检索,直到重复或者链表没有下一个对象,准备插入最后的时候判断链表是否超过 8 个对象,当链表超过 8 个对象时,总存储的对象大于等于 64 则把链表转换成红黑树当 hash 表总存储的对象大于负载因子值(0.75),则会进行数组的扩容,扩容为原本的 2 倍。
3、线程池的最大的线程数如何设计
江湖谣传是:
IO 密集型: 2 * CPU核心数
计算密集型: CPU核心数 + 1
但私以为下面这个更可信:
理论公式:cpu核心数 * cpu利用率 * (1+ 线程等待时间 / 线程计算时间)
但是理论公式仅供参考,实际使用还需要不断的细微调节。
4、Java死锁怎么排查?
1、使用 jconsole。这个是 java 自带的简单可视化工具,用来监控和管理 jvm,通过这个可以链接运行中的 java 进程,并且查看到里面的线程信息,包括里面是否有死锁。
2.使用 jstack。通过这个工具去捕获java 中线程堆栈信息,并去检查是否存在死锁。通过 jstack -l 进程id,去查看堆栈跟踪信息。
3、阿里巴巴提供的arthas 工具。通过 thread -b 找出目前阻塞的线程,在通过 thread 线程id 进而分析是否发生死锁。
5、乐观锁和悲观锁的区别
乐观锁:假设并发冲突不会频繁发生,因此处理数据的时候并不会直接锁定数据,而是更新数据的时候判断有没有其他线程修改过数据,如果存在,那么就撤销当前更新操作;如果不存在冲突,就继续执行当前更新操作。
乐观锁适用于读多写少的场景。
悲观锁:假设最坏的情况发生总是认为并发冲突会发生,所以会锁定操作过程中涉及到的数据,避免其他线程操作,例如数据库中行锁表锁就算是悲观锁。java代码中synchronized 关键字或者 lock接口或者实现类,比如ReentrantLock等等来实现悲观锁。
悲观锁适用于写操作频繁的场景。可以保证数据的一致性,但是会牺牲一部分性能。
6、synchronized 和 ReentrantLock 有什么区别?
1.来源不同。synchronized 是java 内置的关键字,用来给对象或者方法加锁,依赖于 jvm 解释器来锁定的。而 ReentrantLock 是 java5 里面新增的类。是 jdk 实现的。
2、释放不同。synchronized 发生异常的时候,会自动释放线程占用的锁,因此不会导致死锁的发生。而ReentrantLock 发生异常的时候如果没有调用 unlock() 释放锁,可能造成死锁,因此使用 ReentrantLock 锁的时候需要在 finally 代码块里面释放锁。
3、锁等待时是否可中断不一样。synchronized 是不可中断锁,等待锁的线程会一直等待不能中断。而ReentrantLock 就可以中断等待,等待锁的线程能够响应中断,从而停止等待。
4、公平性不同。synchronized 是非公平锁,线程获取到锁的顺序并不是按照线程加锁的顺序来的,有可能后加锁的线程比先加锁的线程更早获得锁。而ReentrantLock 可以是公平锁也可以是非公平锁,在构造函数里面传入 true 就是公平锁。
如果需要更灵活的控制锁的行为就考虑使用 ReentrantLock ,如果只是简单同步的需求就考虑synchronized 。
7、线程池拒绝策略有哪些?
4种。
1、AbortPolicy(默认策略):这种策略明确告诉调用者任务被拒绝了,此时可以根据业务逻辑选择重试或者放弃提交,使用这种策略当线程池无法处理新任务的时候,会直接抛出RejectedExecutionException 的异常。
2、DiscardPolicy:表示当线程池无法处理新任务的时候,那么新任务会直接丢弃,而且不会给任何通知。这种策略会存在一定风险,可能发生数据丢失。
3、DiscardOldestPolicy:这种策略表示当线程池无法处理新任务的时候,会丢弃队列中等待时间最久的任务,然后将新任务加入到队列里面,然后尝试再次提交。这种方式同样存在数据丢失的风险,但是可以确保新任务得到处理。
4、CallerRunsPolicy:这种策略表示线程池无法处理新任务的时候,新任务会由提交任务的线程自己执行,这种策略不会造成业务损失但是提交业务的线程会被占用,进而导致任务提交速度变慢。
如果内置的四种策略不满足使用,可以通过实现RejectedExecutionHandler 接口来自定义拒绝策略。
8、信号量(Semaphore)和互斥信号量(也叫互斥锁:Mutex)有何区别?
都是用来控制对共享资源访问的。
区别:
1、使用场景。信号量一般用来控制对有限资源的并发访问数量,通过信号量可以指定访问资源的线程数量,达到上限的时候,其他线程就需要等待,这个经典场景就如数据库连接池,线程池,限流等
互斥信号量主要用于保护对临界资源的访问,确保一次只有一个线程可以访问目标资源,经典使用场景如保护数据一致性和防止竞态条件。
2、信号量内部维护了一组虚拟许可,许可数量可以通过构造函数去指定,当线程获取信号量的时候,许可数量就减一,释放信号量的时候,许可数量就加一。而互斥锁确保一次只有一个线程可以访问临界资源,所以就不需要许可数量。
3、信号量可以配置公平模式或者非公平模式,而互斥锁也是一样的,两种模式都支持。具体到 java代码的时候,Semaphore 和 ReentrantLock 分别用来实现信号量和互斥信号量。Semaphore提供了计数信号量的实现,而ReentrantLock 提供了可重入的互斥锁实现。
9、Hash值相等,对象一定相等吗?
不一定。hahsmap 底层是 数组+链表+红黑树,链表+红黑树就是为了解决 hash 冲突问题,hash冲突就是不同对象产生了同样的hash 值,解决办法很多种,hashmap使用了拉链法,也就是不同对象产生相同hash 值把这些对象组织成一个链表,挂在数组上面,达到一定条件会转成红黑树用来加快搜索效率。
判断两对象是否相等还需要调用 equals()方法做进一步判断。
10、线程池有哪些队列
常用的主要有五种。
1、ArrayBlockingQueue:基于数组结构的有界队列,按照先进先出原则,对元素进行排序,队列大小在创建的时候已经指定,队列满的时候新任务提交会被阻塞,直到里面有空闲位置。
2、LinkedBlockingQueue:基于链表结构的阻塞队列,同样是先进先出原则排序,但是容量可选不指定容量就是Integer.Max_VAULE ,也就是2的32次方-1 ,这种队列更适用于任务数量多的情况,可以有效缓冲大量任务。
3、SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待相应的删除操作,反之亦然。这种可以保证任务不会被拒绝,但是可能导致线程饥饿,静态工厂方法Executors.newCachedThreadPool 就是使用了这种队列。
4、PriorityBlockingQueue:基于优先级堆的无界阻塞队列,可以按照元素优先级进行出队操作,意味着优先级高的任务会优先被执行。
5、DelayQueue:延迟队列,内部元素必须实现Delayed 接口,只有在延迟期条件满足的时候才会取出元素去执行。
11、线程池有哪些状态?
问的是线程池状态不是线程状态,不要搞混!
1、running:最正常的状态,初始状态。可接受新任务,处理等待队列里面的任务。
2、shutdown:这个状态下不接受新任务提交,会继续处理等待队列里面的任务。
3、stop:这个状态下不仅不接受新任务提交,而且也不再处理等待队列里面的任务,同时还会中断正在执行任务的线程。
4、tidying:所有任务已经完成并且 ctl 记录任务数为 0 的时候,线程池会进入到此状态,这个状态下会执行钩子方法 terminated() 。
5、terminated:线程池彻底终止。
12、Java有哪些类加载器?
主要有四种。
1、启动类加载器 Bootstrap Classloader:负责加载jre/lib/rt.jar 里面的类,也就是核心的 java api,比如java.lang 包里面的类。这个加载器用 C++编写。
2、扩展类加载器 Extension Classloader:负责加载 jre/lib/ext 里面jar 包的类。是java.lang.ClassLoader 的子类,主要用于加载java的扩展库。
3、系统类加载器 System.Classloader(也被称为应用程序类加载器Application Classloader):负责加载 classpath 下或者java命令指定路径下的类,也是 java.lang.ClassLoader 的子类,这个是最常用的。
4、自定义类加载器 custom Classloader :通过继承 classloader 类来创建自定义类加载器,这样可以实现自己的类的加载方式。
13、什么是双亲委派机制?
这是个类加载的常见问题,java存在哪些类加载器看上个问题。
双亲委派也就是当需要加载一个 class 的时候,首先是AppClassLoader,但是不会自己去加载这个类,而是把这个类加载请求委派给父类加载器 ExtClassLoader 去完成,接下来就到 ExtClassLoader 他自己也不会去加载这个类,而是把这个类加载请求委派给 BootStrapClassLoader 去完成,接着到 BootStrapClassLoader ,因为BootStrapClassLoader 没有parent了 ,所以会自己去加载,会到自己的加载目录里面去找这个类,找到了就加载,没找到就通过ExtClassLoader 来加载,ExtClassLoader 也是去加载目录里面加载,找到了就加载,没找到就使用 AppClassLoader 加载,如果 AppClassLoader 也没有加载到,就会抛出一个 ClassNotFountException 异常,这个就是双亲委派。
为何不直接加载类而是使用双亲委派机制?
1、避免类的重复加载,通过这种机制,当一个类加载机制收到类的加载请求的时候,会首先把这个请求请求委派给父类加载器,这样的话父类加载器会先尝试加载类,只有在父类加载器无法找到类的时候,那么子类加载器才会尝试自己加载,那么这种机制就确保了在 jvm中每个类只会被加载一次,避免类的重复加载。
2、安全性的考虑。双休委派机制,有利于维护java核心类库的一个安全性,因为Java的核心类库是由启动类加载器加载的,那么其他类加载器我没有办法加载或者修改这些核心类库,这样就可以防止恶意代码的话,通过自定义类加载器来篡改核心类库,进而增强 Java运营环境的安全性。
3、模块化开发的一个支持。在实际应用中的话,我们可能需要在一个应用程序中使用多个第三方库,而这些库之间可能存在同名的类,那么通过创新委派机制不同的类加载器只会加载自己的类这样就可以避免类名称的一个冲突的问题,然后使得这个模块化开发也更加便捷。
14、什么是CAS?
cas全称 compare-and-swap ,翻译就是对比和交换,cas是java 中用于实现无锁数据结构的一种原子操作,cas操作需要输入两个数值,一个旧值也就是期望值,还有一个是新值,操作的时候先比较旧值,有无发生变化,如果没有就交换新值,发生变化就不交换,因为cas操作是原子性的,所以多线程并发使用cas更新数据的时候,就不需要加锁,cas实现是基于硬件平台汇编指令,cas其实是靠硬件来实现的,jvm只是封装了汇编的调用,像AtomicInteger 类就是使用了封装后的接口,在大多数处理器下这种操作比直接加锁开销更小,性能更高,像 juc atomic包下的类都是基于cas 实现的。cas其实就是乐观锁的一种思想落地。
15、什么是ABA问题,怎么解决?
多线程环境下相比直接加锁,cas性能更好,但是也有问题,最主要问题就是aba问题,cas操作值的时候需要检查值有没有发生变化,aba问题也就是一个值是a变成b又变成a,使用cas进行检查的时候,看起来没有变化,但实际已经变化了两次;要解决这个问题可以通过加版本号来解决,直接在变量前面加上版本号,每次变量更新版本号+1,aba就会变成 1a 2b 3a,这样就能区分值的变化。
从 jdk1.5开始,juc atomic包里面提供了一个叫 AtomicStampedReference 的类来解决aba问题,这个类本质上也是通过版本号来解决问题,内部维护了一个叫 pair 的数据结构,用 volatile 来修饰确保可见性,这个 pair将数据对象和版本号打包到一起,进行比较,通过这种方式解决aba问题。
16、如何确保Bean的加载顺序?
是初始化顺序而不是执行的先后顺序,如果是执行的先后顺序,可以通过 @Order 注解控制相同类型bean 的执行顺序,或者实现Ordered接口。
那么如何调整bean 的加载顺序,确保先加载 b 后加载 a呢?
1、使用 @DependsOn 注解,比如:
2、利用 BeanFactoryPostProcessor 处理器,因为这个处理器在其他的 bean 初始化之前就被触发了,
所以可以在这个处理器里面手动调用 getBean() 方法,把 bean 初始化提前:
17、缓存预热有哪些方案?
主要考察对系统启动是否熟悉,能否找到系统启动过程中预留的钩子函数。
1、SpringBoot预留的两个系统启动任务CommandLineRunner 和 ApplicationRunner :
CommandLineRunner :
2、事件监听机制:SpringBoot启动过程中,不同阶段会发布相应的事件,通过对这些事件的监听,就可完成缓存预热:
还有 bean 初始化过程中预留的钩子函数完成预热:
1.自定义 bean 实现 InitializingBean 接口 并且重写 afterPropertiesSet 方法:
也可利用 @PostConstruct注解在bean 初始化过程中执行指定的方法进行缓存预热:
18、FactoryBean 和 BeanFactory 有什么区别
BeanFactory 是Spring 框架的基础容器接口,也就是常说的 Bean 工厂,用来生产 Bean 对象,本质上就是 Ioc 容器,负责管理和实例化应用程序里面的 bean 对象,平时使用的 getBean 方法也是该工厂提供。
FactoryBean 就是一个 bean,FactoryBean 本身就是一个接口,这个接口允许开发者通过自定义逻辑去创建和初始化 bean 实例,比如配置比较麻烦的 bean 可以通过代码对 bean 进行配置,然后返回。
FactoryBean 最终注册到容器中的 bean 有两个,一个是 getObject() 方法返回的对象,另外一个是 FactoryBean 本身,
二、Jvm相关
1、JVM垃圾回收器有哪些?
Serial收集器(新生代、复制算法)
ParNew收集器(新生代、复制算法)
Parallel Scavenge收集器(新生代、复制算法)
Serial Old收集器(老年代、整理算法)
Parallel Old收集器(老年代、整理算法)
CMS收集器(Concurrent Mark Sweep)
G1回收器(Garbage First)
2、JVM的优化配置哪些参数
参考网址:https://blog.csdn.net/wyn_365/article/details/120381440
-Xms10g :JVM启动时申请的初始堆内存值
-Xmx20G :JVM可申请的最大堆内存值
-Xmn3g : 新生代大小,一般设置为堆空间的1/3 1/4左右,新生代大则老年代小
-Xss :Java每个线程的栈内存大小
-XX:PermSize :持久代(方法区)的初始内存大小
-XX:MaxPermSize : 持久代(方法区)的最大内存大小
-XX:SurvivorRatio : 设置新生代eden空间和from/to空间的比例关系,关系(eden/from=eden/to)
-XX:NewRatio : 设置新生代和老年代的比例老年代/新生代
3、JVM怎么判断对象已经死去(垃圾回收)?
也就是判断是否可以被垃圾回收,有两种方式。引用计数器法和可达性分析算法(目前jvm主流)。
1.引用计数算法:给对象添加一个引用计数器,每当有一个地方引用了这个对象的时候计数器就加1,引用失效就减一,任何时刻只要计数器为0就表示对象不再被使用可以回收了。但是这种方式有致命问题,很难处理循环引用的情况比如 a引用b,b引用a,然后 ab没有被其他对象引用,这时候按理说应该被回收了,但是因为 ab 引用计数器都不为 0 所以无法回收。
2、可达性分析算法:基本思路是通过一系列称作 GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径叫做引用链,当一个对象到任何 GC Roots都没有引用链的时候,用图论的话来说就是 GC Roots 到这个对象不可达,这个说明对象是不可用的,可以回收了。
java中可以作为 GC Roots 对象主要有6种:
虚拟机栈里面引用的对象、
方法区里面类静态属性引用的对象、
方法区里面 final 常量引用的对象、
本地方法栈里面 JNI 所引用的对象、
java虚拟机内部的引用(比如基本数据类型对应的Class对象,一些常驻的异常对象如空指针等)、
被同步锁 synchronized 持有的对象,
这六种可以作为 GC Roots。
JVM 回收的时候还需要判断对象是否真正死去,因为可能存在不可达但是仍然有必要存活的情况,比如对象可能被 finalize 的方法复活(注意这个 finalize 方法只会被调用一次):
三、Spring相关
1、spring里面除了单例,工厂,代理模式,哪些功能还用到了什么模式?
a、工厂设计模式(Spring 框架中 BeanFactory 和 ApplicationContext 类使用工厂模式创建 Bean 对象)
(1)BeanFactory:延迟注入,即使用到某个Bean时才会进行注入,和 ApplicationContext 相比会占用更少的内存,程序启动速度更快。
(2)ApplicationContext:容器启动时就创建所有的Bean,和 BeanFactory 相比 ,BeanFactory 仅提供了最基本的依赖注入支持 . ApplicationContext 扩展了 BeanFactory, 除了 BeanFactory 的功能外还包含其余更多的功能,通常使用ApplicationContext 创建 Bean。
(3)ApplicationContext的三个实现类:
ClassPathXmlApplication: 将上下文文件作为类路径资源。
FileSystemXmlApplication: 从文件系统中的 XML 文件中载入上下文定义信息。
XmlWebApplicationContext: 从Web系统中的 XML 文件中载入上下文定义信息。
b、单例设计模式(Spring 中的 Bean 的作用域默认就是单例 Singleton 的)
Spring底层通过ConcurrentHashMap实现单例注册表来实现单例模式。
c、模板方法模式
模板方法模式是一种行为型模式,基于继承的代码复用、定义一个操作的算法骨架,将一些实现步骤延迟到子类中、模板方法使得子类可以不改变一个算法结构的情况下即可重新定义算法的某些特定步骤的实现方式
Spring 中以 Template 结尾的类,比如 jdbcTemplate 等,都是使用了模板方法模式。
1、通常情况下,都是使用继承来实现模板模式。
2、在 Spring 中,使用了 Callback 与模板方法相结合的方式,既达到了代码复用的效果,又增加了系统的灵活性
d、观察者模式
观察者模式表示的是一种对象和对象之间具有依赖关系,当一个对象发生改变,依赖于这个对象的对象也会发生改变。
Spring事件驱动模型就是基于观察者模式实现的,包含三种角色:事件Event角色、事件监听者Listener角色、事件发布者Publisher角色
(1)事件角色Event:
ApplicationEvent 事件角色抽象类,继承了 Event 并实现 Serializable 接口。
Spring中默认存在以下事件,都是继承自 ApplicationContext 事件角色抽象类:
ContextStartedEvent:ApplicationContext 启动后触发的事件。
ContextStoppedEvent:ApplicationContext 停止后触发的事件。
ContextRefreshedEvent:ApplicationContext 初始化或者刷新后触发的事件。
ContextClosedEvent:ApplicationContext 关闭后触发的事件。
(2)事件监听者角色Listener:
ApplicationListener(事件监听者角色,ApplicationListener 接口中定义了一个 onApplicationEvent() 方法来处理ApplicationEvent。只要实现 onApplicationEvent() 方法即可完成监听事件)
(3)事件发布者角色Publisher:
ApplicationEventPublisher(事件发布者角色,ApplicationEventPublisher 接口中定义了 publishEvent() 方法来发布事件,这个方法在 AbstractApplicationContext 中实现,在 AbstractApplicationContext 中,事件是通过ApplicationEventMulticaster 广播的)
Spring的事件发布流程:
定义一个事件(实现一个继承自ApplicationEvent的事件类,并写出相应的构造函数)——>
定义一个事件监听者(实现ApplicationListener接口、重写onApplicationEvent()方法)——>
使用事件发布者发布消息(使用ApplicationEventPublisher的publishEvent() 方法、重写publishEvent() 方法发布消息)
e、AOP中的适配器模式
适配器模式:将一个接口转换为调用方需要的接口,适配器使得接口不兼容的类之间可以一起工作.适配器又被称为包装器Wrapper
Spring AOP中的增强和通知Advice使用了适配器模式,接口是AdvisorAdapter。
每个通知Advice都有对应的拦截器:
BeforeAdvice - MethodBeforeAdviceInterceptor
AfterAdvice - MethodAfterAdviceInterceptor
AfterReturningAdvice - MethodAfterReturningAdviceInterceptor
Spring中预定义的通知要通过对应的适配器,适配成为MethodInterceptor接口类型的对象
f、Spring MVC中的适配器模式
Spring MVC中 ,DispatchServlet 根据请求信息调用 HanlderMapping, 解析请求对应的 Handler, 解析到对应的Handler 后,开始由 HandlerAdapter 适配器进行处理。
HandlerAdapter 作为期望接口,具体的适配器实现类对具体目标类进行适配 。controller 作为需要适配的类。
通过使用适配器 AdapterHandler 可以对 Spring MVC 中众多类型的 Controller 通过不同的方法对请求进行处理。
g、装饰器模式
装饰器模式:动态地给对象添加一些额外的属性或者行为,和继承相比,装饰器模式更加灵活
使用场景:当需要修改原有的功能,但是不想直接修改原有的代码,就可以设计一个装饰器 Decorator 类在原有的代码的外面,这样可以在不修改原有的类的基础上扩展新的功能。
Spring 中配置 DataSource 时 ,DataSource 可以是不同的数据库和数据源.为了在少修改原有类的代码下动态切换不同的数据源,这时就用到了装饰器模式。
Spring 中含有 Wrapper 和含有 Decorator 的类都用到了装饰器模式,都是动态地给一个对象添加一些额外的属性或者功能。
2、spring中事务没有回滚(或者说失效)是什么原因造成的?
a.方法访问权限不是 public(因为如果你的方法不是public,就没有办法去给你生成动态代理,那事务当然就没有办法生效) 。
b.方法被 final 修饰,这种情况代理类无法重写该方法,无法添加事务功能、static 也是。
c.方法自调用(同一个类里面的方法调用:因为Spring里边是通过动态代理去处理的@Transactional注解的,A方法直接调用B方法隐藏的含义就是A调用了当前对象的B方法,那么当前对象又不是代理对象,他就是个普通对象,所以事务就不会生效。)
d.未被 Spring 管理(事务注解不生效,spring没办法生成动态代理)
e.多线程调用:可能存在两个方法不在同一个线程中、获取到的数据库链接不一样,从而是两个不同的事务。
f.不正确的异常捕获会导致事务失效(如果方法上面添加了@Transactional注解,同时又在方法里面去捕获了异常,那么事务注解就不会生效,因为事务注解本质上是通过什么Aop去处理的,那么异常捕获之后 AOP就感知不到你方法出错了,那么失误当然也就不会生效了。)
g.数据库不支持事务
h.rollbackFor可能配置有问题(这个属性是用来去配置有哪些异常需要回滚的,默认情况下是RuntimeException 这种异常需要回滚。)
3、Bean的初始化流程
4、Spring如何解决循环依赖的
循环依赖就是:多个bean之间相互依赖,形成了一个闭环。 比如:A依赖于B、B依赖于c、c依赖于A。
a、三级缓存名词解释
第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。
第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
第三级缓存:Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂。
b、解决循环依赖具体说明
(1)A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B。
(2)B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
(3)B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放到一级缓存中。
c、为什么使用三级缓存?二级缓存能解决循环依赖吗?
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。
d、B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?
虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。
5、AOP 的增强过程
6、BeanFactory 和 ApplicationContext 的区别
BeanFactory 和 ApplicationContext 都是接口,并且 ApplicationContext 是 BeanFactory 的子接口。
BeanFactory 是 Spring 中最底层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。而ApplicationContext 是 Spring 的一个更高级的容器,提供了更多的有用的功能。
ApplicationContext 提供的额外的功能:国际化的功能、消息发送、响应机制、统一加载资源的功能、强大的事件机制、对 Web 应用的支持等等。
加载方式的区别:BeanFactory 采用的是延迟加载的形式来注入 Bean;ApplicationContext 则相反的,它是在 IOC 启动时就一次性创建所有的 Bean,好处是可以马上发现 Spring 配置文件中的错误,坏处是造成浪费。
7、@Resource和@Autowired的区别(五个)
1、装配方式不同:
@Resource 默认是按照名称(id)来装配注入的,只有当找不到与名称匹配的 bean 才会按照类型(class)来装配注入。(指定了name就先按照name,指定了类型就按照类型(type),如果两个都指定,那么就查找两个都同时满足的bean。如果都没有指定,就先按照name,再按照type)
@Autowired 默认是按照类型装配注入的,如果找到多个bean,继续以变量名作为 name,根据name 去查找这个 bean,此时若还是找到多个 bean,这时才会报异常。如果想按照名称来注入,则需要结合 @Qualifier 一起使用。
2、优先级:@Autowired支持优先级配置,可以添加@Primary注解标记bean的优先级。这个能力@Resource不支持。
3、来源不同:@Resource 遵循JSR-250规范,是由 J2EE(JAkArtAEE) 提供;而 @Autowired 是由 Spring 框架提供,在 Spring-bean依赖下面;@Resource 理论上可以搭配任何框架使用。
4、使用范围不一样:@Autowired可以用在很多地方,比如构造器,方法,方法的参数,成员变量,注解都能使用。@Resource只能用在 类,方法,成员变量上面。
5、参数不一样:@Autowired只有一个参数required,表示是否开启自动注入。@Resource有7个参数,最为熟悉的就是 name 和 type 这两个参数。
8、Spring中的bean是否存在线程安全
关于bean线程安全的问题,首先从作用域考虑,非单例的bean不存在安全问题。
其次从bean的状态考虑,bean的状态分为两种,第一种是无状态(一般只有查询,不涉及修改);第二种就是有状态的bean(指的是多线程环境,操作bean的成员变量的时候,既去查询他,又去修改他),有状态的bean就会存在线程安全问题。
如何解决 spring 中 bean 的线程安全:
1.作用域方面入手(改成prototype)
2.避免 bean 里面定义需要修改的成员变量
3.使用 Threadlocal 保存 Bean 的成员变量。
9、Spring中 Bean 的生命周期
也就是从创建到销毁的整个过程。
五个阶段
1:bean的实例化。 根据开发者的配置信息,通过反射或者工厂方法创建 bean 的实例,
2:属性赋值。 实例化后就给bean 的成员变量注入属性值。比如@Autowired 注解,@Value 注解等等。这个过程也就是依赖注入。
3:初始化。 属性赋值完成后,Spring容器会调用 bean 的一些初始化方法,比如 init-method,afterPropertiesSet,Aware接口的一些回调,BeanPostProcessor的一些前置处理器,后置处理器方法等等。
4:使用阶段。 初始化完成后bean已经可以使用了。
5:销毁。 当程序不需要使用bean 的时候就会被Spring容器负责销毁,这个过程可以实现DisposableBean这个接口或者配置文件里面配置 destory-method 方法来实现。
10、Spring中 bean 的作用域
1、singleton:默认作用域,单例。在整个Spring容器里面 bean 只会被实例化一次。
2、prototype:多例。每次从容器里面请求都会拿到一个新的 bean 的实例。使用这种需要谨慎,可能导致内存消耗过多。
下面四个跟 WEB 环境相关:
3、request:在一个 http 请求里面一个 bean 只会被实例化一次。意味着不同的 http 请求可能是多例的。
4、session会话:在一个 http 会话里面,这个 bean 只会被实例化一次。不同会话 bean 可能是多例的。
5、application:在一个 servletContext 里面,bean 只会被实例化一次。在一个 web 应用的生命周期里面,这个 bean 是单例的。
6、websocket:在一个 webSocket 会话里面,bean 只会被实例化一次。
四、SpringBoot相关
1、Spring Boot 怎么完成自动化配置的?
结论: SpringBoot不需要写配置文件的原因是,SpringBoot所有配置都是在启动的时候进行扫描并加载,SpringBoot的所有自动配置类都在Spring.factories里面,但是不一定会生效,生效前要判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器就能帮我们进行自动配置类
以前我们需要自己配置的东西,自动配置类帮我们做了:
1、SpringBoot在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值。
2、将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
3、整个J2EE的整体解决方案和自动配置都在 springboot-autoconfigure 的 jar 包中;
4、它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;
有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
Spring的自动装配原理:
Spring Boot启动的时候会通过 @EnableAutoConfiguration 注解找到 META-INF/spring.factories 配置文件中的所有自动配置类,并对其进行加载,这些自动配置类都是以 AutoConfiguration 结尾来命名的,它实际上就是一个 JavaConfig 形式的 Spring 容器配置类,通过 @Bean 导入到 Spring 容器中,以 Properties 结尾命名的类是和配置文件进行绑定的。它能通过这些以 Properties 结尾命名的类中取得在全局配置文件中配置的属性,我们可以通过修改配置文件对应的属性来修改自动配置的默认值,来完成自定义配置。
额外说一句:
run 方法的作用:
1、推断应用的类型是普通的项目还是Web项目
2、查找并加载所有可用初始化器 , 设置到initializers属性中
3、找出所有的应用程序监听器,设置到listeners属性中
4、推断并设置main方法的定义类,找到运行的主类
2、SpringBoot 初始化时的启动扩展点
a、监听容器刷新完成扩展点 ApplicationListener
容器刷新成功意味着所有的 Bean 初始化已经完成,当容器刷新之后 Spring 将会调用容器内所有实现了ApplicationListener 的 Bean 的 onApplicationEvent 方法,应用程序可以以此达到监听容器初始化完成事件的目的。
易错的点这个扩展点用在 web 容器中的时候需要额外注意,在 web 项目中(例如 spring mvc),系统会存在两个容器,一个是 root application context,另一个就是我们自己的 context(作为 root application context 的子容器)。如果按照上面这种写法,就会造成 onApplicationEvent 方法被执行两次。
解决此问题的方法如下:
自定义事件,可以借助 Spring 以最小成本实现一个观察者模式:
先自定义一个事件,注册一个事件监听器,发布事件,执行单元测试可以看到邮件的地址和内容都被打印出来了。
b、SpringBoot 的 CommandLineRunner 接口
c、@PostConstruct 注解
@PostConstruct 注解一般放在 Bean 的方法上,被 @PostConstruct 修饰的方法会在 Bean 初始化后马上调用。
d、InitializingBean 接口
InitializingBean 的用法基本上与 @PostConstruct 一致,只不过相应的 Bean 需要实现 afterPropertiesSet 方法
e、@Bean 注解中定义初始化方法(销毁方法也一样)
在创建对象的时候里面可以有多种方法,假设其中两个方法分别为 init() 和 destory();那么就可以通过 @Bean 注解来指定初始化方法和销毁方法:
@Bean(initMethod=“init”, destroyMethod=“destroy”)
bean 的销毁方法时在容器关闭的时候被调用的,applicationContext.close()。
bean对象的初始化方法调用的时机:
对象创建完成,如果对象中存在一些属性,并且这些属性也都已经赋值了,那么就会调用bean的初始化方法。对于单实例bean来说,在Spring容器创建完成后,Spring容器会自动调用bean的初始化方法;对应多实例bean来说,在每次获取bean对象的时候,调用bean的初始化方法。
bean对象的销毁方法调用的时机:
对于单实例bean来说,在容器关闭的时候,会调用bean的销毁方法;对于多实例bean来说,Spring容器只负责创建bean不会管理这些bean,也就不会自动调用这个bean的销毁方法了。小伙伴只能手动调用多实例bean的销毁方法了。
初始化、销毁方法的使用场景:
一个典型的场景就是对于数据源的管理。例如,在配置数据源时,在初始化的时候,会对很多的数据源的属性进行赋值操作;在销毁的时候,我们需要对数据源的连接等信息进行关闭和清理。这个时候,我们就可以在自定义的初始化和销毁方法中自定义操作逻辑。
3、如何优雅关闭 SpringBoot
1、利用 actuator(监控)提供的接口 shutdown 接口,调用这个接口就可以关闭,这个接口很敏感,默认情况下不开启。通过配置文件开启后,通过 /actuator/shutdown 地址调用接口,post请求。
2、调用应用程序的上下文的 close 方法。
这种方案跟第一种一样敏感,需要处理好认证和权限。
3、调用 SpringApplication 里面的 exit 方法,调用这个方法会触发SpringBoot程序,执行关闭的钩子函数,还有bean 销毁的钩子函数。能关闭是因为每一个 SpringApplication 都向JVM 注册一个关闭钩子,这个关闭钩子确保ApplicationContext 在退出的时候能优雅的关闭。
4、直接杀进程kill,这个也是可以正常关闭的。
五、SpringCloud(微服务)相关
1、分布式和集群有什么区别呢?
a、集群是个物理形态,分布式是个工作方式。集群一般是物理集中、统一管理的;一个程序或系统,只要运行在不同的机器上,就可以叫分布式。
b、分布式是相对中心化而来,强调的是任务在多个物理隔离的节点上进行,除了解决部分中心化问题,也倾向于分散负载,但分布式会带来很多的其他问题,最主要的就是一致性。集群就是逻辑上处理同一任务的机器集合。
c、分布式:一个业务分拆多个子业务,部署在不同的服务器上;集群:同一个业务,部署在多个服务器上。
d、集群是解决高可用的;而分布式是解决高性能、高并发的。
2、nacos里面要配置 namespace 和 group id 这两个的作用是什么?
group 是分组,namespace 是命名空间。一般 namespace 区分环境,group区分项目。
namespace:不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。
group:通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。
3、nacos 的配置变化了是服务器主动推送的还是微服务每次循环去拉取的?
(1)Nacos 采用的是客户端主动拉 pull 模型,应用长轮询(Long Polling)的方式来获取配置数据。
(2)Nacos 获取配置数据的逻辑比较简单,先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP 请求从远端拉取对应 dataId 配置数据,并保存到本地快照中,请求默认重试3次,超时时间 3s。获取配置有getConfig() 和 getConfigAndSignListener() 这两个接口,但 getConfig() 只是发送普通的 HTTP 请求,而getConfigAndSignListener() 则多了发起长轮询和对dataId数据变更注册监听的操作addTenantListenersWithContent()。
(3)注册监听:
客户端注册监听,先从 cacheMap 中拿到 dataId 对应的 CacheData 对象。如没有则向服务端发起长轮询请求获取配置,默认的 Timeout 时间为30s,并把返回的配置数据回填至 CacheData 对象的 content 字段,同时用 content 生成MD5 值;再通过 addListener() 注册监听器。其中 listeners 是对 dataId 所注册的所有监听器集合,其中的ManagerListenerWrap 对象除了持有 Listener 监听类,还有一个 lastCallMd5 字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。在添加监听的同时会将 CacheData 对象当前最新的 md5 值赋值给ManagerListenerWrap 对象的 lastCallMd5 属性。
(4)变更通知:
客户端又是如何感知服务端数据已变更呢?
我们还是从头看,NacosConfigService 类的构造器中初始化了一个 ClientWorker,而在 ClientWorker 类的构造器中又启动了一个线程池来轮询 cacheMap。而在 executeConfigListen() 方法中有这么一段逻辑,检查 cacheMap 中 dataId的 CacheData 对象内,MD5 字段与注册的监听 listener 内的l astCallMd5 值,不相同表示配置数据变更则触发safeNotifyListener 方法,发送数据变更通知。safeNotifyListener() 方法单独起线程,向所有对 dataId 注册过监听的客户端推送变更后的数据内容。客户端接收通知,直接实现 receiveConfigInfo() 方法接收回调数据,处理自身业务就可以了。
4、RestTemplate 和 Feign 的区别
a.请求方式:
RestTemplate需要每个请求都拼接url+参数+类文件,灵活性高但是消息封装臃肿。
feign可以伪装成类似SpringMVC的controller一样,将rest的请求进行隐藏,不用再自己拼接url和参数,可以便捷优雅地调用HTTP API。
b.底层实现:
RestTemplate在拼接url的时候,可以直接指定ip地址+端口号,不需要经过服务注册中心就可以直接请求接口;也可以指定服务名,
请求先到服务注册中心(如nacos)获取对应服务的ip地址+端口号,然后经过HTTP转发请求到对应的服务接口
(注意:这时候的restTemplate需要添加@LoadBalanced注解,进行负载均衡)。
Feign的底层实现是动态代理,如果对某个接口进行了@FeignClient注解的声明,Feign就会针对这个接口创建一个动态代理的对象,
在调用这个接口的时候,其实就是调用这个接口的代理对象,代理对象根据@FeignClient注解中name的值在服务注册中心找到对应的服务,
然后再根据@RequestMapping等其他注解的映射路径构造出请求的地址,针对这个地址,再从本地实现HTTP的远程调用。
5、docker怎么解决容器间的通信
docker network 来创建一个桥接网络,在 docker run 的时候将容器指定到新创建的桥接网络中,这样同一桥接网络中的容器就可以通过这个桥接网络互相访问。
六、MySQL 相关
1、mybatis的缓存(一级缓存二级缓存那些)
a. 一级缓存
SqlSession 级别的缓存,缓存的数据只在 SqlSession 内有效。
默认开启;一级缓存是 sqlSession 级别的缓存。在操作数据库时需要构造 sqlSession 对象,在对象中有一个基于 PerpetualCache 的 HashMap 本地缓存数据结构,用于缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互不影响的。一个SqlSession 结束后那么它里面的一级缓存也就不存在了。
b.二级缓存
mapper 级别的缓存,同一个 namespace 公用这一个缓存,所以对 SqlSession 是共享的。二级缓存需要我们手动开启。(全局级别)二级缓存是 mapper 级别的缓存,多个 sqlSession 去操作同一个Mapper 的 sql 语句,多个 sqlSession可以共用二级缓存,二级缓存是跨 sqlSession 的。二级缓存是基于映射文件的缓存(namespace),缓存范围比一级缓存更大,不同的 SQLSession 可以访问二级缓存的内容。哪些数据放入二级缓存需要自己指定
c.一级缓存原理
(1)查询数据时,会先到缓存中查询是否有,如果没有则到数据库中查找,找到后存到缓存中。
(2)如果 sqlSession 去执行 commit 操作(插入、更新、删除),清空 sqlSession 中的一级缓存,保证缓存中始终保存的是最新的信息,避免脏读。
(3)两次查询须在同一个 sqlsession 中完成,否则将不会走 mybatis 的一级缓存。在 mybatis 与 spring 进行整合开发时,事务控制在 service 中进行,重复调用两次 servcie 将不会走一级缓存,因为在第二次调用时 session 方法结束,SqlSession 就关闭了
d.二级缓存原理
(1)二级缓存与一级缓存原理相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper ( Namespace ),并且可自定义存储源,如 Ehcache。作用域为 namespance 是指对该 namespance 对应的配置文件中所有的 select 操作结果都缓存,这样不同线程之间就可以共用二级缓存。
e.二级缓存设置对象策略
(1)readOnly=“true”(只读):MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户 。不安全,速度快。
(2)readOnly=“false”(读写,默认):MyBatis 觉得获取的数据可能会被修改,MyBatis 会利用序列化或反序列化的技术克隆一份新的数据。安全,速度相对慢。
f.二级缓存注意事项
(1)如果 SqlSession 执行了 DML 操作(insert、update、delete),并 commit 了,那么 mybatis 就会清空当前mapper 缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读。
(2)mybatis 的缓存是基于[ namespace:sql语句:参数 ]来进行缓存的,意思就是,SqlSession 的 HashMap 存储缓存数据时,是使用[ namespace:sql:参数 ]作为key,查询返回的语句作为value保存的。
g.开启二级缓存
通过application.yml配置二级缓存开启:mybatis.configuration.cache-enabled=true
2、mysql数据库的 innodb 和 myisam 的区别?
a、InnoDB支持事务,MyISAM不支持
b、MyISAM适合纯读或者纯写为主,InnoDB适合频繁修改以及涉及到安全性较高的应用
c、InnoDB支持外键,MyISAM不支持
d、从MySQL5.5.5以后,InnoDB是默认引擎
e、InnoDB不支持FULLTEXT类型的索引
g、InnoDB中不保存表的行数,如select count() from table时,InnoDB需要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。但是当count()语句包含where条件时MyISAM也需要扫描整个表。
h、对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引。
i、清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表。
j、innodb默认表锁,使用索引检索条件时是行锁,而myisam是表锁(每次更新增加删除都会锁住表)。
k、innodb和myisam的索引都是基于b+树,但他们具体实现不一样,innodb的b+树的叶子节点是存放数据的,myisam的b+树的叶子节点是存放指针的。
l、innodb是聚簇索引,必须要有主键,一定会基于主键查询,但是辅助索引就会查询两次,myisam是非聚簇索引,索引和数据是分离的,索引里保存的是数据地址的指针,主键索引和辅助索引是分开的。
3、innodb支持几种索引?
参考网址:https://www.cnblogs.com/aluna/p/15850620.html
主键索引(B+树)、普通索引、唯一索引、全文索引(5.6.4版本以后。char、varchar、text的列才支持)、哈希索引
聚簇索引(是一种数据存储方式,所有的用户记录都存储在了叶子节点,也就是所谓的索引即数据,数据即索引),特点:
使用记录主键值的大小进行记录和页的排序:
1、页内的记录是按照主键的大小顺序排成一个单向链表
2、各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
3、.存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
B+树的叶子节点存储的是完整的数据记录:
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
优点:
数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,
聚簇索引对于主键的排序查找和范围查找速度非常快,
按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,
所以节省了大量的IO操作。
缺点:
插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,
对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
索引的代价:
空间上的代价:
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,
一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
时间上的代价:
每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。增、删、改操作可能会对节点和记录的排序造成破坏,
所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收等操作来维护好节点和记录的排序。如果建了许多索引,
每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。
4、对sql优化的理解
1、sql语句中IN包含的值不应过多(MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。
但是如果数值较多,产生的消耗也是比较大的。再例如:select id from t where num in(1,2,3) 对于连续的数值,能用between就不要用in了;
再或者使用连接来替换。)
2、数据分页 limit
3、当只需要一条数据的时候,使用limit 1(这是为了使EXPLAIN中type列达到const类型,如果加上limit1,查找到就不用继续往后找了)
4、如果排序字段没有用到索引,就尽量少排序,可以在程序中排序。
5、如果限制条件中其他字段没有索引,尽量少用or。(or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。
很多时候使用union all或者是union(必要的时候)的方式来代替“or”会得到更好的效果。)
6、尽量用union all代替union。(union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,
增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。)
7、小表驱动大表(in 的话里面驱动外面,in适合子查询是小表;exist 的话外面驱动里面,适合外面是小表)
8、用链接查询代替子查询(子查询缺点:需要创建临时表,查询完成后再删除临时表,有一些额外开销。)
9、控制索引的数量(一般不超过五个)
10、选择合理的字段类型
11、提升group by的效率
12、避免用 select * (浪费数据库资源,内存,cpu查出来的数据多,通过网络IO传输过程中也会增加传输时间)
13、批量插入(mybatis plus 的insertBatch,当然一次插入量也不能太大,可以分批插入。)
14、join表不宜过多,不宜超过3个。(如果join太多,MySQL在选择索引时会非常复杂,很容易选错索引)
15、join的注意事项:
inner join:mysql会自动选择小表驱动
left join:左边的表驱动右边的表
16、选择合理的字段类型
char:固定字符串类型,该类型在的字段在存储空间上是固定的,固定长度的可以用
varchar:变长字符串类型。
能用数字类型就不用字符串,字符串处理速度比数字类型慢。
尽量用小类型,比如:用bit存布尔值,用tinyint存枚举值等。
17、提升group by效率(先过滤数据,减少数据,再分组)
5、你说说死锁怎么解决
死锁(DeadLock)指的是两个或两个以上的运算单元(进程、线程或协程),都在等待对方释放资源,但没有一方提起释放资源,从而造成了一种阻塞的现象就称为死锁。
死锁产生的四个必要条件:
1、互斥:一个资源一次只能被一个进程使用,当该进程使用该资源的时候,其他进程就不能使用,具有独占性;
2、请求与保持:一个进程要请求新的资源,但同时对已获得的资源不释放,要等待其他进程释放资源;
3、循环等待:若干进程都要申请资源,但是都对已获得的资源不释放,都要等待其他进程释放资源,若干进程陷入循环等待资源;
4、不可剥夺:进程已获得的资源,在未使用完之前,不能被强行剥夺。
只要上述条件之一不满足,就不会发生死锁:
比如当一个运算单元获取到锁的时候其他就等待,或者给锁加一个时间,时间到了强制释放锁。
6、resultMap 标签干什么用的?
sql代码的复用,类似于java的继承。
7、一个 sql 查询变得很慢,你会怎么去分析这个 sql
1、检查 SQL 是否走索引,或者检查是否出现索引失效的情况。如果是单条 sql,则使用 explain 进行相关分析。
2、单表数据量数据过多,导致查询瓶颈:可以考虑对表进行切分,或者分库。
3、网络原因或者机器负载过高:可以进行读写分离。
8、什么样的情况下不会走索引
1、用 != 或者 <> 导致索引失效
2、字段编码类型不一致进行关联查询不走索引;如:a表和b表,一个utf8编码,一个utf8mb4编码
3、函数导致的索引失效
4、运算符导致的索引失效
5、OR引起的索引失效。比如:or链接同一个字段走索引,不同就不走索引。或者两个字段都有索引才走索引
比如 name 有索引,email 没有索引。
6、模糊搜索导致的索引失效,如 like的百分号放左边不走索引,或者两边都有。
7、NOT IN、NOT EXISTS导致索引失效
8、is null,is not null也无法使用索引,不走索引!
9、数据类型隐式转换,字符串列与数字直接比较,不走索引
10.可能某张表存在可用的索引,但是数据量少或者索引列选择性不高,mysql优化器可能选择全表扫描,可能判断次数全表扫描更快。
9、mybatis的加载流程是怎样的?
启动程序,通过 springfactoryBuilder 去加载 mybatis 全局配置文件,生成 sqlSessionFactory,通过这个去生成 sqlSession,sqlSession 代表的是一个会话,通过这个去获取 mapper,获取到代理类执行即可找到对应的 mapper.xml文件,扫描 mapper 接口。生成 mapperProxyFactory 生成代理类,再执行具体的增删改查。
10、哪些情况会导致MySQL索引失效?
1、索引列上使用函数或者表达式
2、查询条件中数据类型和索引列中数据类型不匹配的时候,会发生隐式类型转换
比如user_id 是整数,这里用的是字符串。
3、使用 or
11、非聚集索引一定会回表吗?
不一定。
1、比如ab组成复合索引,根据a查询b,这时候不需要回表。
2、如果在非聚集索引中查询的是主键值,就不需要。
12、怎么定位慢SQL?
最常用方法就是开启慢查询日志。在mysql配置文件里面配置。
13、MySQL中有哪些锁?
1、读锁(共享锁,S锁):当一个事务加上读锁,其他事务只能只能对这条数据加读锁,而不能加写锁。直到这条数据所有读锁都释放之后,其他事务才能加写锁进来。
2、写锁(排他锁,X锁):当一个事务为数据加上写锁,其他事务不能对这条数据加任何锁。
3、意向锁:是一种表级锁,可继续细分为意向共享锁和意向排他锁,作用是想获取表锁的时候,不用去遍历表的每一行数据添加什么锁,可以直接判断表是否存在意向锁就可以了。
4、记录锁:是行级锁的实现方式,这种锁允许对数据库中特定的行进行加锁,这种细粒度可以提高并发性能。
5、间隙锁:对索引记录之间的空隙进行锁定,以及对第一个索引记录之前或者最后一个索引记录之后的空隙进行锁定,这种锁机制可以防止其他事务在这些缝隙里面插入新数据,这个锁用来解决幻读问题。
6、Next-key Lock:记录锁+间隙锁。
7、元数据锁:特殊的锁机制,主要用来保护元数据对象,比如表结构,索引,触发器等等的一致性和完整性。多个并发事务对元数据锁对象修改的时候,能够保证只有一个事务可以成功修改。
14、如何防止SQL注入?
使用 PrepareedStatement,最有效方法,因为sql语句是固定的,用户输入只是参数,传递给sql,并不会把用户输入拼接到 sql 语句里面,这样就可以确保用户输入被正确转义,可以防止 sql 注入。
七、Redis 相关
1、redis的数据类型以及你们公司具体的应用
1、String(动态字符串):说是字符串但它的内部结构更像是一个 ArrayList,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配:
redis的内存分配机制:
(1)当字符串的长度小于 1MB时,每次扩容都是加倍现有的空间。
(2)如果字符串长度超过 1MB时,每次扩容时只会扩展 1MB 的空间。
(3)字符串最大长度为 512MB。
应用场景:计数器:string类型的incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性,总能安全地进行加减操作,
因此可以用string类型进行计数,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。
2、List(一个列表最多可以存储2^32 - 1个元素。):
可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,
可以充当栈和队列的角色。
列表类型有以下特点:
(1)列表中的元素是有序的,即可以通过索引下标获取某个元素或者某个范围内的元素列表。
(2)列表中的元素可以是重复的。
(3)redis中的list底层可不是一个双向链表那么简单。当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),
它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。
应用场景:
(1)消息队列:
Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的
争抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用;
(2)最新列表:
list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,
如朋友圈的点赞列表、评论列表。
3、hash(字典:Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,
值得注意的是在 Redis 的 Hash 中 value 只能是字符串):
应用场景:
购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量为value,恰好构成了购物车的3个要素。
存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
4、set(集合:Redis中的 set和Java中的HashSet 有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,
字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。)
应用场景:
好友、关注、粉丝、感兴趣的人集合:
(1)sinter命令可以获得A和B两个用户的共同好友;
(2)sismember命令可以判断A是否是B的好友;
(3)scard命令可以获取好友数量;
(4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而 srandmember 命令则可以从中随机获取几个。
存储某活动中中奖的用户ID :因为有去重功能,可以保证同一个用户不会中奖两次。
5、zset(有序集合:zset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。
它的内部实现用的是一种叫作“跳跃列表”的数据结构。)
应用场景:
zset 可以用做排行榜,但是和list不同的是zset它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,
我们可以对粉丝列表按关注时间进行排序。
zset 还可以用来存储学生的成绩, value 值是学生的 ID, score 是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。
2、redission的分布式锁原理
参考网址:https://www.cnblogs.com/crazymakercircle/p/14731826.html#autoid-h3-5-0-0
watchdog 的具体思路是:加锁时,默认加锁 30秒,每10秒钟检查一次,如果存在就重新设置 过期时间为30秒。
底层主要是通过 lua 脚本去完成原子性的命令操作。
注意:
(1)watchDog 只有在未显示指定加锁时间时才会生效。
(2)lockWatchdogTimeout设定的时间不要太小 ,比如我之前设置的是 100毫秒,由于网络直接导致加锁完后,watchdog去延期时,这个key在redis中已经被删除了。
3、redis为什么这么快(为什么被设计为单线程)?
5个方面:
1、redis是单线程,相比于多线程,CPU避免了上下文切换和锁的竞争消耗,提高了利用率,以及IO多路复用和虚拟内存机制(冷热数据分离,冷数据从内存交到磁盘中,腾出内存处理热数据)
2、单线程设计避免多线程设计的复杂问题,比如锁,并发控制。
3、避免锁带来的额外性能开销(获取锁释放锁甚至死锁问题)。
4、充分利用了cpu的缓存,cpu通常有多级缓存功能,用来加速数据访问,多线程环境由于不同线程同时访问不同数据,导致缓存的命中率下降,影响性能。
5、易于拓展和部署,单线程设计使得扩展和部署更加简单方便,不需要考虑线程同步和并发控制问题,非常容易实现集群和高可用的特点。
4、redis 是单线程还是多线程(6.0之前以及6.0之后)
redis6.0中的变化导致有些人的一些误解。
6.0之前: 在6.0之前主要是单线程,这个单线程指的是键值对的读写,还有网络 io 的操作,是由一个线程来完成的。redis在处理客户端请求的时候,包括请求数据的读取,数据的解析,命令的执行,内容的返回,这些操作是由一个顺序串行的主线程去处理的。
但是redis 的其他功能,比如持久化,异步删除,集群数据的同步等等,是由额外的线程去处理的,这些额外的线程一般用来处理一些后台任务,防止主线程被阻塞,从而确保redis 的性能还有稳定性。
6.0之后: 6.0开始redis 引入了一个新的多线程处理场景,这个多线程主要处理网络请求处理这块,6.0提出的东西叫 Threaded IO,也就是网络IO处理方面的多线程,引入这东西后在网络请求过程中,redis 会采用多线程的操作,提高网络传输的性格。但是执行命令的核心模块还是 单线程 。
5、Redis内存淘汰策略有哪些?
大概8种,内存空间不足的时候自动清除一部分已有数据。大致分为两大类:
第一类,在设置了过期时间的数据中进行淘汰:
1.volatile-random:从已经设置的过期时间数据中随机选择数据进行淘汰
2.volatile-ttl:优先淘汰更早过期的key
3.volatile-lru:在所有设置了过期时间的key里面,淘汰最长时间没有使用的key——redis3.0之前默认方案
4.volatile-lfu:在所有设置了过期时间的key里面,淘汰最少使用的key——redis4.0后引入的新的淘汰策略
第二类,在所有数据范围内进行淘汰:
1.allkeys-random:随机淘汰任意key
2.allkeys-lru:淘汰整个键值中,最久没有使用的key
3.allkeys–lfu:淘汰整个键值中最少使用的key
特殊策略(默认策略):noevication:当运行内存超过设置的最大内存时,不淘汰任何数据,直接报错。
6、如何发现Redis中的热点Key?
1.通过业务预估,比如今天某个商品做促销,那么这个商品大概率是热点key。
2.客户端收集。操作redis会用到客户端工具,比如 jedis, Spring Data redis,对这些工具进行封装,再去对操作key进行统计。
3.利用 redis monitor,redis cli里面有个命令,叫 monitor 命令,这个命令可以实时查看redis 实例的执行情况,通过分析输出的日志就能找到访问频率比较高的热点 key。
4.redis-stat。这是个实时监控 redis实例的一个工具,这个工具可以展示包括命令的执行次数,内存的使用情况等一系列的指标。
5.redis 的4.0.3版本里面添加了一个 hotkeys 查找特性,可以用过 redis-cli --hotkeys去获取当前keyspace 里面的热点 key,这种方式不需要二次开发,是现成的就可以实现,但是因为要扫描整个 keyspace,所以在实时性上面差点意思。
7、RedLock有哪些优点?
RedLock 是 redis 官方提出的基于 redis 分布式锁的方法。这种方法比原先单节点方法更安全,主要解决了分布式系统中如何保证多个客户端对共享资源互斥访问的问题。
特性和有优点:
1、互斥访问:通过 RedLock 可以确保永远只有一个客户端能拿到锁。从而实现对共享资源的互斥访问。
2、避免死锁:RedLock在设计的时候就考虑了各种可能得异常情况,比如锁定资源的服务崩溃或者网络分区。
3、容错性:只要有一半以上的 redis 节点存活,RedLock 就能正常的提供服务。
RedLock通过从多节点获取锁,解决主节点可能宕机时导致的锁丢失问题。 但是也有多节点挂机导致的性能安全问题,追求可靠性的话也可以用比如Zookeeper实现
8、redis 如何实现延时队列
专业的事情交给专业的工具来做,redis能做,但最好还是交给MQ来做。
通过 zset 来实现延时队列,zset里面有个 score,存储任务的时候 score 设置为 任务的执行时间戳,消费的时候客户端通过 zrangebyscore 的命令去查找和当前时间相符合的任务,找到就执行,没找到就休息一秒继续通过 zrangebyscore 的命令去查找任务,这个过程中如果有涉及原子性的操作通过 lua 脚本去实现。 这种延时队列并没有很大价值,因为没有消息重试机制,也没有ack,消息可靠性得不到保障。
9、Redis怎么实现消息队列
专业的事情交给专业的工具来做,redis能做,但最好还是交给MQ来做。
1、发布/订阅模式
2、redis 中的 list 数据类型,然后使用 lpush 操作实现入队,brpop操作实现出队,这种支持阻塞式获取消息,不过没有消息广播功能,也没有ack机制,
3、redis的 zset 数据类型,利用 zset 的 score 做时间戳,可以实现延迟消息队列(看上题)
4、从 redis5开始新增了一个叫 stream 的数据类型,这是对消息队列的完善,支持消息广播,消息持久化,也可以阻塞式获取消息,也支持并发消费,提供了ack机制,但是受到本身限制没办法做到消息百分百可靠。超过队列长度的消息也没有很好的办法去处理
10、Redis6为什么要引入多线程
是对 redis6之前的单线程操作的补充,核心还是单线程。redis数据放到内存里边,内存的响应时间是 100纳秒左右,对于小数据包 redis服务器可以处理 8W——10W 的qps,对于绝大多数公司已经够用,但是互联网公司动辄上亿的交易量就不够看了,需要更大的 qps。
redis作者发现读写网络系统的调用占用了 redis 的执行期间大部分cpu时间,redis瓶颈主要在于 io 网络消耗,所以redis6开始处理网络请求的时候引入多线程,提高网络的性能。
八、消息队列相关
九、Linux 相关
1、linux查看端口命令
netstat -ntlp——查看当前所有tcp端⼝
netstat -ntulp | grep 80——查看所有80端⼝使⽤情况
2、linux常用指令
cp
midir
cat xxx | grep “xx” | more
tail -f
kill
docker
sh xx
倒叙日志:tac
等等
3、linux杀死进程的指令、-9跟-15的区别
-9是强制杀死进程,立马停止;-15是通知程序安全干净的退出。
4、线上服务器CPU飙升如何定位问题
四个步骤:
1.使用 top 命令去找到 cpu 里面占用过高的一个进程,获取进程 id:
2.使用 ps -mp 命令找到这个进程下面占用 cpu 比较高的线程 id,这时候拿到的线程 id 是十进制的一个数,不能直接使用,要转成16进制
3.使用 printf 命令把十进制转换成 16 进制,这样一来就知道哪个线程导致 cpu 过高
4.通过 jstack 命令输出线程的运行日志,根据日志的提示检查代码里面的问题。
5、Nginx负载均衡策略有哪些?
4种。
**1、轮询。**每个请求按时间顺序逐一分配到不同的后端服务器,如果某个服务器宕机了,nginx会将宕机的服务器踢出服务列表。这种策略适用于服务器性能相当,无状态,且短平快的服务。
**2、加权轮询。**在轮询的基础上,给不同的后端服务器设置不同的权重,权重越高,能获得更多请求。这种方式可以根据服务器的性能还有负载情况去分配请求,能更好的利用服务器资源。
**3、IP Hash。**根据客户端的ip地址进行哈希运算,把相同的ip地址请求始终发到同一个后端服务器上,这样可以保证来自同一客户端的请求始终被同一台后台服务器去处理。这种策略适用于需要进行会话保持或者缓存一致性的场景。
**4、最少链接。**这种策略会把请求发给当前连接数最少得后台服务器,nginx会动态统计每个服务器的连接数,然后把请求分配给连接数最少得服务器。适用于对于长连接和短连接混合的场景。
十、场景题
1、有没有遇到过超卖的情况,怎么解决?
参考网站:http://www.pingtaimeng.com/article/detail/id/2143325
商城设计的过程中,必然会考虑到一个库存扣除的问题,超卖将会带来一定的损失和麻烦,在某一个时间段的瞬间的流量会造成库存的并发性操作带来库存超额扣除。
1、流量较少时,可以采用锁定最后库存+乐观锁解决方案。但是缺点就是操作的时间内只能有一个用户操作成功,如果执行一个200ms,则其他用户等待时间会很长。
2、redis的队列来实现。将要促销的商品数量以队列的方式存入redis中,每当用户抢到一件促销商品则从队列中删除一个数据,确保商品不会超卖。这个操作起来很方便,而且效率极高。
3、分阶段排队下单方案(将提交操作变成两段式):
第一阶段申请,申请预减减库,申请成功之后,进入消息队列;
第二阶段确认,从消息队列消费申请令牌,然后完成下单操作。 查库存 -> 创建订单 ->扣减库存。通过分段锁保障解决多个provider实例并发下单产生的超卖问题。
3-1、申请阶段:
将存库从MySQL前移到Redis中,所有的预减库存的操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。(通过redis自增或者自减操作来实现原子性)
3-2、确认阶段:
然后通过队列等异步手段,将变化的数据异步写入到DB中。引入队列,然后数据通过队列排序,按照次序更新到DB中,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
但是下单阶段的性能比较差,如何提升性能呢?
可以使用Redis 分布式锁。为了达到每秒600个订单,可以将锁分成 600 /5 =120 个段,每个段负责5个订单,600个订单,在第二个阶段1秒钟下单完成。
该方案优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
该方案缺点:数据不一致。由于异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
2、sku跟spu的区别
计量单位不同、描述特性不同、产品分类不同。
1、计量单位不同:
SPU = Standard Product Unit (标准产品单位),SPU是商品信息聚合的最小单位;
SKU=stock keeping unit(库存量单位),SKU即库存进出计量的单位。
2、描述特性不同:
SPU是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性;
SKU的计量可以是以件、盒、托盘等为单体,就是物理上不可分割的最小存货单元。
3、产品分类不同:
SPU描述的就是属性值、特性相同的商品。例如:iphone4就是一个SPU,与商家,与颜色、款式、套餐都无关。
SKU在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。例如:纺织品中一个SKU通常表示:规格、颜色、款式。
3、假设现在需要调用第三方接口,这个接口限制每秒请求5次,我们的系统需求是对这个接口调用每秒100次,你怎么解决这个问题?
redis令牌桶或者mq队列
4、假设现在有一万个数,你要对这些数进行累加,每个加法耗时0.5秒,你怎么样以最小的时间把累加的值算出来?
这里考的是线程如何分配最优
多线程,由于是cpu计算密集型,cpu核心数 * cpu利用率 * (1+ 线程等待时间/线程计算时间)
5、假设接到一个项目,某些部分的接口,比如 controller,在 jar 包里面,我们只有class,没有其他,要对他原有的功能进行修改,你怎么做?
通过反编译软件 jd-gui 反编译得到反编译文件,然后去修改文件。
还有种非反编译操作:
新建一个包,使其满足该文件所需的包名路径。编写修改好的java文件–>编译该java文件为class文件–>解压jar包–>找打待修改文件的class文件并将其替换–>压缩源码文件,并改为jar后缀格式。随后解压该jar包,替换目标class文件,再次压缩回为jar文件即可!大功告成!
6、春节抢红包怎么设计随机金额?
一般来说,红包基本分配了两种不同的算法,第一种的话叫做金额随机法,第二种叫做二倍均值法。
a、金额随机法(不太公平,极端情况差异大,但是刺激)
那么金额随机法的话可能是最容易想到的一种方式,这种方式比较简单也比较直观,在这个方法里面。里面随机生成的,比如说我们的范围是0-100对吧?然后用红包你就在这个区间里面去生成随机红包的金额,这个方法比较简单,直接把红包金额都是随机生成的;这种方式有一个缺点就是特别不公平,比如说我现在想发100块钱发10个红包,那么宣传第一个红包的时候我的取值在0.01-99.9之间,或者是0.0~100块钱举个例子当取到100了对吧?后面9个就没有了。
b、二倍均值法(相对更公平)
首先确定红包的总金额,还有红包的一个数量,然后的话去计算每个红包的一个平均金额,就是总金额除以数量就是平均金额。
然后把每个红包的金额范围都设定在一个平均值的两倍以内。这样的话每个红包金额在0.01到平均金额的两倍之间的话就随机生成。
比如还是100块钱发10个红包,那么平均数就是10,乘以2也就是20,也就是红包金额在0.1~20之间取一个随机数,假设第一次生成了一个12,那么剩下的就是88,还有9个人,那么此时平均数就是 88/9*2 = 19,那么随机数就是0.01-19之间生成,后面依次类推。
两种方法无所谓好坏,看需求,是需要刺激还是需要公平。
7、如何设计订单自动超时
主流使用消息中间件的方案,以 rabbitMQ 为例:
1.死信队列和死信交换机。
2.用rabbitmq 的插件 rabbitmq-delayed-message-exchange ,有了这个插件就可以发送消息的时候设置延迟时间。
8、如何解决幂等性问题
由于用户重复提交或者网络抖动,恶意攻击等原因导致请求多次重复发送,服务器要识别出这种请求并且只解决依次,这就是解决幂等性。方法有五种
1.利用数据库唯一主键,或者唯一索引处理,插入数据前检查是否已经存在这个主键或者索引,已经存在就再执行插入操作。
2.业务状态的校验,比如支付系统的订单状态,如果订单状态已经处于已支付/支付成功,再次支付就应该拒绝或者返回错误。
3.通过分布式锁,分布式系统通过分布式锁来保证幂等性,执行操作之前先获取锁,如果获取成功就执行操作,否则说明这个操作已经被其他节点操作过了。这个可以利用 redis 的 setnx 来实现。
4.token机制,可以在请求里面加全局唯一的token,服务器收到请求先检查token是否存在,一般token可以存在redis里面,如果存在说明请求已经被处理过了。
5.乐观锁,通过版本号或者时间戳等方式去实现乐观锁。执行操作前先检查版本号或者时间戳是否一致,如果不一致说明数据已经被其他节点修改过。
在解决幂等性问题时要注意不要产生性能下降或者系统复杂度增加。
9、生产服务器变卡,怎么排查?
四个维度考虑:网络、CPU利用率,IO效率,内存瓶颈
首先的话可以先通过 netstat 还有 iftop 等这样一些工具去查看网络流量和网络连接的情况,检查一下是否存在网络拥塞丢包等等一些问题,然后根据你检查结果来进行优化;
(第二步具体可查看目录第九的第4题)第二步的话我们可以通过Top命令的话去定位一些占用的CPU过高的一些进程,然后进一步定位到进程里面比较活跃的一些线程,然后通过这些命令输出现成的一个运行日志,再根据这些日志的话去排查问题,好这是第二步。
第三步的话我们要去检查一下磁盘IO 是否导致了卡顿,这个话我们可以使用iostat,iotop的一些工具去查看磁盘的一个读写情况,检查磁盘是否负载过高,如果磁盘负载过高的话,我们可以使用一些缓存系统,或者优化的一些策略,减少随机写入等等方式去进行一些优化。
第四步的话就是我们检查内存这块是否存在瓶颈,因为如果内存使用过高的话,可能会导致频繁的垃圾回收,频繁的垃圾回收的话,肯定会影响到服务器的一个性能的,这个话我们可以使用dump命令去导出来 jvm 的堆内存,然后用 idea 或者是 mat 工具,进行分析,找出来内存占用过高的对象,同时排插是否存在内存泄露的问题。
如果dump 出来的堆内存是正常的,继续借助pmap命令,检查进程的内存分配是否正常。如果都正常,需要进一步开启 GC日志,通过 jstack 命令,分析用户线程暂停的时间和各部分区域 GC 的次数,检查问题是否出在GC上面,去进行参数调优。
10、如何平滑系统突发流量?
三种
1、滑动窗口算法:顾名思义实际上就是允许窗口在时间轴上面去滑动这种算法,它会把一个时间窗口的时间分成很多的时间片,然后它把每个时间片内的请求进行累加累加之后再去计算当前时间点向前一个时间窗口的内的请求总数,这几个总数如果超过预设的阈值的话,后续的就会被拒绝相比第一种的话这种方案会比较好的去解决突刺的问题,因为窗口被滑动了,但是这种算法的缺点的话就实现起来比较麻烦一些。
2、漏斗算法:这种算法像是大家平时常见的漏斗,一边大一边小,请求从大口进,从小出口出,如果都满了你就会被拒绝,出口的速率就相对于恒定的,这种方式就很好的去平滑系统突发流量的,因为不管入口流量有多少,出口流量都是恒定的。
3、redis令牌桶算法。
这三种算法无所谓好坏,看实际需求。一般用令牌桶会多一些。
11、get 和 post 这两种请求有何不同?
1、使用场景不同。get 请求通常用来获取或者查询资源。post用于创建和修改资源。
2、幂等性不一样。get请求幂等,post不幂等。
3、请求参数格式不一样。get请求参数通常放在 url 里面。post 请求参数通常放在请求体里面。因此get请求相对更容易泄露敏感数据。
4、因为 get 请求幂等,所以可以被浏览器或者其他中间节点比如网关或者代理给缓存起来,可以提高性能和效率。post请求就不适合。
12、Getaway 和 nginx 反向代理的区别?
这两个不是一个东西,但还是有人面试问到,遂记录。
这两个都可以做代理。
1、应用场景不同。getaway 适用于微服务架构里面的服务间通信,关注的是服务之间的交互效率,还有可靠性。nginx 主要用于 web 服务器、反向代理服务器还有负载均衡服务器,可以代理客户端和服务端之间的一个请求,提高网站访问的速度还有可靠性。
2、底层实现不同。nginx使用 C语言编写,性能优化和扩展主要依赖C语言的优势,getaway 用 java 语言编写,所以 getaway 更好的对微服务实现扩展功能,比如安全控制,异常统一处理,性能监控等等。
3、功能特性不同。nginx有强大的并发处理能力,最高支持5万个并发。getaway主要具有路由,断言,过滤器等等这些功能。
13、项目哪些地方用过AOP?
1、记录日志,给方法加个注解就可以记录方法的执行日志
2、权限控制
3、事务管理
4、幂等性处理