springcloud中文API:https://springcloud.cc/spring-cloud-dalston.html
springcloud中国社区:http://springcloud.cn/
springcloud中文网:https://springcloud.cc/
一.Eureka(注册中心)
1.原理图
- Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
- 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
- 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
2.入门案例
Eureka也是一个独立的服务
这里单独创建Eureka服务
1.创建Eureka服务
修改项目启动类
package com.register;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
// 在启动类上添加注解 @EnableEurekaServer 标记这个类是 Eureka服务 启动器
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
2.Eureka服务配置
启动主程序后发现报错
只要是因为Eureka服务也是以集群形式存在的(因为Eureka服务也有挂掉的可能),所以Eureka服务要向其他Eureka服务服务注册自己
打开application.properties
server.port=8083
# 这里看源码可知springCloud eureka.client.service-url 接收map参数
# 并且默认添加了一组键值对(defaultZone,http://localhost:8761/eureka),这里对其进行覆盖
# 这里自己向自己进行注册
eureka.client.service-url.defaultZone=http://localhost:8083/eureka
# 向eureka服务注册的服务名
spring.application.name=eureka-server
# 失效剔除
# 有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
# 可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不要修改。
# 这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒
eureka.server.eviction-interval-timer-in-ms=60000
这里配置完还会报错,因为是自己注册自己,启动时注册自己,由于自己还没启动完,所以报错
3.服务提供方向Eureka注册服务
- 服务提供方pom添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 服务提供方改造springBoot启动器,增加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
- 修改配置文件application.properties增加
# 向eureka注册服务,如果eureka注册中心存在集群,这里要向每个节点注册
eureka.client.service-url.defaultZone=http://localhost:8083/eureka
# 注意如果有多个相同的服务要注册,这里的名字要一样
spring.application.name=user-service
# 默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着(lease-renewal-interval-in-seconds)。
# 如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除(lease-expiration-duration-in-seconds)。
# 这两个值在生产环境不要修改,默认即可,开发环境下两个参数尽量改小
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
4.服务消费方调用服务
- 服务提供方pom添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 服务提供方改造springBoot启动器,增加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
}
- 修改配置文件application.properties增加
# 调用eureka提供的服务
eureka.client.service-url.defaultZone=http://localhost:8083/eureka
spring.application.name=consumer-service
# 服务消费端会定时拉去Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改:
# 生产环境中,我们不需要修改这个值。
# 但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
eureka.client.registry-fetch-interval-seconds=30
这里服务消费方也要向eureka注册自己
- 服务调用示例
package com.consumer.controller;
import com.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/{username}")
public User hello(@PathVariable String username){
// 根据服务id去eureka查找服务
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 从服务list中取出一个 instance
ServiceInstance serviceInstance = instances.get(0);
// 从服务中取出ip,port
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
// 拼接请求地址
User user = restTemplate.getForObject( "http://"+ host + port + "/service/" + username, User.class);
return user;
}
}
3.高可用的Eureka Server
Eureka Server即服务的注册中心,EurekaServer也可以是一个集群,形成高可用的Eureka中心。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
- 1)搭建Eureka Server集群
同上,只需要再创建一个Eureka Server,application.properties中配置如下
# 这里两个Eureka Server直接port各不相同
server.port=8084
# 这里要两个 Eureka Server 相互注册
eureka.client.service-url.defaultZone=http://localhost:8083/eureka
# 由于是集群,提供相同的服务,因此服务名一样
spring.application.name=eureka-server
- 2)服务提供方的注册配置要改
application.properties
# 向eureka注册服务,这里要向所有的 Eureka Server 注册
eureka.client.service-url.defaultZone=http://localhost:8083/eureka,http://localhost:8084/eureka
spring.application.name=user-service
- 3)服务调用方的注册配置要改
application.properties
# 调用eureka提供的服务,这里要向所有的 Eureka Server 注册
eureka.client.service-url.defaultZone=http://localhost:8083/eureka,http://localhost:8084/eureka
spring.application.name=consumer-service
二.Ribbon(负载均衡)
1.简介
Ribbon 是 Netflix 发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址列表后,Ribbon就可以基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询,随机等。当然,我们也可以为Ribbon实现自定义的负载均衡算法。
2.示例
- 1)在上面示例的基础上,再创建一个服务
- 2)在服务消费方的pom文件中引入Ribbon的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
- 3)服务调用方调用代码修改
HttpClientConfig.java
增加 @LoadBalanced 注解
package com.consumer.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class HttpClientConfig {
@Bean // 向容器中添加 RestTemplate
@LoadBalanced // 添加 @LoadBalanced 注解,实现负载均衡
public RestTemplate restTemplate() {
/**
* 这里可以选择底层实现(有三种):
* HttpClient
* OkHttp
* JDK原生的URLConnection(默认的)
*/
// 创建 JDK原生的URLConnection(默认的)
return new RestTemplate();
}
}
业务层调用时代码
package com.consumer.controller;
import com.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/{username}")
public User hello(@PathVariable String username){
// "http://{serviceId}/service/" + username
String url = "http://USER-SERVICE/service/" + username;
// 拼接请求地址
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
- 4)修改负载均衡算法
默认的负载均衡算法是轮询
可以在服务调用方的application.properties配置文件中配置
# 修改默认的负载均衡算法(轮询)
# 格式:服务ID(这个服务id是服务提供方的id).ribbon.NFLoadBalancerRuleClassName=实现类的全类名
# 具体实现类有哪些,可以找com.netflix.loadbalancer.IRule的实现类
user-service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
三.Hystrix(熔断器)
1.概念
Hystix,即熔断器。Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
- 雪崩问题
- 解决手段
- 1)线程隔离
- 2)服务熔断
正常工作的情况下,客户端请求调用服务API接口:
当有服务出现异常时,直接进行失败回滚,服务降级处理:
当服务繁忙时,如果服务出现异常,不是粗暴的直接报错,而是返回一个友好的提示,虽然拒绝了用户的访问,但是会返回一个结果。
2.代码示例
- 1)服务调用方的pom文件引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 2)服务调用方的启动程序上添加注解@EnableCircuitBreaker
package com.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
// 下面这个三个注解可以由 @SpringCloudApplication 注解代替
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
}
- 3)服务调用方的调用代码修改
package com.consumer.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
/**
* 添加 @HystrixCommand 注解
* 开启线程隔离,服务降级处理,并指定调用失败的方法
* 1.@HystrixCommand 注解,加在方法上,单独给不同的方法指定不同的降级处理函数(fallbackMethod = "helloFallback" 属性指定失败处理函数)
* 要求:原方法和降级处理方法的参数,返回值类型要一致
*
* 2.如果想给类中的所有方法指定同一个降级处理函数:
* 在类上加:@DefaultProperties(defaultFallback = "helloFallback")
* 在方法上加:
* 在要熔断的方法上加:@HystrixCommand
* 注意: helloFallback方法不能写参数了,且各个方法的返回值类型要统一
*
* hystrix判断服务调用方,调用服务,如果超过1s(默认的),没有得到结果,则启动服务降级,执行Fallback方法
* 也可以在 application.properties 配置全局的 服务熔断降级超时时间
* ( 配置示例
* # 配置全局的服务熔断降级超时时间
* hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
* # 指定某个服务熔断降级超时时间(user-service:为服务id)
* hystrix.command.user-service.execution.isolation.thread.timeoutInMilliseconds=2000
* )
* 由于不同业务执行时间各不相同,有的业务执行时间长,因此需要单独调整没个业务的执行超时时间
* 通过 @HystrixCommand -> commandProperties -> @HystrixProperty(name = "", value = "")
* name:execution.isolation.thread.timeoutInMilliseconds, value:时长(毫秒)
*/
@GetMapping("/consumer/{username}")
@HystrixCommand(fallbackMethod = "helloFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
}
)
public String hello(@PathVariable String username){
// "http://{serviceId}/service/" + username
String url = "http://USER-SERVICE/service/" + username;
// 拼接请求地址
String user = restTemplate.getForObject(url, String.class);
return user;
}
/**
* 调用失败后调用的方法
* 要求:参数和返回值类型要和原方法一致
*/
public String helloFallback(String username){
return username + " 不好意思,服务器太拥挤了!";
}
}
- 4)服务调用方的配置文件修改
# 配置全局的服务熔断降级超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
# 指定某个服务熔断降级超时时间(user-service:为服务id),这个配置如果在上面调用方法上配置了,则这里就不用配置
# hystrix.command.user-service.execution.isolation.thread.timeoutInMilliseconds=2000
四.Feign
在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:
String baseUrl = "http://user-service/user/";
User user = this.restTemplate.getForObject(baseUrl + id, User.class)
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?
这就是我们接下来要学的Feign的功能了。
1.简介
Feign(伪装的意思)
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。主要用于服务调用方
2.使用
- 1)在服务调用方导入pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
由于feign内部包含了RestTemplate,因此向ioc容器中注入RestTemplate的配置可以删除,如下:
package com.consumer.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class HttpClientConfig {
@Bean // 向容器中添加 RestTemplate
@LoadBalanced // 添加 @LoadBalanced 注解,实现负载均衡
public RestTemplate restTemplate() {
/**
* 这里可以选择底层实现(有三种):
* HttpClient
* OkHttp
* JDK原生的URLConnection(默认的)
*/
// 创建 JDK原生的URLConnection(默认的)
return new RestTemplate();
}
}
- 2)在服务调用方启动类上添加注解@EnableFeignClients
package com.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
}
- 3)编写接口
package com.consumer.client;
import com.consumer.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 接口反应出以下信息
* 1.请求的方式
* 2.请求的路径
* 3.请求的参数
* 4.返回的结果
*/
@FeignClient("USER-SERVICE") //指定服务名称
public interface CustomerClient {
@GetMapping("/service/{username}")
User hello(@PathVariable String username);
}
- 4)调用方法编写
package com.consumer.controller;
import com.consumer.client.CustomerClient;
import com.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
// 直接注入 CustomerClient 即可
@Autowired
CustomerClient customerClient;
@GetMapping("/consumer/{username}")
public User hello(@PathVariable String username){
// 调用 customerClient 的 hello 方法
User user = customerClient.hello(username);
return user;
}
}
- 5)使用Feign需要编写的配置
由于引入 Feign 之后 ,Feign 内部自带hystrix,Ribbon的依赖,这里 hystrix,Ribbon 不是springcloud的,是netflix公司的,
因此相关配置要重新编写 - application.properties
# 如果500毫秒连接建立不成功,抛出异常
ribbon.ConnectionTimeOut=1000
# 如果建立连接,超过2000毫秒,服务没有返回结果,抛出异常
ribbon.ReadTimeOut=2000
# hystrix默认是关闭的,首先启用
feign.hystrix.enabled=true
# 开启请求和响应的gzip压缩,以减少通信过程中性能的损耗
feign.compression.request.enabled=true
# 设置压缩的数据类型
feign.compression.request.mime-types=text/html,application/xml,application/json
# 设置触发压缩的大小下限
feign.compression.request.min-request-size=2048
feign.compression.response.enabled=true
- 在CustomerClient接口,定义Feign的熔断配置
package com.consumer.client;
import com.consumer.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 接口反应出以下信息
* 1.请求的方式
* 2.请求的路径
* 3.请求的参数
* 4.返回的结果
* value 指定服务id
* fallback 指定服务熔断后的回调函数
* fallback 的值 为 class,这个class要实现 CustomerClient 接口
* 然后在实现类的对应方法写熔断的业务逻辑
*/
@FeignClient(value = "USER-SERVICE", fallback = CustomerClientFallback.class) //指定服务名称
public interface CustomerClient {
@GetMapping("/service/{username}")
User hello(@PathVariable String username);
}
- 在CustomerClientFallback实现类的实现方法中定义Feign的熔断业务逻辑,同时将这个类通过@Component注解注册到spring容器中
五.Zuul网关
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
1.pom文件引入依赖
<!--引入zuul依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--引入eureka-client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.配置启动类
package com.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3.配置application.properties
server.port=8086
# 配置eureka,注册服务
eureka.client.service-url.defaultZone=http://localhost:8083/eureka,http://localhost:8084/eureka
spring.application.name=gateway-service
# 配置路由规则(请求 和 服务id的对应关系)
# 配置写法
# zuul.routes.服务id=/拦截路径/**
# 外部访问规则:假设原来访问的路径是:/consumer/zxb ,现在通过网关访问:/consumer-service/consumer/zxb
# 由于zuul会自动去eureka上拉取所有服务,进行路由配置,默认将 服务id 和 拦截路径 配置成一样,
# 因此下面内容可以不配置,除非想修改zuul的默认配置
# 但是由于zuul默认会将所有服务进行映射,但是实际情况,有些服务不想对外暴露,
# 可以通过下述配置,将部分服务不做映射
zuul.routes.consumer-service=/consumer-service/**
# user-service服务不做默认映射
zuul.ignoredServices=user-service
# 网关访问前缀,springCloud推荐为 /api
zuul.prefix=/api
4.zuul过滤器介绍
zuul作为网关的其中一个重要功能,就是实现请求的鉴权,而这个动作我们往往是通过zuul提供的过滤器来实现的
- 1)ZuulFilter介绍
ZuulFilter是过滤器的顶级父类,在这里我们看一下其中定义的4个重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();// 过滤器类型
abstract public int filterOrder();// 过滤器优先级
boolean shouldFilter();// 来自IZuulFilter,要不要过滤
Object run() throws ZuulException;// IZuulFilter,过滤的逻辑
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
- 2)过滤器执行生命周期
- 正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
4.zuul的高可用
zuul注册集群,用户直接访问nginx,由nginx转发至zuul集群
5.zuul实现Cors跨域
大家都知道spring boot 可以通过@CrossOrigin实现跨域。但是在spring cloud 里,如果要粒度那么细的去控制跨域,这个就太繁琐了,所以一般来说,会在路由zuul里实现。
实现代码
在zuul服务下添加一个corsFilter实现跨域,实现起来方便。代码如下
@Configuration
public class GateWayCorsConfig {
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
//这个请求头在https中会出现,但是有点问题,下面我会说
//config.addExposedHeader("X-forwared-port, X-forwarded-host");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}