spring和springboot
关于spring中切面AOP的一些常用的面试题
以下是 Spring AOP 的核心面试题总结,涵盖原理、实现及高频考点,结合最新面试趋势整理:
一、AOP 核心概念
-
AOP 的作用与原理
-
核心目的:通过动态代理分离横切关注点(如日志、事务),避免代码重复14
-
实现机制:
- JDK 动态代理:基于接口(需实现接口)27
- CGLIB 代理:基于类继承(可代理无接口类)27
-
-
核心术语
术语 作用 示例 切面(Aspect) 封装横切逻辑的模块(如 @Aspect注解的类)47@Aspect public class LogAspect {}通知(Advice) 切面中执行的具体逻辑(5 种类型)24 @Before,@Around等切点(Pointcut) 通过表达式匹配连接点(目标方法)47 @Pointcut("execution(* com.service.*.*(..))")连接点(Join Point) 程序执行点(如方法调用)7 UserService.addUser()
二、高频面试问题
- 通知类型与执行顺序
| 通知类型 | 触发时机 | 使用场景 |
|---|---|---|
@Before | 目标方法执行前 | 参数校验、权限检查47 |
@AfterReturning | 目标方法正常返回后 | 记录返回结果2 |
@AfterThrowing | 目标方法抛出异常后 | 异常报警、事务回滚6 |
@After(@Finally) | 目标方法结束后(无论成功/异常) | 资源清理4 |
@Around | 包裹目标方法(可控制是否执行) | 性能监控、事务管理47 |
javaCopy Code
@Around("logPointcut()")
public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long duration = System.currentTimeMillis() - start;
System.out.println("方法执行耗时: " + duration + "ms");
return result;
}
执行顺序:
@Around→@Before→ 目标方法 →@Around后续 →@AfterReturning/@AfterThrowing→@After4
- 切点表达式(Pointcut Expression)
-
语法规则:
javaCopy Code execution(修饰符? 返回类型 包名.类名.方法名(参数类型) 异常类型?) -
常用写法:
- 匹配
com.service包下所有方法:execution(* com.service.*.*(..))7 - 匹配
@Transactional注解的方法:@annotation(org.springframework.transaction.annotation.Transactional)4
- 匹配
- AOP 代理失效场景与解决
-
失效场景:
- 同类方法内部调用(未经过代理)4
javaCopy Code public class UserService { public void save() { this.update(); // 内部调用,AOP 不生效 } @Transactional public void update() {...} }- 静态方法或 final 方法(无法被代理)7
-
解决方案:
- 通过
AopContext.currentProxy()获取代理对象再调用4
javaCopy Code ((UserService) AopContext.currentProxy()).update(); - 通过
三、进阶考点
-
AOP 与事务管理的关系
@Transactional本质是基于 AOP 的环绕通知实现,通过TransactionInterceptor控制事务边界6。
-
多切面执行顺序
- 默认按切面类名字母序执行,可通过
@Order注解手动指定优先级(值越小优先级越高)7:
javaCopy Code @Aspect @Order(1) // 先执行 public class LogAspect {...} - 默认按切面类名字母序执行,可通过
-
AOP 性能优化
- 避免在切点表达式中使用过于宽泛的匹配(如
*.*(..))4 - 高频调用方法慎用
@Around,减少代理链长度7
- 避免在切点表达式中使用过于宽泛的匹配(如
总结
Spring AOP 面试核心:
- 动态代理机制(JDK vs CGLIB)占面试 40%27
- 通知类型与顺序(尤其
@Around控制流程)必考47 - 代理失效场景是高频陷阱题,需结合代码示例理解
spring的作用是什么
Spring 框架的核心作用是通过 控制反转(IoC) 和 面向切面编程(AOP) 两大核心机制,简化企业级 Java 应用开发,提升代码可维护性与扩展性。其主要作用如下:
一、核心功能
-
IoC 容器管理
- 控制反转(IoC) :将对象的创建、依赖注入和生命周期管理交由 Spring 容器处理,开发者无需手动
new对象。 - 依赖注入(DI) :通过注解(如
@Autowired)或配置自动装配对象间的依赖关系,实现解耦12。
javaCopy Code @Service public class UserService { @Autowired // Spring 自动注入依赖 private UserRepository userRepo; } - 控制反转(IoC) :将对象的创建、依赖注入和生命周期管理交由 Spring 容器处理,开发者无需手动
-
面向切面编程(AOP)
- 将横切关注点(如日志、事务、权限)与业务逻辑分离,通过动态代理实现非侵入式增强13。
- 支持 声明式事务管理(
@Transactional),避免手动编写事务代码6。
二、开发简化与集成
-
模块化支持
- 提供标准化模块(如 Spring MVC、Spring Data、Spring Security),覆盖 Web 层、数据层、安全等场景14。
-
整合第三方框架
- 无缝集成 Hibernate、MyBatis、Struts 等主流技术,统一管理其资源16。
-
模板化封装
- 提供
JdbcTemplate、RedisTemplate等工具类,简化数据库和中间件操作36。
- 提供
三、设计优势
-
轻量级与低侵入性
- 框架本身对业务代码无强制污染,支持 POJO(普通 Java 对象)开发35。
-
解耦与可测试性
- 依赖注入使组件易于替换和单元测试27。
-
灵活扩展
- 支持通过 XML、注解或 Java 配置定制 Bean,适应不同项目需求38。
四、企业级能力
-
事务管理
- 通过 AOP 实现声明式事务,支持传播行为和隔离级别配置6。
-
国际化与事件机制
- 内置消息国际化(i18n)、事件监听等企业级功能3。
-
Web 开发支持
- Spring MVC 提供清晰的 MVC 分层架构,简化 RESTful API 开发14。
总结
Spring 的核心价值是 通过 IoC 和 AOP 实现组件解耦,结合模块化设计降低企业应用复杂度,同时提供丰富的集成能力,使开发者专注于业务逻辑而非基础设施12。
IoC 容器的实现机制
Spring IoC 容器的实现机制主要包括以下核心环节:
一、配置元数据加载
容器首先加载配置元数据(XML、注解或 Java Config),描述 Bean 的名称、类型及依赖关系。例如 XML 中通过 <bean> 定义,注解使用 @Component 等标注类。6
二、Bean 的实例化
根据配置信息,容器通过反射调用构造函数或工厂方法创建 Bean 实例。例如:
javaCopy Code
// 反射实例化示例
Class<?> clazz = Class.forName("com.example.UserService");
Object bean = clazz.newInstance();
此阶段仅完成对象创建,尚未注入依赖。69
三、依赖注入(DI)
容器解析 Bean 的依赖关系,通过以下方式注入:
-
构造函数注入:
javaCopy Code public UserService(UserRepository repo) { this.repo = repo; } -
Setter 注入:
javaCopy Code public void setRepo(UserRepository repo) { this.repo = repo; } -
字段注入(通过反射直接赋值)。
容器自动查找依赖的 Bean 并注入,实现解耦。46
四、生命周期回调
-
初始化:
- 调用
@PostConstruct注解方法 - 执行
InitializingBean.afterPropertiesSet() - 触发自定义
init-method
- 调用
-
销毁:
- 容器关闭时调用
@PreDestroy - 执行
DisposableBean.destroy() - 触发自定义
destroy-method8
- 容器关闭时调用
五、容器扩展机制
-
**
BeanPostProcessor**:- 在初始化前后拦截 Bean,例如处理
@Autowired(AutowiredAnnotationBeanPostProcessor)或生成 AOP 代理。6
- 在初始化前后拦截 Bean,例如处理
-
**
BeanFactoryPostProcessor**:- 在 Bean 实例化前修改配置元数据,例如解析
${}占位符(PropertySourcesPlaceholderConfigurer)。2
- 在 Bean 实例化前修改配置元数据,例如解析
六、容器分层设计
-
**
BeanFactory**:- 基础容器接口,延迟加载 Bean(首次请求时创建)。
-
**
ApplicationContext**:-
增强容器,支持事件发布、国际化等,启动时预加载单例 Bean。
-
实现类:
ClassPathXmlApplicationContext(类路径 XML 配置)AnnotationConfigApplicationContext(注解配置)27
-
流程总结
mermaidCopy Code
graph LR
A[加载配置元数据] --> B[实例化 Bean]
B --> C[依赖注入]
C --> D[初始化回调]
D --> E[Bean 就绪]
E --> F[容器关闭销毁]
通过控制反转(IoC)和依赖注入(DI),容器统一管理对象生命周期与依赖关系,降低组件耦合度。
spring中bean的生命周期是怎样的
在 Spring 框架中,Bean 的生命周期是 Spring 容器(如 ApplicationContext)管理 Bean 从创建到销毁的全过程。整个过程分为多个阶段,核心步骤如下(以单例 Bean 为例):
1. 实例化(Instantiate)
- 方式:通过构造器或工厂方法创建 Bean 的实例。
- 触发时机:容器启动时(单例 Bean)或首次请求时(原型 Bean)。
2. 属性赋值(Populate Properties)
- 依赖注入:通过 Setter、构造函数或字段注入(
@Autowired)为 Bean 的属性赋值。 - 注入资源:如
@Value注入配置值、@Resource注入其他 Bean。
3. Aware 接口回调(处理容器资源)
Spring 调用实现了 Aware 接口的方法,向 Bean 注入容器信息:
BeanNameAware→setBeanName(String name)BeanFactoryAware→setBeanFactory(BeanFactory factory)ApplicationContextAware→setApplicationContext(ApplicationContext ctx)EnvironmentAware、ResourceLoaderAware等。
4. BeanPostProcessor 前置处理
-
调用点:
BeanPostProcessor.postProcessBeforeInitialization(Object bean, String beanName) -
典型应用:
@PostConstruct注解的解析(通过CommonAnnotationBeanPostProcessor)。- AOP 代理的生成(在此阶段生成代理对象)。
5. 初始化(Initialization)
Bean 的自定义初始化逻辑按顺序执行:
-
@PostConstruct注解的方法(JSR-250 标准)。 - **
InitializingBean.afterPropertiesSet()** 接口方法。 - **自定义
init-method**(XML 中init-method属性或@Bean(initMethod = "init"))。
6. BeanPostProcessor 后置处理
-
调用点:
BeanPostProcessor.postProcessAfterInitialization(Object bean, String beanName) -
典型应用:
- AOP 的最终代理增强(如
AnnotationAwareAspectJAutoProxyCreator)。 - 返回包装后的 Bean(如事务代理)。
- AOP 的最终代理增强(如
7. Bean 就绪(Ready for Use)
- 此时 Bean 已被完全初始化,可被应用程序使用(如通过
context.getBean()获取)。
8. 销毁(Destruction)
当容器关闭时(context.close()),销毁单例 Bean:
-
@PreDestroy注解的方法(JSR-250 标准)。 - **
DisposableBean.destroy()** 接口方法。 - **自定义
destroy-method**(XML 中destroy-method属性或@Bean(destroyMethod = "cleanup"))。
注意:原型(Prototype)Bean 的销毁不由 Spring 管理,需手动调用销毁方法或依赖 GC。
springboot的循环依赖是怎么解决的?
Spring Boot 通过 三级缓存机制 和 多种编程策略 解决循环依赖问题,具体方案如下:
一、三级缓存机制(核心解决方案)
-
缓存层级与作用
- 一级缓存(
singletonObjects) :存储已完成初始化的单例 Bean15 - 二级缓存(
earlySingletonObjects) :存储已实例化但未初始化的半成品 Bean,用于暴露早期引用15 - 三级缓存(
singletonFactories) :存储 Bean 工厂对象,用于生成代理或原始对象(解决 AOP 代理场景)15
- 一级缓存(
-
解决流程示例(A→B→A)
- 实例化 A → 存入三级缓存 → 发现依赖 B
- 实例化 B → 从三级缓存获取 A 的早期引用 → 完成 B 初始化 → 存入一级缓存
- 将完整 B 注入 A → 完成 A 初始化15
二、编程层解决方案
-
@Lazy 延迟加载
- 延迟依赖注入时机,避免启动时直接循环引用24
javaCopy Code @Autowired @Lazy private ServiceB serviceB; -
Setter/Field 注入替代构造器注入
- 构造器注入无法通过三级缓存解决,需改用 Setter 或字段注入36
-
@DependsOn 控制初始化顺序
- 强制指定 Bean 加载顺序,打破循环链2
javaCopy Code @Component @DependsOn("serviceB") public class ServiceA { ... } -
重构代码设计
- 提取公共逻辑到第三方 Bean,或改用接口解耦46
三、无法解决的场景
-
构造器注入循环依赖
- 双方均需先获取对方实例,导致死锁35
-
原型(Prototype)作用域 Bean
- Spring 不缓存原型 Bean,无法提前暴露引用5
-
Spring 2.6+ 默认禁止循环引用
- 需手动开启配置:
spring.main.allow-circular-references=true8
- 需手动开启配置:
四、方案对比
| 方案 | 适用场景 | 限制 |
|---|---|---|
| 三级缓存 | 单例 Bean 的 Setter/Field 注入 | 不适用构造器注入和原型 Bean57 |
| @Lazy | 延迟初始化场景 | 可能掩盖设计问题24 |
| 代码重构 | 长期架构优化 | 需修改业务逻辑46 |
最佳实践建议
- 优先通过 设计优化 避免循环依赖46
- 必须循环引用时,使用 Setter 注入 + 三级缓存 方案15
- 升级 Spring Boot 2.6+ 时注意检查循环依赖配置
spring的循环依赖是怎么解决的?
Spring 通过 三级缓存机制 解决单例 Bean 的循环依赖问题,核心流程如下:
一、三级缓存结构与作用
| 缓存级别 | 源码中的变量名 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完整的 Bean(已完成初始化) | 存放最终可用的 Bean |
| 二级缓存 | earlySingletonObjects | 早期 Bean(已实例化但未填充属性) | 暴露半成品 Bean 解决循环依赖 |
| 三级缓存 | singletonFactories | ObjectFactory(Bean 工厂对象) | 生成代理对象或原始对象,解决 AOP 代理场景 12 |
二、循环依赖解决流程(以 A→B→A 为例)
-
创建 Bean A
- 实例化 A:调用构造函数创建原始对象
A(此时属性为空) - 暴露早期引用:将
A的工厂对象存入 三级缓存(singletonFactories)23
javaCopy Code addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); - 实例化 A:调用构造函数创建原始对象
-
填充 A 的属性
- 发现依赖
B,触发 创建 Bean B 35
- 发现依赖
-
创建 Bean B
-
实例化
B,将其工厂对象存入三级缓存 -
填充
B的属性时发现依赖A,从三级缓存获取A的工厂对象- 工厂对象执行生成
A的早期引用(若需 AOP 则返回代理对象) - 将
A的早期引用放入 二级缓存(earlySingletonObjects),并移除三级缓存中的工厂 23
- 工厂对象执行生成
-
-
完成 B 的初始化
- 将早期
A注入B的属性 - 初始化后的
B放入一级缓存 58
- 将早期
-
完成 A 的初始化
- 将完整的
B注入A的属性 - 初始化后的
A放入一级缓存,移除二级缓存中的早期引用 35
- 将完整的
最终结果:
A和B均完成初始化,循环依赖被解决。
三、无法解决的场景
-
构造器注入循环依赖
- 实例化前需先获取依赖对象,但双方均未创建,导致死锁 57
javaCopy Code @Service public class A { private final B b; public A(B b) { this.b = b; } // 构造器依赖 B } -
原型(Prototype)Bean 循环依赖
- Spring 不缓存原型 Bean,无法提前暴露早期对象 510
-
@Async 或复杂 AOP 代理
- 某些代理机制可能破坏三级缓存逻辑 10
四、开发中的规避方案
-
避免构造器注入循环 → 改用 Setter/Field 注入 9
-
延迟加载 → 使用
@Lazy注解javaCopy Code @Autowired @Lazy // 延迟注入代理对象 private B b; -
代码重构 → 提取公共逻辑到第三个 Bean,打破闭环 9
总结
Spring 通过 三级缓存 + 提前暴露早期对象 解决单例 Bean 的循环依赖:
- 核心机制:三级缓存分离 Bean 的创建阶段(实例化 vs 初始化)12
- 适用场景:单例 Bean + Setter/Field 注入 59
- 设计启示:优先通过代码设计避免循环依赖,而非依赖框架兜底
eureka
Eureka是做什么工作的?
我们都知道,在SOA服务化治理中,有一个最明显的特征,那就是ESB企业服务总线,ESB服务总线负责了消息路由,服务注册,服务交互和通信等到一系列的功能,所有的服务都可以在总线上进行插拔,这种模式有着很大的问题,主要是ESB虽然也是SOA实现的一种方式,但是ESB本身就是一个过重的服务,是一个传统的JEE服务,而且ESB管理的东西太多,如果系统变更或者ESB宕机,影响的面会更广。
所以后续出现了微服务。微服务去除了ESB企业服务总线,服务之间的通信,不再强调服务通信之间的多样性,而是使用restfull风格的api在http或者https上门传输json格式的数据来通信。
但是服务之间的通信,没有了ESB企业服务总线,那么就出现了一个问题,在微服务架构下,众多服务的相互调用,不可能每个服务都掌握所有其他服务的ip端口等信息,而且每个服务还会做集群,服务节点还会临时加入和掉线,所以需要一个统一的注册中心来保存所有提供服务的消息中心,提供给其他服务互相之间的调用。
而且使用eureka,可以解耦、屏蔽服务之间对ip和端口的依赖,因为我们知道ip和端口没有任何特殊含义而且不方便理解和记忆,且如何程序部署的ip和端口改变了,那么服务调用者也需要改变,那么如果注册的是服务名,那么就可以避免这些问题的出现。
Eureka如何识别其他服务是否上线或者下线?
首先我们知道,我们的其他服务,要想在eureka服务端注册成功,首先是要引入eureka的依赖和配置,然后要在启动类上加上@EnableEurekaClient注解。
然后当这个服务启动以后,就会向eureka服务端拉取和自己相关的配置信息,封装成EurekaClientConfig,然后读取自己的配置信息,封装成EurekaInstanceConfig。这两个配置类封装完成了之后,服务才会向Eureka拉取注册表信息并缓存到本地,要注意的是,这次的拉取是全量拉取所有的注册表,然后后面会每隔30s增量拉取服务注册表。
等把所有的注册表信息拉取到服务的本地缓存中后,就会开始进行服务注册,eureka服务端会对外提供一个注册接口,且服务端会一直监听这个接口,等待客户端的调用。客户端注册的方式就是调用这个接口,会将自己的ip和端口发送给eureka服务端。在注册完成之后,客户端会向服务端发送心跳,进行服务上的续约,这个心跳的默认周期是30s。如果eureka服务端在一段时间内没有收到eureka client端发送的心跳,那么就会将这个服务从服务注册表中删除,这个时间周期默认是90s。
当然,eureka有一个服务自我保护措施,这个配置项是默认关闭的,如果开启这个配置,那么如果服务端在15分钟内超过85%的客户端没有发送心跳,那么就会自动进入到自我保护措施中,在这个时候,eureka服务端不会删除任何服务,只能接受新客户端的注册和服务查询。
当我们的客户端正常下线的时候,会发送一个rest请求到服务端,通知eureka服务端服务下线了,eureka服务端就会将服务设置down状态,并且同步到其他服务端节点。
Eureka如何保存注册表信息?
我们知道,eureka的客户端在eureka服务端注册自己的ip和端口,那么这些消息肯定是缓存到了eureka的服务端了,那么是如何保存这些信息的呢?
Eureka使用了两个缓存来保存这些数据,一个是只读缓存readOnlyCacheMap,另外一个是读写缓存readWriteCacheMap,当一个客户端在服务端注册服务的时候,eureka服务端会先将这个客户端的信息注册到读写缓存readWriteCacheMap中,但是如果其他客户端来拉取这些注册表的时候,只能从只读缓存readOnlyCacheMap中读取,所以eureka服务端是会将读写缓存readWriteCacheMap中的服务注册的信息,定时的同步到只读缓存ReadOnlyCacheMap中的,默认时间大概是30s一次。
为什么做多级缓存,一方面是为了减少读写冲突问题,并且也可以保证eureka中的大量请求,都是可以快速的走纯内存,性能也高。
Eureka如何保证高可用和数据一致?
Eureka服务端本身肯定是需要做集群高可用搭建的,不然一旦eureka挂了,那么整个服务之间的通信就会受到很大的影响。
但是搭建的eureka服务端的集群要面临两个问题,一是服务端集群之间,如何保证各个服务端数据的一致性?二是对于客户端来说,应该选择哪个eureka服务端进行通信?
对于第一点,数据的一致性,eureka选择的是CAP理论中A可用性和P分区容错性。与eureka不同的zookepper使用的则是C一致性和P分区容错性。
为什么选择AP呢?因为一般来说,服务发现中的数据,都是属于eureka客户端的一些ip,端口,状态的元数据,元数据不会像我们的业务数据变化频繁,导致数据不一致的情况还是比较小的。而且对于服务注册来说,拿到一份过期的数据甚至是不一致的数据,也比拿不到任何数据要强,因为没有注册在服务端的服务,不一定就真的宕机了,还有可能是网络阻塞了而已。
Eureka采用的数据同步方式是peer to peer对等模式,也就是说在集群中所有的节点都没有高下之分,任何副本都可以进行读写操作,副本和副本之间可以进行同步操作。
那么,如果数据之间没有来得及数据同步怎么办?这种情况下,可能会导致服务查询不到。当然也有数据冲突的问题,一个服务,两个eureka服务端都注册了,这个就看哪边的时间是最新的,就遵循哪边的同步。
那么这么的服务端,也地位上也是平等的,那么我们的客户端,要选择哪个服务端注册呢?首先,客户端自己可以配置默认优先的服务端,如果服务端和客户端在同一个机房的时候,我们能可以指定客户端优先在这个服务端注册。
另外,客户端自己本身是维护着两个服务端列表的,一个是可用列表,一个不可用列表。客户端可以在可用列表选择一个服务端进行通信,如果连续三次都失败,再从可用列表中选择一个服务端进行通信。
如果eureka的服务集群都挂了,那么其他服务之间还能继续通信吗?
如果eureka的集群的所有节点都挂了,其他服务之间也是可以继续进行通信的。因为我们的客户端在eureka上进行注册的时候,是会将所有服务的注册表拉取下来保存在本地的,而且在后续的话,会开一个定时器定时的拉取服务端的服务注册表。
当然这个通信是有限制的,必须是其他服务的地址没有变,如果其他服务的地址变了,那么就没有办法和其他服务进行通信了。
除了eureka以外,还有其他的服务注册的组件吗?
当然有,除了eureka以外,还有springCloud Alibaba的nacos组件,也是可以进行服务注册的,还有zookeeper,consul等等也是可以进行服务注册的。
先来说一下zookeeper,zookeeper谨遵了CP原则,这和eureka的AP不一样,也因此可以看出,zookeeper是能保证数据一致性的,这其实对于数据存储来说是没有问题的,但是对于服务治理来说,就不怎么合适了,如果zookeeper中leader副本挂了,那么集群就要进行选举,那么就无法接收外界请求,那么就等于我们的服务注册或者关闭都会失败。这对服务注册来说,即使调用错误也比不知道怎么调用服务要强。
Nacos是阿里开源的,支持基于DNS和基于RPC的服务发现,而且nacos是完全封装好的,只需要下载nacos并启动nacos server,并且简单的配置就可以完成服务的注册发现。而且nacos还可以支持动态配置服务,也就是说,除了服务注册的功能,nacos还能提供配置中心的功能,而且封装的很好,不需要我们创建服务,直接启动jar包就可以了,nacos还支持服务在线管理和负载均衡,这一点是要比eureka要强的。Eureka还需要swagger和ribbon来支持在线管理和负载均衡,并且eureke服务端也是需要我们创建的一个springboot服务。
Hystrix
Hystrix的作用是什么?
在分布式系统中,服务与服务之间的调用时非常复杂的,假如一个服务的调用失败了,而我们没有合适的解决方案,就会导致调用它的服务也报错,导致系统的可用性降低了,而且一般来说,在分布式系统中,服务之间的调用不出现问题是不可能的,所以Hystrix就出现了,它通过提供了逻辑上延时和错误容忍的解决力来协助我们完成分布式系统的交互,避免了服务的雪崩情况。
Hystrix的作用主要有以下几点:
1. 对网络延迟以及故障进行容错。
2. 阻断分布式系统雪崩。
3. 快速失败并平缓回复
4. 服务降级
5. 实时监控,以及发出警报。
Hystrix的隔离机制是怎么做到的?
我们知道,在分布式系统中,各个服务之间是通过rest api来进行调用的,从而建立依赖关系。但是tomcat只有一个线程池,那么当服务调用,和业务代码的线程都在同一个线程池中,那么tomcat线程池中的资源就会非常紧张,也因此,hystrix为了避免因为线程池资源紧张的情况,提供了资源隔离的机制,而且在tomcat中的线程,如果因此报错而会导致这个线程需要长时间的恢复,而线程隔离,可以让tomcat中的线程将任务给了hystrix生成的线程池,当hystrix的线程池完成调度后,再去通知tomcat中的线程,这样就不会影响到tomcat中的线程,这样就可以规避因为一个服务除了问题,而导致整个分布式系统雪崩的情况。
Hystrix的线程隔离可以在我们HystrixComand中实现,它可以为每一个command提供一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,不会对其他线程进行影响。
一般来说,配置线程池隔离就是通过@HystrixComand注解来配置,在注解里面将线程池的大小,线程存活时间,判断熔断的最小请求数,超时时间,熔断的阈值百分比等等。
但是我们知道,不管怎么样,线程的资源是有限的,如果每一个command都设计一个线程池,资源肯定不够用的,所以实际上hystrix是command group设计一个线程池,也因此,我们可以把逻辑上是同一类型的接口设计同一个groupkey,这样他们会共享同一个线程池。当然,如果在必须需要单独隔离的时候,我们可以将一个command设计一个线程池。
Hystrix通过一个ConcurrentHashMap来维护这些线程池,每个线程池都默认创建10个线程,当线程池创建好了以后,会被存入到这个ConcurrentHashMap中,key值就是我们设置的groupkey,每次执行命令的时候,都会根据这个key值去获取这个对应的线程池
Hystrix的熔断器是怎么做到的?
如果有某个服务调用很慢而且出现了大量超时的情况,这就意味着这个服务的资源已经是非常紧张了,即使还有请求进来,也是没有什么意义的,这个时候我们就可以通过hystrix的熔断措施,将后续的请求直接熔断降级返回,不会再去访问hystrix为command生成的线程池了。
熔断的配置也是可以直接Hystrix的@HystrixCommand注解上进行配置的,首先要开启熔断器circuitBreaker.enable=true,然后还要配置三个参数:快照时间窗,值默认为10s。请求总数下限,值默认为20次。错误百分比下限,默认值为50%。
这三个参数的意义就是,在10s内,当请求次数最少达到20次的时候,统计它的错误次数是否超过50%,如果是,那就开启熔断。
在熔断之前,我们是配置了超时时间的,超时时间如果设置为5s,那么我们的请求过来,如果阻塞了,那么也需要达到5s后,请求才会降级,调用我们的降级方法直接返回。但是如果已经开启了熔断,那么一个请求过来了,会直接调用降级方法返回,而不会再继续等待5s。
那么,服务熔断之后,如何恢复呢?当hystrix熔断后,会启动一个休眠时间窗,再这个时间窗内,降级逻辑为主要逻辑。当这个时间过去了,断路器就变成半开状态,释放一个请求过来访问主逻辑,如果请求正常返回,那么断路器就会关闭,如果请求依然失败,那么断路器就还是启动。
Hystrix的降级和熔断之间的区别是什么?
下面通过一个日常的故事来说明一下什么是服务降级,什么是熔断。
故事的背景是这样的:由于小强在工作中碰到一些问题,于是想请教一下业界大牛小壮。于是发生了下面的两个场景:
小强在拿起常用手机拨号时发现该手机没有能够拨通,所以就拿出了备用手机拨通了某A的电话,这个过程就叫做降级(主逻辑失败采用备用逻辑的过程)。
由于每次小壮的解释都属于长篇大论,不太容易理解,所以小强每次找小壮沟通的时候都希望通过常用手机来完成,因为该手机有录音功能,这样自己可以慢慢消化。由于上一次的沟通是用备用电话完成的,小强又碰到了一些问题,于是他又尝试用常用电话拨打,这一次又没有能够拨通,所以他不得不又拿出备用手机给某A拨号,就这样连续的经过了几次在拨号设备选择上的“降级”,小强觉得短期内常用手机可能因为运营商问题无法正常拨通了,所以,再之后一段时间的交流中,小强就不再尝试用常用手机进行拨号,而是直接用备用手机进行拨号,这样的策略就是熔断(常用手机因短期内多次失败,而被暂时性的忽略,不再尝试使用)。
zuul
为什么需要服务网关?
在实际的业务场景中,我们需要给分布式系统之外的服务提供调用的api,比如我们的web前端服务,或者我们对外提供的api接口,那么这些接口进来我们的分布式系统进行数据查询或者修改,那么必然需要一些权限校验,限流乃至监控。
那么,如果在每一个服务节点上都加上权限校验、限流、监控等行为,那肯定是不行的,因为耦合的太重了,一旦有一个检验需要修改,那么等于要去全部的服务节点修改。但是如果将其放到一个公共的服务中,让其他服务依赖这个服务,也是会有相同的问题,因为每当我们修改一次权限校验,那么就要重新打包,其他的服务也需要修改依赖的版本,所以不如就将这些校验、限流、监控等行为,放到我们的网关中,所有的请求过来后都需要请求网关,然后网关进行校验、限流、监控等操作。
而且不同的微服务都会有不同的网络地址,而外部客户端有时候需要调用多个服务的接口,如果让客户端和各个微服务通信,那么增加了客户端的复杂性,而且存在跨域的情况,再加上多个微服务的认证也困难,耦合性也强。这个时候,就需要网关来进行校验,认证,然后进行路由转发,将请求转发到对应的微服务上去。
即便我们想修改校验、限流、监控的逻辑,那我们只需要在网关上修改就行了,这就实现了完全的解耦操作。
所有的请求都走网关,为什么网关的性能依然很好?
我们在我们的微服务增加了网关,确实,外界所有的请求都需要走网关,进行校验、限流、监控等行为,而且还多了一层转发,这样对网关的要求比较高。
那么,为什么zuul网关的性能依然很好呢?
首先,zuul2.0版本的网关是运行在异步和无阻塞框架上的,是基于netty处理请求的,而且它使用了每个CPU核就一个线程,处理所有的请求和响应,而请求和响应都是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较少。
而且可以将zuul网关搭建多个实例,在zuul前面加一层nginx做负载,每个实例承担一些请求,这样整体的支持高并发能力也会提高很多。
另外,zuul的参数设置中也需要注意,因为zuul在转发过程中,,还需要hystrix做降级熔断服务,这样可以防止其他服务出问题后拖垮zuul网关服务。
Zuul的核心是什么?都有哪些过滤器?
Zuul的核心是过滤器。
一般来说,我们会创建很多个过滤器,但是不同的过滤器必然是有个前后顺序,而且还需要进行路由,所以我们大概可以得到四种过滤器。
第一个是pre filters前置过滤器,这个过滤器是请求进来的时候就会执行的过滤器,请求必须要通过这个过滤,才能被路由过滤器进行路由。
第二个是routing filter路由过滤器,这个过滤器是将请求转发到具体的后端服务上。
第三个是post filter过滤器,这个过滤器是将请求在具体的后端服务执行后,又回到了网关,然后就会执行这个过滤器,这个过滤器的目的,一般都是收集一些统计数据,或者是添加http响应头数据,或者封装响应数据等等。
第四个是error filters过滤器,这个过滤器是当之前三个过滤器执行出现错误的时候,就会执行这个过滤器。一般来说,我们是用这个过滤器将错误信息转换为json格式的数据返回的。
如何实现这些过滤器,首先创建一个java类,然后继承ZuulFilter类,就会需要重写一些方法,一个是要重写filterType方法,返回的就是我们定义的过滤器的类型。还有就是重写run()方法,就是写过滤器的业务逻辑的。然后就是重写filterOrder()方法,我们知道,一个类型的过滤器可能有多个,这个filterOrder()就是定义这个过滤器的执行顺序的,返回的值越小,就先执行。还有一个方法是shouldFilter(),这个方法是这个过滤器的开关,如果返回值是true是过滤器打开,这个请求需要走过滤器的逻辑,返回false就是对于这个请求,不需要走这个过滤器的过滤逻辑。
sentinel 限流算法,时间窗口算法怎样实现的?
Sentinel是一个流量控制框架,它可以通过一些算法实现流量控制。其中比较常用的限流算法有漏桶算法和令牌桶算法,而时间窗口算法则是一种简单的限流策略。
时间窗口算法的实现非常简单,它的基本思路是将时间分成若干个固定的时间窗口,每个时间窗口内允许通过的请求个数是固定的,超过该限制的请求将被拒绝。
具体实现时,可以使用一个定长的数组来记录每个时间窗口内通过的请求个数。每当有请求到来时,就检查当前时间所属的时间窗口内通过的请求个数是否已经达到了限制。如果达到了限制,就拒绝该请求;否则,就通过该请求,并更新对应时间窗口内通过的请求个数。
nacos和eureka的对比?
Nacos和Eureka都是注册中心,用于服务发现和服务注册。它们都有着类似的功能,但在一些方面上有所不同。
下面是Nacos和Eureka之间的一些比较:
- 功能方面:Nacos比Eureka功能更为全面,除了服务注册和发现外,还提供了配置管理和流量管理等功能。Eureka只提供了基本的服务注册和发现功能。
- 数据存储:Nacos支持多种数据存储方式,包括内存、MySQL、Oracle等,而Eureka只支持内存存储。
- 协议支持:Nacos支持多种协议,包括Dubbo、gRPC、Spring Cloud等,而Eureka只支持Spring Cloud。
- 高可用性:Nacos在高可用性方面表现更优秀,支持集群部署和数据备份,可以实现高可用性和数据可靠性。Eureka在这方面的表现相对较差,只能通过多个Eureka服务器来提高可用性。
- 社区支持:Eureka的社区支持相对较弱,而Nacos则得到了阿里巴巴等公司的强力支持,社区活跃度较高。
总体来说,Nacos比Eureka更加全面、功能更为强大,具有更好的可扩展性和可靠性,尤其在高可用性方面表现更出色。但是,如果只是进行基本的服务注册和发现,Eureka也是一个很好的选择。
spring cloud feign的实现原理
Spring Cloud Feign是一个声明式的Web服务客户端,它使得编写Web服务客户端变得更加容易。在使用Feign时,开发者只需要编写接口并标注注解,就可以调用远程服务,无需编写繁琐的Http客户端代码。
下面是Spring Cloud Feign的实现原理:
- Feign基于Ribbon实现了客户端负载均衡。Feign通过使用Ribbon来实现负载均衡的功能,当多个服务实例提供相同的服务时,Feign会自动进行负载均衡,选择其中一个服务实例进行调用。
- Feign使用动态代理技术生成接口的实现。在使用Feign时,开发者只需要定义接口,并通过注解的方式来描述接口的调用方式,Feign会根据这些注解生成对应的接口实现类。生成的接口实现类包含了与远程服务通信的逻辑。
- Feign使用了Spring MVC的注解来描述接口的调用方式。开发者可以使用Spring MVC的注解来描述接口的调用方式,比如@RequestMapping、@RequestParam等。这些注解的使用方式与在Spring MVC中的使用方式类似。
- Feign支持多种编码方式。Feign支持多种编码方式,包括HTTP、JSON、XML等。通过使用不同的注解来配置不同的编码方式。
- Feign支持拦截器。Feign支持自定义拦截器,可以在接口调用前后进行一些自定义的处理,比如添加请求头、记录日志等。
总体来说,Spring Cloud Feign通过使用动态代理技术、注解描述、Ribbon负载均衡等技术,实现了声明式Web服务客户端的功能,使得开发者可以更加方便地调用远程服务。