1. SpringCloud常用组件有哪些?
最常用的5个组件是Nacos、Gateway网关、Feign远程调用、Sentinel熔断降级和Ribbon负载均衡。
其中Nacos是eureka功能的升级版,是阿里巴巴开发的,不仅仅可以作为注册中心,还可以作为配置中心。
1.1 注册中心
是服务实例信息存储的地方,是服务间相互交互数据的桥梁。
- 服务注册:所有的服务启动时,向注册中心登记服务信息(ip与端口),并定时发送心跳包给注册中心
- 服务发现:当注册中心长时间没收到服务的心跳包时,会把服务从列表中移除;当有新的服务注册时,更新服务列表。以上2种操作后,消费者从注册中心拉取最新列表更新到本地。
服务消费者在需要调用服务时,可以通过Nacos的服务发现功能获取服务提供者的地址列表,从而实现负载均衡和服务调用
1.2 配置中心
配置中心则是负责管理应用程序的配置信息,nacos提供了一个可视化的配置管理界面,支持多种格式的配置文件,支持动态配置管理,当配置信息发生变化时,Nacos会自动推送最新的配置信息给应用程序,从而实现配置的实时更新
1.3 网关Gateway
用于实现服务的路由、负载均衡、过滤和熔断等功能。采用了异步非阻塞的模型,采用Netty作为底层的HTTP库,提高了网关的吞吐量和性能。
网关中的过滤器:
- 默认过滤器(只能使用SpringCloudGateway自带的过滤器)。
- 路由过滤器(只能使用SpringCloudGateway自带的过滤器)。
- 全局过滤器(创建一个类实现GlobalFilter接口,重写filter方法.可用于鉴权)。
- 三种过滤器默认执行顺序(默认 -> 路由 -> 全局)全局过滤器实现Ordered接口,重写getOrder方法,返回-1即可实现在其他两个过滤器前执行(默认 和 路由 过滤器的Order默认是1开始的)。
1.4 Feign远程调用
- 定义接口:在Feign客户端的接口中定义需要调用的远程服务的方法,使用注解指定服务名称、请求方式、请求路径、请求参数,响应结果等信息。
- 解析接口:在应用启动时,Feign会扫描所有带有@FeignClient注解的接口,并将其解析成可执行的HTTP请求,生成动态代理对象并保存在Spring的容器中。
- 发起请求:当应用调用Feign客户端的接口方法时,Feign中依赖的ribbon组件会去获取@FeignClient注解中name属性的值,即服务名,通过这个服务名去找注册中心拉取服务提供者列表,缓存到本地。基于负载均衡的方式选择一个服务提供者,根据接口声明的方法上的请求路径,请求参数,请求方法,发送http请求。
- 响应结果:远程服务接收到请求后,会根据请求路径和请求参数执行相应的逻辑,并将结果封装成HTTP响应返回。Feign客户端接收到响应后,根据响应结果类型进行反序列化,并返回给应用程序。以接口和注解的方式,实现对HTTP请求的映射和调用。
底层原理:
Feign的底层实现基于动态代理和HTTP客户端,它将接口与HTTP请求/响应绑定在一起,通过注解配置的方式简化了服务调用的过程,提高了代码的可读性和可维护性。
1.5 Sentinel熔断降级
Sentinel是一款开源的服务治理组件,主要提供流量控制、熔断降级和系统负载保护等功能。熔断降级是指在分布式系统中,即当系统出现异常或不可用时,通过断路器的方式将请求快速失败,避免请求不断堆积,引起系统的崩溃(雪崩)。Sentinel在防止微服雪崩上有以下4种方案:
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
- 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
1.6 Ribbon负载均衡
主要作用是在服务消费者与服务提供者之间进行负载均衡,将请求分发到不同的服务实例上,从而提高系统的可用性和性能。Ribbon通过向注册中心获取服务实例信息,并通过算法选择合适的实例进行请求转发,从而实现负载均衡。可以通过配置不同的负载均衡策略,实现不同的负载均衡方式,如轮询、随机、加权随机、最少活跃数。
2. Spring Bean的生命周期
bean的生命周期是指:Spring 容器在管理 Bean 时,会根据特定的顺序进行构造方法、依赖注入、初始化、执行业务逻辑等一系列操作,最终销毁 Bean。
-
实例化(构造方法):获取BeanDefinition,通过反射或工厂方法执行bean的构造方法。
-
属性注入:bean的属性值的依赖注入,如执行@Autowired, @Resource。
-
执行Aware的接口实现,主要有:
- BeanNameAware 设置bean对象名称
- BeanFactoryAware获取bean对象信息
- ApplicationContextAware暴露容器对象
-
执行postProcessBeforeInitialization方法,允许用户对bean对象属性注入后,对bean对象进行处理。
-
初始化:如果bean实现了InitializingBean接口,Spring容器将调用afterPropertiesSet()方法;如果在bean配置中声明了init-method属性(xml文件配置方式)或注解@PostContruct,则Spring容器将调用指定的方法。同时将bean对象存入单例池中(此时就可以使用bean了)。
-
执行postProcessAfterInitialization方法,允许在bean对象创建后再次进行处理。
-
销毁:如果bean实现了DisposableBean接口,Spring容器将在关闭应用程序上下文时调用destroy()方法;如果在bean配置中声明了destroy-method属性,则Spring容器将调用指定的方法。
【注意】:
单例bean,只有一个实例会在应用程序上下文中存在,并且在容器关闭时销毁;对于原型(多例prototype)bean,每次调用getBean()方法时都会创建一个新实例,不纳入spring管理,由用户自行管理,最终由垃圾回收器进行回收。
3. SpringMVC 执行流程
mvc的执行流程大致分为11步:
- 用户发送出请求到前端控制器DispatcherServlet。
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。
- HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter(处理器适配器)。
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。
- Controller执行完成返回ModelAndView对象。
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
- ViewReslover解析后返回具体View(视图)。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- DispatcherServlet响应用户。
4. Spring Boot自动装配原理
- 启动类上@SpringBootApplication注解。
- 底层用了3个核心注解,@SpringBootConfiguration声明当前类是一个配置类,@ComponentScan默认扫描启动类所在的包及其子包,@EnableAutoConfiguration开启自动配置。
- @EnableAutoConfiguration底层封装了@Import注解,指定了一个ImportSelector接口的实现类,低版本调用selectImports(),高版本调用getAutoConfigurationEntry(),读取当前项目下所有依赖jar包中META-INF/spring.factories等两个文件里面定义的配置类。
- 在配置类中定义一个@Bean标识的方法,还定义了@Conditional开头的注解,条件如果满足,Spring会自动调用配置类中@Bean标识的方法,并把方法的返回值注册到IOC容器中。
5. Sql优化(索引类型、失效场景、执行计划)b+tree底层
5.1 sql优化的方法有
- 执行计划:是SQL在数据库中执行时的表现情况,通常用于SQL性能分析,优化等场景。在MySQL使用 explain 关键字来查看SQL的执行计划。
- 如何查看sql执行计划:在查询语句前加explain关键字。
explain的作用:
- 查看表的读取顺序
- 查看数据库读取操作的操作类型
- 查看哪些索引有可能被用到
- 查看哪些索引真正被用到
- 查看表之间的引用
- 查看表中有多少行记录被优化器查询
-
字段分析:
- 1.id:select 查询的序列号,包含一组数字,表示查询中执行Select子句或操作表的顺序(id值相同:执行顺序由上而下;id值不同:id值越大优先级越高)。
- 2.select_type:表示SELECT语句的类型。
- 3.table:显示这查询的数据是关于哪张表的。
- 4.type:区分索引,这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL;一般来说,得保证查询至少达到range级别,最好能达到ref,到index就可以。
- 5.possible_keys:指出MySQL能使用哪个索引在该表中找到行。如果是空的,就是没有相关的索引。
- 6.key:实际使用到的索引。如果为NULL,则没有使用索引。
- 7.key_len:最长的索引宽度。如果键是NULL,长度就是NULL。在不损失精确性的情况下,长度越短越好。
- 8.ref:显示使用哪个列或常数与key一起从表中选择行。
- 9.rows:显示MySQL认为它执行查询时必须检查的行数。
- 10.Extra:执行状态说明,该列包含MySQL解决查询的详细信息。
其中4.type是重点
-
优化
- SELECT语句务必指明字段名称(避免直接使用select * )。
- SQL语句要避免造成索引失效的写法。
- SQL语句中IN包含的值不应过多。
- 当只需要一条数据的时候,使用limit 1。
- 如果排序字段没有用到索引,就尽量少排序。
- 如果限制条件中其他字段没有索引,尽量少用or。
- 尽量用union all代替union。
- 避免在where子句中对字段进行null值判断。
- 不建议使用%前缀模糊查询。
- 避免在where子句中对字段进行表达式操作。
- Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要已小表为驱动 A left join B。
-
其中常见索引的类型:普通索引、唯一索引、主键索引、复合索引
-
失效场景:
当索引的列出现这些操作时会导致索引失效:
- 使用函数或表达式,如left(索引列,4)
- 使用 NOT 或 != 操作符、使用or操作符
- 对索引列进行运算(where 索引列+3>?)
- like模糊查询前置%
- 使用复合索引时(必须要用到第一个列,索引才会生效。即最左前缀原则)
- b+树的底层:
B+树是一种多路平衡查找树,它采用平衡树的思想,能够高效地支持数据的CRID操作。
B+Tree中所有数据都存储在叶子节点中,叶子节点之间通过指针连接形成链表,便于范围查询;非叶子节点只存储索引,不存储实际数据,使得非叶子节点能够存储更多的索引,一个非叶子节点可以储存的数据为16kb,一个key一般为8字节,指针一般是6个字节,所以一个非叶子节点大概就可以储存1000条数据,第二层就可以达到百万条数据,第三层就达到了千万条数据,所以b+树一般是2-4层。
6. SpringIOC与AOP
IOC:把对象的创建与销毁都交给spring来管理,当要用的时候只需从它的容器里获取即可。Spring的IOC通常指IOC容器与DI注入。
IOC容器: Spring创建的bean对象都放到一个容器,这个容器也称为IOC容器,我也称它为对象共享池。
DI:依赖注入也称为属性注入。如果对象都是由spring来管理的,用的时候也可以通过DI进入依赖注入即可。常用的DI包括以下:
- setXXX方法
- @Autowired (不推荐):Spring的注解,通过类型匹配,如果找不到则报错,如果这个类型下有多个该类型对象时,则按属性名匹配,如果属性名与beanName对不上,则报错,需要通过@Qualifier来指定beanName。
- @Resource(推荐):java标准注解,优先通过指定的beanName注入,效率较@Autowired高。当不指定beanName时,默认使用属性名,如果属性名没到到则以类型方式匹配注入
- 构造方法(推荐)
AOP:
- 面向切面编程,在不修改原有代码的基础上对目标方法进行增强。
- Spring实现AOP的底层原理包含2种:
- JDK动态代理,针对接口时使用
- CGLIB,以子类继承父类的方式,重写父类方法来增强。
- AOP的概念术语:切面=切入点(匹配的目标对象方法)+通知(对方法实现增强的那部分代码)。
- Spring实现通知的类型有5种:
- Before前置通知
- After后置通知
- After-Throwing异常通知
- After-Returning最终通知
- Around-环绕通知:可实现以上4种通知
- 使用场景如:spring的事务管理、封装管理日志的保存、权限控制、异常处理...等。
7. Java中的线程池
7.1 什么是线程池及它的作用
线程池是一种管理与复用线程的机制。主要的作用如下:
- 降低资源消耗。通过重复利用已创建线程来降低线程创建与销毁时所带来的资源消耗。线程的创建与销毁需要耗费比较多的系统资源。
- 提高响应速度。任务提交后,可以无需等待线程的创建就能立即执行。
- 提高线程的可管理性。无限的创建销毁不仅消耗系统资源还会降低系统稳定性,使用线程池可以进行统一的分配、调优与监控。
7.2 为什么创建线程会消耗比较多的资源或成本比较高
java创建成本高是因为jvm底层需要进行大量的工作:
- 为线程堆栈分配和初始大量内存块(1个线程栈默认占用1M的内存空间)。
- 需要进行系统调用(jvm本地方法调用操作系统api),以便在主机OS中创建/注册本机线程。
- 描述符需要创建、初始化并添加到jvm内部数据结构中。
jvm需要对以上过程中所创建的堆栈内存对象的访问、JVM线程的描述符、OS本机线程线程的描述符进行绑定,当销毁时,需要对以上资源的解绑,释放与恢复。
描述符:定义对某种对象类型的属性/方法的访问方式。
7.3 线程池的运行流程或工作原理
- 线程池新创建时,并不会马上创建线程,当有任务提交时才会去创建。
- 当提交一个任务时,如果存在空闲线程,则会分配一个空闲的线程来执行任务。
- 如果没有空闲的线程时,则先判断存活的线程数是否超过核心线程数,如果没超过,则创建线程并执行任务。
- 如果存活线程数超过核心线程数,则判断工作队列是否已满,如果未满则将任务存入工作队列中等待空闲线程处理。
- 如果工作队列已满,则判断存活线程数是否超过最大线程数,未超过则创建临时线程并执行任务,如果存活线程数超过最大线程数,则执行拒绝策略。
- 关闭线程池:调用shutdown()或shutdownNow()方法关闭线程池,线程池会拒绝接受新的任务并尝试停止已有的任务执行,同时会等待所有线程执行完毕并释放资源 shutdown()是会等待所有已经提交的任务执行完成,再关闭。 shutdownNow()则是让线程池会立即停止所有正在执行的任务,并尝试中断所有处于等待状态的线程,同时返回一个未执行的任务列表。
7.4 线程池的分类
- 手工创建,通过new ThreadPoolExecutor方式创建线程池。
- 通过jdk自带的4种:
- FixedThreadPool:固定大小线程池
- CachedThreadPool缓存线程池
- SingleThreadPool单线程池
- ScheduledThreadPool定时线程池
7.5 线程池的主要参数
- corePoolSize:核心线程数大小
- maximumPoolSize:线程池最大线程数
- keepAliveTime:线程空闲时间
- unit:keepAliveTime的时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- RejectHandler:拒绝策略
7.6 线程池的4种拒绝策略
- CallerRunsPolicy:在任务被拒绝添加后,会在调用者线程中直接执行被拒绝的任务。
- DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。
- AbortPolicy:直接抛出 RejectedExecutionException 异常。
- DiscardOldestPolicy:将最早被放入等待队列的任务丢弃,然后将新任务加入等待队列)。
7.7 线程池状态
- RUNNING:线程池处于正常运行状态,可以接受新的任务并处理已有的任务。
- SHUTDOWN:线程池不再接受新的任务,但是会处理完队列中已有的任务。
- STOP:线程池不再接受新的任务,并且会尝试中断正在执行的任务。
- TIDYING:线程池中的所有任务都已经执行完毕,正在进行资源回收和清理工作。
- TERMINATED:线程池已经被完全终止,不再接受新的任务并且已经释放所有的资源。
通过ThreadPoolExecutor类的getPoolSize()方法获取当前线程池中的线程数量;
通过getActiveCount()方法获取正在执行任务的线程数量;
通过getTaskCount()方法获取已经提交但还未执行的任务数量;
通过shutdown()、shutdownNow()等方法来改变线程池的状态,以达到暂停或终止线程池的目的。
7.8 线程池核心数的设置
基础知识 CPU、L1/L2/L3 缓存、内存、硬盘/网络的执行速度
1秒=1000毫秒=1000000微秒=1000000000纳秒
1s=10^3ms=10^6μs=10^9ns
名称 |
速度计量单位 |
CPU |
0.几纳秒 |
L1 |
0.几纳秒 |
L2 |
10-20纳秒 |
L3 |
20-30纳秒 |
物理内存 |
微秒(CPU操作) |
硬盘(机械) |
10几毫秒 |
网络 |
几十到几百毫秒 |
线程任务类型分为2种
CPU密集型: 进行大量的计算,消耗CPU资源,比如计算圆周率、视频高清解码、数据的加解密等。这种任务操作都是比较耗时间的操作,任务越多花在任务切换的时间就越多,CPU执行任务效率就越低。所以,应当减少任务时切换的时间,即理想状态下,一个CPU处理一个线程时就不需要切换线程了,CPU密集型任务同时进行的数量应当等于CPU的核心数。
IO密集型:以数据流的传输、存取为主的操作,如:网络(调用三方接口)、磁盘IO(文件操作)等。这类任务操作是CPU消耗较少,任务大部分时间都在等待IO操作完成(IO的速度远低于CPU和内存的速度)。对于这种任务,任务越多,CPU效率越高(把等待的时间用来处理其它任务),但是也有限度。
咱们开发接口时,像调别的应用接口、数据库操作、文件操作等,基本上都是属于IO密集型任务。
核心线程数的设置
所以咱们在设置核心线程数时,得根据这2种类型来选择。而IO密集型还有一种情况是线程易阻塞型的,需要计算阻塞系数,他是这么配置线程核心线程数的:CPU核数 / 1 – 阻塞系数(0.8~0.9之间)所以:
CPU密集型:核心线程数=cpu核心数+1
IO密集型:核心线程数=cpu核心数*2
java中获取CPU核心数: Runtime.getRuntime().availableProcessors()
7.9 项目中是怎么创建使用线程池的?
- 开启异步使用 启动类上添加@EnableAsync。
- 注册线程池 在配置文件中通过@Bean(name = xxx)进行配置。
- 通过new ThreadPoolTaskExecutor来创建线程池
- 设置核心线程数
- 设置最大线程数
- 设置等待队列容量
- 设置临时线程活跃时间
- 设置默认线程名称前缀
- 设置拒绝策略
- 设置 等待所有任务结束后再关闭线程池
- 添加@Async(value=xxx线程池bean对象名称)注解。
7.10 为什么不使用JDK自带的线程池?
JDK自带的4种线程池,在高并发下可能出问题:
线程池创建方式 |
缺点 |
Executors.fixedThreadPool固定大小线程池 |
所创建的等待队列任务最大值为Integer.MAX_VALUE,并高发时,可能导致内存溢出 |
Executors.cachedThreadPool缓存线程池 |
所使用的等待队列为SynchronousQueue(队列不存储任务),最大线程数为Integer.MAX_VALUE。在高并发下创建过多的线程而导致内存溢出 |
Executors.SingleThreadPool单线程池 |
同fixedThreadPool |
Executors.scheduledThreadPool定时线程池 |
最大线程数为Integer.MAX_VALUE,临时线程存活时长为0ns(0纳秒,即超出核心线程数时,会经常创建与销毁线程),等待队列上限为Integer.MAX_VALUE。 |
7.11 使用线程池时需要注意哪些问题?
- 线程池默认使用无界队列,任务过多导致OOM。
- 线程池创建线程过多,导致oom。
- 共享线程池使用时,次要逻辑拖垮主要逻辑。
- 线程池拒绝策略使用不当,导致阻塞。当使用DiscardPolicy或DiscardOldestPolicy并且在被拒绝任务时,使用submit返回的future对象调用了get方法时,由于任务被放弃而不会执行,所以永远不会有get结果返回。
- 单独使用@Async时,spring内置的线程池SimpleAsyncTaskExecutor 不复用线程,即每次都是创建新的线程去执行任务。
- 使用线程池时,没有使用自定义命名,或设置线程名称前缀。日志中不便区分是系统线程还是某个业务线程(可通过CustomizableThreadFactory来设置线程名称前缀)。
- 线程池参数设置不合理 如:核心线程数,导致效率不高。
- 线程池异常处理:submit提交异常没有处理,需要自己捕获。
- 线程池使用完后,没关闭。
- 线程池与ThreadLocal搭配时,可能导致线程复用问题。线程使用完后没有清理threadLocalMap里的内容,最终导致堆中内存OOM。
8. 多线程的三大特性?如何解决?线程有哪些状态?
8.1 三大特性是指
- 原子性:原子性是指一个操作是不可被中断的,要么执行成功,要么全部失败。在多线程中,如果多个线程同时访问同一份数据,可能会发生数据竞争的问题,导致数据不一致。使用synchronized关键字、Lock对象等方式可以保证原子性。
- 可见性:可见性是指一个线程修改的状态对其他线程是可见的。在多线程中,由于线程之间的数据共享,一个线程修改的变量可能对其他线程不可见,导致数据不一致的问题。使用volatile关键字可以保证可见性。
- 有序性:有序性是指程序执行的顺序按照代码的先后顺序执行。在多线程中,由于线程的交替执行,程序执行的顺序可能发生变化,导致程序出现错误。使用synchronized关键字、Lock对象等方式可以保证有序性。
8.2 线程的状态
- 新建(New):线程被创建但未启动;
- 运行(Runnable):线程正在执行;
- 阻塞(Blocked):线程被阻塞,等待某个条件的唤醒;
- 等待(Waiting):线程处于无限期等待状态,需要其他线程显式地唤醒;
- 计时等待(Timed Waiting):线程处于有限期等待状态,等待一定时间后自动唤醒;
- 终止(Terminated):线程执行完毕或因异常退出。
9. 如何优雅的退出线程?
- 设置退出标志:在程序需要退出时,可以设置一个标志位,标识线程需要退出。【推荐】:可记录到业务具体执行到哪个步骤,还有哪些没执行等,以便后续启动后可继续执行下去。
- 中断阻塞:对于正在阻塞的线程,可以通过调用 interrupt() 方法中断线程的阻塞状态,使其抛出 InterruptedException 异常,从而退出阻塞。
- 等待线程结束:对于正在运行的线程,可以调用 join() 方法等待其结束。这个方法会阻塞当前线程,直到目标线程结束。
- 清理资源:线程结束后,需要清理线程所占用的资源,如关闭文件、网络连接等。
10. HashMap
- Hashmap是Java中的一个数据结构,可以实现键值对的存储和查询,它基于哈希表实现储存结构。初始大小为16,扩容因子为0.75,每次扩容2倍。扩容阈值:当容器中的数据数量size>=数组长度扩容因子时,扩容后数组长度为原有长度的2倍。
- JDK1.7及以前的版本使用的是数组+链表的结构1.7使用的是拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值通过头插法加入链表,1.8是尾插法。
- JDK1.8之后使用的是数组+链表+红黑树的结构jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时且数组长度大于等64,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
- 死循环问题在数组进行扩容的时候,因为1.7 链表是头插法,在多线程下进行数据迁移的过程中(扩缩容时),有可能导致死循环。在JDK1.8中将扩容算法做了调整,不再将元素加入链表头,而是使用尾插法,解决死循环问题。
对于死循环问题,还可以这样答,HashMap本身就是线程不安全的,不应在多线程下使用,如果不在多线程下使用,则不会出现死循环问题。
如果要使用线程安全的,可以使用HashTable、或Collections.synchronizedMap、ConcurrentHashMap(推荐)。