目录
系统架构
演变
- 单体应用架构
- 垂直应用架构
- 分布式架构
4. SOA架构
5. 微服务架构
为了解决SOA架构抽取服务粒度较大的问题,尽可能的拆分服务层;为了解决耦合度较高的问题,客户端和服务层之间使用轻量级协议。
SOA与微服务的关系:
核心概念
流行的调用技术:RPC,http
RPC(Remote Procedure Call)一种进程间通信方式,允许像调用本地服务一样调用远程服务。RPC框架的主要目标是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式(TCP或者UDP)、序列化方式(XML/JSON/二进制)和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。
RESTful和RPC的比较:
CAP
SpringCloud介绍
SpringCloud是一系列框架的有序结合,它利用SpirngBoot的开发便利性巧妙简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用SpringBoot的开发风格做到一键启动和部署。它没有重复造轮子,只是将目前各家公司开发的比较成熟、经得起考验的服务框架组合起来,通过SpringBoot风格进行再封装。
模拟微服务环境
- 创建父工程springcloud-01,在pom文件中引入需要的包
<!--springboot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<!--springcloud版本控制-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
- 创建子工程product_service,在pom文件中引入需要的包
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
</dependencies>
- 创建实体类Product
@Data
@Entity
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
}
- 创建dao接口,实现JpaRepository和JpaSpecificationExecutor
public interface ProductDao extends JpaRepository<Product,Long>, JpaSpecificationExecutor<Product> {
}
- 创建service接口及实现类
public interface ProductService {
//根据id查询
Product queryById(Long id);
}
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductDao productDao;
@Override
public Product queryById(Long id) {
return productDao.findById(id).get();
}
}
- 创建启动类ProductApplication
@SpringBootApplication
@EntityScan("com.jnf.entity")
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class,args);
}
}
- 配置文件application.yml
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 12345
url: jdbc:mysql://localhost:3306/test?useUnicode=true&charactorEncoding=utf8&serverTimezone=Asia/Shanghai
jpa:
database: mysql
show-sql: true
open-in-view: true
- ProductController
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
//根据id查询商品
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Product queryById(@PathVariable Long id){
return productService.queryById(id);
}
//增加商品
@RequestMapping
public String insertProduct(@RequestBody Product product){
productService.insertProduct(product);
return "增加商品成功!";
}
}
此时启动项目,可以通过url访问到指定id的商品,路径如:http://localhost:9001/product/1。这个url可以通过浏览器直接访问,也可以通过java代码访问,从而在其他项目中调用这个服务。可以使用RestTemplate实现这一点。
- 在父工程下创建新的子工程order_product,仍然使用刚刚的pom文件、实体类、yml配置(只是端口改为9002)
- 将RestTemplate注册为bean
@SpringBootApplication
@EntityScan("com.jnf.entity")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 在controller中调用其方法(get、set、put)
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
public Product queryById(@PathVariable Long id){
Product product=null;
product=restTemplate.getForObject("http://localhost:9001/product/"+id,Product.class);
return product;
}
}
由此,就实现了服务之间的调用。
模拟的微服务体现出几个问题:
- 用户调用order服务,需要随端口的改变而改变请求地址,两者之间的耦合性较高
- 实际中为了实现高可用,order服务会有很多个,就需要实现每个服务的负载均衡
- 服务很多时配置文件也很多,修改起来不易,需要统一管理配置文件
- 日志保存在order、product等不同的服务中,实际请求时是链路式的,需要保存链路的日志
注册中心Eureka
常见的注册中心:Zookeeper、Eureka、Consul、Nacos
Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。
它的基本架构由三个角色组成:
- Eureka Server:提供服务注册和发现
- Service Provider:服务提供方;将自身服务注册到Eureka,从而使服务消费方能够找到
- Service Consumer:服务消费方;从Eureka获取注册服务列表,从而能够消费服务
配置eureka注册中心
- pom文件引入spring-cloud-starter-netflix-eureka-server(本来父工程配置了版本这里不需要再写)
<dependencies>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<!--eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
- yml配置文件中配置端口和eureka
server:
port: 9000
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 是否把自己注册到注册中心
fetch-registry: false # 是否从注册中心获取信息
service-url: # 暴露给eureka client的请求地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
- 启动类,注意加上注解
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
启动项目后,访问http://localhost:9000/
,可以进入eureka的后台页面。
将服务注册到eureka
现在将product_service注册得到eureka
- 仍然是在pom文件先引入依赖
- 修改yml配置文件,添加eureka配置内容
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka
instance:
prefer-ip-address: true #使用ip地址注册
- 给启动类添加@EnableEurekaClient注解,或者@EnableDiscoveryClient(是springcloud自己开发的)注解。(新版springcloud中,不需要添加注解,会自动注册到注册中心)
启动注册中心和被注册的服务后,访问http://localhost:9000/,即可看到被注册的服务:
这个服务名是在yml中设置,不设置就是unknown:
spring:
application:
name: product-service
服务消费者获取服务
服务消费者可以在注册中心获取服务列表,eureka中的元数据包括:服务的主机名、ip等信息,可以通过eurekaserver进行获取,用于服务之间的调用。
对于order_service:
- pom文件引入依赖
- yml文件中配置eureka
- 使用DiscoveryClient可以获得eureka的元数据
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/{id}")
public Product queryById(@PathVariable Long id){
List<ServiceInstance> instances=discoveryClient.getInstances("product-service");
ServiceInstance instance=instances.get(0);
Product product=restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/product/"+id,Product.class);
return product;
}
}
高可用
如果一个eureka server宕机,为了保证服务仍然可用,需要准备多个eureka server,并且他们之间互相注册,并且信息同步。
- 1号server端口9000,先启动。(注意这里就要将eureka本身也注册到注册中心,默认为true)
server:
port: 9000
eureka:
instance:
hostname: localhost
client:
#register-with-eureka: false # 是否把自己注册到注册中心
#fetch-registry: false # 是否从注册中心获取信息
service-url: # 暴露给eureka client的请求地址
defaultZone: http://${eureka.instance.hostname}:8000/eureka
- server端口改为8000,右键copy一份,即为2号server
server:
port: 8000
eureka:
instance:
hostname: localhost
client:
#register-with-eureka: false # 是否把自己注册到注册中心
#fetch-registry: false # 是否从注册中心获取信息
service-url: # 暴露给eureka client的请求地址
defaultZone: http://${eureka.instance.hostname}:9000/eureka
- 从eureka的控制台可以发现,两个微服务此时已经也注册到8000端口的eureka上了,这是因为eureka一旦互相注册,就会保持信息的同步,但为了防止微服务注册的eureka宕机而无法使用,还需要在yml文件中注册另一eureka server
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/
instance:
prefer-ip-address: true
细节问题
- 在服务提供者处,可以通过eureka.instance.instance-id配置注册中心的控制台显示服务id
eureka:
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port} # 向注册中心注册服务id
- 在服务提供者处,设置监测心跳间隔,和续约到期时间
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 续约到期的时间
lease-renewal-interval-in-seconds: 2 # 发送心跳懂得间隔
- eureka的自我保护机制,是防止服务由于网络等其他影响而暂时未响应,却被删除的情况。在开发过程中可以关闭,使得调试时查看服务更直观。在注册中心的yml进行关闭:
eureka:
server:
enable-self-preservation: false # 关闭自我保护机制
eviction-interval-timer-in-ms: 4000 # 每4s删除不在运行的服务
但是,在服务上线阶段不建议修改心跳及自我保护机制,仅用于调试。
Ribbon
Ribbon的两个作用:
- 服务调用:拉取服务列表组成的(服务名-请求路径)映射关系,借助RestTemplate最终进行调用
- 负载均衡:当有多个服务提供者时,Ribbon可以根据负载均衡算法自动地选择需要调用的服务地址。
服务调用
- 给RestTemplate加上注解@LoadBalanced
@SpringBootApplication
//@EntityScan("com.jnf.entity")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 使用服务名替换地址
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
public Product queryById(@PathVariable Long id){
//服务名称替换部分路径
Product product=restTemplate.getForObject("http://product-service/product/"+id,Product.class);
return product;
}
}
负载均衡
Ribbon是一个典型的客户端负载均衡器,Ribbon会获取服务的所有地址,根据内部的负载均衡算法,获取本次请求的有效地址。
策略选择:
- 如果每个机器配置一样,则建议不修改策略(默认轮询RoundRobinRule)
- 如果部分机器配置强,则可以改为权重策略(WeightedResponseTimeRule)
- 在消费服务的yml中进行配置:
# 服务名 ribbon NFLoadBalancerRuleClassName 策略
product-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机
- 给RestTemplate加上注解@LoadBalanced
@SpringBootApplication
//@EntityScan("com.jnf.entity")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 修改一下product-service的controller,便于查看访问的端口:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@Value("${server.port}")
private String port;
//根据id查询商品
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Product queryById(@PathVariable Long id){
Product product=productService.queryById(id);
product.setName("访问的接口是:"+port);
return product;
}
}
- 启动两个product-service服务,端口为9001/9011。访问
http://localhost:9002/order/1
,可以发现访问的端口是随机的:
重试机制
在连接服务A失败时,ribbon可以尝试连接其副本,或者尝试再次重连服务A。
现使用两个product-service,端口为9001、9011。使用order-service去轮询调用服务,在其中一个服务失效(关闭)后,调用另一个。
在order-service中,进行:
- pom文件引入
<!--ribbon重试机制-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
- yml配置重试机制
product-service:
ribbon:
#NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ConnectTimeout: 250 # Ribbon的连接超时时间(该时间内没有连接上则选择其他服务)
ReadTimeout: 1000 # Ribbon的数据读取超时时间(连接上了以后读取数据,如果没有读取到进行重试)
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数(连接失败后重新选择服务的次数)
MaxAutoRetries: 1 # 对当前实例的重试次数(连接失败后不切换服务而是继续重试当前服务)
Consul
上面学习的是Eureka1.0的版本,Eureka2.0已经开发出来,但是目前是闭源的状态,Eureka的替换方案有这几种:Zookeeper、Consul、Nacos。
Consul的三个主要应用场景:服务发现、服务隔离、服务配置。
Consul和Eureka的区别:
- 一致性和可用性区别
- Consul有强一致性,可能出现服务短暂不可用现象
- Eureka为了保证高可用,可能出现数据短暂不一致的情况
- 开发语言
- Consul使用go语言开发,安装启动即可
- Eureka使用java语言开发,是一个servlet程序
安装
从官网下载consul
进入consul的安装目录,命令行输入
# 以开发者模式快速启动
consul agent -dev -client=0.0.0.0
在浏览器输入http://ip:8500/
进入到管理后台界面:
服务提供者注册
现在回到之前有两个服务:product-service、order-service的状态,order-service需要调用product-service服务。
先将product-service注册到consul:
- 服务提供者pom文件引入依赖
<dependencies>
<!--springcloud提供的基于consul的服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--actuator的健康检查-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
- yml文件配置consul
## consul
cloud:
consul:
host: 127.0.0.1 #主机地址
port: 8500 # ip地址
discovery:
register: true # 是否需要注册
instance-id: ${spring.application.name} # 注册的实例ID(唯一标志)
service-name: ${spring.application.name} # 服务的名称
port: ${server.port} # 服务的请求端口
prefer-ip-address: true # 指定开启ip地址注册
ip-address: ${spring.cloud.client.ip-address} # 当前服务的请求ip
- 访问consul后台可以看到注册的服务
拉取注册的服务
- 按同样方式对order-service进行配置。
- 给RestTemplate加上@LoadBalanced注解
- 由于springcloud已经对consul进行了集成,可以使用服务名替换访问服务url的ip地址和接口部分:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
public Product queryById(@PathVariable Long id){
Product product=restTemplate.getForObject("http://product-service/product/"+id,Product.class);
return product;
}
}
consul集群
刚刚我们在命令行中使用的是开发者模式的consul,开发中consul一般以另外两种方式运行,将dev替换成这两种:
- client:是consul代理,和consul server进行交互。一个微服务对应一个client,微服务和client部署到一台机器上。
- server:真正干活的consul服务,一般有3-5个。
Gossip:流言协议
client和server所有节点都参与到Gossip协议中进行赋值。类似感冒,按照随机传播的形式直至所有节点都接收到。
Raft协议:
用于server集群间的数据交互,保持server集群的数据一致。其中节点有三种状态:
- leader:是server集群中唯一可以处理端请求的(接受和发送给外界)
- follower:被动接收数据
- 候选人:可以被选为leader
consul集群的搭建
consul存在的问题
节点注销
consul在节点停止服务后不会注销节点,需要手动注销
节点注销:
- 注销任意节点和服务:
/catalog/deregister
- 注销当前节点的服务:
/agent/service/deregister/:service_id
健康检查
在集群环境下,健康检查是由服务注册到的agent来处理的,如果agent挂掉了,那么此节点的健康检查就处于无人管理的状态。
Feign
- Feign可帮助我们更加便捷,优雅地调用HTTP API
- SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解,并整合了Ribbon和Eureka
- 导入依赖
<!--Feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 配置调用接口
//服务提供者的名称
@FeignClient("product-service")
public interface ProductFeignClient {
//配置需要调用的微服务接口
@RequestMapping(value = "/product/{id}",method = RequestMethod.GET)
Product queryById(@PathVariable Long id);
}
- 在启动类上使用@EnableFeignClients注解激活
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
- 通过自定义接口调用微服务
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private ProductFeignClient productFeignClient;
@RequestMapping("/{id}")
public Product queryById(@PathVariable Long id){
//不再自己用url拼接,直接调用接口方法
Product product=productFeignClient.queryById(id);
return product;
}
}
负载均衡
由于feign集成了fibbon,自带负载均衡功能,启动两个product-service,同样能观察到访问时采用的是轮询算法。如果想进行修改,在服务调用者的yml文件里修改即可。
数据压缩
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 压缩的数据类型
min-request-size: 2048 # 触发压缩的大小下限
response:
enabled: true # 开启响应压缩
日志
# 四种级别:
# NONE:不输出
# BASIC:适用于生产环境追踪
# HEADERS:在basic基础上记录请求和响应头
# FULL:记录所有
feign:
client:
config:
product-service: # 需要调用的服务名
loggerLevel: FULL # 输出所有日志
logging:
level:
com.jnf.feign.ProductFeignClient: debug # 输出日志的类
可以在控制台看到日志输出:
Hystrix
问题分析
高并发问题
当多个线程访问下单服务时,也会影响查询订单服务的访问,其原因:
雪崩效应
在一次成功的服务中可能依赖多个服务才能完成,一旦请求的服务链中出现响应变慢或是资源耗尽的问题,整个请求都无法完成,更多请求的积压,会造成严重后果。
雪崩的根本原因来源于服务之间的强依赖,有几种提前预防的方式:
-
服务隔离:将系统按照一定原则划分为多个模块,各个模块之间相互独立,当有故障发生时,能将问题隔离在某个模块内部,而不影响整体的系统服务。
-
熔断降级:当下游服务因访问压力而变慢或失败,上游服务为了保证系统整体的可用性,可以暂时切断对下游服务的调用,牺牲局部而保全整体。此时客户端可以自己准备一个本地的fallback回调,展示友好的错误信息。
-
服务限流:是服务降级的一种,限制系统的输入输出,一旦达到需要限制的阈值,就需要采用措施限制流量,如推迟解决、拒绝解决、部分拒绝解决等。
Hystrix用于隔离访问远程系统、服务或第三方库,防止级联失败,从而提升系统的可用性与容错性。
对RestTemplate的支持
- 引入hystrix依赖
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 在启动类中使用@EnableCircuitBreaker激活
@SpringBootApplication
@EnableCircuitBreaker //激活熔断降级
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 配置熔断触发方法
- 在需要收到保护的接口上使用@HystrixCommand(“方法名”)
@RestController
@RequestMapping("/order-rest")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
//配置降级方法
@HystrixCommand(fallbackMethod = "orderFallBack")
public Product queryById(@PathVariable Long id){
return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
}
/*
降级方法:
和需要受到保护的方法返回值一致,方法参数一致
*/
public Product orderFallBack(Long id){
Product product=new Product();
product.setName("触发降级方法");
return product;
}
}
测试:在ProductService的controller中,返回Product前设置Thread.sleep(2000),配置熔断机制触发为1000ms,则可以在页面上观察到:
这样自定义配置触发时间:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000 # 设置hystrix的超时时间为1000ms
还可以创建统一的降级方法,再给类添上@DefaultProperties注解,此时@HystrixCommand注解上不需要再写方法名:
@RestController
@RequestMapping("/order-rest")
@DefaultProperties(defaultFallback = "defaultFallBack")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
@HystrixCommand
public Product queryById(@PathVariable Long id){
return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
}
/*
统一的降级方法:
方法参数为空
*/
public Product defaultFallBack(){
Product product=new Product();
product.setName("触发统一的降级方法");
return product;
}
}
...
注意使用这种方法时,所有接口的返回值应保持一致才能使用。
对feign的支持
- 引入依赖(feign中已经集成了hystrix)
- 在yml中配置开启hystrix
feign:
hystrix:
enabled: true
- 自定义feignClient接口的实现类,这个实现类就是熔断触发的降级逻辑
@Component
public class ProductFeignClientFallBack implements ProductFeignClient{
/*
熔断降级方法
*/
public Product queryById(Long id) {
Product product=new Product();
product.setName("feign调用触发熔断降级");
return product;
}
}
- feignClient接口添加降级方法的支持
//name:服务提供者的名称,fallback:实现类(熔断方法)
@FeignClient(name = "product-service",fallback = ProductFeignClientFallBack.class)
public interface ProductFeignClient {
@RequestMapping(value = "/product/{id}",method = RequestMethod.GET)
Product queryById(@PathVariable Long id);
}
Actuator获取监控信息
- pom引入依赖
<!--Acuator监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>
- 不管是rest还是feign形式,都要在启动类添加允许熔断的注解
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker //激活hystrix
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
- 暴露所有actuator监控的端点
management:
endpoints:
web:
exposure:
include: '*'
- 浏览器访问
http://localhost:监控所在项目端口/actuator/hystrix.stream
可观察到页面的监控信息,但是并不直观,通过dashboard能够更方便地查看:
1.在启动类添加允许dashboard注解
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker //激活hystrix
@EnableHystrixDashboard //激活web监控平台
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
2.yml里添加所有节点允许
hystrix:
dashboard:
proxy-stream-allow-list: '*'
2.浏览器访问http://localhost:监控所在项目端口/hystrix
把刚刚流式数据页面的路径输入,并且访问项目,可以看到监控的情况:
断路器聚合监控Turbine:
hystrix断路器的工作状态
熔断器有三个状态:CLOSED、OPEN、HALF_OPEN。熔断器默认关闭状态,当触发熔断后状态变更为OPEN,在等待到指定时间,Hystrex会请求监测服务是否开启,这期间熔断器会变为HALF_OPEN状态(尝试释放一个请求到远程微服务发起调用),熔断探测服务可用则继续变更为CLOSED关闭熔断器。
在yml中可以设置熔断相关条件:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000 # 设置hystrix的超时时间
circuitBreaker:
requestVolumeThreshold: 5 # 触发熔断的最小请求次数,默认20次/10秒
sleepWindowInMilliseconds: 10000 #熔断多少秒后去尝试请求
errorThresholdPercentage: 50 # 触发熔断的失败请求占比,默认50%
hystrix的隔离策略
- 线程池隔离策略:使用一个线程池来存储当前的请求,这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量。
- 信号量隔离策略:使用一个原子计数器来记录当前有多少个线程在运行,请求到来时判断计数器数值,若超过最大线程个数则丢弃该类型的新请求,这种方法无法应对突发流量。
在yml中可以配置隔离策略:
hystrix.command.default.execution.isolation.strategy:配置隔离策略
- ExecutionIsolationStrategy.SEMAPHORE
- ExecutionIsolationStrategy.THREAD
hystrix.command.default.execution.isolation.maxConcurrentRequests:最大信号量上限
Alibaba Sentinel
Hystrix的替换方案。
和hystrix的区别:
- 下载sentinel-dashboard.jar包
- 在jar包所在目录打开命令行,输入以下命令启动sentinel:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.6.3.jar
- 浏览器访问
http://localhost:8080
进入sentinel控制台,默认用户名和密码都是sentinel
- 将服务交给sentinel管理
父工程引入alibaba实现的SpringCloud:
<dependencyManagement>
<dependencies>
<!--alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子工程引入sentinel:
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 创建熔断方法
- 使用@SentinelResource注解需要熔断降级的方法
@RestController
@RequestMapping("/order-rest")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{id}")
//给需要熔断保护的方法添加 @SentinelResource注解
@SentinelResource(blockHandler = "orderBlockHandler",fallback = "orderFallback")
public Product queryById(@PathVariable Long id){
return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
}
/*
两个方法:熔断的降级方法、抛出异常的降级方法
*/
public Product orderBlockHandler(Long id){
Product product=new Product();
product.setName("触发了熔断的降级方法");
return product;
}
public Product orderFallback(Long id){
Product product=new Product();
product.setName("触发了抛出异常的降级方法");
return product;
}
}
- 可以在控制台直接进行熔断降级配置的添加;也可以在yml中进行本地配置:
spring:
application:
name: order-service-rest
cloud:
sentinel:
transport:
dashboard: localhost:8080
datasource:
ds1:
file:
file: classpath:flowrule.json
data-type: json
rule-type: flow
[
{
"resource": "orderFindById",
"controlBehavior": 0,
"count": 1,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]
对RestTemplate的支持
- 导入依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 给RestTemplate类添加@SentinelRestTemplate注解,注解中标注出熔断降级方法名和所在的类
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
@Bean
@LoadBalanced
@SentinelRestTemplate(fallbackClass = ExceptionUtils.class,fallback = "handleFallback",blockHandlerClass = ExceptionUtils.class,blockHandler = "handleBlock")
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 新增熔断降级的两个方法:限流和异常(注意此处的HttpRequest 是springframework的,不要导错包)
public class ExceptionUtils {
/*
静态方法:
返回值:SentinelClientHttpResponse
参数:request,byte[],ClientHttpRequestExecution,BlockException
*/
public static SentinelClientHttpResponse handleBlock(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution, BlockException e){
Product product=new Product();
product.setName("限流熔断降级方法");
return new SentinelClientHttpResponse(JSON.toJSONString(product));
}
public static SentinelClientHttpResponse handleFallback(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution, BlockException e){
Product product=new Product();
product.setName("异常熔断降级方法");
return new SentinelClientHttpResponse(JSON.toJSONString(product));
}
}
对feign的支持
- 导入依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 在yml中添加sentinel的支持
feign:
client:
config:
product-service: # 需要调用的服务名
loggerLevel: FULL # 输出所有日志
sentinel:
enabled: true #激活sentinel的支持
- 添加feign接口的实现类,和熔断降级方法
@Component
public class ProductFeignClientCallback implements ProductFeignClient {
@Override
public Product queryById(Long id) {
Product product=new Product();
product.setName("feign调用触发熔断降级方法");
return product;
}
}
- 在接口注解处激活fallback
@FeignClient(name = "product-service",fallback = ProductFeignClientCallback.class)
public interface ProductFeignClient {
@RequestMapping(value = "/product/{id}",method = RequestMethod.GET)
Product queryById(@PathVariable Long id);
}
微服务网关
如果客户端直接与多个微服务通讯,可能会有的问题:
- 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
- 在某些场景下存在跨域请求的问题
- 加大身份认证的难度,每个微服务需要独立认证
因此,引入微服务网关作为客户端与服务器的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可。这样:
- 易于监控
- 易于认证
- 减少了客户端与各个微服务之间的交互次数
常见的API网关实现方式:Kong、Zuul、SpringCloud Gateway
Zuul
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的数量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 身份认证和安全:识别每一个资源的验证要求
- 创建网关工程,导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
- 给启动类添加@EnableZuulServer注解
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class,args);
}
}
路由配置
基础路由配置:
在yml中:
server:
port: 8080
spring:
application:
name: zuul-server
zuul: # 路由配置
routes:
product-service: #路由id,随便写
path: /product-service/** # 映射路径
url: http://127.0.0.1:9001 # 映射路径对应的实际url
此时,要访问原来的http://localhost:9001/product/1
,现在的路径是:http://localhost:8080/product-service/product/1
像这样,每个服务都要手动配置映射到的路径,在服务多时较难管理,因此一般使用eureka,从注册中心获得接口和ip,只需要使用服务名就能获取路径:
- 添加eureka依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 在启动类上添加注解
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class,args);
}
}
- 修改yml配置
server:
port: 8080
spring:
application:
name: zuul-server
zuul: # 路由配置
routes:
product-service: #路由id,随便写
path: /product-service/** # 映射路径
#url: http://localhost:9001 # 映射路径对应的实际url
serviceId: product-service # 使用服务名代替
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
instance:
prefer-ip-address: true #使用ip地址注册
同样访问路径http://localhost:8080/product-service/product/1
简化配置
简化1:路由id与微服务名相同
zuul: # 路由配置
routes:
product-service: /product-service/** # 微服务名(同时也是路由id):映射路径
简化2:微服务名的默认映射路径是/微服务名/**
此时yml中不用进行任何zuul路径的配置。
ZuulFilter
作用:对请求的处理过程进行干预,实现请求校验、服务聚合等功能。
分类:
- “pre” filter:转发到微服务之前。身份验证
- “routing” filter:路由请求时执行。负载均衡
- “post” filter:转发到微服务之后。为响应添加header,收集日志
- “error” filter:发生错误执行
SpringCloud Gateway
zuul网关存在的问题:
- zuul1.x版本本质是一个同步Servlet,采用多线程阻塞模型进行请求转发,每来一个请求,Servlet容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台调用比较费时,这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其他事情。servlet容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。
- 不支持任何长连接,如websocket
- zuul2.x还没有被springcloud整合
Spring Cloud Gateway是spring官方基于Spring 5.0、Spring Boot 2.0、Project Reactor等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的API路由管理形式。它是基于Nttey的响应式开发模式。性能大约是zuul的1.6倍。
路由配置
- 创建工程导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 创建启动类,无需除@SpringBootApplication的额外注释
- 在yml中进行配置
server:
port: 8080
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: product-service # 路由id
uri: http://localhost:9001 # 路由到微服务的uri
predicates:
- Path=/product/** # 断言(条件判断,这种形式的路径会被路由,并且这一部分会被加到uri之后)
注意:springcloud gateway的内部通过netty+webflux实现,webflux和springmvc存在冲突,因此不能给父工程配置spring-boot-starter-web依赖。
断言规则
predicates:
- Before: xxx # 在某时间节点之前进行匹配
- After: xxx # 在某时间节点之前进行匹配
- Between: xxx,xxx # 在两时间节点之间进行匹配
---
predicates:
- Cookie=chocolate,ch.p #满足cookie中含有指定名称和正则表达式
- Header=X-Request-Id,\d+ # Header的名称和正则匹配
- Host=**.somehost.org # host主机地址
- Method=GET # 匹配http方法
- Path=/product/** # 路径参数
- Query=baz #请求参数
- RemoteAddr=192.168.1.1/24 # ip地址从192.168.1.1~192.168.1.254,其中24为子网掩码位数即255.255.255.0
动态路由
1.引入eureka依赖
2.yml文件配置eureka信息
3.使用lb://服务名
替换原来的url
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: product-service
uri: lb://product-service # lb://微服务名称
predicates:
- Path=/product/**
服务名自动转发
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true # 开启根据服务名称自动转发
lower-case-service-id: true # 微服务名称以小写形式呈现
此时,要访问http://localhost:9001/product/1
,路径为http://localhost:8080/product-service/product/1
,同理访问http://localhost:9002/order/1
路径为http://localhost:8080/order-service/order/1
过滤器
Spring Cloud Gateway的Filter的生命周期只有两个:“pre”和“post”
- pre:在请求路由之前调用。
- post:在路由到微服务以后执行。
从作用范围分为两种:
- GatewayFilter:应用到单个路由或者一个分组的路由上
- GlobalFilter:应用到所有的路由上。可用于权限的统一校验。
自定义全局过滤器
创建一个类实现GlobalFilter、Ordered接口:
/*
自定义全局过滤器,需要实现两个接口:
globalfilter、ordered
*/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
/*
执行过滤器中的业务逻辑
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行了自定义的全局过滤器");
return chain.filter(exchange);//继续向下
}
/*
指定过滤器的执行顺序
返回值越小,执行优先级越高
*/
public int getOrder() {
return 0;
}
}
统一鉴权
在全局过滤器中,进行token认证:
@Component
public class LoginFilter implements GlobalFilter, Ordered {
/*
执行过滤器中的业务逻辑
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行了自定义的全局过滤器");
//获取请求参数access-token,如果不存在,认证失败
String token=exchange.getRequest().getQueryParams().getFirst("access-token");
if(token==null){
System.out.println("没有登录");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();//请求结束
}
return chain.filter(exchange);//继续向下
}
/*
指定过滤器的执行顺序
返回值越小,执行优先级越高
*/
public int getOrder() {
return 0;
}
}
此时,只有当请求带有access-token参数时,才能正常访问,如http://localhost:8080/order-service/order/1?access-token=1
网关限流
限流算法
- 计数器算法:规定单位时间最大请求数量,每有一个请求计数器+1,大于该数量的请求过来时,直接拒绝。缺点:流量的限制不平滑。
- 漏桶算法:在网关内部维护一个内存,外部的请求都会先进入内存中,然后稳定向微服务发送请求。这种方法可以平滑网络上的突发流量,实现流量整形,提供稳定的流量。
需要关注的两个变量:漏桶的大小(支持流量突增时的容量)和漏洞的大小(输出的频率)。
漏桶算法主要保护微服务。
- 令牌桶算法:是对漏桶算法的改进,能在限制调用的平均速率的同时还允许一定程度的突发调用。令牌桶里存放一定数量的令牌,每秒增加一些令牌,请求到来时,如果令牌桶里有令牌,就获取令牌并请求微服务,如果没有则等待或拒绝。这种方式考虑到微服务的承受能力,避免浪费,主要保护了网关。springcloud官方就提供了基于令牌桶的限流支持。
基于filter的限流
- 引入redis依赖和监控依赖
<!--redis依赖-->
<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-actuator</artifactId>
</dependency>
- 修改网关的yml配置
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: product-service # 路由id
uri: lb://product-service
predicates:
- Path=/product/**
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@pathKeyResolver}' # 使用spell从容器中获取对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶的上限
- 新建KeyResolverConfiguration类,创建KeyResolver解析器
@Configuration
public class KeyResolverConfiguration {
/*
编写限流规则
1.基于请求路径
2.基于ip
3.基于请求参数
*/
@Bean
public KeyResolver pathKeyResolver(){
return new KeyResolver() {
/*
ServerWebExchange:上下文参数
*/
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());//基于请求路径
}
};
}
}
在该例中,超过2次/秒的频率访问这一请求路径将被限流:
除了路径限流,还有参数限流:
@Configuration
public class KeyResolverConfiguration {
@Bean
public KeyResolver paramKeyResolver(){
//此时的访问路径:http://localhost:8080/product/1?userId=1
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("userId"))
);
}
}
基于请求ip的限流:
@Configuration
public class KeyResolverConfiguration {
@Bean
public KeyResolver ipKeyResolver(){
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"))
);
}
}
基于sentinel的限流
网关的高可用
ngnix结合网关集群构建高可用网关:
- 启动两个gateway_server项目,端口分别为8080、8081
- 下载ngnix,解压
- 修改config文件夹下的nginx.conf:
- 双击nginx.exe启动
- 访问
http://localhost/product
,同样可以看到查询结果(相当于负载均衡访问http://localhost:8080/product、http://localhost:8081/product)
链路追踪
分布式链路追踪,就是将一次分布式请求还原成调用链路,进行日志记录、性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等。
目前流行的链路追踪系统:Twitter的Zipkin,阿里的鹰眼,美团的Mtrace,大众点评的cat等,大部分都是基于google发表的Dapper。Dapper阐述了分布式系统,特别是微服务架构中链路追踪的概念、数据表示、埋点、传递、收集、存储与展示等技术细节。
Sleuth
Spring Cloud Sleuth主要功能就是在分布式系统中提供追踪解决方案,并且兼容了支持zipkin。
重要概念:
- trace:整个调用链路。一个链路中有一个traceID
- span:每个最小的工作单元,多个span组成一个trace。span里包含调用者和被调用者的id
- 在链路涉及到的服务中都引入sleuth的依赖
<!--sleuth链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
- 在需要打印日志的服务处配置yml
logging:
level:
root: info
org.springframework.web.servlet.DispatcherServlet: DEBUG
org.springframework.cloud.sleuth: DEBUG
进行服务的访问后,即可看到日志输出。
Zipkin
通过查看日志文件并不是一个很好的方法,日志太多时并不直观清晰。zipkin能够收集服务的定时数据,完成数据的存储、查找和展现。
Zipkin主要由四个核心组件构成:
- Collector:收集器组件,主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的span格式。
- Storage:存储组件,对处理器收集到的跟踪信息,默认会将这些信息存储在内存中。
- RESTful API:API组件,提供外部访问接口。
- Web UI:UI组件,基于API组件实现的上层应用。通过UI组件用户可以方便而又直观地查询和分析跟踪信息。
部署
- 从官网下载zipkin的jar包:zipkin-server-2.12.9-exec.jar
- 命令行输入
java -jar zipkin-server-2.12.9-exec.jar
启动zipkin - 访问
http://localhost:9411
可以进入后台 - 给所有要用到的服务添加依赖
<!--zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
- 给所有要用到的服务配置yml文件
zipkin:
base-url: http://127.0.0.1:9411/
sender:
type: web # 数据的传输方式,以http的形式向server端发送数据
sleuth:
sampler:
probability: 1 # 采样比,默认0.1,即只收集十分之一的数据
- 访问服务后,进入zipkin的后台,点击查找可以看到链路情况
链路数据的持久化
zipkin server重启后,链路的数据将丢失,为了解决这个问题需要将数据存储到内存或数据库中。 - 使用zipkin提供的数据库表:官网提供的数据库脚本
在zipkin启动时可以挂载一些请求参数指定存储的位置和存储类型
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql
--MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306
--MYSQL_USER=root --MYSQL_PASS=12345 --MYSQL_DB=zipkin(database名称)
SpringCloudStream
消息中间件主要用于解决应用解耦、异步消息、流量削锋等问题,实现高性能、高可用、可伸缩和最终一致性架构。不同的中间件其实现方式、内部结构是不一样的。如常见的RabbitMQ和Kafka,由于其架构上的不同,如果使用了两个消息队列中的一种,后面想用另一种消息队列进行迁移,就需要推倒重做。这是因为它跟我们的系统耦合了,springcloud stream提供了一种解耦的方式。
通过更换绑定器即可使用不同的消息中间件:
rabbitmq
- 在github下载otp_win64,配置环境变量
- 下载rabbitmq,配置环境变量
- 访问
http://localhost:15672
可以进入RabbitMQ的页面,用户名和密码都默认为guest
消息生产者入门案例
- 创建子工程stream_producer和stream_consumer
- stream_producer、stream_consumer均引入所需依赖
<!--SpringCloudStream-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
- stream_producer的yml文件配置
server:
port: 7001
spring:
application:
name: stream-producer
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
output:
destination: aim-default #指定消息发送目的地,即发送到一个叫aim-default的交换机上
binders: #配置绑定器
defaultRabbit:
type: rabbit
- 发送消息需要定义一个通道接口,通过接口中内置的messagechannel进行。而springcloudstream中内置有这个通道接口Source(org.springframework.cloud.stream.messaging.Source),不需要自己再创建。
- 在stream_producer工程里创建ProducerApplication.class,使用@EnableBiding注解,并标明使用的通道接口。
@SpringBootApplication
@EnableBinding(Source.class)
public class ProducerApplication implements CommandLineRunner {
@Autowired
private MessageChannel output;
@Override
public void run(String... args) throws Exception {
//发送消息
output.send(MessageBuilder.withPayload("hello msg").build());
}
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class);
}
}
- 启动stream_producer工程,在RabbitMQ的Exchages一栏可以看见我们自定义的交换机:
消息消费者入门案例
- stream_consumer的yml文件中进行配置
server:
port: 7002
spring:
application:
name: rabbitmq-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #从指定的消息通道获取消息
destination: aim-default
binders:
defaultRabbit:
type: rabbit
- 创建启动类。与消息生产者类似,springcloudstream提供了消费者获取消息的通道:Sink,使用它可以监听yml中指定通道的消息。
@SpringBootApplication
@EnableBinding(Sink.class)
public class ConsumerApplication {
//监听binding中的消息
@StreamListener(Sink.INPUT)
public void input(String message){
System.out.println("获取到消息:"+message);
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
- 启动消息消费者,再启动消息生产者,可以从消息消费者的控制台看到获取的消息:
基于入门案例的优化
在实际应用中,使用一个单独的消息发送类:
@Component
@EnableBinding(Source.class)
public class MessageSender {
@Autowired
private MessageChannel output;
//发送消息
public void send(Object obj){
output.send(MessageBuilder.withPayload(obj).build());
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ProducerTest {
@Autowired
private MessageSender messageSender;
@Test
public void testSend(){
messageSender.send("hello");
}
}
消息接收类:
@Component
@EnableBinding(Sink.class)
public class MessageListener{
//监听binding中的消息
@StreamListener(Sink.INPUT)
public void input(String message){
System.out.println("接收到消息:"+message);
}
自定义消息通道
自定义通道:
public interface MyProcessor{
//消息生产者通道
String MYOUTPUT="myoutput";
@Output("myoutput")
MessageChannel myoutput();
//消息消费者通道
String MYINPUT="myinput";
@Input("myinput")
SubscribableChannel myinput();
}
然后在yml中配置myoutput、myinput的desitination,并且改变各注解里的值。
消息分组
在xml中给消息消费者配置group,同组内只有一个消费者会收到消息。
消息分区
使得同一个特征的数据被同一个实例消费。
producer的yml配置:
server:
port: 7001
spring:
application:
name: stream-producer
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
output:
destination: aim-default
producer:
partition-key-expression: payload #分区关键字,这里是根据发送的内容分区
partition-count: 2 #分区大小
binders:
defaultRabbit:
type: rabbit
consumer的yml配置:
server:
port: 7002
spring:
application:
name: rabbitmq-consumer
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
instanceCount: 2 #消费者总数
instanceIndex: 0 #当前消费者索引
bindings:
input: #从指定的消息通道获取消息
group: group1
destination: aim-default
consumer:
partitioned: true #开启分区支持
binders:
defaultRabbit:
type: rabbit
server:
port: 7003
spring:
application:
name: rabbitmq-consumer
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
instanceCount: 2 #消费者总数
instanceIndex: 1 #当前消费者索引
bindings:
input: #从指定的消息通道获取消息
destination: aim-default
group: group1
consumer:
partitioned: true #开启分区支持
binders:
defaultRabbit:
type: rabbit
当发送的消息为"1",只有索引为"1"的消费者才能接收到。
Spring Cloud Config
Spring Cloud Config项目是一个解决分布式系统的配置管理方案,它包含了Client和Server两部分,server提供配置文件的存储,以接口的形式将配置文件的内容提供出去,client通过接口获取数据,并依据此数据初始化自己的应用。
入门案例
- 在码云上创建仓库,上传用到的配置文件,命名规则:
{application}-{profile}.yml
{application}-{profile}.properties
(application为应用名称profile为开发环境、测试环境、生产环境等)
- 新建工程config_server,引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
- 创建启动类,添加注解
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class);
}
}
- 添加配置类
server:
port: 7000
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/jennyf/config.git
username: jennyf
password: ......
- 访问
http://localhost:7000/product-dev.yml
即可看到上传的配置文件 - 给使用配置文件的微服务product-service添加依赖:
<!--config-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
- 删除原来的配置文件application.yml,增加获取配置文件的配置文件bootstrap.yml
spring:
cloud:
config:
name: product #应用名称,需要对应git中配置文件前半部分
profile: dev #开发环境
label: master #git中的分支
uri: http://localhost:7000 #config_server的请求地址
- 访问
http://localhost:9001/product/test
,能够获取到配置文件中的name。
自动更新
此时有一个问题,在修改上传的yml文件后,想要调用的微服务也更新,就必须重启微服务。如何能够不重启微服务也能自动更新呢?
- product_service引入依赖:
<!--健康检查-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 在需要请求刷新的地方添加@RefreshScope注解,开启动态刷新
@RestController
@RequestMapping("/product")
@RefreshScope
public class ProductController {
@Autowired
private ProductService productService;
@Value("${server.port}")
private String port;
@Value("${name}")
private String name;
//根据id查询商品
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Product queryById(@PathVariable Long id){
Product product=productService.queryById(id);
product.setName("访问的接口是:"+port);
return product;
}
//获取yml中的name
@RequestMapping(value = "/test")
public String test(){
return name;
}
}
- 在yml中进行配置
spring:
cloud:
config:
name: product #应用名称,需要对应git中配置文件前半部分
profile: dev #开发环境
label: master #git中的分支
uri: http://localhost:7000 #config_server的请求地址
management: #开启动态刷新的请求路径端点
endpoints:
web:
exposure:
include: refresh
- 更改配置文件中的name,此时访问微服务,结果并不会改变
- 使用postman向微服务发送post请求,再次访问
http://localhost:9001/product/test
,可以看到结果随配置文件已经改变
高可用
将config_server注册到eureka,即可实现config_server的高可用。此时微服务通过eureka获取配置信息。
- 修改config_server的配置文件
<!--高可用-->
<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-actuator</artifactId>
</dependency>
- 在config_server的配置文件中添加eureka相关信息
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
- 在product_service的配置文件中添加eureka相关信息(因为不是先请求git,而是要先请求eureka),以及开启服务发现
spring:
cloud:
config:
name: product
profile: dev
label: master
#uri: http://localhost:7000
discovery:
enabled: true #开启服务发现
service-id: config-server
management:
endpoints:
web:
exposure:
include: refresh
eureka: # 注册到eureka
client:
service-url:
defaultZone: http://localhost:8000/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
- 启动两个不同端口号的config_server,再次访问
http://localhost:9002/product/test
,修改配置文件并发送post请求,可以发现微服务随之改变;关闭其中一个config_server,仍然可以正常使用。实现了其高可用。
消息总线bus
以上发送post请求刷新缓存的方法也存在一定问题:当微服务个数较多时,若都修改了配置文件,需要发送多个post请求来刷新,较为麻烦。SpringCloud Bus将分布式的节点用轻量的消息代理连接起来,搭建消息总线,配合SpringCloud Config实现微服务应用配置的动态更新。
- 在config_server、product_service的pom文件中都引入依赖
<!--消息总线-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
- 修改config_server的配置文件,添加暴露的端点和rabbitmq
server:
port: 7001
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/jennyf/config.git
username: jennyf
password: 580968xyj888
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
management:
endpoints:
web:
exposure:
include: bus-refresh
- 修改product_service的配置文件,删除暴露的端点(此时所有微服务的缓存刷新都由消息总线进行控制)
- 修改product_service在git的配置文件,添加rabbitmq的配置。
- 修改git上的name值,向config_server所在端口发送post请求,访问微服务
http://localhost:9002/product/test
可以看到已经通过消息总线刷新了缓存。
开源配置中心Apollo
SpringCloud Config在改变配置后还需要发送消息,较为不便,且其本身还需要服务器来放置。
Apollo能够集中化管理不同环境、不同集群的配置,配置修改后能实时推送到应用端。
环境搭建
环境要求:
java:1.8+
mysql:5.6.5+