Java面试题整理合集
一、Java基础面试题
1. JVM内存模型有哪些组成部分?
主要有堆、栈(包括虚拟机栈、本地方法栈)、方法区和PC寄存器几个部分组成。
2. JVM常用的GC有哪些?
- G1 GC:适用于大型应用服务器,要求低延迟和高吞吐量的场景。
- CMS GC:适用于对响应时间敏感的应用,如Web服务器和交互式应用程序。
- Parallel Scavenge GC:适用于需要高吞吐量的后台任务和批处理应用。
3. JVM的调优参数有哪些?
-Xms
:设置JVM堆内存的初始大小,例如-Xms512m
表示初始堆内存为512MB。-Xmx
:设置JVM堆内存的最大大小,例如-Xmx2g
表示最大堆内存为2GB。-Xmn
:设置年轻代内存的大小,例如-Xmn256m
表示年轻代的大小为256MB。-Xss
:设置每个线程的栈大小,例如-Xss512k
表示每个线程的栈大小为512KB。-XX:NewSize=1g
设置年轻代的初始大小为1g。-XX:MaxNewSize=1g
:设置年轻代的最大大小为1g。-XX:NewRatio=4
设置老年代和新生代的比例,默认为2。-XX:SurvivorRatio=8
设置Eden区和s区的比例,默认为8。-XX:MetaspaceSize=256m
:设置元空间的初始大小为256m。-XX:MaxMetaspaceSize=256m
:设置元空间的最大大小为256m。-XX:+UseParallelGC
:选择并行垃圾回收器(Parallel GC),适用于多处理器环境。它在年轻代使用多线程进行垃圾回收,也称为吞吐量优先垃圾回收器。-XX:+UseParallelOldGC
:启用老年代的并行回收(Parallel Old GC),与Parallel GC配合使用,进一步提高垃圾回收的效率。
4. 什么是双亲委派模型?
双亲委派模型是Java类加载机制中的一种处理方式,它通过一种层次化的类加载机制来确保类的唯一性和安全性。当一个类加载器收到类加载请求时,它不会立即尝试加载该类,而是将请求委派给其父类加载器。父类加载器会继续向上委派,直到到达启动类加载器。如果父类加载器无法加载该类,子类加载器才会尝试自己加载。如果所有类加载器都无法加载该类,则抛出ClassNotFoundException
异常。双亲委派模型保证了每个类只会被加载一次,避免了重复加载相同的类。
5. 多线程里的sleep和wait的区别是什么?
所属类和方法定义:
sleep
:属于Thread
类的方法,用于让当前线程暂停执行一段时间。调用时需要捕获InterruptedException
异常。wait
:属于Object
类的方法,用于线程间的通信和同步。调用wait方法前必须获取对象的锁,并且通常在synchronized
块中使用。
唤醒方式:
sleep
:线程会进入TIMED_WAITING
状态,等待指定的时间后自动返回就绪状态,无需其他线程唤醒。wait
:线程会进入WAITING
状态,等待其他线程调用notify
或notifyAll
方法唤醒。无参数的wait方法会永久等待,直到被唤醒。
释放锁资源:
sleep
:不会释放锁,线程在休眠期间仍然持有锁。wait
:会释放锁,线程在调用wait
方法后会释放持有的对象锁,其他线程可以获取该对象的锁。
使用场景:
sleep
:通常用于当前线程需要暂停执行一段时间,比如模拟时间间隔、节流处理或简单的等待。wait
:多用于线程间的通信和同步,确保在特定条件满足时才继续执行。
异常处理:
sleep
:调用时需要捕获InterruptedException
异常。wait
:同样需要捕获InterruptedException
异常,并且通常在synchronized
块中使用。
6. 什么是AQS?底层如何实现?
AQS(AbstractQueuedSynchronizer
),即抽象队列同步器,是Java并发编程中的一个核心框架,用于构建各种同步器,如ReentrantLock
、CountDownLatch
、Semaphore
等。
AQS内部维护一个整数变量来表示同步状态,使用CAS原子操作保证了同步状态的原子性,通过先进先出(FIFO)的等待队列来管理同步状态,简化了同步器的实现,提高了代码的可靠性和可维护性。
7. 什么是CAS?底层如何实现?
CAS(Compare-And-Swap)是一种实现并发算法时常用的技术。它通过比较当前内存中的值与期望值,如果两者相等,则将内存中的值更新为新值,否则不进行任何操作或重新尝试。CAS操作是原子性的,意味着在执行过程中不会被线程调度机制打断,从而避免了传统锁机制带来的性能问题。
CAS的底层实现依赖于硬件的原子性支持。在硬件层面,CAS是通过CPU的原子指令(如cmpxchg
指令)实现的,这些指令保证了操作的原子性,不会造成数据不一致的问题。在Java中,CAS操作通常通过sun.misc.Unsafe
类实现,该类提供了直接访问底层资源的方法,通过native方法调用硬件指令来完成CAS操作。
8. synchronized和ReentrantLock的区别是什么?
synchronized 是Java内置的关键字,用于修饰方法和代码块,通过隐式加锁和释放锁的方式实现线程同步,当线程进入synchronized
代码块时会自动获取锁,离开时自动释放锁。它不支持中断和超时的操作,灵活性较低。
ReentrantLock 通过Java API实现,通过显式地创建锁对象并调用lock
方法加锁,使用完毕后需要调用unlock
方法释放锁,通常在finally块中释放锁以确保锁最终被释放。它提供了中断和超时的操作,使用起来更加灵活,适合更细粒度地并发控制。
9. 懒汉式加载中的双重检查以及synchronized关键字的作用是什么?
在Java中,懒汉式加载通常指的是单例模式的一种实现方式,其中双重检查(Double-Checked Locking)是一种优化技术,用于减少锁的使用频率,从而提高性能。synchronized
关键字用于实现线程同步,确保在多线程环境下,synchronized
代码段不被多个线程同时执行。
10. 异常机制中的finaly语句块一定会执行吗?
不一定。在大多数情况下,finally
块中的代码会执行,但在某些特殊情况下可能不会执行,例如:
- 如果在
finally
块之前调用System.exit()
方法或Runtime.getRuntime().halt()
方法,JVM会立即终止运行,finally
块中的代码不会被执行。 - 当发生
OutOfMemoryError
、StackOverflowError
等严重错误导致JVM崩溃时,finally
块中的代码也不会执行。 - 使用
kill -9
命令强制终止进程,也会导致finally
块中的代码不被执行。 - 如果
finally
块在一个守护线程中执行,并且所有非守护线程都结束了,那么守护线程可能在执行finally
块之前就被终止。 - 如果
try
或catch
块中有无限循环且未被打断,finally
块将不会执行。
11. JDK动态代理的三要素是什么?
JDK动态代理的三要素包括:原始对象、接口和InvocationHandler
。原始对象是被代理的对象,通常是一个实现了特定接口的类实例,代理对象与被代理对象需要共同实现的接口。InvocationHandler
是代理对象需要额外执行的逻辑,也就是代理的功能。
12. Java 8有哪些新特性?
有Lamda表达式、函数式接口、接口的默认方法、Stream API、Optional类。
13. 什么是线程池?
线程池是一种用于管理和复用线程的机制,它包含一组预先创建的线程,用于执行提交的任务。线程池通过减少线程的创建和销毁开销,提高了系统的性能和资源管理效率。通过线程池,任务的提交和线程的管理被分离,有效地控制了线程的数量,避免了因线程过多导致的系统资源耗尽问题。
14. 创建线程池的几个参数是什么?
- corePoolSize:线程池中的常驻核心线程数。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了
allowCoreThreadTimeOut
。这个参数确保了线程池中始终保持一定数量的线程,以应对持续的任务处理需求。 - maximumPoolSize:线程池能够容纳的同时执行的最大线程数。当任务队列满且线程数达到最大值时,线程池将根据拒绝策略处理新提交的任务。
- keepAliveTime:空闲线程的存活时间。当线程池中的线程数超过
corePoolSize
时,多余的空闲线程在达到keepAliveTime
后将被销毁,直到线程数量恢复到corePoolSize
为止。 - unit:
keepAliveTime
的时间单位,如秒、毫秒等。 - workQueue:任务队列,用于存储等待执行的任务。当任务队列满时,线程池会根据拒绝策略处理新提交的任务。
- threadFactory:用于创建新线程的工厂,通常使用默认设置即可。
- handler:拒绝策略,当任务队列满且线程数达到
maximumPoolSize
时,如何处理新提交的任务。
15. 谈谈创建线程池的四种方式?
使用Executors类创建线程池
newFixedThreadPool
:创建一个固定大小的线程池,线程池的大小一旦创建后就不再改变。newCachedThreadPool
:创建一个可缓存的线程池,线程池的大小可根据需要动态调整。newScheduledThreadPool
:创建一个支持定时及周期性任务执行的线程池。newSingleThreadExecutor
:创建一个只包含一个线程的线程池。newSingleThreadScheduledExecutor
:创建一个单线程的执行器,支持定时及周期性任务。
使用
ThreadPoolExecutor
构造函数创建线程池可以根据需要设置核心线程数、最大线程数、非核心线程的存活时间、存活时间单位、任务队列、线程工厂和拒绝策略等参数,提供更精细的控制。
使用
Future
和Callable
Java 5 引入了
Future
和Callable
,用于创建异步任务并获取结果,使用ExecutorService.submit
方法提交任务并获取Future
对象。使用Spring的
ThreadPoolTaskExecutor
使用Spring框架提供的
ThreadPoolTaskExecutor
来创建并配置线程池。
16. 线程池的工作队列有哪些?
- ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。
- LinkedBlockingQueue:基于链表的无界阻塞队列(可选容量),按FIFO排序。
- PriorityBlockingQueue:具有优先级的无界阻塞队列,按优先级排序。
- SynchronousQueue:不存储元素的阻塞队列,每个插入必须等待一个相应的移除,反之亦然。
- DelayQueue:基于优先级的无界阻塞队列,只有在延迟期满时才能从中提取元素。
17. 线程池的拒绝策略有哪些?
- AbortPolicy:这是默认的拒绝策略。当线程池达到其最大容量,并且工作队列也满了时,使用
AbortPolicy
策略会直接抛出RejectedExecutionException
异常。这个异常表明任务因为线程池的资源不足而被拒绝。适用于需要立即处理任务失败的情况,例如在电商平台高流量期间处理订单任务时,如果继续提交任务,系统会抛出异常。 - CallerRunsPolicy:当任务无法被线程池执行时,会直接在调用者线程中运行这个任务。如果调用者线程正在执行一个任务,则会创建一个新线程来执行被拒绝的任务。适用于不允许任务失败但对性能要求不高的场景,因为调用者线程会处理被拒绝的任务,可能会导致调用者线程阻塞。
- DiscardPolicy:当任务无法被线程池执行时,任务将被丢弃,不抛出异常也不执行任务。适用于任务不重要,可以忽略的情况。
- DiscardOldestPolicy:当任务无法被线程池执行时,线程池会丢弃队列中最旧的任务,然后尝试再次提交当前任务。适用于允许丢弃最早进入队列的任务的场景。
除了上述四种内置策略外,用户还可以自定义拒绝策略,通过实现RejectedExecutionHandler
接口来定义自己的拒绝策略。
18. 如果一个线程池的核心线程数是10,最大线程数是20,工作队列长度是50,拒绝策略为直接丢弃,那么当同时提交了100个任务,线程池的大小是怎么变化的?有多少任务会被丢弃?
当同时提交了100个任务时,线程池的大小和任务处理情况会按以下步骤变化:
提交任务初期:
- 线程池首先会使用核心线程数处理任务,即最初会有10个线程同时运行任务。
工作队列填充:
- 这10个线程在处理任务的同时,剩余的任务(100 - 10 = 90个任务)会被放入工作队列中。
- 工作队列可以容纳50个任务,因此前50个任务会成功进入队列等待执行,剩下40个任务(90 - 50 = 40)仍需要处理。
线程池扩容:
- 此时,核心线程数10个已满,工作队列也已满(50个任务),线程池会继续增加线程处理任务,直到达到最大线程数20。
- 因此,线程池会再增加10个线程(达到最大线程数20),这10个线程会开始处理剩下的40个任务中的10个任务。
- 到目前为止,共有20个线程正在运行任务,工作队列中有50个等待的任务,还剩下30个任务未被处理。
任务拒绝:
- 剩下的30个任务由于已经超出了线程池的处理能力(20个线程正在运行,50个任务在队列中等待),会被直接丢弃(根据拒绝策略
DiscardPolicy
)。
- 剩下的30个任务由于已经超出了线程池的处理能力(20个线程正在运行,50个任务在队列中等待),会被直接丢弃(根据拒绝策略
因此,当同时提交了100个任务时,线程池会运行70个任务(10个初始任务 + 10个新增线程任务 + 50个队列任务),而剩下的30个任务会被直接丢弃。
二、Spring面试题
1. 什么是IoC?
IoC(Inversion of Control Container)是Spring控制反转的容器,用于管理对象的生命周期和依赖关系。它通过将对象的创建和管理过程从应用程序代码中分离出来,使得开发者可以专注于编写业务逻辑,而不必关心底层对象的创建和管理。
IoC容器的核心思想是通过依赖注入(Dependency Injection)来实现组件之间的松耦合,从而提高代码的可维护性和可测试性。IoC容器可以自动完成对象的创建、初始化、注入等操作,简化开发流程,提高开发效率。
通过IoC容器,开发者可以通过配置文件或注解的方式定义对象之间的关系,容器在运行时会自动处理这些依赖关系,确保对象能够正确地协作,从而减少手动编码的错误,提高系统的稳定性和可扩展性。
2. 什么是AOP?
AOP(Aspect-Oriented Programming)即面向切面编程,是一种编程范式。通过定义切面(Aspect),切面中包含了一系列的操作,这些操作会在特定的方法调用时自动执行。切面可以看作是一个特殊的类,它通过代理模式在运行时被插入到目标对象的方法调用前后。
AOP的实现机制包括:
- 预编译方式:在编译时将切面逻辑织入程序。
- 动态代理:在运行时通过代理对象拦截方法调用,执行切面逻辑。
AOP的应用场景包括:
- 日志操作:可以在业务方法前后自动记录日志,避免在每个业务方法中重复编写日志代码。
- 权限管理:在调用目标方法前进行权限验证。
- 事务管理:在调用业务方法前开启事务,方法执行完成后提交事务。
3. BeanFactory和FactoryBean的区别是什么?
BeanFactory 是Spring框架的核心接口,用于管理和获取Bean对象。它是一个容器,负责创建、配置和管理应用程序中的Bean。BeanFactory
提供了基本的IOC容器功能,包括实例化、定位、配置应用程序中的对象及建立这些对象的依赖。
FactoryBean 是一个特殊的Bean,它本身也是一个BeanFactory
,用于创建其他Bean。FactoryBean
提供了一个灵活的方式来创建和配置复杂的Bean对象。通过实现FactoryBean
接口,开发者可以自定义Bean的创建过程,从而控制Bean的创建逻辑。
4. Bean的生命周期有哪些?
- 实例化:Bean对象通过new关键字或反射机制创建,分配内存空间。
- 设置属性:在实例化后,通过依赖注入设置Bean的属性值。
- 初始化:在设置完属性后,执行初始化操作。可以通过配置文件中的
init-method
属性指定初始化方法,也可以通过实现InitializingBean
接口的afterPropertiesSet
方法进行初始化。 - 使用:初始化完成后,Bean可以被应用程序使用。
- 销毁:当Bean不再需要时,通过配置文件中的
destroy-method
属性指定的方法进行销毁操作,释放资源。
5. Spring是如何解决循环依赖的?
Spring 使用三级缓存来解决循环依赖:
- 单例工厂的早期引用缓存(
SingletonFactory
) - 单例工厂的晚期引用缓存(
ObjectFactory
) - 成品实例的缓存(
singletonObjects
)
以下是解决循环依赖的大致步骤:
- 当Spring容器启动,开始构造A,但是A中依赖B,所以转到构造B。
- 构造B时发现依赖C,所以转到构造C。
- 构造C时发现依赖A,但是A已经在构造中,所以Spring提前用一个代理对象(
ObjectFactory
)代替A,并将这个代理对象放入三级缓存中。 - 构造C完成后,C被缓存。然后B可以使用代理的C。
- B构造完成后,B被缓存。A现在可以使用代理的B。
- A最后完成构造,此时A中依赖的B已经是完全初始化好的对象了。
这样就解决了循环依赖的问题。
6. Spring实现动态代理的方式是什么?
Spring实现动态代理的方式主要有两种:JDK动态代理和CGLIB代理。
JDK动态代理是利用了JDK的反射机制,要求被代理的对象必须实现至少一个接口。
CGLIB代理是利用字节码技术,可以在运行时动态地创建一个子类,覆盖需要代理的方法。与JDK动态代理不同,CGLIB代理不需要被代理对象实现任何接口。
7. Spring声明式事务@Transactional注解在什么情况下会失效?
没有被IOC容器管理
使用
@Transactional
进行事务管理时,Spring需要通过代理对象来管理事务。如果你的类没有被代理,则事务注解将无法生效。非public方法
@Transactional
只能用于public
的方法上,否则事务不会失效,如果要用在非public
方法上,可以开启AspectJ代理模式。内部方法调用
事务的生效是基于AOP代理的,如果在同一个类中的一个方法内调用同类中的另一个方法,事务注解可能不会起作用,因为代理机制不会被激活。
发生异常且被捕获
如果在事务方法中捕获了异常并处理了它,但没有将其重新抛出,那么事务将不会回滚。
非受检异常
Spring默认只会在遇到非受检异常(继承自
RuntimeException
)时回滚事务。如果你的事务方法抛出了受检异常(继承自Exception
),则事务可能不会回滚。事务管理器配置问题
如果 Spring 配置文件中没有启用事务注解配置,或者配置不正确,事务不会生效。或者开启了多个事务管理器,而事务注解没有指定使用哪个事务管理器,事务注解可能无法生效。确保事务注解中指定了正确的事务管理器。
数据库引擎不支持事务
某些数据库引擎不支持事务,例如
MyISAM
引擎,如果你使用这些引擎,则不能正确地使用@Transactional
注解。
8. Spring事务的传播特性有哪些?
Spring事务的传播特性主要有七种,这些特性定义在TransactionDefinition
接口中,具体如下:
- PROPAGATION_REQUIRED:如果当前没有事务,则新创建一个事务;如果上下文存在事务,则加入到这个事务中。这是默认的传播特性。
- PROPAGATION_SUPPORTS:如果当前上下文存在事务,则加入这个事务;如果上下文不存在事务,则使用无事务的方式执行。
- PROPAGATION_MANDATORY:如果当前上下文存在事务,则加入这个事务;如果上下文不存在事务,则报错。
- PROPAGATION_REQUIRES_NEW:每次都新创建一个事务。如果当前上下文有事务,则挂起上下文的事务,重新创建一个事务;如果当前上下文没有事务,则新创建一个事务。
- PROPAGATION_NOT_SUPPORTED:不支持事务执行。如果当前上下文存在事务,则挂起上下文的事务。
- PROPAGATION_NEVER:总是不开启事务;如果存在外层事务,则抛出异常。
- PROPAGATION_NESTED:如果不存在外层事务,则主动创建事务;否则创建嵌套的子事务。
9. Spring用到了哪些设计模式?
- 控制反转(IoC)和依赖注入(DI):IoC是Spring中的一个核心概念,通过IoC容器管理对象的创建和依赖关系,降低了代码之间的耦合度。DI是实现IoC的一种设计模式,通过依赖注入将实例变量传入到对象中。
- 工厂模式:Spring中的
BeanFactory
和ApplicationContext
都是工厂模式的体现。BeanFactory
是简单工厂模式的实现,根据传入的标识创建Bean对象。ApplicationContext
则是工厂方法模式的实现,通过实现FactoryBean
接口来创建和管理Bean对象。 - 单例模式:在Spring中,单例模式通过
scope="singleton"
实现,确保整个应用中只有一个实例共享。 - 原型模式:通过
scope="prototype"
实现,每次获取的都是通过克隆生成的新实例。 - 迭代器模式:Spring中的
CompositeIterator
实现了Iterator
和Iterable
接口,用于对象的迭代。 - 代理模式:Spring中的AOP(面向切面编程)通过动态代理实现,支持JDK动态代理和CGLIB动态代理。
- 适配器模式:在AOP中,
AdvisorAdapter
类根据不同的AOP配置使用对应的Advice
,实现了适配器模式。
这些设计模式在Spring中的应用不仅简化了对象的创建和管理,还提高了代码的复用性和系统的可维护性。
三、Spring Boot面试题
1. Spring Boot是如何实现配置自动化的?
在Spring Boot应用程序启动时,自动配置流程如下:
- 加载主配置类(标有
@SpringBootApplication
的类)。 - 通过
@EnableAutoConfiguration
触发自动配置机制。 AutoConfigurationImportSelector
加载并处理META-INF/spring.factories
文件中定义的自动配置类。- 根据条件化配置的结果,创建和注册相应的Bean到Spring容器中。
- 最终,应用程序获得了一个根据依赖和条件自动配置好的运行环境。
2. Spring Boot的常用注解有哪些?
Spring Boot的常用注解包括以下几种:
- @SpringBootApplication:这是一个组合注解,包含了
@Configuration
、@EnableAutoConfiguration
和@ComponentScan
三个注解。它用于标识Spring Boot应用程序的入口类,简化配置和启动过程。 - @Configuration:标注一个类作为配置类,相当于一个Spring XML配置文件。配置类可以包含一个或多个
@Bean
注解的方法,这些方法会返回要注册到Spring应用上下文中的Bean。 - @EnableAutoConfiguration:启用Spring Boot的自动配置机制,根据项目中的依赖和应用上下文自动配置Spring应用程序。
- @ComponentScan:自动扫描指定包及其子包中的Spring组件。
- @RestController:与
@Controller
类似,但@RestController
会自动将返回值转换为JSON格式。它用于标注一个类,表示这个类是一个RESTful风格的控制器,可以处理HTTP请求并返回JSON/XML格式的响应。
3. 如何自定义一个Spring Boot Starter?
- 创建一个新的Maven项目,并在
pom.xml
中添加Spring Boot Starter起步依赖。 - 创建一个自动配置类,例如
MyAutoConfiguration.java
。 - 在
resources
目录下创建META-INF/spring.factories
文件,并指定自动配置类。 - 打包并发布你的Starter。
4. @ConditionOn注解的用途是什么?
@ConditionOn
注解主要用于在运行时根据特定的条件动态地决定是否创建和加载某个Bean。例如:
@ConditionOnClass
注解通过判断类路径下是否存在给定的类,来决定一个Bean是否应该注入到Spring容器中。它接收Class对象或类的全类名字符串作为参数。
@ConditionOnProperty
注解用于根据应用程序配置文件中的属性值来控制Bean的创建和加载。它通常用于需要基于属性值进行条件控制的场景,例如根据配置文件中的开关来决定是否启用某些功能。
5. 同一个类上有多个@ConditionOn注解时,它们之间是“或”的关系还是“且”的关系?
当同一个类上有多个@ConditionOn
注解时,这些注解之间默认是逻辑“且”(AND)的关系,即所有条件都必须满足才会执行相应的配置。例如,如果一个类上同时使用了@ConditionalOnClass
和@ConditionalOnBean
注解,那么这两个条件都必须满足,相应的配置才会生效。
四、Spring Cloud面试题
1. 你们项目里用到了哪些Spring Cloud组件?
Eureka 注册中心
Eureka是Spring Cloud的注册中心,负责服务的注册与发现。服务提供者会向Eureka注册自己的信息,而服务消费者可以从Eureka中获取服务提供者的信息,实现服务调用。Eureka通过心跳机制实现服务的注册与发现。
Ribbon 负载均衡
Ribbon是一个客户端负载均衡器,提供多种负载均衡策略,可以根据需求进行定制化。它主要提供客户端的软件负载均衡算法,将负载均衡的逻辑封装在客户端,运行在客户端的进程中,可以很好地控制HTTP和TCP客户端的负载均衡行为。
Hystrix 熔断降级
Hystrix是一个断路器组件,用于保护系统,控制故障范围。当服务出现异常或超时时,Hystrix会直接返回一个默认的结果,避免服务出现雪崩效应。它实现了断路器模式,防止分布式系统中的级联故障。
Spring Cloud Gateway 网关路由
Spring Cloud Gateway是Spring Cloud生态系统中的一个API网关解决方案,专为微服务架构设计。它主要用于路由请求、处理流量转发、增强安全性、应用内的负载均衡等功能,旨在替代Netflix Zuul,提供更现代化和高效的服务。
Spring Cloud Config 配置中心
Spring Cloud Config是配置中心组件,用于统一管理各微服务的配置。它提供了集中式的配置管理和分布式配置管理两种方式,适用于各种复杂的分布式应用场景。
Spring Cloud Task 任务调度
Spring Cloud Task为微服务应用提供了任务调度和执行的功能。它支持定时任务和一次性任务,并且可以与Spring Boot应用无缝集成。
2. Ribbon的负载均衡策略有哪些?
- 轮询策略(RoundRobinRule):这是Ribbon的默认策略,按照固定的顺序将请求依次发送到每个服务实例,实现均衡负载。
- 随机策略(RandomRule):从服务实例列表中随机选择一个实例来处理请求,这种策略可以带来更好的负载均衡效果,但可能导致某些服务实例接收到的请求数量不均匀。
- 最少活跃调用数策略(LeastActiveRule):跟踪每个服务实例的活跃请求数,选择活跃请求数最少的服务实例来处理新的请求,这种策略可以使得各个服务实例的负载更加均衡。
- 响应时间加权策略(WeightedResponseTimeRule):根据服务实例的响应时间来分配权重,响应时间越短的实例权重越大,被选中的概率也越高。
- 区域感知策略(ZoneAwareRoundRobinRule):当服务实例部署在不同的区域时,优先选择与客户端处于同一区域的服务实例,以减少跨区域的网络延迟。
- 重试策略(RetryRule):在请求失败时,该策略会尝试重新发送请求到另一个服务实例,增加系统的容错能力。
- 过滤性线性轮询策略(PredicateBasedRule):通过内部定义的过滤器过滤出一部分服务实例清单,然后用线性轮询的方式从过滤出来的服务实例中选择一个服务实例。
- 可用性过滤策略(AvailabilityFilteringRule):根据服务状态(如宕机或繁忙)来分配权重,过滤掉一直连接失败或高并发的服务实例。
- 区域感知轮询策略(ZoneAvoidanceRule):以区域和可用性为基础,选择服务实例并对服务实例进行分类,优先选择与客户端处于同一区域的服务实例。
3. Hystrix是如何实现熔断降级的?
- 线程池隔离:Hystrix通过将每个依赖服务的调用放入独立的线程池中执行,实现对依赖服务的隔离。当某个依赖服务出现延迟或故障时,只会影响当前线程池的执行,不会影响整个系统的稳定性。
- 超时控制:Hystrix会为每个依赖服务设置一个超时时间,如果依赖服务的执行时间超过设定的超时时间,Hystrix会快速失败,防止长时间等待导致资源浪费。
- 熔断器:Hystrix通过熔断器(Circuit Breaker)监控依赖服务的调用情况,当调用失败次数达到设定阈值时,熔断器会打开,暂时阻止对该依赖服务的调用,避免连锁故障。在熔断器打开状态下,Hystrix会执行降级逻辑,返回默认值或者缓存数据。
- 降级逻辑:当熔断器打开后,Hystrix会执行预设的降级逻辑,返回备用数据或者默认值,保证系统的正常运行。降级逻辑可以是返回固定值、调用备用接口、返回缓存数据等。
- 缓存:Hystrix会对请求的结果进行缓存,避免重复调用相同的依赖服务。缓存可以减少对依赖服务的调用次数,提高系统性能。
4. Hystrix的资源隔离实现方式有哪些?
线程池隔离
线程池隔离是Hystrix的默认隔离方式。在这种方式下,每个依赖服务调用都会被分配到一个独立的线程池中执行。这种方式可以支持异步调用,支持超时调用,并且支持直接熔断。当某个服务的调用延迟或失败时,不会影响其他服务的线程资源,从而防止了“雪崩效应”。适用于需要异步执行、支持超时和熔断的场景,如网络请求。
信号量隔离
信号量隔离则是在调用线程上直接执行,没有线程切换的开销,因此开销相对较小。这种方式适用于那些不需要异步执行且开销较小的场景。信号量隔离不支持超时调用和直接熔断,因为它是同步的请求模式。适用于不需要异步执行、开销较小的场景,如本地方法调用。
五、MySQL面试题
1. MySQl的索引有哪些?
- 普通索引:最基本的索引类型,没有唯一性的限制。
- 唯一索引:与普通索引类似,但区别在于唯一索引列的每个值都必须是唯一的。
- 主键索引:特殊的唯一索引,不允许为空,常用于唯一标识表中的每一行。
- 组合索引:由多个列组合创建的索引,在使用查询时能够有效地利用索引提高效率。
- 全文索引:主要用于全文检索,可用于MyISAM和InnoDB引擎。
- 覆盖索引:索引包含查询所需的所有列,无需回表查询数据行。可以显著提高查询性能。
- 空间索引:MySQL在MySQL 5.7.6之后支持空间索引,主要用于GIS数据类型。
2. 哪些情况会导致索引失效?
- LIKE操作符的模糊查询:对于LIKE操作符进行的模糊查询,如果通配符放在索引列的开头,数据库无法利用索引,导致索引失效。
- 未使用索引字段进行过滤:如果查询条件没有使用到创建的索引字段,数据库可能不会使用该索引。
- 数据类型不匹配:如果查询条件的数据类型与索引字段的数据类型不匹配,数据库无法使用索引。
- 使用函数操作:如果查询条件中对字段进行了函数操作(如
LOWER(column)
),索引可能失效,因为数据库无法直接使用索引。 - 使用OR运算:在OR运算中,如果其中一个条件使用了索引,而另一个条件没有使用索引,整个查询可能会导致索引失效。
- 使用NOT运算:NOT运算通常会使索引失效,因为数据库无法使用索引来高效处理NOT运算。
- 表连接中的索引失效:如果在表连接查询中,连接条件中的字段没有索引,可能导致索引失效。
- 使用不等于操作符:对于不等于操作符(!= 或 <>),数据库通常无法利用索引进行加速查询,因为索引是按照排序顺序组织的,而不等于操作符无法利用索引的排序特性。
- 对索引列进行了数据类型转换:如果查询条件中对索引列进行了数据类型转换,数据库可能无法利用索引,导致索引失效。
- 联合索引的使用不当:在联合索引中,如果查询时的条件列不是联合索引中的第一个列,索引可能会失效。
- MySQL优化器预估全表扫描更快:在某些情况下,MySQL优化器可能会预估全表扫描比使用索引更快,从而导致不使用索引。
3. 解决索引失效的方法有哪些?
- 确保查询条件中使用到创建的索引字段。
- 保持数据类型的一致性,避免在查询条件中对索引列进行数据类型转换。
- 避免在查询条件中对索引列使用函数操作。
- 合理使用OR和NOT运算,尽量避免导致索引失效的情况。
- 在进行模糊查询时,尽量避免前导通配符的使用。
- 确保表连接查询中连接条件中的字段有索引。
- 优化查询语句,使其尽可能满足联合索引的最佳左前缀法则。
4. 什么是回表查询?
回表查询是指在数据库查询过程中,当一个索引不能包含查询所需的所有列时,数据库需要先通过索引查找到相关的记录位置(通常是主键或行号),然后再回到表中读取完整的行数据。这种情况通常发生在查询语句中包含了索引无法覆盖的字段或者涉及到了复杂的查询条件时。
5. 什么是覆盖索引?
覆盖索引(Covering Index)是指一个索引包含了查询所需的所有字段,从而可以直接通过索引来获取查询结果,而不需要再回表(访问表中的数据行)。换句话说,覆盖索引能够“覆盖”查询中所有涉及的列,因此查询可以完全依赖索引,提高查询效率。
6. Explain命令的输出有哪些关键字段?
- id:查询中每个选择表的标识符。如果id值相同,表示这些行是按顺序执行的;如果id不同,表示这些行是嵌套子查询或联合查询。
- select_type:查询类型,包括SIMPLE(简单查询,不包括子查询或联合查询)、PRIMARY(主查询)、UNION(联合中的第二个或随后的查询)、DEPENDENT UNION(依赖于外部查询的联合中的第二个或随后的查询)、SUBQUERY(子查询)、DEPENDENT SUBQUERY(依赖于外部查询的子查询)等。
- table:正在访问的表的名称。如果FROM子句中有子查询或UNION操作,table列会显示相应的标识。
- type:表示MySQL在表中找到所需行的方式,从最优到最差依次为:system > const > eq_ref > ref > range > index > ALL。其中,const和eq_ref是最理想的,ALL表示全表扫描,通常是性能较差的标志。
- possible_keys:显示了MySQL可以使用的索引选项,指示了可以在表中查找记录的索引。如果查询涉及到的列上有索引,则该索引将显示出来,但并不意味着会被查询使用。
- key:显示MySQL实际决定使用的键。如果为NULL,表示没有使用索引。
- key_len:使用索引的长度,可以帮助理解索引的选择性。
- ref:显示哪些列或常量被用于与key列中列出的索引进行比较。
- rows:预计要读取并检查的行数。越小越好。
- filtered:表示MySQL通过条件过滤出的数据比例,值越接近100%越好。
- Extra:提供额外的信息,比如是否使用了临时表、是否进行了文件排序、是否使用了覆盖索引等。
优化SQL查询时,重点关注以下几个字段:
- type:表示访问类型,最优的是
const
和eq_ref
,最差的是ALL
。确保查询达到range
级别,最好达到ref
级别。 - possible_keys和key:查看可能使用的索引和实际使用的索引,优化索引选择。
- Extra:查看是否使用了
Using filesort
或Using index
,尽量通过order by
和where
配合,避免Using filesort
。
7. MySQL有哪几种锁?
MySQL的锁可以分成三类:总体、类型、粒度。
- 总体上分成两种:乐观锁和悲观锁
- 类型上也是两种:读锁(共享锁)和写锁(排它锁)
- 锁的粒度上可以分成五种:表锁,行锁,页面锁,间隙锁,临键锁(Next Key锁)
8. 什么是共享锁和排它锁?
共享锁,也称为读锁,允许多个事务同时获取锁,并发访问共享资源。它是一种乐观锁,适用于读操作。当一个事务对某个数据对象加上共享锁后,其他事务可以读取该数据,但不能对该数据对象进行修改。共享锁可以保证最大的并发性,任何数量的用户可以同时对相同的数据施加共享锁。
排它锁,也称为写锁或独占锁,每次只能有一个事务获得锁。当一个事务对某数据加上排它锁后,其他事务不得对该数据对象施加任何封锁,直到该事务完成操作并释放锁。排它锁适用于插入、删除或更新操作,确保数据的一致性和完整性。排它锁是一种悲观保守的加锁策略,避免了不必要的并发性,因为读操作不会影响数据的一致性。
9. MySQL乐观锁有哪些实现方式?
- 使用数据版本(Version)记录机制:这是最常用的实现方式。通过为数据库表增加一个数字类型的
version
字段来实现。当读取数据时,将version
字段的值一同读出,数据每更新一次,对version
字段加1。更新数据时,检查记录当前version
是否与之前读取时的相同,只有相同才给予更新。 - 使用时间戳:另一种实现方式是为每条记录增加一个时间戳字段。每次更新时检查时间戳是否一致,如果一致则更新,否则表示其他事务已修改该记录,需要进行回滚或者重新尝试。
- 使用哈希值:还可以为每条记录增加一个哈希值字段,每次更新时重新计算哈希值并检查是否一致。如果一致则更新,否则表示其他事务已修改该记录,需要进行回滚或者重新尝试。
10. 事务的四个基本特性是什么?
- 原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。事务内的操作如果成功,则整个事务内的所有操作都会提交;如果失败,则所有操作都会撤销,不会对数据库造成任何影响。
- 一致性(Consistency):一致性要求事务执行前后数据库的状态保持一致。事务的执行不能破坏数据库的完整性约束和规则。例如,在一个转账操作中,无论账户之间如何转账,最终所有账户的总金额应保持不变。
- 隔离性(Isolation):隔离性是指在并发环境中,多个事务之间互相隔离,一个事务的执行不能被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间,确保并发事务之间不会互相影响。
- 持久性(Durability):持久性是指一个事务一旦提交,对数据库的更改就会永久保存,即使系统发生故障也不会丢失。一旦事务提交,其更改会永久反映在数据库中,系统故障不会导致数据丢失。
11. 什么是脏读、不可重复读、幻读?
脏读是指一个事务读取了另一个事务尚未提交的数据。如果被读取的数据在后续被回滚,那么第一个事务读取的数据将是无效的。脏读会导致事务基于错误的数据做出决策,从而影响数据的正确性。为了避免脏读,可以使用锁机制确保事务在读取数据时,其他事务不能修改相同的数据。
不可重复读是指在同一个事务内,多次读取同一数据时,由于其他事务的修改,导致两次读取的结果不一致。例如,事务A读取某条记录后,事务B修改了这条记录,当事务A再次读取时,会发现数据已经改变。为了避免不可重复读,可以使用更严格的隔离级别,如可串行化隔离级别,或者使用行级锁或多版本并发控制(MVCC)。
幻读是指在同一个事务内,进行范围查询时,由于其他事务的插入操作,导致第二次查询的结果集与第一次不同。例如,事务A查询某个范围内的记录后,事务B插入了新的记录,当事务A再次查询时,会发现结果集中多了新的记录。幻读会破坏事务的一致性,因为它依赖于查询结果的变化。解决幻读的方法包括使用间隙锁或更严格的隔离级别。
12. 事务的隔离级别有哪些?
事务的隔离级别主要有四种,从低到高依次为:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和序列化(Serializable)。
各个隔离级别的定义和特性:
读未提交(Read Uncommitted)
- 最低的隔离级别。
- 允许事务读取另一个事务尚未提交的数据,可能导致脏读、不可重复读和幻读。
- 数据一致性无法保证,实际应用中很少使用。
读已提交(Read Committed)
- 允许事务读取已经提交的数据,防止脏读。
- 但可能发生不可重复读和幻读。
- 常用的隔离级别,适用于需要数据一致性但不需要完全隔离的场景。
可重复读(Repeatable Read)
- 保证在同一个事务中多次读取同一数据时结果一致。
- 防止脏读和不可重复读,但可能发生幻读。
- 适用于需要确保数据一致性但又不想引入过多并发开销的场景。
序列化(Serializable)
- 最高的隔离级别,要求事务序列化执行。
- 完全避免脏读、不可重复读和幻读。
- 对系统性能影响较大,适用于对数据一致性要求极高的场景。
13. 什么情况下会走行锁?什么情况下会走表锁?
走行锁的情况:
- 基于主键操作:对于基于主键
UPDATE
、DELETE
和INSERT
语句,MySQL会使用行锁,因为这些操作只涉及特定的数据行。 - 条件匹配索引:对于写入操作且
WHERE
条件匹配到索引时,MySQL也会使用行锁,因为InnoDB引擎会基于索引进行加锁。
走表锁的情况:
- 没有索引或索引不匹配:如果SQL语句没有使用索引或者查询条件不明确,MySQL会进行全表扫描,此时会使用表锁。
- 执行DDL语句:在进行DDL操作(如修改表结构)时,MySQL会使用表锁,因为这些操作需要锁定整个表。
- 事务隔离级别设置不当:如果事务隔离级别设置过高,MySQL可能会自动将行锁升级为表锁,以确保数据的一致性。
14. MySQL和PostgreSQL在使用场景上各有什么优势?
- MySQL 适用于需要处理大量读操作的应用,如 Web 应用程序、电子商务网站和博客平台等。它的简单性和高性能使得它成为许多小型和中型项目的首选。MySQL 还适用于需要大规模水平扩展和高可用性的应用场景。它的主从复制和分片技术可以提供更好的性能和容量。
- PostgreSQL 适用于需要复杂数据类型和高级特性的应用,如地理信息系统 (GIS)、大数据分析和科学研究等。它的灵活性和丰富的功能使得它成为处理复杂数据和查询的首选。PostgreSQL 还适用于需要高度并发和可扩展性的应用场景,如金融交易系统、物联网应用和大型企业解决方案。
六、Redis面试题
1. Redis的基本数据类型有哪些?
Redis的基本数据类型包括以下五种:
- String(字符串):String是Redis中最基本的数据类型,可以用来存储任何类型的数据,如字符串、整数、浮点数、图片(图片的base64编码或者解码或者图片的路径)、序列化后的对象等。String类型是二进制安全的,最大可以存储512MB的数据。
- List(列表):List是一个字符串元素按插入顺序排序的集合。它可以添加一个元素到列表的头部或尾部。List的底层实现依赖于LinkedList、ZipList或QuickList。在Redis 3.2之前,List的底层实现主要是LinkedList或ZipList;从Redis 3.2开始,QuickList成为主要的实现方式。
- Set(集合):Set是一个无序的字符串集合,集合中的元素是唯一的。Set通过哈希表实现,支持添加、删除和查找操作,复杂度为O(1)。Set的底层实现可以是Intset或哈希表。
- Hash(散列):Hash是一个键值对的集合,适合存储对象。每个Hash可以存储2^32-1个键值对。Hash的底层实现是哈希表。
- Zset(有序集合):Zset是一个字符串元素的集合,元素唯一且每个元素关联一个double类型的分数,用于从小到大排序。Zset的底层实现也是哈希表,支持添加、删除和查找操作,复杂度为O(1)。
这些数据类型提供了丰富的功能,适用于不同的应用场景,如缓存、计数器、分布式session、分布式锁、限流等。
2. 什么情况下用Hash类型,为什么?
在需要存储对象信息或频繁更新的数据时,使用Hash类型更为合适。 Hash类型特别适合存储对象信息,例如用户信息、统计数据等。这是因为Hash类型可以方便地存储和查询对象的多个属性,而且在进行频繁更新时,其性能优于String类型。使用Hash类型可以避免使用多个String键来存储同一对象的多个属性,从而简化数据管理。
3. Redis的事务支持回滚吗?
Redis事务不支持回滚。在Redis中,事务是一组命令的集合,这些命令会按照顺序依次执行,并且在执行过程中不会被其他客户端的命令打断,保证了事务的原子性、一致性和隔离性。然而,如果事务中的某个命令执行失败,Redis并不会自动回滚事务,而是会继续执行事务中的其他命令。
4. Redis的持久化方式有哪些?
Redis的持久化方式主要有三种:RDB、AOF和混合持久化。
持久化方式 | 定义 | 优点 | 缺点 |
---|---|---|---|
RDB | 快照式的持久化方法,通过定时生成内存中数据集的快照来实现持久化。 | 数据恢复速度快、占用内存小、配置简单 | 数据丢失风险、性能影响 |
AOF | 通过记录每次写命令的方式来实现持久化。 | 数据安全性高、性能影响小 | 文件体积大、性能影响 |
混合持久化 | 在快照生成时使用RDB的方式,但在快照生成前后使用AOF的方式记录操作日志 | 减少数据丢失的风险,同时保持较高的启动速度和数据安全性 |
5. Redis有哪几种淘汰策略?
Redis提供了8种淘汰策略,分别是:
- noeviction:默认策略,当内存不足时,Redis会拒绝新的写入操作并返回错误,但不会淘汰任何数据。
- volatile-lru:从设置了过期时间的key中基于LRU算法进行淘汰。
- volatile-lfu:从设置了过期时间的key中基于LFU算法进行淘汰。
- volatile-ttl:从设置了过期时间的key中,淘汰那些剩余过期时间最短的数据。
- volatile-random:从设置了过期时间的key中随机选择数据进行淘汰。
- allkeys-lru:从所有key中基于最近最少使用(LRU)算法进行淘汰。
- allkeys-lfu:从所有key中基于最少频率使用(LFU)算法进行淘汰。
- allkeys-random:从所有key中随机选择数据进行淘汰。
6. 说说Redis主从、哨兵、集群几种模式的区别?
- 主从模式:主从模式是一种数据备份和读写分离的模式。在主从模式下,主节点负责处理所有的写操作,并将写操作记录在内存中的缓冲区。从节点从主节点获取这些写操作记录,并在自己的数据库上执行这些操作,从而保持与主节点的数据一致。此外,读请求可以在主节点和从节点上进行,从而实现读写分离,提高系统的读取性能。
- 哨兵模式:哨兵模式是在主从模式的基础上,增加了故障转移的功能。在哨兵模式下,除了主节点和从节点,还有一个或多个哨兵节点(Sentinel)。哨兵节点会定期检查主节点和从节点的运行状态。如果发现主节点发生故障,哨兵节点会在从节点中选举出一个新的主节点,并通知其他的从节点和哨兵节点。此外,哨兵节点还可以接收客户端的查询请求,返回当前的主节点信息,从而实现客户端的透明切换。
- 集群模式:集群模式是一种分布式的解决方案,它允许多个Redis节点(服务器)协同工作,提供更高的性能和可用性。在集群模式下,Redis使用一种叫做哈希槽的技术来实现数据的分片。整个哈希空间被分成16384个哈希槽,每个节点负责一部分哈希槽。当一个键需要被存储时,Redis会根据键的值计算出一个哈希值,然后根据哈希值决定将这个键存储在哪个节点上。这样,读写请求就可以在多个节点上并行处理,提高了系统的性能。
7. 如何保证Redis和数据库双写时的数据一致性?
采用延时双删策略,延时双删是一种用于解决在高并发场景下,由于网络延迟、并发控制等原因造成的数据库与缓存数据不一致问题的策略。
基本思想是在更新数据库后,首先删除缓存,然后设置一个短暂的延迟后再进行第二次删除操作,以确保在数据库更新传播到所有节点,并且在缓存中的旧数据彻底过期失效之前,第二次删除操作可以消除缓存中可能存在的旧数据,从而提高数据一致性。
具体实现步骤如下:
- 更新数据库:首先将数据更新到数据库中。
- 删除缓存:删除对应的缓存项,以确保后续的读请求会从数据库加载最新数据。
- 设置延迟:设定一段短暂的延迟时间,例如几百毫秒。
- 再次删除缓存:在延迟时间结束后,再次尝试删除缓存,以确保在数据库更新传播到所有节点,并且在缓存中的旧数据彻底过期失效之前,第二次删除操作可以消除缓存中可能存在的旧数据。
8. 说说缓存雪崩、缓存穿透和缓存击穿的区别及其应对方案?
缓存雪崩
定义:缓存雪崩是指缓存中大量数据同时失效,导致所有请求都直接落到数据库上,造成数据库压力过大甚至崩溃。这通常是由于缓存服务器重启、故障或大量数据同时过期导致的。
原因:缓存服务器重启、故障或大量数据同时过期,导致缓存失效,所有请求都直接落到数据库上。
应对方案:
- 设置随机过期时间:避免大量数据同时过期。
- 使用分布式锁:在更新缓存时,使用分布式锁来避免多个请求同时更新缓存。
- 增加备用缓存:在主缓存失效时,使用备用缓存作为临时解决方案。
- 限流和降级:在缓存失效时,通过限流和降级策略减少对数据库的压力。
缓存穿透
定义:缓存穿透是指查询一个不存在的数据,导致每次请求都要去数据库查询。这通常是由于恶意攻击或程序错误导致的。
原因:查询一个不存在的数据,缓存层和持久层都不会命中。恶意攻击者通过发送大量不存在的key请求,导致数据库压力过大。
应对方案:
- 缓存空对象:在数据库中没有命中的情况下,将空值缓存起来,并设置较短的过期时间。
- 使用布隆过滤器:在访问数据库前,使用布隆过滤器拦截不存在的key,减少对数据库的查询。
缓存击穿
定义:缓存击穿是指热点数据在失效时,大量请求同时到达数据库,导致数据库压力过大。这通常发生在热点数据过期失效时。
原因:热点数据在缓存中失效,大量请求同时到达数据库。
应对方案:
- 设置热点数据永不过期:对于需要频繁访问的数据,设置永不过期。
- 定时更新:在热点数据过期前,通过定时任务更新缓存。
- 使用互斥锁:在更新缓存时使用互斥锁,避免多个请求同时更新缓存。
9. 什么是Redis的“脑裂”?
Redis脑裂是指在Redis集群中,由于网络分区或故障导致节点之间无法正常通信,从而出现数据不一致的现象。具体来说,当发生网络分区或故障时,Redis集群的某些节点可能会被切断,无法与其他节点进行通信,导致集群分裂成多个子集群。这种情况下,可能会出现两个或多个独立运行的部分,它们之间失去了通信和数据同步能力
10. 说说Redis集群的分片机制?
Redis Cluster 采用了 哈希槽 (hash slot) 的机制来实现数据的分片。Redis Cluster 有 16384 个哈希槽,每个 key 通过 CRC16(key) mod 16384
来计算它应该在哪个哈希槽中。
Redis Cluster 中的每个 Redis 节点会负责管理一部分哈希槽。当集群中的节点数量发生变化时,负责的哈希槽和相关数据会根据集群的配置和规则进行重新分配。
Redis Cluster 的分片机制保证了数据分布式存储在不同的节点上,同时也提供了数据读写的高可用性和扩展性。
七、RabbitMQ面试题
1. 如何保证消息不丢失?
RabbitMQ 提供了一些机制来确保消息不会丢失,包括持久化机制和消费者确认。
持久化机制:
- 交换器持久化:在声明交换器时设置持久化标志,这样即使RabbitMQ重启,交换器也不会丢失。
- 队列持久化:在声明队列时设置持久化标志,这样即使RabbitMQ重启,队列和它的内容也不会丢失。
- 消息持久化:发布消息时设置持久化标志,这样消息会被写入磁盘,即使RabbitMQ服务器重启,消息也不会丢失。
消费者确认:
自动确认(auto_ack)设置为false,表示需要手动确认。当消费者处理完消息后,发送一个确认消息给RabbitMQ,这样即使消费者宕机,未确认的消息也会被重新投递。
2. 如何保证消息消费的顺序性?
RabbitMQ 本身不保证消息的严格顺序性,但可以通过一些配置和策略来尽可能保证消息消费的顺序性。
- 单消费者模式:确保每个队列只有一个消费者,这样可以保证在单个消费者内消息的顺序性。
- 消息分片:将需要保持顺序的消息放入同一个队列中。
- 消息持久化:确保队列和消息都是持久化的,这样即使消费者下线,消息也不会丢失,但注意这会影响性能。
- 独立的队列:为每个消费者创建独立的队列,并确保队列名包含消费者的标识。
3. 如何解决消息积压问题?
RabbitMQ 消息积压问题通常是指生产者发送消息的速度超过了消费者处理消息的速度。为了解决这个问题,可以采取以下措施:
- 调整消费者的消费能力:增加消费者的数量,让他们处理更多的消息。
- 使用流量控制:通过调整
prefetchCount
设置,限制单个消费者可以处理的未确认消息的数量。 - 使用消息的TTL:设置消息的过期时间,自动删除过期的消息。
- 使用死信队列:当消息因为过期或者消费者拒绝而进入死信队列,可以考虑暂停或者减少消息生产。
- 监控系统:实时监控RabbitMQ的消息堆积情况,并采取相应的措施。
八、Kafka面试题
1. kafka的消费者是pull(拉)还是push(推)模式,这种模式有什么好处?
Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从broker 拉取消息。
优点:pull模式消费者自主决定是否批量从broker拉取数据,而push模式在无法知道消费者消费能力情况下,不易控制推送速度,太快可能造成消费者奔溃,太慢又可能造成浪费。
缺点:如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到到达。为了避免这点,Kafka 有个参数可以让 consumer阻塞直到新消息到达(当然也可以阻塞直到消息的数量达到某个特定的量这样就可以批量发送)。
2. Kafka是如何跟踪消息的消费状态的?
Kafka中的Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消费。然后再通过offset进行消息位置标记,通过位置偏移来跟踪消费状态。相比其他一些消息队列使用“一个消息被分发到consumer 后 broker 就马上进行标记或者等待 customer 的通知后进行标记”的优点是,避免了通信消息发送后,可能出现的程序奔溃而出现消息丢失或者重复消费的情况。同时也无需维护消息的状态,不用加锁,提高了吞吐量。
3. Zookeeper对于Kafka的作用是什么?
Zookeeper 主要用于在集群中不同节点之间进行通信,在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取,除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。
4. 讲一讲Kafka的ack的三种机制?
Kafka的ack配置request.required.acks
有三个值 0 1 -1(all),具体如下:
- 0:生产者不会等待 broker 的 ack,这个延迟最低但是存储的保证最弱,当 server 挂掉的时候就会丢数据。
- 1:服务端会等待 ack 值,leader 副本确认接收到消息后发送 ack,但是如果 leader挂掉后他不确保是否复制完成,新 leader 也会导致数据丢失
- -1(all):服务端会等所有的 follower 的副本受到数据后才会收到 leader 发出的ack,这样数据不会丢失。
5. kafka 如何保证消息的顺序消费?
Kafka 发送消息的时候,可以指定(topic, partition, key) 3 个参数,partiton 和 key 是可选的。
Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证FIFO 的顺序。不同 partition 之间不能保证顺序。因此你可以指定 partition,将相应的消息发往同 1个 partition,并且在消费端,Kafka 保证1 个 partition 只能被1 个 consumer 消费,就可以实现这些消息的顺序消费。
另外,也可以指定 key(比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个partition,那这样也实现了消息的顺序消息。
6. kafka 如何保证消息不被重复消费?
这个问题换种问法,就是kafka如何保证消息的幂等性。对于消息队列来说,出现重复消息的概率还是挺大的,不能完全依赖消息队列,而是应该在业务层进行数据的一致性幂等校验。比如你处理的数据要写库(mysql,redis等),你先根据主键查一下,如果这数据都有了,你就别插入了,进行一些消息登记或者update等其他操作。另外,数据库层面也可以设置唯一健,确保数据不要重复插入等 。一般这里要求生产者在发送消息的时候,携带全局的唯一id。
7. Kafka 消费者端的 Rebalance 操作什么时候发生?
- 同一个消费者组中,新增了消费者进来,会执行 Rebalance 操作。
- 消费者离开当期所属的消费者组,比如宕机。
- 分区数量发生变化时(即 topic 的分区数量发生变化时)。
- 消费者主动取消订阅。
8. 请简述下你在哪些场景下会选择 Kafka?
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、HBase、Solr等。
- 消息系统:解耦和生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
九、ElasticSearch面试题
1. 什么是倒排索引(反向索引)?
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,某个文档经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。那么,倒排索引就是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。有了倒排索引,搜索引擎可以很方便地响应用户的查询。
要注意倒排索引的两个重要细节:
- 倒排索引中的所有词项对应一个或多个文档
- 倒排索引中的词项根据字典顺序升序排列
2. 说说text和keyword类型的区别?
两个的区别主要分词的区别:keyword
类型是不会分词的,直接根据字符串内容建立倒排索引,keyword
类型的字段只能通过精确值搜索到;text
类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引。
3. ES在高并发情况下如何保证读写一致性?
更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖。
每个文档都有一个
_version
版本号,这个版本号在文档被改变时加一。Elasticsearch使用这个_version
保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。
利用_version
的这一优点确保数据不会因为修改冲突而丢失。比如指定文档的version来做更改。如果那个版本号不是现在的,我们的请求就失败了。
写操作:写操作,一致性级别支持 quorum/one/all
,默认为 quorum
,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
- one:要求我们这个写操作,只要有一个primary shard是active活跃可用的,就可以执行。
- all:要求我们这个写操作,必须所有的primary shard和replica shard都是活跃的,才可以执行这个写操作。
- quorum:默认的值,要求所有的shard中,必须是大部分的shard都是活跃的,可用的,才可以执行这个写操作。
读操作:可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference
为 primary 来查询主分片,确保文档是最新版本。
4. ES建立索引阶段有哪些性能提升方法?
- 使用 SSD 存储介质。
- 使用批量请求并调整其大小:每次批量数据 5–15 MB是个不错的起始点。
- 如果你在做大批量导入,考虑通过设置
index.number_of_replicas: 0
关闭副本。 - 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的
index.refresh_interval
改到30s。 - 增加
index.translog.flush_threshold_size
设置,从默认的 512 MB 到更大一些的值,比如 1 GB。
十、应用安全性面试题
1. 什么是JWT,有什么优势?
JWT(Json-Web-Token)是基于json形式的用于身份验证和授权机制的开放标准。
JWT由三部分组成:
- 头部(Header):包含关于生成该JWT的信息以及所使用的算法类型。
- 载荷(Payload):包含要传递的数据,如身份信息和其他附属数据。官方规定了7个字段:iss(签发者)、sub(主题)、aud(接收者)、exp(过期时间)、nbf(生效时间)、iat(签发时间)、jti(JWT ID)。
- 签名(Signature):使用密钥对头部和载荷进行签名,以验证其完整性。
JWT相比于传统session有以下优势:
- 无状态:JWT的验证是基于密钥的,因此它不需要在服务端存储用户信息。这使得JWT可以作为一种无状态的身份认证机制,减轻服务端的压力,并适应微服务架构。
- 跨域支持:由于JWT包含了完整的认证和授权信息,因此可以轻松地在多个域之间传递和使用,实现跨域授权。
- 安全性高:JWT的载荷可以进行加密处理,并且签名机制能够保证数据的完整性和真实性。
- 适应微服务架构:在微服务架构中,使用JWT可以满足认证和授权的无状态性需求,每次请求携带JWT即可实现认证和授权。
- 跨语言支持:JWT的标准化和简单性质使得它可以在多种语言和平台之间使用。
2. 怎么解决跨域问题?
跨域是指浏览器在执行网页中的JavaScript代码时,由于同源策略的限制,只能访问与当前页面同源的资源,而不能访问其他源的资源。这种限制是为了保障用户数据的安全。同源策略要求协议、域名和端口号都相同,否则就会产生跨域问题。
可以通过以下方式解决:
- 配置CORS:CORS是跨域资源共享,是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。原理是通过在响应头中添加一些额外的字段,如
Access-Control-Allow-Origin
字段,添加允许跨域的源,类似于设置白名单之类的操作。使用@CrossOrigin
注解、添加CORS配置类或CorsFilter
过滤器等方式解决跨域问题。 - Nginx代理:利用服务器请求服务器不受浏览器同源策略的限制,前端把请求发给nginx, nginx再把请求转发到后端的服务器,后端的服务器响应给nginx服务器,nginx服务器加上响应头以后,再返回给前端。
- JSONP请求:利用
script
标签不受浏览器同源策略的限制,然后和后端一起配合来解决跨域问题的。原理是当通过script
标签请求时,服务器端根据相应的参数(json,handleResponse)
生成相应的json数据(handleResponse({"data": "zhe"}))
,最后这个返回的json数据(代码)就会被放在当前js文件中被执行。
3. 什么是XSS和CSRF,它们有什么区别?
XSS是跨站脚本攻击(Cross Site Scripting),是一种常见的网络安全漏洞,攻击者通过在目标网站上注入恶意脚本,使得这些脚本在其他用户的浏览器上执行,从而获取用户的敏感信息、劫持会话或进行其他恶意活动。
CSRF是跨站请求伪造(Cross-site request forgery),是伪造请求,冒充用户在站内的正常操作。我们知道,绝大多数网站是通过 cookie 等方式辨识用户身份,再予以授权的。所以要伪造用户的正常操作,最好的方法是通过 XSS 或链接欺骗等途径,让用户在本机 (即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。
XSS和CSRF的区别:
- 原理不同,CSRF是利用网站A本身的漏洞,去请求网站A的api;XSS是向目标网站注入JS代码,然后执行JS里的代码。
- CSRF需要用户先登录目标网站获取cookie,而XSS不需要登录。
- CSRF的目标是用户,XSS的目标是服务器。
- XSS是利用合法用户获取其信息,而CSRF是伪造成合法用户发起请求。
防御XSS攻击的方法:
- 输入验证和过滤:网站应该对用户输入的数据进行验证和过滤,确保只接受预期的输入。例如,可以使用白名单过滤,只允许特定字符和标记,同时拒绝其他潜在的恶意脚本。
- 输出转义:在将用户输入的数据显示在网页中时,应该对其进行适当的输出转义,以确保浏览器将其视为纯文本而不是可执行的代码。这样可以防止恶意脚本在用户浏览器中执行。
- 使用安全的编程实践:开发人员应遵循安全的编程实践,如避免使用动态拼接 HTML 或 JavaScript 代码,而是使用安全的模板引擎或框架,以确保正确地处理用户输入数据。
- 使用内容安全策略(CSP):CSP可以帮助检测和缓解XSS攻击,通过声明哪些外部资源可以被加载和执行,从而减少恶意代码的执行。
- 定期进行安全审计和代码审查:及时发现和处理潜在的安全漏洞,防止攻击者利用这些漏洞进行攻击。
防御CSRF攻击的方法:
- 验证码:在关键操作中加入验证码,增加自动化攻击的难度。
- Token验证:在每个请求中加入一个随机的token,并在服务器端验证该token的有效性,确保请求是由合法用户发起的。
- HTTP Referer检查:检查请求的来源页面是否合法,防止外部链接伪造请求。
- 禁用长期授权:改用瞬时授权,例如在每个表单中提供隐藏的field,确保每次请求都是最新的授权。