点击:【第一章:Java开发岗:基础篇】
HashMap、Synchronized、ThreadLocal、AQS、线程池、JVM内存模型、内存屏障、class文件结构、类加载 机制、双亲委派、垃圾回收算法、垃圾回收器、空间分配担保策略、安全点、JIT技术、可达性分析、强软弱虚引用、gc的过程、三色标记、跨代引用、 逃逸分析、 内存泄漏与溢出、JVM线上调优经验。
点击:【第二章:Java开发岗:MySQL篇】
隔离级别、ACID底层实现原理、 一致性非锁定读(MVCC的原理)、BufferPool缓存机制、filesort过程、 离散读、ICP优化、全文检索、 行锁、表锁、间隙锁、死锁、主键自增长实现原理、索引数据结构、SQL优化、索引失效的几种情况、聚集索引、辅助索引、覆盖索引、联合索引、redo log、bin log、undolog、分布式事务、SQL的执行流程、重做日志刷盘策略、有MySQL调优、分库分表、主从复制、读写分离、高可用。
点击:【第三章:Java开发岗:Redis篇】
多路复用模式、单线程模型、简单字符串、链表、字典、跳跃表、压缩列表、encoding属性编码、持久化、布隆过滤器、分布式寻址算法、过期策略、内存淘汰策略 、Redis与数据库的数据一致性、Redis分布式锁、热点数据缓存、哨兵模式、集群模式、多级缓存架构、并发竞争、主从架构、集群架构及高可用、缓存雪崩、 缓存穿透、缓存失效。
点击:【第四章:Java开发岗:MQ篇】
RabbitMQ、RockerMQ、Kafka 三种消息中间件出现的消息可靠投递、消息丢失、消息顺序性、消息延迟、过期失效、消息队列满了、消息高可用等问题的解决方案。RabbitMQ的工作模式,RocketMQ的消息类型,Kafka消费模式、主题/分区/日志、核心总控制器以及它的选举机制、Partition副本选举Leader机制、消费者消费消息的offset记录机制、消费者Rebalance机制、Rebalance分区分配策略、Rebalance过程、 producer发布消息机制、HW与LEO、日志分段存储、十亿消息数据线上环境规划、JVM参数设置。
点击:【第五章:Java开发岗:Spring篇】
SpringBean生命周期、Spring循环依赖、Spring容器启动执行流程、Spring事务底层实现原理、Spring IOC容器加载过程、Spring AOP底层实现原理、Spring的自动装配、Spring Boot自动装配、SpringMVC执行流程。
微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪。
【第七章:Java开发岗:待定中】
待定中,大家可以评论区留言,目前市场的行情,高频的一些面试题,适合的我会收录进去。
系列文章:文章以35k为备战面试背景,薪资参考坐标:上海;每个地方,每个时间段薪资待遇都不一样,文章仅做面试参考,具体能否谈到35k取决于面试表现、平时的积累、市场行情、机遇。
提示:系列文章还未全部完成,后续的文章,会慢慢补充进去的。
这里总结一下35k的Java开发岗需要掌握的面试题,帮助大家快速复习,突破面试瓶颈。本章主讲SpringCould知识点,知识点有:微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪。大致估算可以讲三小时左右,作为备战面试的Spring相关知识点还是很不错的。35k薪资参考的坐标:上海,参考时间:2022年8月。
一般来说,在设计微服务体系结构的时候,遵循业务边界的概念,按照业务进行拆分、同时隐藏实现细节、把内容组件化模块化、可伸缩性可扩展性要求较高、并且可以实现隔离应用故障,避免整体系统不可用、要求独立部署,持续交付。
对于用户激增、并发量较高、数据量较大还得考虑:
一般来说,利用Spring Boot快速构建应用,利用Spring Cloud Alibaba Nacos实现动态服务发现、服务配置管理、服务及流量管理,利用Open feign实现与其他系统进行交互,利用Hystrix 实现熔断和错误处理,利用Ribbon实现客户端负载均衡,利用 Nginx 实现服务端负载均衡,利用 Gateway管理外部系统访问、利用Spring Security Oauth2作为权限框架进行请求校验,权限拦截、利用Seata作为分布式事务组件、利用Zipkin/Skywalking作为链路追踪、利用Sentinel作为服务降级、使用arthas/VM作为Java诊断工具、引入swagger作为在线文档、使用Redis作为分布式缓存、使用MySQL作为关系型数据库、使用MongoDB作为非关系型数据库、使用ElasticSearch作为全文搜索、有大数据的情况下使用Spark或者炎凰数仓进行读时建模。
对于一些老的项目或者特定业务的项目可能还是没有分离前后端,不过目前主流基本都是前后端分离,前后端交互更清晰,就剩下了接口模型,后端的接口更简洁明了,更容易维护,前端多渠道集成场景更容易。
后端采用统一的数据模型,支持多个前端,比如:H5前端、PC前端、安卓前端、IOS前端。
对于请求方式,比如GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,当然也需要根据自己项目的实际情况出发,请求方式尽量统一起来。另外需要考虑后期接口参数是否会新增,如果后期参数不确定,尽量使用POST,方便后期扩展参数。
对于返回响应的实体类,后端响应统一起来,不能每个后端都用自己的响应实体类,这样前端会炸的。另外对于后端抛出来的错也需要拦截封装,给到前端一个友好的响应,而不是直接抛出去,这样前端那边的感知不友好。
对于前端请求,一般会携带几个校验参数放到请求头中,比如:App私钥(前后端约定好)、时间戳、Token令牌、校验码等等,后端一般会在网关服务或者权限服务里面,将更新数据的请求,比如POST,PUT,DELETE请求方式,对这些请求进行校验,请求头里App私钥、时间戳、Token令牌通过某种运算或者算法计算出结果,最后通过加密的方式加密这个结果,作为最终的校验码,对比前后端的校验码是否一致,判断这个请求是否合法、请求的Token是否过期/失效。
对于项目系统部署安全方面,建议改私有网络+堡垒机+密码复杂度,建立锁机机制使用第三方知名云厂商托管数据库,降低运维复杂度,将项目部署在VPC私网内,使用cloudwatch Log+lambda+Network Firewall服务,检查mysql连续登录失败的IP次数,触发lambda执行脚本更新Network Firewall规则,禁用该IP访问。出于安全考虑,服务器只允许通过堡垒机进行运维。在没有提供安全访问策略表的情况下,除了被堡垒机访问之外,所有虚拟机无法访问任何主机,也无法被任何主机访问。
一般我们所说的负载均衡通常都是服务器端负载均衡,服务器端负载均衡又分为两种,一种是硬件负载均衡,还有一种是软件负载均衡。
硬件负载均衡主要通过在服务器节点之前安装专门用于负载均衡的设备,常见的如:F5。
软件负载均衡则主要是在服务器上安装一些具有负载均衡功能的软件来完成请求分发进而实现负载均衡,常见的如:LVS 、 Nginx 。
微服务为负载均衡的实现提供了另外一种思路:把负载均衡的功能以库的方式集成到服务的消费方,不再是由一台指定的负载均衡设备集中提供。这种方案称为软负载均衡客户端负载均衡。常见的如:Spring Cloud中的 Ribbon。
当我们将Ribbon和Eureka一起使用时,Ribbon会到Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用,客户端负载均衡也需要心跳机制去维护服务端清单的有效性,当然这个过程需要配合服务注册中心一起完成。
在 pom.xml 文件中引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<properties>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka-Client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- SpringCloud 版本控制依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后在启动类里面向Spring容器中注入一个带有@LoadBalanced注解的RestTemplate Bean
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
public class MessageCenterApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(MessageCenterApplication.class).web(WebApplicationType.SERVLET).run(args);
}
}
调用那些需要做负载均衡的服务时,用上面注入的RestTemplate Bean进行调用就可以了
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/api/v1/center")
public class MessageCenterController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/msg/get")
public Object getMsg() {
String msg = restTemplate.getForObject("http://message-service/api/v1/msg/get", String.class);
return msg;
}
}
在application.yml配置文件里添加好配置,比如:
spring:
application:
name: message-service
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
启动的时候,Run As --> Run Configurations->VM arguments,分别使用 8771、8772、8773 三个端口各启动一个MessageApplication应用。
-Dserver.port=8771
-Dserver.port=8772
-Dserver.port=8773
三个服务启动完成后,浏览器输入:http://localhost:8761/
应用启动之后,连续三次请求地址 http://localhost:8781/api/v1/center/msg/get
RoundRobinRule: 轮询策略,Ribbon以轮询的方式选择服务器,这个是默认值。启动的服务会被循环访问。
RandomRule: 随机策略,也就是说Ribbon会随机从服务器列表中选择一个进行访问。
BestAvailableRule: 最大可用策略,先过滤出故障服务器后,选择一个当前并发请求数最小的。
WeightedResponseTimeRule: 带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后在采用轮询的方式来获取相应的服务器。
AvailabilityFilteringRule: 可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。
ZoneAvoidanceRule: 区域感知策略,先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数(默认1)和最小过滤百分比(默认0),最后对满足条件的服务器则使用RoundRobinRule(轮询方式)选择一个服务器实例。
例如,message-service的负载均衡策略设置为随机访问RandomRule,application.yml配置如下
message-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果你不用Eureka,也可以继续使用Ribbon和Feign。
假设不使用Eureka,通过将ribbon.eureka.enabled 属性设置为 false, 可以在Ribbon中禁用Eureka,用@RibbonClient声明了一个"stores"服务,这个时候Ribbon Client 默认会引用一个配置好的服务列表,你可以在application.yml进行配置:
ribbon:
eureka:
enabled: false
stores:
ribbon:
listOfServers: example.com,google.com
公司的系统是由几百个微服务构成的,每一个微服务又有多个实例,服务数量较多,服务之间的相互依赖成网状,所以微服务系统需要服务注册中心来统一管理微服务实例,方便查看每一个微服务实例的健康状态。
服务提供者将自己的服务信息(比如服务名、IP地址等)告诉服务注册中心。
当服务消费者需要消费另外一个服务时,服务注册中心需要告诉服务消费者它所要消费服务的实例信息(如服务名、IP地址等)。
服务注册中心会检查注册的服务是否可用,通常一个服务实例注册后,会定时向服务注册中心提供“心跳”,以表明自己还处于可用的状态。如果一个服务实例停止向服务注册中心提供心跳一段时间后,服务注册中心会认为这个服务实例不可用,会把这个服务实例从服务注册列表中剔除。如果这个被剔除掉的服务实例过一段时间后继续向注册中心提供心跳,那么服务注册中心会把这个服务实例重新加入服务注册中心的列表中。
Eureka Client:负责将这个服务的信息注册到Eureka Server中
Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
Eureka的自我保护特性主要用于减少在网络分区或者不稳定状况下的不一致性问题,默认情况下,如果Server在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Server将会注销该实例。如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,启动自我保护机制。
建议关闭自我保护机制。因为在本地开发环境中,EurekaServer端相对来说重启频率不高,但是在EurekaClient端,可能改动代码之后需要重启,频率相对来说比较高;那么EurekaClient端重启之后就不会及时去向EurekaServer端发送心跳包,EurekaServer端就会认为是网络延迟或者其他原因,不会剔除服务,这样的话就会影响开发效率。
建议开启自我保护机制。因为生产环境不会频繁重启服务器,并且EurekaClient端与EurekaServer端存在网络延迟的几率较高,所以需要开启自我保护机制避免误删服务。
Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。
eureka.server.enable-self-preservation # 设为false,关闭自我保护
eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000)
Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间
eureka.instance.lease-renewal-interval-in-seconds # 续约更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒)
公司client 的配置:
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.client.ipAdress}:${server.port}
lease-expiration-duration-in-seconds: 30 #服务过期时间配置,超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除
lease-renewal-interval-in-seconds: 10 #服务刷新时间配置,每隔这个时间会主动心跳一次
Nacos采用长轮训机制来实现数据变更的同步
Nacos是采用长轮训的方式向Nacos Server端发起配置更新查询的功能。所谓长轮训就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开,直到服务端有配置或者连接超时后返回。Nacos Client端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。
一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。于是Nacos针对这个场景,做了两个方面的优化。
减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是3000,也就是说,每次最多拿3000个配置去Nacos Server端进行比较。
分阶段进行比较和更新,
第一阶段,客户端把这3000个配置的key以及对应的value值的md5拼接成一个字符串,然后发送到Nacos Server端
进行判断,服务端会逐个比较这些配置中md5不同的key,把存在更新的key返回给客户端。
第二阶段,客户端拿到这些变更的key,循环逐个去调用服务单获取这些key 的value值。
这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。虽然会增加网络通信次数,但是对整体的性能有较大的提升。最后,再采用长连接这种方式,既减少了pull轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。
web客户端代码
//向后台长轮询消息
function longPolling(){
$.ajax({
async : true,//异步
url : 'longPollingAction!getMessages.action',
type : 'post',
dataType : 'json',
data :{},
timeout : 30000,//超时时间设定30秒
error : function(xhr, textStatus, thrownError) {
longPolling();//发生异常错误后再次发起请求
},
success : function(response) {
message = response.data.message;
if(message!="timeout"){
broadcast();//收到消息后发布消息
}
longPolling();
}
});
}
web服务器端代码
public class LongPollingAction extends BaseAction {
private static final long serialVersionUID = 1L;
private LongPollingService longPollingService;
private static final long TIMEOUT = 20000;// 超时时间设置为20秒
public String getMessages() {
long requestTime = System.currentTimeMillis();
result.clear();
try {
String msg = null;
while ((System.currentTimeMillis() - requestTime) < TIMEOUT) {
msg = longPollingService.getMessages();
if (msg != null) {
break; // 跳出循环,返回数据
} else {
Thread.sleep(1000);// 休眠1秒
}
}
if (msg == null) {
result.addData("message", "timeout");// 超时
} else {
result.addData("message", msg);
}
} catch (Exception e) {
e.printStackTrace();
}
return SUCCESS;
}
public LongPollingService getLongPollingService() {
return longPollingService;
}
public void setLongPollingService(LongPollingService longPollingService) {
this.longPollingService = longPollingService;
}
}
相同点:
不同点:
Zookeeper采用CP保证数据的一致性的问题,原理是采用ZAB原子广播协议。当我们ZK领导者宕机或出现了故障,会自动重新实现选举新的领导角色,整个选举的过程中为了保证数据一致性的问题,整个微服务无法实现通讯,可运行的节点必须满足过半机制,整个zk才可以使用,要不然会奔溃。
Eureka采用AP设计理念架构注册中心,相互注册完全去中心化,也就是没有主从之分,只要有一台Eureka节点存在整个微服务就可以实现通讯。Eureka中会定时向注册中心发送心跳,如果在短期内没有发送心跳,则就会直接剔除。会定时向注册中心定时拉去服务,如果不主动拉去服务,注册中心不会主动推送。
Nacos中注册中心会定时向消费者主动推送信息 ,这样就会保持数据的准时性。它会向注册中心发送心跳,但是它的频率要比Eureka快。Nacos从1.0版本选择Ap和CP混合形式实现注册中心,默认情况下采用Ap保证服务可用性,CP形式底层采用Raft协议保证数据的一致性问题。默认采用AP方式,当集群中存在非临时实例时,采用CP模式。选择Ap模式,在网络分区的的情况允许注册服务实例。选择CP模式,在网络分区的产生了抖动情况下不允许注册服务实例。
在微服务架构中,一个请求要调用多个服务是非常常见的。如客户端访问A服务,A服务访问B服务,B服务调用C服务,由于网络原因或者自身的原因,如果B服务或者C服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩效应”。
在高并发的情况下,一个服务的延迟可能导致所有服务器上的所有资源在数秒内饱和。比起服务故障,更糟糕的是这些应用程序还可能导致服务之间的延迟增加,导致整个系统出现更多级联故障。
造成服务雪崩的原因可以归结为以下三点:
解决方案:
所谓的容错处理其实就是捕获异常了,不让异常影响系统的正常运行,正如java中的try catch一样。在微服务调用中,自身异常可自行处理外,对于依赖的服务发生错误,或者调用异常,或者调用时间过长等原因时,为了避免长时间等待,造成系统资源耗尽, 一般上都会通过设置请求的超时时间,如http请求中的ConnectTimeout和ReadTimeout;而微服务提供了Hystrix熔断器,隔离问题服务,防止级联错误的发生。
Hystrix是一个实现了超时机制和断路器模式的工具类库,用于隔离访问远程系统、服务或第三方库,提升系统的可用性和容错性。
每个请求都会在 hystrix 超时之后返回 fallback,每个请求时间延迟就是近似 hystrix 的超时时间,假设是 5 秒,那么每个请求都要延迟 5 秒后才返回。当熔断器在 10 秒内发现请求总数超过 20,并且错误百分比超过 50%,此时熔断打开。
熔断打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就会快速返回,而不是等待 5 秒才返回 fallback。通过断路器,实现了自动发现错误并将降级逻辑切为主逻辑,减少响应延迟。
当断路器打开,主逻辑被熔断后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑就是主逻辑;当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将闭合,主逻辑恢复,如果这次请求依然失败,断路器继续打开,休眠时间窗重新计时。
请求并发大,耗时短,采用信号量隔离,因为这类服务的返回通常很快,不会占用线程太长时间,而且也减少了线程切换的开销。
每个请求线程通过计数信号进行限制,当信号量大于了最大请求数maxConcurrentRequest时,调用fallback接口快速返回。另外由于通过信号量计数器进行隔离,它只是个计数器,资源消耗小。
信号量的调用是同步的,每次调用都得阻塞调用方的线程,直到有结果才返回,这样就导致了无法对访问做超时处理,只能依靠协议超时,无法主动释放。
@HystrixCommand注解实现线程池隔离,通过配置超时时间,信号量隔离,信号量最大并发,以及回退方法,基于注解就可以对方法实现服务隔离。
// 信号量隔离
@HystrixCommand(
commandProperties = {
// 超时时间,默认1000ms
@HystrixProperty(name = HystrixPropertiesManager.
EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "5000"),
// 信号量隔离
@HystrixProperty(name = HystrixPropertiesManager.
EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
// 信号量最大并发
@HystrixProperty(name = HystrixPropertiesManager.
EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "5")
},
fallbackMethod = "selectProductByIdFallBack"
)
@Override
public Product selectProductById(Integer id) {
System.out.println(Thread.currentThread().getName());
return productClient.selectProductById(id);
}
请求并发大,耗时长,采用线程池隔离策略。这样可以保证大量的线程可用,不会由于服务原因一直处于阻塞或等待状态,快速失败返回。还有就是对依赖服务的网络请求涉及超时问题的都使用线程隔离。
优点:
缺点:
@HystrixCommand注解实现线程池隔离,通过配置服务名称,接口名称,线程池,以及回退方法,基于注解就可以对接口实现服务隔离。
// 线程池隔离
@HystrixCommand(groupKey = "productServiceSinglePool", // 服务名称,相同名称使用同一个线程池
commandKey = "selectProductById", // 接口名称,默认为方法名
threadPoolKey = "productServiceSinglePool", // 线程池名称,相同名称使用同一个线程池
commandProperties = {
// 超时时间,默认1000ms
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
},
threadPoolProperties = {
// 线程池大小
@HystrixProperty(name = "coreSize", value = "10"),
// 等待队列长度(最大队列长度,默认值-1)
@HystrixProperty(name = "maxQueueSize", value = "100"),
// 线程存活时间,默认1min
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
// 超出等待队列阈值执行拒绝策略
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
},
fallbackMethod = "selectProductByIdFallBack"
)
@Override
public Product selectProductById(Integer id) {
System.out.println(Thread.currentThread().getName());
return productClient.selectProductById(id);
}
private Product selectProductByIdFallBack(Integer id) {
return new Product(888, "未知商品", 0, 0d);
}
// 线程池隔离
@HystrixCommand(groupKey = "productServiceListPool", // 服务名称,相同名称使用同一个线程池
commandKey = "selectByIds", // 接口名称,默认为方法名
threadPoolKey = "productServiceListPool", // 线程池名称,相同名称使用同一个线程池
commandProperties = {
// 超时时间,默认1000ms
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
},
threadPoolProperties = {
// 线程池大小
@HystrixProperty(name = "coreSize", value = "5"),
// 等待队列长度(最大队列长度,默认值-1)
@HystrixProperty(name = "maxQueueSize", value = "100"),
// 线程存活时间,默认1min
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
// 超出等待队列阈值执行拒绝策略
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
},
fallbackMethod = "selectByIdsFallback"
)
@Override
public List<Product> selectByIds(List<Integer> ids) {
System.out.println(Thread.currentThread().getName());
return productClient.selectPhoneList(ids);
}
private List<Product> selectByIdsFallback(List<Integer> ids) {
System.out.println("==call method selectByIdsFallback==");
return Arrays.asList(new Product(999, "未知商品", 0, 0d));
}
服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为了防止造成整个系统故障,从而采用的一种保护措施,所以很多地方也把熔断称为过载保护。
使用@HystrixProperty注解,通过配置请求数阈值、错误百分比阈值、快照时间窗口,基于注解就可以对方法实现服务熔断。
// 服务熔断
@HystrixCommand(
commandProperties = {
// 请求数阈值:在快照时间窗口内,必须满足请求阈值数才有资格熔断。打开断路器的最少请求数,默认20个请求。
//意味着在时间窗口内,如果调用次数少于20次,即使所有的请求都超时或者失败,断路器都不会打开
@HystrixProperty(name = HystrixPropertiesManager.
CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"),
// 错误百分比阈值:当请求总数在快照内超过了阈值,且有一半的请求失败,这时断路器将会打开。默认50%
@HystrixProperty(name = HystrixPropertiesManager.
CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"),
// 快照时间窗口:断路器开启时需要统计一些请求和错误数据,统计的时间范围就是快照时间窗口,默认5秒
@HystrixProperty(name = HystrixPropertiesManager.
CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000")
},
fallbackMethod = "selectProductByIdFallBack"
)
@Override
public Product selectProductById(Integer id) {
System.out.println(Thread.currentThread().getName()+
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
if (id == 1) {
throw new RuntimeException("模拟查询ID为1导致异常");
}
return productClient.selectProductById(id);
}
开启条件
// 服务降级
@HystrixCommand(fallbackMethod = "selectProductByIdFallBack")
@Override
public Product selectProductById(Integer id) {
System.out.println(Thread.currentThread().getName()+
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
if (id == 1) {
throw new RuntimeException("模拟查询ID为1导致异常");
}
return productClient.selectProductById(id);
}
类全局服务降级
在类中添加注解@DefaultProperties(defaultFallback = "selectProductByIdFallback")
在需要降级的方法上添加注解@HystrixCommand
创建一个全局fallbakc方法
public Product selectProductByIdFallback(){
return new Product(999, "undefined", 0, 0d);
}
当我们方法很多时,要是分别编写一个fallback估计也是崩溃的,虽然可以使用一个通用的fallback,但未进行特殊设置下,也是无法知道具体是哪个方法发生熔断的。
而对于Feign,我们可以使用一种更加优雅的形式进行。我们可以指定@FeignClient注解的fallback属性,或者是fallbackFactory属性,后者可以获取异常信息的。Feign是自带断路器的,在D版本的Spring Cloud中,它没有默认打开。
需要在配置文件中配置打开它,在配置文件加以下代码:
feign.hystrix.enabled=true
需要在FeignClient的SchedualServiceHi接口的注解中加上fallback的指定类就行了
@FeignClient(value = "service-hi",fallback = SchedualServiceHiHystric.class)
public interface SchedualServiceHi {
@RequestMapping(value = "/hi",method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}
SchedualServiceHiHystric需要实现SchedualServiceHi 接口,并注入到Ioc容器中
@Component
public class SchedualServiceHiHystric implements SchedualServiceHi {
@Override
public String sayHiFromClientOne(String name) {
return "sorry "+name;
}
}
servcie-feign工程,浏览器打开http://localhost:8765/hi?name=forezp,注意此时service-hi工程没有启动,网页显示:
sorry forezp
打开service-hi工程,再次访问,浏览器显示:
hi forezp,i am from port:8762
这证明断路器起到作用了。
除了实现服务容错之外,Hystrix还提供了近乎实时的监控功能,将服务执行结果、运行指标、请求数量、成功数量等这些状态通过Actuator进行收集,然后访问/actuator/hystrix.stream即可看到实时的监控数据。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: hystrix.stream
添加@EnableHystrix注解
http://localhost:9090/actuator/hystrix.stream
Hystrix提供的一套可视化系统,Hystrix-Dashboard,可以非常友好的看到当前环境中服务运行的状态。Hystrix-Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix-Dashboard我们可以直观地看到各Hystrix Command的请求响应时间,请求成功率等数据。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// 开启数据监控
@EnableHystrixDashboard
// 开启熔断器
@EnableHystrix
// 开启缓存注解
@EnableCaching
@EnableFeignClients
@SpringBootApplication
public class ServiceConsumerApplication
{
public static void main( String[] args )
{
SpringApplication.run(ServiceConsumerApplication.class);
}
}
访问:http://localhost:9090/hystrix,控制中心界面如下:
Turbine是聚合服务器发送事件流数据的一个工具,dashboard只能监控单个节点,实际生产环境中都为集群,因此可以通过Turbine来监控集群服务。
新建一个聚合监控项目,添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
添加配置文件
server:
port: 8181
spring:
application:
name: eureka-turbine
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
turbine: # 聚合监控
app-config: service-consumer,service-provider # 监控的服务列表
cluster-name-expression: "'default'" # 指定集群名称
启动类添加注解
@EnableTurbine
@EnableHystrix
@EnableHystrixDashboard
@SpringBootApplication
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class);
}
}
访问http://localhost:8181/hystrix
比如一个订单服务知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,建立连接、构造请求、接着发送请求过去、解析响应等等。
使用Feign组件直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。
对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理,接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心,Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址,最后针对这个地址,发起请求、解析响应。
请求拦截、服务分发、统一的降级、限流、认证授权、安全
关于业务网关,市场上也有蛮多的技术。一些大的公司一般选择定制化开发。但是从开发语言,可维护性上出发,能选的只有getway和zuul,但是zuul使用的阻塞IO,损失性能极大,虽然新版本有支持,但是spring并没有很好的支持升级后的zuul。gatway是spring做出来的,性能也比较好,支持长连接。
Spring Cloud Gateway明确区分了Router和Filter,位于请求接入:作为所有API接口服务请求的接入点
比如可以基于Header、Path、Host、Query自由路由。
gateway的组成
config配置中心
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,需要分布式配置中心组件。
支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。config 组件中,分两个角色,一是config server,二是config client。config-client可以从config-server获取配置属性。
Bus数据总线:将分布式的节点用轻量的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控。
应用场景:实现通知微服务架构的配置文件的更改。去代码仓库将foo的值改为“foo version 4”,即改变配置文件foo的值。如果是传统的做法,需要重启服务,才能达到配置文件的更新。我们只需要发送post请求:http://localhost:8881/bus/refresh,会发现config-client会重现肚脐配置文件,重新读取配置文件。
案例:当git文件更改的时候,通过pc端用post 向端口为8882的config-client发送请求/bus/refresh/;此时8882端口会发送一个消息,由消息总线向其他服务传递,从而使整个微服务集群都达到更新配置文件。
SpringCloud Stream:SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
设计思想:Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange,在Kakfa中就是Topic。
微服务架构上通过业务来划分服务的,通过REST调用,对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂。一个 HTTP 请求会调用多个不同的微服务来处理返回最后的结果,在这个调用过程中,可能会因为某个服务出现网络延迟过高或发送错误导致请求失败,所以需要对服务追踪分析,提供一个可视化页面便于排查问题所在。
Sleuth 整合 Zipkin,可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助查询跟踪数据以实现对分布式系统的监控程序,从而及时发现系统中出现的延迟过高问题。除了面向开发的 API 接口之外,它还提供了方便的 UI 组件来帮助我们直观地搜索跟踪信息和分析请求链路明细,比如可以查询某段时间内各用户请求的处理时间等。
Skywalking是本土开源的基于字节码注入的调用链路分析以及应用监控分析工具,特点是支持多种插件,UI功能较强,接入端无代码侵入。
CAT是由国内美团点评开源的,基于Java语言开发,目前提供Java、C/C++、Node.js、Python、Go等语言的客户端,监控数据会全量统计,国内很多公司在用,例如美团点评、携程、拼多多等,CAT跟下边要介绍的Zipkin都需要在应用程序中埋点,对代码侵入性强。
性能对比:skywalking探针对吞吐量影响最小,zipkin对吞吐量影响适中,pinpoint的探针对吞吐量影响最大。对于内存和cpu的使用,都差不多,相差在10%之内。