SpringCloud
学习
微服务架构的系统是个分布式系统,按业务领域划分,为独立的服务单元,有自动化运维、容错、快速演进的特点,它能够解决传统单体架构系统的痛点,同时也能满足越来越复杂的业务需求。
Eureka
组件
spring cloud eureka
和 zookeeper
的区别
问:为什么zookeeper
不适合做注册中心?
CAP
原则又称CAP
定理,指的是在一个分布式系统中,
- 一致性(
Consistency
) - 可用性(
Availability
) - 分区容错性(
Partition tolerance
)(这个特性是不可避免的)
CAP
原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
分布式的特征
C
: 数据的一致性(A,B,C
里面的数据是一致的)
zookeeper
注重数据的一致性。Eureka
不是很注重数据的一致性!
A
: 服务的可用性(若zookeeper
集群里面的master
挂了怎么办)Paxos
(多数派)
- 在
zookeeper
里面,若主机挂了,则zookeeper
集群整体不对外提供服务了,需要选一个新的出来(120s
左右)才能继续对外提供服务! Eureka
注重服务的可用性,当Eureka
集群只有一台活着,它就能对外提供服务
P
:分区的容错性(在集群里面的机器,因为网络原因,机房的原因,可能导致数据不会里面
同步),它在分布式必须需要实现的特性!
Zookeeper
注重数据的一致性,CP zookeeper(
注册中心,配置文件中心,协调中心)Eureka
注重服务的可用性AP eureka
(注册中心)
搭建Eureka
服务端
pom
文件:这里只记录了重要的内容,其余内容省略了
<!--本质还是springboot项目-->
<parent>
<!-- 实质还是springboot 项目-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--各个依赖的版本-->
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<!---->
<dependencies>
<!--rureka依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
启动类
@EnableEurekaServer // 开启eureka注册中心服务端
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
配置文件
server:
port: 8761
spring:
application:
name: eureka-server
这里的端口号只能是8761
,其他端口号会报错
原因:Eureka-Server
不仅提供让别人注册的功能,它也能注册到别人里面,自己注册自己所以,在启动项目时,默认会注册自己,我们也可以关掉这个功能。
具体原因可以查看一下源码,源码的位置为EurekaClientConfigBean
,在下面的代码片段中,我们可以看到,serviceUrl
默认的为本地的8761
端口
public EurekaClientConfigBean() {
this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
this.gZipContent = true;
this.useDnsForFetchingServiceUrls = false;
this.registerWithEureka = true;
this.preferSameZoneEureka = true;
this.availabilityZones = new HashMap();
this.filterOnlyUpInstances = true;
this.fetchRegistry = true;
this.dollarReplacement = "_-";
this.escapeCharReplacement = "__";
this.allowRedirects = false;
this.onDemandUpdateStatusChange = true;
this.clientDataAccept = EurekaAccept.full.name();
this.shouldUnregisterOnShutdown = true;
this.shouldEnforceRegistrationAtInit = false;
this.order = 0;
}
访问地址:http://localhost:8761/
搭建Eureka
客户端
pom
文件:这里只记录了重要的内容,其余内容省略了
记录一个问题,如果你忘记加spring-boot-starter-web
依赖,此时项目并不会报错,在启动完成之后,就退出了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动类
@EnableEurekaClient // 开启eureka客户端
@SpringBootApplication
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
配置文件
server:
port: 8001
spring:
application:
name: eureka-client-a
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # eureka服务端和客户端的交互地址
客户端注册到服务端的效果
注册中心的状态
UP
: 服务是上线的,括号里面是具体服务实例的个数,提供服务的最小单元DOWN
: 服务是下线的UN_KONW
: 服务的状态未知
服务端常用的配置
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka # 注册中心地址,如果集群的话用,隔开
fetch-registry: true # 是否拉取服务列表
register-with-eureka: true # 是否注册自己(单机eureka 一般关闭注册自己,集群注意打开)
server:
eviction-interval-timer-in-ms: 30000 # 清除无效节点的频率(毫秒) 定期删除
enable-self-preservation: true # server的自我保护机制,避免因为网络原因造成误删除,生产环境建议打开
renewal-percent-threshold: 0.85 # 85% 如果在一个机房的client端,15分钟内有85%的client没有续约,那么则可能是网络原因,认为服务实例没有问题,不会提出他们,宁可放过一万,不可错杀一个,确保高可用
instance:
hostname: localhost # 服务主机名称
instance-id: ${eureka.instance.hostname}.${eureka.application.name}.${server.port} # 实例的id
prefer-ip-address: true # 服务列表以ip的形式展示
lease-renewal-interval-in-seconds: 10 # 表示eureka client发送心跳给server的频率
lease-expiration-duration-in-seconds: 20 # 表示eyreka server 至 上一次收到client的心跳之后,等待下一此心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该实例
客户端常用的配置
server:
port: 8080
spring:
application:
name: eureka-client
eureka:
client:
service-url: #eureka 服务端和客户端的交互地址,集群用,隔开
defaultZone: http://localhost:8761/eureka
register-with-eureka: true #注册自己
fetch-registry: true #拉取服务列表
registry-fetch-interval-seconds: 5 # 表示eureka-client 间隔多久去拉取服务注册信息
instance:
hostname: localhost # 服务主机名称
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 实例id
prefer-ip-address: true # 服务列表以ip 的形式展示
lease-renewal-interval-in-seconds: 10 # 表示eureka client 发送心跳给server 端的频率
lease-expiration-duration-in-seconds: 20 #表示eureka server 至上一次收到client 的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该实例
构建高可用的eureka
集群
server-1
server:
port: 8761 #为什么是8761,其他端口就报错
spring:
application:
name: eureka-server #服务名称
eureka:
client:
fetch-registry: true #是否拉取服务列表
register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
service-url:
defaultZone: http://localhost:8762/eureka/,http://localhost:8763/eureka/
server:
eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
instance:
lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量
server-2
server:
port: 8762
spring:
application:
name: eureka-server #服务名称
eureka:
client:
fetch-registry: true #是否拉取服务列表
register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8763/eureka/
server:
eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
instance:
lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量
server-3
server:
port: 8763
spring:
application:
name: eureka-server #服务名称
eureka:
client:
fetch-registry: true #是否拉取服务列表
register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
server:
eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
instance:
lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量
Eureka
相关概念
服务的注册
当项目启动时(eureka
的客户端),就会向eureka-server
发送自己的元数据(原始数据)(运行的ip
,端口port
,健康的状态监控等,因为使用的是http/ResuFul
请求风格),eureka-server
会在自己内部保留这些元数据(内存中)。(有一个服务列表)(restful
风格,以http
动词的请求方式,完成对url
资源的操作)
服务的续约
项目启动成功了,除了向eureka-server
注册自己成功,还会定时的向eureka-server
汇报自己,心跳,表示自己还活着。(修改一个时间)
服务下线(主动下线)
当项目关闭时,会给eureka-server
报告,说明自己要下机了。
服务的剔除(被动下线,主动剔除)
当项目超过了指定时间没有向eureka-server
汇报自己,那么eureka-server
就会认为此节点死掉了,会把它剔除掉,也不会放流量和请求到此节点了。
Ribbon
组件
Spring Cloud Ribbon
是一个基于HTTP
和TCP
的客户端负载均衡工具,它基于NetflixRibbon
实现。通过Spring Cloud
的封装,可以让我们轻松地将面向服务的REST
模版请求自动转换成客户端负载均衡的服务调用。轮询hash
权重…简单的说Ribbon
就是netfix
公司的一个开源项目,主要功能是提供客户端负载均衡算法和服务调用。Ribbon
客户端组件提供了一套完善的配置项,比如连接超时,重试等。
在Spring Cloud
构建的微服务系统中, Ribbon
作为服务消费者的负载均衡器,有两种使用方式,一种是和RestTemplate
相结合,另一种是和OpenFeign
相结合。OpenFeign
已经默认集成了Ribbon
,关于OpenFeign
的内容将会在下一章进行详细讲解。Ribbon
有很多子模块,但很多模块没有用于生产环境!
入门案例
编写两个服务提供者
pom
文件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
测试接口A
@RestController
public class ProviderController {
@GetMapping("hello")
public String hello(){
return "我是提供者aaaa的接口";
}
}
测试接口B
@RestController
public class ProviderController {
@GetMapping("hello")
public String hello(){
return "我是提供者bbbb的接口";
}
}
配置文件A
server:
port: 8080
spring:
application:
name: provider # 名字不用改
eureka:
client:
service-url:
defaultZone: http://192.168.76.128:8761/eureka
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
配置文件B
server:
port: 8081
spring:
application:
name: provider # 名字不用改
eureka:
client:
service-url:
defaultZone: http://192.168.76.128:8761/eureka
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
启动类A
@SpringBootApplication
@EnableEurekaClient
public class ProviderAApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderAApplication.class, args);
}
}
启动类B
@SpringBootApplication
@EnableEurekaClient
public class ProviderBApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderBApplication.class, args);
}
}
消费者
使用RestTemplate
进行Eureka Client
(包括服务提供者以及服务消费者,在这里其实是服务消费者使用RestTemplate
)之间的通信,为RestTemplate
配置类添加@LoadBalanced
注解即可,如下所示:
@Bean
@LoadBalanced // ribbon的负载均衡注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
启动类
@SpringBootApplication
@EnableEurekaClient
// 在启动该微服务的时候就能去加载我们的自定义Ribbon配置类,从而使配置生效
@RibbonClient(name="MICROSERVICECLOUD-DEPT")
public class DeptConsumer80_App{
public static void main(String[] args){
SpringApplication.run(DeptConsumer80_App.class, args);
}
@Bean
@LoadBalanced // ribbon的负载均衡注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
消费者的方法
@GetMapping("/testRibbon")
public String testRibbon(String serviceName){
// 正常来讲需要拿到ip和port 以及路径才可以用
// http://provider/hello
String result = restTemplate.getForObject("http://" + serviceName + "/hello", String.class);
// 只要你给restTemplate 加了ribbon的注解 项目中这个对象发起的请求 都会走ribbon的代理
// 如果你想使用原生的restTemplate 就需要重新创建一个对象
// RestTemplate myRest = new RestTemplate();
// String forObject = myRest.getForObject("http://localhost:8888/aaa", String.class);
return result;
}
配置文件
server:
port: 8082
spring:
application:
name: consumer
eureka:
client:
service-url:
defaultZone: http://47.100.238.122:8761/eureka
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
测试
http://localhost:8082/testRibbon?serviceName=provider
Ribbon
实现思路
- 拦截这个请求
- 截取主机名称
- 借助
eureka
来做服务发现list<>
- 通过负载均衡算法 拿到一个服务
ip port
reConstructURL
重构url
- 发起请求
注意:启动的时候,先启动服务端,在启动提供者,最后启动消费者。
Ribbon
要做什么事情?
先通过"http://" + serviceId + "/info"
我们思考ribbon
在真正调用之前需要做什么?
restTemplate.getForObject(“http://provider/info”, String.class);
想要把上面这个请求执行成功,我们需要以下几步
- 拦截该请求;
- 获取该请求的
URL
地址:http://provider/info
- 截取
URL
地址中的provider
- 从服务列表中找到
key
为provider
的服务实例的集合(服务发现) - 根据负载均衡算法选出一个符合的实例
- 拿到该实例的
host
和port
,重构原来URL
中的provider
- 真正的发送
restTemplate.getForObject(“http://ip:port/info”,String.class)
轮询算法:
取余运算,但设计到线程安全问题。
怎么能做一个线程安全的轮训算法 加锁 效率极低 CAS
自旋锁 没有线程的等待和唤醒的开销
CAS
优点 性能好 java
层面无锁的状态 但是在jvm
层面 有锁的cmpxchg
CAS
缺点 会导致短暂时间内 CPU
飙升 还有ABA
问题
源码分析
这里学习到了一个查看源码的小技巧,首先debug
项目。【结合ribbon轮询算法进行解释】
ServiceInstance choose = loadBalancerClient.choose(serviceName);
进入choose
方法,点击choose
方法的实现,这里我们肯定看的是RibbonLoadBalancerClient
,在方法return
语句打上断点。这里为了方便观看,我把格式调整了一下
public ServiceInstance choose(String serviceId) {
return this.choose(serviceId, (Object)null);
}
public ServiceInstance choose(String serviceId, Object hint) {
Server server = this.getServer(this.getLoadBalancer(serviceId), hint);
return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(
serviceId,
server,
this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)
);
}
此时,debug
运行项目,访问该请求,点击this.getServer
方法
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
可以看到chooseServer
方法,点进去之后,发现是一个接口,但是我们此时是不知道它的实现类是什么的。在上面的return
语句打上断点,f9
跳到这个断点,按住Alt
点击chooseServer
方法,会弹出一个框,显示是谁调用了它,显示的是ZoneAwareLoadBalancer
public Server chooseServer(Object key) {
if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
Server server = null;
try {
LoadBalancerStats lbStats = this.getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (this.triggeringLoad == null) {
this.triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2D);
}
if (this.triggeringBlackoutPercentage == null) {
this.triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999D);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key); // 看这里
}
}
} catch (Exception var8) {
logger.error("Error choosing server using zone aware logic for load balancer={}", this.name, var8);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
} else {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
}
上面的代码片段中,我们可以发现最终是进入了 server = zoneLoadBalancer.chooseServer(key);
方法中,继续深入,
public Server chooseServer(Object key) {
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
if (this.rule == null) {
return null;
} else {
try {
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}
在这个方法中继续打断点,调用this.rule.choose(key);
是PredicateBasedRule
public Server choose(Object key) {
ILoadBalancer lb = this.getLoadBalancer();
Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
return server.isPresent() ? (Server)server.get() : null;
}
可以看到chooseRoundRobinAfterFiltering
这个方法,点击进去之后发现有一个方法incrementAndGetModulo
从字面可以看出是有取余操作的,点进去看看
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
return eligible.size() == 0 ? Optional.absent() : Optional.of(
eligible.get(this.incrementAndGetModulo(eligible.size()))
);
}
最终的实现
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextIndex.get();
next = (current + 1) % modulo;
} while(!this.nextIndex.compareAndSet(current, next) || current >= modulo);
return current;
}
ribbon
中如何设置负载均衡算法规则
修改默认负载均衡算法,几种算法的全限定类名
NFLoadBalancerClassName
:loadBalance
策略
NFLoadBalancerPingClassName
:ping
机制策略
NIWSServerListClassName
: 服务列表策略
NIWSServerListFilterClassName
: 服务列表过滤策略
ZonePreferenceServerListFilter
: 默认是优先过滤非一个区的服务列表
# 访问不用的服务可以使用不用的算法规则
provider: # 先写服务提供者的应用名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #几种算法的全限定类名
ribbon:
eager-load:
enabled: false # ribbon它只有自己的话 能不能做服务发现 借助eureka # ribbon需要去eureka中获取服务列表 如果false就懒加载
eureka:
enabled: true
http: # 我们使用ribbon 用的restTemplate发请求 java.net.HttpUrlConnection 发的请求 很方便 但是它不支持连接池
client: # 发请求的工具有很多 httpClient 它支持连接池 效率更好 如果你想改请求的工具 记得加这个依赖即可
enabled: false
okhttp: # 这个也是请求工具 移动端用的比较多 轻量级的请求
enabled: false
自定义算法
@Component
public class MyRule implements IRule {
@Override
public Server choose(Object key) {
return null;
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
}
@Override
public ILoadBalancer getLoadBalancer() {
return null;
}
}
启动类
@SpringBootApplication
@EnableEurekaClient
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
/**
* 往容器中放一个rule对象
* 你访问任何一个提供者 都是这个算法
* @return
*/
@Bean
public IRule myRule(){
return new RandomRule();
}
}
Ribbon
默认配置DefaultClientConfigImpl
,一下是部分内容,可以根据类名搜索进行查看。
public static final Boolean DEFAULT_PRIORITIZE_VIP_ADDRESS_BASED_SERVERS;
public static final String DEFAULT_NFLOADBALANCER_PING_CLASSNAME = "com.netflix.loadbalancer.DummyPing";
public static final String DEFAULT_NFLOADBALANCER_RULE_CLASSNAME = "com.netflix.loadbalancer.AvailabilityFilteringRule";
public static final String DEFAULT_NFLOADBALANCER_CLASSNAME = "com.netflix.loadbalancer.ZoneAwareLoadBalancer";
public static final boolean DEFAULT_USEIPADDRESS_FOR_SERVER;
public static final String DEFAULT_CLIENT_CLASSNAME = "com.netflix.niws.client.http.RestClient";
public static final String DEFAULT_VIPADDRESS_RESOLVER_CLASSNAME = "com.netflix.client.SimpleVipAddressResolver";
public static final String DEFAULT_PRIME_CONNECTIONS_URI = "/";
public static final int DEFAULT_MAX_TOTAL_TIME_TO_PRIME_CONNECTIONS = 30000;
public static final int DEFAULT_MAX_RETRIES_PER_SERVER_PRIME_CONNECTION = 9;
public static final Boolean DEFAULT_ENABLE_PRIME_CONNECTIONS;
public static final int DEFAULT_MAX_REQUESTS_ALLOWED_PER_WINDOW = 2147483647;
public static final int DEFAULT_REQUEST_THROTTLING_WINDOW_IN_MILLIS = 60000;
public static final Boolean DEFAULT_ENABLE_REQUEST_THROTTLING;
public static final Boolean DEFAULT_ENABLE_GZIP_CONTENT_ENCODING_FILTER;
public static final Boolean DEFAULT_CONNECTION_POOL_CLEANER_TASK_ENABLED;
public static final Boolean DEFAULT_FOLLOW_REDIRECTS;
public static final float DEFAULT_PERCENTAGE_NIWS_EVENT_LOGGED = 0.0F;
public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
public static final int DEFAULT_BACKOFF_INTERVAL = 0;
public static final int DEFAULT_READ_TIMEOUT = 5000;
public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = 2000;
public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
public static final Boolean DEFAULT_ENABLE_CONNECTION_POOL;
OpenFeign
组件
Feign
是声明性(注解)Web
服务客户端。它使编写Web
服务客户端更加容易。要使用Feign
,请创建一个接口并对其进行注解。它具有可插入注解支持,包括Feign
注解和JAX-RS
注解。Feign
还支持可插拔编码器和解码器。Spring Cloud
添加了对Spring MVC
注解的支持,并支持使用HttpMessageConverters
,Spring Web
中默认使用的注解。Spring Cloud
集成了Ribbon
和Eureka
以及Spring Cloud LoadBalancer
,以在使用Feign
时提供负载平衡的http
客户端。
Feign
是一个远程调用的组件(接口,注解)http
调用的
Feign
集成了ribbon
ribbon
里面集成了eureka
入门案例
创建订单服务
pom
文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
启动类
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
controller
@RestController
public class OrderController {
@GetMapping("doOrder")
public String doOrder(){
//try {
// // 模拟操作数据库等 耗时2s
// TimeUnit.SECONDS.sleep(2);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
return "油条豆浆-热干面";
}
}
配置文件
server:
port: 8080
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
创建用户服务,模拟下单的简单过程。
pom
文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.powernode.feign"}) // 开启feign的客户端功能 才可以帮助我们发起调用
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
controller
@RestController
public class UserController {
@Autowired
public UserOrderFeign userOrderFeign;
@GetMapping("userDoOrder")
public String userDoOrder() {
System.out.println("有用户进来了");
// 这里需要发起远程调用
String s = userOrderFeign.doOrder();
return s;
}
配置文件
server:
port: 8081
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
创建feign
文件夹,并创建想要远程调用的接口
@FeignClient(value = "order-service")
public interface UserOrderFeign {
/**
* 你需要调用哪个controller 就写它的方法签名
* 方法签名(就是包含一个方法的所有的属性)
* @return
*/
@GetMapping("doOrder")
String doOrder();
}
问题
在user
服务中调用order
服务,而order
服务由于很复杂,耗时很长,此时上面的代码会出现超时的问题
改造order
服务中的controller
,验证是否是超时的
@GetMapping("doOrder")
public String doOrder(){
try {
// 模拟操作数据库等 耗时5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "油条豆浆-热干面";
}
错误信息
java.net.SocketTimeoutException: Read timed out # 请求超时
解决办法:在user服务中的配置文件中添加如下配置,经过6秒的等待还是可以成功的。
同时还有服务器在连接的时候,也需要指定超时时间
# feign只是帮你封装了远程调用的功能 底层还是ribbon 所以我们需要去修改ribbon的时间
ribbon:
ReadTimeout: 6000 # 给3s超时时间
ConnectTimeout: 5000 # 连接服务的超时时间
feign
的调用过程
- 接口是不能做事情的
- 如果想做事 必须要有对象
- 那么这个接口肯定是被创建出代理对象的
- 动态代理
jdk``(java interface
接口$Proxy
)cglib
(subClass
子类) jdk
动态代理 只要是代理对象调用的方法必须走java.lang.reflect.InvocationHandler#invoke(java.lang.Object,java.lang.reflect.Method, java.lang.Object[])
@Autowired
private RestTemplate restTemplate;
/**
* 手写feign的核心步骤
*/
@Test
void contextLoads() {
UserOrderFeign o = (UserOrderFeign) Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 能去拿到对方的ip和port 并且拿到这个方法上面的注解里面的值 那么就完事了
GetMapping annotation = method.getAnnotation(GetMapping.class);
String[] paths = annotation.value();
String path = paths[0];
Class<?> aClass = method.getDeclaringClass();
FeignClient annotation1 = aClass.getAnnotation(FeignClient.class);
String applicationName = annotation1.value();
String url = "http://" + applicationName + "/" + path;
String forObject = restTemplate.getForObject(url, String.class);
return forObject;
}
});
String s = o.doOrder();
System.out.println(s);
}
OpenFeign
参数
模拟集中不同的请求方式
编写请求接口
// RESTFul风格的,在路径中填写参数
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
System.out.println(name + ":" + age);
return "ok";
}
// 普通风格
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name) {
System.out.println(name);
return "ok";
}
// 普通风格多个参数
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
System.out.println(name);
System.out.println(age);
return "ok";
}
// post请求带请求体参数
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order) {
System.out.println(order);
return "ok";
}
// 一个请求体,一个路径参数
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
System.out.println(name);
System.out.println(order);
return "ok";
}
OpenFeign
中的写法,只要将你写的请求的请求签名等信息copy
过来即可,没有方法体
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name);
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order);
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order, @RequestParam("name") String name);
测试接口
@GetMapping("testParam")
public String testParam(){
String cxs = userOrderFeign.testUrl("ljq", 18);
System.out.println(cxs);
String t = userOrderFeign.oneParam("111");
System.out.println(t);
String lg = userOrderFeign.twoParam("12", 31);
System.out.println(lg);
Order order = Order.builder()
.name("可乐")
.price(188D)
.time(new Date())
.id(1)
.build();
String s = userOrderFeign.oneObj(order);
System.out.println(s);
String param = userOrderFeign.oneObjOneParam(order, "ljq");
System.out.println(param);
return "ok";
}
单独传递Data
问题
- 不建议单独传递时间参数
- 转成字符串
2022-03-20 10:25:55:213
因为字符串不会改变 jdk
LocalDate
年月日LocalDateTime
会丢失秒- 改
feign
的源码
建议,使用String
类型进行传递
OpenFeifn
的日志处理
在启动类中
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.powernode.feign"}) // 开启feign的客户端功能 才可以帮助我们发起调用
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 打印fein日志信息 级别
* @return
*/
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
}
在配置文件中添加
logging:
level:
com.powernode.feign.UserOrderFeign: debug # 我需要打印这个接口下面的日志
Hystrix
组件
熔断器,也叫断路器!(正常情况下断路器是关的只有出了问题才打开)用来保护微服务不雪崩的方法。
当有服务调用的时候,才会出现服务雪崩,所以
Hystrix
常和OpenFeign
,Ribbon
一起出现
入门案例
创建customer
服务
pom
文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置文件:这里需要注意下,如果没有写最后的feign.hystrix.enabled
配置, hystrix
默认是关闭状态的,我们的测试是不会有效果的
server:
port: 8081
spring:
application:
name: customer-service
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
feign:
hystrix:
enabled: true # 在cloud的F版以前 是默认开启的 但是因为后来有了其他的熔断组件
controller
代码
@RestController
public class CustomerController {
@Autowired
private CustomerRentFeign customerRentFeign;
@GetMapping("customerRent")
public String CustomerRent(){
System.out.println("客户来租车了");
// RPC
String rent = customerRentFeign.rent();
return rent;
}
}
feign包下的远程调用
注意:这里在
FeignClient
注解中会有稍微的不同,因为我们这里需要定义在远程调用不生效的情况下, 进行熔断的场景,注解中的fallback = CustomerRentFeignHystrix.class
就意思是在远程调用失败的情况下,程序会去调用哪个类下的熔断方法
/**
* 这里需要指定熔断的类
*/
@FeignClient(value = "rent-car-service",fallback = CustomerRentFeignHystrix.class)
public interface CustomerRentFeign {
@GetMapping("rent")
public String rent();
}
hystrix
包下的CustomerRentFeign
接口的实现类
注意:这里的实现类需要实现
feign
的接口,并将类纳入spring
进行管理
@Component
public class CustomerRentFeignHystrix implements CustomerRentFeign {
/**
* 这个方法就是备选方案
* @return
*/
@Override
public String rent() {
return "我是备胎";
}
}
启动类
@SpringBootApplication
@EnableEurekaClient // 开启eureka客户端
@EnableFeignClients // 开启openfeign客户端功能
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}
创建rent-car-service
服务
pom
文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
server:
port: 8080
spring:
application:
name: rent-car-service
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
controller
文件:简单模拟一下
@RestController
public class RentCarController {
@GetMapping("rent")
public String rent() {
return "租车成功";
}
}
启动类
@SpringBootApplication
@EnableEurekaClient
public class RentCarServiceApplication {
public static void main(String[] args) {
SpringApplication.run(RentCarServiceApplication.class, args);
}
}
然后就可以测试了。
常用配置文件
server:
port:8081
spring:
application:
name:consumer-user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
fetch-registry: true
register-with-eureka: true
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
feign:
hystrix:
enabled: true
hystrix: #hystrix 的全局控制
command:
default: #default 是全局控制,也可以换成单个方法控制,把 default 换成方法名即可
circuitBreaker:
enabled: true #开启断路器
requestVolumeThreshold: 3 #失败次数(阀值) 30次 30s
sleepWindowInMilliseconds: 20000 #窗口时间
errorThresholdPercentage: 60 #失败率
fallback:
isolation:
semaphore:
maxConcurrentRequests: 1000 #信号量隔离级别最大并发数
execution:
isolation:
Strategy: thread #隔离方式 thread 线程隔离集合和 SEMAPHORE 信号量隔离
thread:
timeoutInMilliseconds: 3000 #调用超时时长
ribbon:
ReadTimeout: 5000 #要结合feign 的底层ribbon 调用的时长
ConnectTimeout: 5000
#隔离方式两种隔离方式 thread 线程池按照 group(10 个线程)划分服务提供者,用户请求的线程和做远程的线程不一样
# 好处当 B 服务调用失败了或者请求B 服务的量太大了不会对C 服务造成影响用户访问比较大的情况下使用比较好异步的方式
# 缺点线程间切换开销大,对机器性能影响
# 应用场景调用第三方服务并发量大的情况下
# SEMAPHORE 信号量隔离每次请进来有一个原子计数器做请求次数的++ 当请求完成以后--
# 好处对 cpu 开销小
# 缺点并发请求不易太多当请求过多就会拒绝请求做一个保护机制
# 场景使用内部调用,并发小的情况下
# 源码入门 HystrixCommand AbstractCommand HystrixThreadPool
熔断器模拟
新建请求
@RestController
public class FishController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("doRpc")
@MyFish
public String doRpc(){
String result = restTemplate.getForObject("http://localhost:8989/abc", String.class);
return result;
}
}
自定义注解
/**
* 熔断器切面注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyFish {
}
面向切面,使用AOP
来模拟拦截器
@Component
@Aspect
public class FishAspect {
// public static final String POINT_CUT = "execution (* com.powernode.controller.FishController.doRpc(..))";
// 因为一个消费者可以去调用多个提供者 每个提供者都有自己的断路器
// 在消费者里面去创建一个断路器的容器
public static Map<String, Fish> fishMap = new HashMap<>();
static {
// 假设 是需要去调用order-service的服务
fishMap.put("order-service", new Fish());
}
Random random = new Random();
/**
* 这个就类比拦截器
* 就是要判断 当前断路器的状态 从而决定是否发起调用(执行目标方法)
*
* @param joinPoint
* @return
*/
@Around(value = "@annotation(com.powernode.anno.MyFish)")
public Object fishAround(ProceedingJoinPoint joinPoint) {
Object result = null;
// 获取到当前提供者的断路器
Fish fish = fishMap.get("order-service");
FishStatus status = fish.getStatus();
switch (status) {
case CLOSE:
// 正常 去调用 执行目标方法
try {
result = joinPoint.proceed();
return result;
} catch (Throwable throwable) {
// 说明调用失败 记录次数
fish.addFailCount();
return "我是备胎";
}
case OPEN:
// 不能调用
return "我是备胎";
case HALF_OPEN:
// 可以用少许流量去调用
int i = random.nextInt(5);
System.out.println(i);
if (i == 1) {
// 去调用
try {
result = joinPoint.proceed();
// 说明成功了 断路器关闭
fish.setStatus(FishStatus.CLOSE);
synchronized (fish.getLock()) {
fish.getLock().notifyAll();
}
return result;
} catch (Throwable throwable) {
return "我是备胎";
}
}
default:
return "我是备胎";
}
}
}
熔断器模型类
/**
* 这个是断路器的模型
*/
@Data
public class Fish {
/**
* 窗口时间
*/
public static final Integer WINDOW_TIME = 20;
/**
* 最大失败次数
*/
public static final Integer MAX_FAIL_COUNT = 3;
/**
* 断路器中有它自己的状态
*/
private FishStatus status = FishStatus.CLOSE;
/**
* 当前这个断路器失败了几次
* i++
* AtomicInteger 可以保证线程安全
*/
private AtomicInteger currentFailCount = new AtomicInteger(0);
/**
* 线程池
*/
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
4,
8,
30,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
private Object lock = new Object();
{
poolExecutor.execute(() -> {
// 定期删除
while (true) {
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果断路器是开的 那么 不会去调用 就不会有失败 就不会记录次数 没有必要清零 这个线程可以不执行
if (this.status.equals(FishStatus.CLOSE)) {
// 清零
this.currentFailCount.set(0);
} else {
// 半开或者开 不需要去记录次数 这个线程可以不工作
// 学过生产者 消费者模型 wait notifyAll condition singleAll await 它们只能随机唤醒某一个线程
// lock锁 源码 CLH 队列 放线程 A B C D E park unpark 可以 唤醒指定的某一个线程
// LockSupport.park();
// LockSupport.unpark();
synchronized (lock) {
try {
lock.wait();
System.out.println("我被唤醒了,开始工作");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
/**
* 记录失败次数
*/
public void addFailCount() {
int i = currentFailCount.incrementAndGet(); // ++i
if (i >= MAX_FAIL_COUNT) {
// 说失败次数已经到了阈值了
// 修改当前状态为 open
this.setStatus(FishStatus.OPEN);
// 当断路器打开以后 就不能去访问了 需要将他变成半开
// 等待一个时间窗口 让断路器变成半开
poolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setStatus(FishStatus.HALF_OPEN);
// 重置失败次数 不然下次进来直接就会打开断路器
this.currentFailCount.set(0);
});
}
}
}
熔断器状态枚举类
public enum FishStatus {
CLOSE,
OPEN,
HALF_OPEN
}
springcloud
工程简介
这章讲的是常见的工程结构,可以为以后自己写项目做参考
注意,所有工程的包路径需要统一
这里进行说明:
- 我们把所有的实体类信息存放在
project-domain
中 order-center
依赖于project-domain
,common-api
模拟实现订单服务的代码user-center
依赖于project-domain
,common-api
模拟实现用户服务的代码common-api
用来实现feign
的远程调用功能,所有需要暴露出来的接口都可以在里面创建,在order-center
、user-center
分别有feign
的实现类,在common-api
同时还包含了关于熔断的一些操作
首先创建maven
工程:主要做的是依赖管理,版本控制等工作
pom
文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>project-domain</module>
<module>common-api</module>
<module>user-center</module>
<module>order-center</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>04-feign-project</artifactId>
<version>1.0-SNAPSHOT</version>
<!--全局版本号控制的地方-->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<!-- 这里的依赖所以的子模块都会有-->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<!-- 加在这里的依赖不会被这的引入进项目 只是做了一个版本控制-->
<dependencyManagement>
<dependencies>
<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>
<!-- 打包 仓库 等配置 -->
<!-- <build>-->
<!-- </build>-->
</project>
创建common-api
模块
common-api
模块主要存放一些通过openfeign
开放出来的api
,这样所有的项目只要依赖于这个项目就可以实现远程连接了方法是:定义
openfeign
相关接口
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>04-feign-project</artifactId>
<groupId>com.powernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-api</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.powernode</groupId>
<artifactId>project-domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
</project>
定义feign
文件下的远程调用接口
@FeignClient(value = "order-service", fallback = UserOrderFeignHystrix.class)
public interface UserOrderFeign {
// 查询订单
@GetMapping("/order/getOrderByUserId")
Order getOrderByUserId(@RequestParam Integer userId);
}
hystrix
文件夹下的熔断实现类
@Component
public class UserOrderFeignHystrix implements UserOrderFeign {
/**
* 一般远程调用的熔断可以直接返回null
* @param userId
* @return
*/
@Override
public Order getOrderByUserId(Integer userId) {
return null;
}
}
创建order-center
模块
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>04-feign-project</artifactId>
<groupId>com.powernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-center</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.powernode</groupId>
<artifactId>common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApp {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApp.class,args);
}
}
controller
@RestController
public class OrderController implements UserOrderFeign {
@Override
public Order getOrderByUserId(Integer userId) {
System.out.println(userId);
Order order = Order.builder()
.name("青椒肉丝盖饭")
.price(15D)
.orderId(1)
.build();
return order;
}
}
配置文件
server:
port: 8080
spring:
application:
name: order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #配置采样率 默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
rate: 10 #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int) 默认为10。
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
创建project-domain
模块
该模块用来存放左右的实体类
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>04-feign-project</artifactId>
<groupId>com.powernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>project-domain</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
实体类信息
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Order {
private Integer orderId;
private String name;
private Double price;
}
创建user-center
模块
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>04-feign-project</artifactId>
<groupId>com.powernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-center</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.powernode</groupId>
<artifactId>common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 暴露自身检查端点 endPoints 一个依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
controller
类
@RestController
public class UserController {
@Resource
public UserOrderFeign userOrderFeign;
@GetMapping("find")
public Order findOrder() {
return userOrderFeign.getOrderByUserId(1);
}
}
启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class UserServiceApp {
public static void main(String[] args) {
SpringApplication.run(UserServiceApp.class,args);
}
}
配置文件
server:
port: 8081
spring:
application:
name: user-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #配置采样率 默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
rate: 10 #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int) 默认为10。
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
feign:
hystrix:
enabled: true # 开启熔断
management:
endpoints:
web:
exposure:
include: '*'
Sleuth
链路追踪组件
远程调用最好不要超过三层
pom
文件中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
配置文件中添加配置
spring:
application:
name: user-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #配置采样率 默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
rate: 10 #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int)
启动服务,然后访问http://loaclhost:9411
端口
admin
监控
用于监控模块的所有信息,可以在
idea
中的Endpoints
中查看
admin
模块可以直接监控eureka
注册中心中的所有服务信息,前提是,服务必须有admin-client
依赖,并且暴露出所有的信息实质就是心跳检测,如果被检测的模块不暴露出信息是无法监测的
暴露自身信息
management:
endpoints:
web:
exposure:
include: '*'
创建admin-server
模块
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>admin-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>05-admin-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-boot-admin.version>2.3.0</spring-boot-admin.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>${spring-boot-admin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类
@SpringBootApplication
@EnableEurekaClient
@EnableAdminServer // 开启admin服务注解
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
配置文件
server:
port: 10086 # 端口号范围 0-65535
spring:
application:
name: admin-server
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
management:
endpoints:
web:
exposure:
include: '*' # 暴露所有的监控端点 # 如果一个服务需要被监控 那么就要讲自身的一些情况(一些信息接口)暴露出去
访问配置的端口信息 :http://localhost:10086
Gateway
组件
网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁
- 没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的
ip:port
,如果user-service
并发比较大,则无法完成负载均衡 - 有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的
ip:prot
)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token
拦截,权限验证,限流等操作
Gateway
三大核心概念
Route
(路由)(重点和eureka
结合做动态路由)
路由信息的组成:由一个ID
、一个目的URL
、一组断言工厂、一组Filter
组成。如果路由断言为真,说明请求URL
和配置路由匹配。
Predicate
(断言)(就是一个返回bool
的表达式)
Java 8
中的断言函数。lambda
四大接口供给形,消费性,函数型,断言型Spring Cloud Gateway
中的断言函数输入类型是Spring 5.0
框架中的ServerWebExchange
。Spring Cloud Gateway
的断言函数允许开发者去定义匹配来自于Http Request
中的任何信息比如请求头和参数。
Filter
(过滤) (重点)
一个标准的Spring WebFilter
。Web
三大组件(servlet
listener
filter
) mvc
、interceptor
Spring Cloud Gateway
中的Filter
分为两种类型的Filter
,分别是Gateway Filter
和Global Filter
。过滤器Filter
将会对请求和响应进行修改处理。
一个是针对某一个路由(路径)的filter
对某一个接口做限流
一个是针对全局的filter token ip
黑名单
入门案例
创建login-service
模块
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>login-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>02-login-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
controller
方法
@RestController
@CrossOrigin // 加上这个注解之后 这个controller里面的方法就可以直接被访问了
public class LoginController {
@Autowired
public StringRedisTemplate redisTemplate;
@GetMapping("doLogin")
@CrossOrigin
public String doLogin(String name, String pwd) {
System.out.println(name);
System.out.println(pwd);
// 这里假设去做了登录
User user = new User(1, name, pwd, 18);
// token
String token = UUID.randomUUID().toString();
return token;
}
}
配置文件
server:
port: 8081
spring:
application:
name: login-service
创建gateway
模块
pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>gateway-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>01-gateway-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
<build>
<finalName>aaaa</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
server:
port: 80 # 网关一般是80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # =只要加了依赖 默认开启
routes: # 如果一个服务里面有100个路径 如果我想做负载均衡 ?? 动态路由
- id: login-service-route # 这个是路由的id 保持唯一即可
# uri: http://localhost:8081 # uri统一资源定位符 url 统一资源标识符
uri: lb://login-service # uri统一资源定位符 url 统一资源标识符
predicates: # 断言是给某一个路由来设定的一种匹配规则 默认不能作用在动态路由上
- Path=/doLogin # 匹配规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- After=2022-03-22T08:42:59.521+08:00[Asia/Shanghai]
- Method=GET,POST
路由方式
代码方式路由
@Configuration
public class RouteConfig {
/**
* 代码的路由 和yml不冲突 都可以用
* 如果你的uri后面给了一个访问地址 和匹配地址相同 那么就不会再凭借
* @param builder
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("guochuang-id",r->r.path("/guochuang").uri("https://www.bilibili.com/guochuang"))
.route("dance-id",r->r.path("/v/dance").uri("https://www.bilibili.com"))
.route("kichiku-id",r->r.path("/v/kichiku").uri("https://www.bilibili.com"))
.build();
}
}
动态路由,需要将gateway
注册到eureka
中
访问的时候需要加上注册中心的实例名称
http://localshot/login-service/doLogin
server:
port: 80 # 网关一般是80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # =只要加了依赖 默认开启
discovery:
locator:
enabled: true # 开启动态路由 开启通用应用名称 找到服务的功能
lower-case-service-id: true # 开启服务名称小写
eureka:
client:
service-url:
defaultZone: http://192.168.153.1:8761/eureka
registry-fetch-interval-seconds: 3 # 网关拉去服务列表的时间缩短
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
断言工厂
文档地址 https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories
server:
port: 80 # 网关一般是80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # =只要加了依赖 默认开启
routes: # 如果一个服务里面有100个路径 如果我想做负载均衡 ?? 动态路由
- id: login-service-route # 这个是路由的id 保持唯一即可
# uri: http://localhost:8081 # uri统一资源定位符 url 统一资源标识符
uri: lb://login-service # uri统一资源定位符 url 统一资源标识符
predicates: # 断言是给某一个路由来设定的一种匹配规则 默认不能作用在动态路由上
- Path=/doLogin # 匹配规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- After=2022-03-22T08:42:59.521+08:00[Asia/Shanghai]
- Method=GET,POST
# - Query=name,admin. #正则表达式的值
Filter过滤器工厂
gateway
里面的过滤器和Servlet
里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http
请求和返回Http
响应
自定义过滤器
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
/**
* 这个就是过滤的方法
* 过滤器链模式
* 责任链模式
* 网关里面有使用 mybatis的 二级缓存有变种责任链模式
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 针对请求的过滤 拿到请求 header url 参数 ....
ServerHttpRequest request = exchange.getRequest();
// HttpServletRequest 这个是web里面的
// ServerHttpRequest webFlux里面 响应式里面的
String path = request.getURI().getPath();
System.out.println(path);
HttpHeaders headers = request.getHeaders();
System.out.println(headers);
String methodName = request.getMethod().name();
System.out.println(methodName);
String ip = request.getHeaders().getHost().getHostString();
System.out.println(ip);
// 响应相关的数据
ServerHttpResponse response = exchange.getResponse();
// .....逻辑处理
// 放行 到下一个过滤器了
return chain.filter(exchange);
}
/**
* 指定顺序的方法
* 越小越先执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
模拟IP
拦截
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
/**
* 网关的并发比较高 不要再网关里面直接操作mysql
* 后台系统可以查询数据库 用户量 并发量不大
* 如果并发量大 可以查redis 或者 在内存中写好
*/
public static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "144.128.232.147");
/**
* 1.拿到ip
* 2.校验ip是否符合规范
* 3.放行/拦截
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String ip = request.getHeaders().getHost().getHostString();
// 查询数据库 看这个ip是否存在黑名单里面 mysql数据库的并发
// 只要是能存储数据地方都叫数据库 redis mysql
if (!BLACK_LIST.contains(ip)) {
return chain.filter(exchange);
}
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type","application/json;charset=utf-8");
HashMap<String, Object> map = new HashMap<>(4);
map.put("code", 438);
map.put("msg","你是黑名单");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return -5;
}
}
Springcloud alibaba Nacos
注册中心
Nacos
致力于发现、配置和管理微服务。Nacos
提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos
帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos
是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式) 的服务基础设施
概念
服务(Service
)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos
支持主流的服务生态,如Kubernetes Service
、gRPC|Dubbo RPC Service
或者Spring Cloud RESTful Service
.
服务注册中心(Service Registry
)
服务注册中心,它是服务实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查API
来验证它是否能够处理请求。
服务元数据(Service Metadata
)
服务元数据是指包括服务端点(endpoints
)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据
服务提供方(Service Provider
)
是指提供可复用和可调用服务的应用方
服务消费方(Service Consumer
)
是指会发起对某个服务调用的应用方
配置(Configuration
) 配置文件中心
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如WAR
,JAR
包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。
配置管理(Configuration Management
)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。
名字服务(Naming Service
)
提供分布式系统中所有对象(Object
)、实体(Entity
)的“名字”到关联的元数据之间的映射管理服务,例如ServiceName -> Endpoints Info
, Distributed Lock Name -> LockOwner/Status Info
, DNS Domain Name -> IP List
, 服务发现和DNS
就是名字服务的2 大场景。
配置服务(Configuration Service
)
在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
目录介绍
bin
:可执行文件夹目录,包含:启动、停止命令等等conf
:配置文件目录target
:存放naocs-server.jar
LICENSE
:授权信息,Nacos
使用Apache License Version 2.0
授权NOTICE
:公告信息
修改配置文件:
1、application.properties
Nacos
默认使用嵌入式数据库实现数据的存储,并不方便观察数据存储的基本情况,这里面我们修改为使用Mysql
数据库做数据的存储,方便我们观察数据的结构。
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
nacos
自带的sql
文件在conf
目录下nacos-mysql.sql
在数据库工具中直接导入即可
启动nacos
startup.cmd -m standalone # 单机版启动
也可以修改nacos
的配置文件,默认是集群的方式。目录是bin
下面的startup.cmd
,以下是简略信息。
@echo off
rem Copyright 1999-2018 Alibaba Group Holding Ltd.
rem Licensed under the Apache License, Version 2.0 (the "License");
rem you may not use this file except in compliance with the License.
rem You may obtain a copy of the License at
rem
rem http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.
if not exist "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! jdk8 or later is better! & EXIT /B 1
set "JAVA=%JAVA_HOME%\bin\java.exe"
setlocal enabledelayedexpansion
set BASE_DIR=%~dp0
rem added double quotation marks to avoid the issue caused by the folder names containing spaces.
rem removed the last 5 chars(which means \bin\) to get the base DIR.
set BASE_DIR="%BASE_DIR:~0,-5%"
set CUSTOM_SEARCH_LOCATIONS=file:%BASE_DIR%/conf/
set MODE="standalone"
set FUNCTION_MODE="all"
set SERVER=nacos-server
set MODE_INDEX=-1
set FUNCTION_MODE_INDEX=-1
set SERVER_INDEX=-1
set EMBEDDED_STORAGE_INDEX=-1
set EMBEDDED_STORAGE=""
入门案例
创建client-a
,因为两个服务的配置文件都是相同的,所以client-b
就不再重复pom
文件内容了
pom
文件
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
配置文件
server:
port: 8080
spring:
application:
name: nacos-client-a
cloud:
nacos: # 如果不指定命名空间会默认注册到public里面去 如果没有指定分组 会注册到DEFAULT_GROUP
server-addr: localhost:8848 # 往这个地址去注册自己
username: nacos
password: nacos
discovery:
namespace: f5e847a0-119d-4baa-9dff-4bf15e418a42 # 对应命名空间的id
group: A_GROUP
client-b
只有配置文件不同
server:
port: 8081
spring:
application:
name: nacos-client-b
cloud:
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
discovery: # 这里是和注册相关的配置
namespace: f5e847a0-119d-4baa-9dff-4bf15e418a42
group: A_GROUP # 往哪个组注册
service: user-service # 这个才是注册列表的名字 如果不写 默认是取${spring.application.name}
启动类:a
服务与b
服务的启动类上的注解是一致的。
@SpringBootApplication
@EnableDiscoveryClient
public class NacosClientBApplication {
public static void main(String[] args) {
SpringApplication.run(NacosClientBApplication.class, args);
}
}
整合openFeign
上面的
pom
文件中已经引入了openfegin
的组件
在client-b
中定义测试接口
@GetMapping("info")
public String info() {
return "测试B";
}
在client-a
项目中添加openfeign
相关代码
@FeignClient(value = "user-service") // 注意这里,这里填写的是b服务的注册名称,别写错了
public interface TestFeign {
@GetMapping("info")
public String info();
}
client-a
中的接口
@Autowired
public TestFeign testFeign;
@GetMapping("test")
public String test() {
return testFeign.info();
}
client-a
中的启动类
@SpringBootApplication
@EnableDiscoveryClient // 开启服务发现客户端
@EnableFeignClients // 开启openfeign的注解
public class NacosClientAApplication {
public static void main(String[] args) {
SpringApplication.run(NacosClientAApplication.class, args);
}
}
nacos
配置文件详解
配置项 | Key | 默认值 | 说明 |
---|---|---|---|
服务端地址 | spring.cloud.nacos.discovery.server-addr | 无 | Nacos Server 启动监听的ip 地址和端口 |
服务名 | spring.cloud.nacos.discovery.service | ${spring.application.name} | 给当前的服务命名 |
服务分组 | spring.cloud.nacos.discovery.group | DEFAULT_GROUP | 设置服务所处的分组 |
权重 | spring.cloud.nacos.discovery.weight | 1 | 取值范围1 到100,数值越大,权重越大 |
网卡名 | spring.cloud.nacos.discovery.network-interface | 无 | 当IP 未配置时,注册的IP 为此网卡所对应的IP 地址,如果此项也未配置,则默认取第一块网卡的地址 |
注册的ip 地址 | spring.cloud.nacos.discovery.ip | 无 | 优先级最高 |
注册的端口 | spring.cloud.nacos.discovery.port | -1 | 默认情况下不用配置,会自动探测 |
命名空间 | spring.cloud.nacos.discovery.namespace | 无 | 常用场景之一是不同环境的注册的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。 |
AccessKey | spring.cloud.nacos.discovery.access -key | 无 | 当要上阿里云时,阿里云上面的一个云账号名 |
SecretKey | spring.cloud.nacos.discovery.secret-key | 无 | 当要上阿里云时,阿里云上面的一个云账号密码 |
Metadata | spring.cloud.nacos.discovery.metadata | 无 | 使用Map 格式配置,用户可以根据自己的需要自定义一些和服务相关的元数据信息 |
日志文件名 | spring.cloud.nacos.discovery.log-name | 无 | |
集群 | spring.cloud.nacos.discovery.cluster-name | DEFAULT | 配置成Nacos 集群名称 |
接入点 | spring.cloud.nacos.discovery.enpoint | UTF-8 | 地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址 |
是否继承ribbon | ribbon.nacos.enabled | true | |
是否开启Nacos Watch | spring.cloud.nacos.discovery.watch.enabled | true | 可以设置成false 来关闭watch |
Springcloud alibaba Nacos
配置中心
使用
Spring Cloud Alibaba Nacos Config
,可基于Spring Cloud
的编程模型快速接入Nacos
配置管理功能
入门案例
pom
文件
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在项目中创建bootstrap.yml
文件
server:
port: 8081
spring:
application:
name: nacos-config-a
# 项目在启动的时候去哪里找它对应的配置文件呢??
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos # nacos的登录名
password: nacos # nacos的登录密码
prefix: nacos-config # 读哪个配置文件 默认用的是应用名称 是可以修改的 默认是spring.application.name
file-extension: yml # 文件类型
profiles:
active: dev
编写配置类,这里建议创建一个类来管理读取的内容
@RefreshScope
注解:动态刷新配置文件中的内容
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@RefreshScope // 给这个类上 添加一个刷新的作用域
public class Hero {
@Value("${hero.name}")
private String name;
@Value("${hero.age}")
private Integer age;
@Value("${hero.address}")
private String address;
}
测试controller
@RestController
public class TestController {
@Autowired
public Hero hero;
@GetMapping("info")
public String heroInfo() {
return hero.getName() + ":" + hero.getAge() + ":" + hero.getAge();
}
}
读取配置文件的方式
nacos
配置中心通过namespace
、dataId
和group
来唯一确定一条配置。
Namespace
:即命名空间。默认的命名空间为public
,我们可以在Nacos
控制台中新建命名空间;dataId
:即配置文件名称Group
: 即配置分组, 默认为DEFAULT_GROUP
, 可以通过spring.cloud.nacos.config.group
配置。
其中:dataId
是最关键的配置字段,格式如下:
${prefix} - ${spring.profiles.active} . ${file-extension}
prefix
默认为spring.application.name
的值, 也可以通过配置项spring.cloud.nacos.config.prefix
来配置;spring.profiles.active
即为当前环境对应的profile 。注意, 当spring.profiles.active
为空时,对应的连接符-也将不存在,dataId
的拼接格式变成${prefix}.${file-extension}
;file-extension
为配置内容的数据格式, 可以通过配置项spring.cloud.nacos.config.file-extension
来配置。
注意: 在写dataId
的时候一定要添加文件类型后缀
如: nacos-config-dev.yml
Nacos
配置中心的namespace
、dataId
和group
可以方便灵活地划分配置。比如,我们现在有一个项目需要开发,项目名称为test
,项目开发人员分为两个组:GROUP_A
和GROUP_B
,项目分为三个环境:开发环境dev
、测试环境test
和生产环境prod
。
如:
server:
port: 8081
spring:
application:
name: nacos-config-a
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos
password: nacos
prefix: nacos-config # 读哪个配置文件 默认用的是应用名称 是可以修改的
file-extension: yml # 文件类型
profiles:
active: dev
获取多配置文件
同一命名空间下,不同的组
server:
port: 8082
spring:
application:
name: nacos-config-test
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: c52e715f-3f00-4ad8-803b-9c8dba19a221 # 目前读取多配置文件的方式只支持在同一个命名空间下
file-extension: yml
extension-configs: # 可以读多个配置文件 需要在同一个命名空间下面 可以是不同的组
- dataId: user-center-dev.yml
group: A_GROUP
refresh: true
- dataId: member-center-dev.yml
group: B_GROUP
refresh: false # 不去动态刷新配置文件
profiles:
active: dev
常用配置
配置项 | key | 默认值 | 说明 |
---|---|---|---|
服务端地址 | spring.cloud.nacos.config.server-addr | ||
DataId 前缀 | spring.cloud.nacos.config.prefix | spring.application.name | |
Group | spring.cloud.nacos.config.group | DEFAULT_GROUP | |
dataID 后缀及内容文件格式 | spring.cloud.nacos.config.file-extension | properties | dataId 的后缀,同时也是配置内容的文件格式 |
配置内容的编码方式 | spring.cloud.nacos.config.encode | UTF-8 | 配置的编码 |
获取配置的超时时间 | spring.cloud.nacos.config.timeout | 3000 | 单位为ms |
配置的命名空间 | spring.cloud.nacos.config.namespace | 常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源隔离等。 | |
AccessKey | spring.cloud.nacos.config.access-key | ||
SecretKey | spring.cloud.nacos.config.secret-key | ||
相对路径 | spring.cloud.nacos.config.context-path | 服务端API 的相对路径 | |
接入点 | spring.cloud.nacos.config.endpoint | 地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址 | |
是否开启监听和自动刷新 | spring.cloud.nacos.config.refresh.enabled | true |
总结
bootstrap.yml
写什么内容?
- 应用名称
spring.application.name
nacos
的注册和拉取的配置
nacos
中的配置文件写什么内容?
- 远端放 端口 数据源
redis
mq
能放远端的全放 因为方便管理和修改 包括自定义配置