各位小伙伴们大家好,欢迎来到这个小扎扎的[spring cloud专栏],在这个系列专栏中我对B站尚硅谷阳哥的spring cloud教程进行一个总结,鉴于 看到就是学到、学到就是赚到 精神,这波依然是血赚 ┗|`O′|┛
一、前置知识点的学习
1.1 版本选择
学习spring cloud之前需要有一定的spring boot基础,按我现阶段的了解就是,spring boot属于是做业务开发的,所谓的微服务就是将一个大的项目分为是很多的使用spring boot进行开发的单体项目。当然如果没有后续的话,那么这一个项目也就成了一盘散沙,没有办法真正的实现它的作用,于是就需要使用spring cloud将所有的spring boot单体项目整合起来,形成一个完整的项目,从而发挥它最大的作用。
既然在一个项目中涉及到了两种框架技术的使用,那么就需要考虑这两种框架会不会产生冲突。那么我们应该如何查看这两种框架的版本适用呢?我们可以参考spring cloud官网给出的答案,这个文档是实时更新的,不用担心过时的问题。https://spring.io/projects/spring-cloud,打开链接直接向下划即可找到
官网在这里只是提供了一个相对模糊的版本对应,想要更加具体的版本对应可以参考以下网站https://start.spring.io/actuator/info,将网站返回的JSON字符串经过工具的转换即可发现其中的版本对应
当然,上述两种方法都是在boot和cloud技术版本选择时的一种参考,真正确定下来还是要看cloud官网上面的参考doc文档,点进去的Supported Boot Version才是最终敲定的依据
1.2 技术选型
前面讲到spring cloud是一个微服务的框架,既然是微服务就说明我们需要使用技术控制各个单体服务之间的通信,还有不同服务的调用等。于是就涉及到了这些不同功能的技术选型,我们都知道技术是在不停迭代更新的,有很多的技术停更淘汰,与此同时也有很多的技术更新迭代出来,于是我们应该知道不同的功能应该选用哪种可行的技术。
服务注册中心:
- Eureka
- Zookeeper
- Consul
- Nacos √
服务调用:
- Ribbon
- LoadBalancer
- Feign
- OpenFeign √
服务降级:
- Hystrix
- Resilience4j
- Sentinel √
服务网关:
- Zuul
- Zuul2
- Gateway √
服务配置:
- Config
- Nacos √
服务总线:
- Bus
- Nacos √
1.3 spring boot单体模块的搭建
1.3.1 创建父工程
🌒 project项目创建
修改项目名 -> 修改项目存储地址 -> 修改maven配置 -> finished
🌒 配置字符编码格式
🌒 配置注解生效激活
🌒 配置项目的Java的版本
1.3.2 父工程的pom文件
父pom中的并不会引入相应的jar包,而是定义了版本号供子模块使用,只有子模块导入依赖坐标的时候(如果父pom中有,可以不指定版本号)才会引入相应的jar包
<?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>
<groupId>项目的组id</groupId>
<artifactId>项目名称</artifactId>
<version>项目版本</version>
<packaging>pom</packaging>
<!--统一管理设置jar包的版本-->
<properties>
..........
</properties>
<!--依赖传递:父pom文件中的依赖版本会被子模块自动继承,除非子模块中自己声明版本,否则就使用父pom的版本,同理,父pom的依赖版本升级,子模块也会升级到相应的版本-->
<dependencyManagement>
<dependencies>
<dependency>
.......
</dependency>
......
<dependency>
.......
</dependency>
</dependencies>
</dependencyManagement>
</project>
1.3.3 创建子模块
在父工程上右键,新建一个model
创建之后需要配置子模块的配置文件,也就是子模块的resources文件夹下的application.yml文件,主要就是配置端口号和数据库相关
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: …… ……… ………
username: …… ……
password: …… …… ……
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.xiaochen.springcloud.entities
然后就是创建相应的mapper、service、serviceImpl、映射文件
1.4 RestTemplate
Rest Template就是一种对HTTPClient的封装,是Spring提供的用于访问Rest服务的客户端模板工具集,其内部提供了多种便捷的用于访问远程http服务的方法,主要就是用于实现微服务项目中几个服务之间的相互调用。
使用步骤
使用配置类向容器中注入RestTemplate对象
/**
* @ClassName: ApplicationContextConfig
* @Description: 程序配置类
* @author: chenhao
* @date: 2022/7/17
*/
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
在需要使用它的地方用@Resource注入对象,然后就是调用api实现相应的功能,全部api参考以下链接:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
1.5 系统重构(子模块间重复代码的抽取)
创建一个公共模块,将公共的代码和依赖全部抽取到这个公共模块中,然后别的模块如果想要使用公共模块的代码,就必须先对公共模块的maven进行clean和install,然后在pom文件中导入这个公共模块的依赖即可使用公共部分代码
二、服务的注册发现——Eureka
2.1 Eureka的作用
服务治理是主要针对分布式服务框架的微服务,处理服务调用之间的关系、服务发布和发现、故障监控与处理,服务的参数配置、服务降级和熔断、服务使用率监控等。
需要服务治理的原因:
- 过多的服务 URL 配置困难
- 负载均衡分配节点压力过大的情况下,需要部署集群
- 服务依赖混乱,启动顺序不清晰
- 过多服务,导致性能指标分析难度较大,需要监控
- 故障定位与排查难度较大
2.1.1 什么是服务注册与发现?
服务注册主要针对服务提供者,服务启动后需要将本身以别名的方式注册到注册中心上;而服务发现主要是针对消费者(服务使用者),它在调用服务的时候需要通过别名去注册中心获取到相应的服务,然后再实现本地RPC调用远程RPC的功能。
2.1.2 Eureka的两大组件
Eureka Server: 提供注册服务,各节点启动时,会在EurekaServer中进行注册,各服务的节点信息会储存在EurekaServer中的服务注册表中。
Eureka Client: 其本质是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的使用负载算法的负载均衡器在启动应用后将会向Eureka Server发送心跳(默认周期30s),如果EurekaServer在多个心跳周期内没有接收到某节点的心跳,将会把该节点从服务注册表上移除(默认周期90s)
2.2 Eureka的使用
2.2.1 Eureka服务端server的创建
首先是创建一个子模块作为eureka的服务器模块,主要与其他的子模块有以下三处不同:
一、导入eureka服务端的依赖
<!-- 服务注册中心的服务端 eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
二、配置文件配置eureka相关
# eureka的单机版配置
eureka:
instance:
hostname: localhost #eureka服务端的实例名字
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
#设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
三、主程序类上使用注解开启服务器
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}
2.2.2 Eureka客户端client的创建
之前创建的支付子模块8001就可以标记为Eureka的客户端,我们需要做的和Eureka服务端的三步一样:
一、导入eureka客户端的依赖
<!--Eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
二、配置文件配置eureka相关
eureka:
client:
register-with-eureka: true #是否向注册中心注册自己
fetchRegistry: true #是否从注册中心抓取已有的注册信息 默认true
service-url:
defaultZone: http://localhost:7001/eureka #服务端的地址
三、主程序类使用注解开启客户端
@SpringBootApplication
@EnableEurekaClient
public class Payment8001Application {
public static void main(String[] args) {
SpringApplication.run(Payment8001Application.class, args);
}
}
红框显示当前名称为CLOUD-PAYMENT-SERVICE的服务状态为UP,也就是说这个模块已经被注册进了eureka服务
目前为止,我们的eureka服务端上就只有一个支付模块作为服务的提供者注册进eureka服务器,并没有服务的调用者,于是乎我们可以将80端口的消费者模块通过以上三步,将其作为eureka客户端注册进eureka服务器,让其实现服务的调用
2.3 集群搭建
2.3.1 Eureka服务端的集群搭建
想必集群的好处这里就不需要再进行赘述了,单体的服务器一旦出现故障整个项目就搞崩了,如果是集群就不一样了,一个故障其他顶上,服务之间可以实现互相注册相互守望
现在我们就创建一个新的服务端子模块命名7002,作为集群中的另一个模块使用。那么,如何实现互相注册相互守望呢?答案就是:分别在配置文件中配置,使集群中的服务都能互相发现,其中最重要的就是defaultZone字段的对应值
server:
port: 7001
# eureka的单机版配置
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名字,这里以localhost代替
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 单机版,指向的是自己
# 集群版,指向集群中的其他服务
defaultZone: http://eureka7002.com:7002/eureka/,
http://eureka7003.com:7003/eureka/
server:
port: 7002
# eureka的单机版配置
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名字,这里以localhost代替
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 单机版,指向的是自己
# 集群版,指向集群中的其他服务
defaultZone: http://eureka7001.com:7001/eureka/,
http://eureka7003.com:7003/eureka/
server:
port: 7003
# eureka的单机版配置
eureka:
instance:
hostname: localhost # eureka服务端的实例名字,这里以localhost代替
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 单机版,指向的是自己
# 集群版,指向集群中的其他服务
defaultZone: http://eureka7001.com:7001/eureka/,
http://eureka7002.com:7002/eureka/
集群搭建完毕之后,服务也要向集群中的所有模块进行注册,那么就需要使用配置文件了,这里以服务调用者80端口为例
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true #是否向注册中心注册自己
fetchRegistry: true #是否从注册中心抓取已有的注册信息 默认true
service-url:
# defaultZone: http://localhost:7001/eureka #服务端的地址
#服务端的地址,服务端为集群版,向所有的模块都注册
defaultZone: http://localhost:7001/eureka,
http://localhost:7002/eureka,
http://localhost:7003/eureka
2.3.2 Eureka客户端的集群搭建
从8001模块copy一个为8002,作为客户端集群的另一个模块,同样使用配置文件,将其也注册进服务端集群。此时就可以使用服务调用者80进行调用了,但是80的controller是直接写死的域名和端口,于是需要将域名端口改为微服务的名称,然后在服务调用者80模块开启负载均衡让请求合理的分配到服务集群的每一个模块
将原先的指定域名+端口改为指定服务
配置中开启负载均衡
这样集群就部署好了,可以实现服务调用对8001和8002的轮询
2.3.3 微服务模块的相关配置
配置服务的名称
eureka:
instance:
instance-id: payment8001
访问路径显示IP地址
eureka:
instance:
prefer-ip-address: true
2.4 服务发现
所谓的服务发现就是对于已经注册进eureka里的微服务,可以通过这个方式来获得该服务的相关信息,实现服务发现功能分三步走:
一、对象注入
@Resource
private DiscoveryClient discoveryClient;
二、调用api获取服务信息
@GetMapping(value = "/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("***********service: " + service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId() + "\t"
+ instance.getHost() + "\t"
+ instance.getPort() + "\t"
+ instance.getUri());
}
return this.discoveryClient;
}
三、主程序入口注解开启发现功能
@EnableDiscoveryClient
这样即可打印服务的相关信息
2.5 自我保护机制
2.5.1 什么是自我保护机制
EurekaServer会检查最近15分钟内所有EurekaClient正常心跳占比,如果低于85%就会触发自我保护机制,此时Eureka暂时把这些失效的服务保护起来,不会立即将其剔除。Eureka在启动完成后,每隔60秒会检查一次服务健康状态,如果这些暂时失效的服务过一段时间后(默认90秒)还是没有恢复,就会把这些服务剔除。反之这个服务就不会因为网络的延时、卡顿、拥挤等问题而被错误删除。
总而言之,自我保护机制就是说某一时刻微服务不可用时,Eureka不会立即将其剔除,而是在一段时间内等待其正常发送心跳。这属于CAP原则的AP分支,主要保证了可用性和分区容错性
2.5.2 如何禁用自我保护机制
服务端使用配置文件关闭eureka的自我保护机制
eureka:
server:
# 关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
#
eviction-interval-timer-in-ms: 2000
客户端使用配置文件设置心跳相关配置
eureka:
instance:
# Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
# Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
三、服务调用——Ribbon
3.1 初识Ribbon
3.1.1 Ribbon是什么?
Ribbon是Netflix发布的开源项目,主要功能是提供对客户端进行负载均衡算法的一套工具,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连接这些机器。我们也可以使用Ribbon实现自定义的负载均衡算法。
3.1.2 Ribbon能干什么?
前面提到说Ribbon的作用就是向客户端提供负载均衡算法的工具,那么什么是负载均衡呢?负载均衡就是将用户发来的请求通过算法均摊到多个服务上,从而达到系统的HA(高可用性)
其中,负载均衡又可分为本地负载均衡(进程内LB)和服务端负载均衡(集中式LB),服务端负载均衡以Nginx为例,用户的所有请求都会交给Nginx,由其决定请求将被转发到哪个服务器;Ribbon是本地负载均衡,在调用接口的时候从 eureka 注册中心服务器端上获取服务注册信息列表缓存到本地,从而可以在本地实现RPC远程调用服务
3.2 使用Ribbon实现负载均衡
实际上Ribbon可以简单的理解为负载均衡算法 + RestTemplate的调用,也就是说想要使用Ribbon实现负载均衡,就可以通过这两个技术加以实现。
3.2.1 RestTemplate三步走
第一步: 引入Ribbon场景启动器依赖,但是之前使用eureka的时候我们在pom文件中导入过netflix-eureka-server的依赖,其中就默认引入了ribbon的场景启动器依赖(netflix-eureka-client也会默认导入),如果再引一次也可以,但是真没那必要。
第二步: 使用配置文件开启 RestTemplate
/**
* @ClassName: ApplicationContextConfig
* @Description: 程序配置类,用于开启RestTemplate服务,以供后面使用
* @author: chenhao
* @date: 2022/7/17
*/
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
第三步: 使用RestTemplate的API实现负载均衡,RestTemplate的API根据请求方式的不同可以被分为get和post,根据返回类型又可以分为Object(响应体转化成的json串)和Entity(响应的重要信息,包括响应头、状态码、响应体等,可以使用对应的get方法获取到值),所以说最主要的四个API是getForObject、postForObject、getForEntity、postForEntity,这里我把四种API的使用方法都向大家介绍一下
@RestController
@Slf4j
@RequestMapping("consumer")
@Api("消费者的订单管理类")
public class OrderController {
// 先注入RestTemplate对象
@Resource
private RestTemplate restTemplate;
// 使用API实现负载均衡
@ApiOperation(value = "创建一条支付记录", tags = ApiVersionConstant.v1_0)
@PostMapping(value = "/payment/create", produces = {"application/json;charset=UTF-8"})
public CommonResult<Integer> create(@RequestBody Payment payment) {
return restTemplate.postForObject(UrlConstant.CLUSTER_PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
@ApiOperation(value = "根据ID查询支付记录", tags = ApiVersionConstant.v1_0)
@GetMapping(value = "/payment/get/{id}", produces = {"application/json;charset=UTF-8"})
public CommonResult<Payment> getPayment(@PathVariable("id") Integer id) {
return restTemplate.getForObject(UrlConstant.CLUSTER_PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
@ApiOperation(value = "根据ID查询支付记录Entity", tags = ApiVersionConstant.v1_0)
@GetMapping(value = "/payment/getForEntity/{id}", produces = {"application/json;charset=UTF-8"})
public CommonResult<Payment> getPaymentEntity(@PathVariable("id") Integer id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(UrlConstant.CLUSTER_PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult<>(444, "操作失败");
}
}
@ApiOperation(value = "创建一条支付记录Entity", tags = ApiVersionConstant.v1_0)
@PostMapping(value = "/payment/createEntity", produces = {"application/json;charset=UTF-8"})
public CommonResult<Integer> createEntity(@RequestBody Payment payment) {
return restTemplate.postForEntity(UrlConstant.CLUSTER_PAYMENT_URL + "/payment/create", payment, CommonResult.class)
.getBody();
}
}
3.2.2 负载均衡算法
经过我们上面的尝试,不难发现使用RestTemplate实现的负载均衡算法是轮询机制,实际上IRule中不仅仅只提供了一种算法
IRule实现算法切换
第一步: 新建一个package,在官方文档中声明了IRule的配置类不能放到@ComponentScan注解所能扫描到的当前包以及子包下,否则自定义的配置类就会被Ribbon的所有客户端所共享,以至于失去客户端定制化的可能性。主程序入口上的@SpringBootApplication注解是复合注解,其中就包含@ComponentScan注解,而且是直接扫描主程序入口所在的当前包以及子包,也就是说配置类必须放到主程序入口之外的包下,于是需要新建一个package
第二步: 在新建的包中新建一个MySelfRule规则类,用于设置轮询算法,如果不设置的话就默认为轮询
@Configuration
public class MySelfRule {
@Bean
public IRule myRule() {
// 修改轮询算法为随机算法
return new RandomRule();
}
}
第三步: 主启动类上使用@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)注解,用于指定应用服务和自定义算法规则的配置类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
轮询算法
所谓的轮询算法就是根据所有的服务,依次将请求均摊到所有的服务依次访问,它的算法实现就是用RestTemplate接收到的请求数量对服务器集群的数量进行取模运算,余数就是服务在服务列表中对应的索引位置,所以说可以实现轮询。但是如果中途服务器关掉的话接收到的请求数量就会从1重新计数
四、服务调用——OpenFeign
4.1 初识OpenFeign
4.1.1 什么是OpenFeign?
openFeign是要声明式的web服务客户端,或叫做声明式REST客户端,它让编写web服务客户端变得简单。它将提供者的restful服务伪装成接口进行消费,消费者只需要通过feign接口+注解就可以直接调用提供者的服务接口,也就是可以实现接口对接口的调用,而无需像ribbon一样通过restTemplate方式对提供者的服务进行调用
值得注意的一点是,openFeign内置了负载均衡器-Ribbon,所以说openfeign也可以使用负载均衡算法
4.1.2 如何使用OpenFeign?
第一步: 引入相关依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步: 配置配置文件,只是基本配置和注册,没有OpenFeign独有的配置
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
#服务端的地址,服务端为集群版,向所有的模块都注册
defaultZone: http://localhost:7001/eureka,
http://localhost:7002/eureka,
http://localhost:7003/eureka
第三步: 主启动类开启OpenFeign客户端
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
第四步: 之前使用ribbon是直接在controller里调用payment服务的controller,但是openfeign则是通过service调用,于是第三步就是创建一个service接口用于调用payment服务的接口
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 用于指定服务名,可在eureka或者服务的配置文件中查看
public interface PaymentFeignService {
@GetMapping(value = "/payment/get/{id}", produces = {"application/json;charset=UTF-8"})
CommonResult<Payment> getPaymentById(@PathVariable("id") Integer id);
}
第五步: controller层调用service接口
@RestController
@RequestMapping("consumer")
public class OrderFeignController {
@Autowired
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/payment/get/{id}", produces = {"application/json;charset=UTF-8"})
public CommonResult<Payment> getPaymentById(@PathVariable("id") Integer id) {
return paymentFeignService.getPaymentById(id);
}
}
如此操作也可实现order服务对payment服务的调用,而且由于OpenFeign默认引入Ribbon,去进行order服务访问的时候,后端会默认轮询名为是“CLOUD-PAYMENT-SERVICE”的微服务,也就是两个payment服务
4.2 OpenFeign超时控制
使用OpenFeign调用服务接口,默认等待时间为1秒,超时就会直接报错。如果有些服务的调用确实会花费超过1s的时间,就需要我们在服务调用方(也就是order服务)的配置文件中进行配置
由于OpenFeign的超时控制由其底层的ribbon实现,于是配置文件中的超时控制也由ribbon进行配置
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下, 两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
4.3 OpenFeign日志打印
OpenFeign提供了日志打印的功能,我们可以通过日志的打印监控接口的调用情况,从而了解接口调用时HTTP请求的具体细节,具体的使用分
第一步: 使用配置类配置日志级别
@Configuration
public class FeignConfig {
/**
* 日志级别
* NONE:默认的,不显示任何日志
* BASIC:仅记录请求方法、URL、响应状态码以及执行时间
* HEADERS:请求方法、URL、响应状态码、执行时间、请求和响应的头信息
* FULL:请求方法、URL、响应状态码、执行时间、请求和响应的头信息、正文以及元数据
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
第二步: 配置文件开启日志打印,并指定监控的接口和级别
logging:
level:
# feign日志以 debug 级别监控 com.atguigu.springcloud.service.PaymentFeignService 接口
com.xiaochen.springcloud.service.PaymentFeignService: debug
打印出来的日志如下:
五、服务熔断降级——Hystrix
5.1 Hystrix是什么?
Java应用程序讲求“高内聚低耦合”,而spring cloud是一种微服务架构理念,将原来的一个应用程序拆分成许多微服务来调用,这样的话就可以满足“低耦合”的要求,但是随之而来的就是“服务雪崩”问题。
所谓的服务雪崩就是指,由于服务提供者不可用导致服务调用者不可用,并且在生产过程中,这种不可用逐渐扩大的现象。想要解决“服务雪崩”问题就需要用到Hystrix(豪猪)
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
5.2 Hystrix三大概念
5.2.1 服务降级(fallback)
5.2.1.1 fallback是什么?
所谓的服务降级就是当服务出现程序运行异常、超时、服务熔断触发服务降级、线程池/信号量打满等情况,此时应该返回一个符合预期的、可处理的备选响应(fallback),以提高用户的使用体验而不是长时间的等待请求或者返回一个超时异常页面。总而言之,当出现以上问题时,要有一个兜底方案来提高用户的使用体验
服务降级的解决方案可以分为服务提供方和服务调用方,两面都可以实现服务降级
5.2.1.2 服务提供方实现服务降级
第一步: 对服务提供方的service接口方法进行加强,主要就是针对可能出现超时等异常情况的接口,新建方法对其进行兜底,如果原接口出现问题则使用兜底方案进行反馈
使用@HystrixCommand注解的fallbackMethod属性指定兜底方法名,使用commandProperties属性的@HystrixProperty注解指定异常类型(超时异常和超时时间)
/**
* 超时访问,设置自身调用超时的峰值,峰值内正常运行,超过了峰值需要服务降级 自动调用fallbackMethod 指定的方法
* 超时异常或者运行异常 都会进行服务降级
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfoTimeOut(Integer id) {
int second = 5;
try {
TimeUnit.SECONDS.sleep(second);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " paymentInfoTimeOut,id: " + id + "\t"
+ "O(∩_∩)O哈哈~" + " 耗时(秒): " + second;
}
public String paymentInfo_TimeOutHandler(Integer id) {
return "超时异常兜底方案!线程池: " + Thread.currentThread().getName() + " paymentInfo_TimeOutHandler,id: " + id + "\t";
}
第二步: 服务提供方的主启动类上使用@EnableCircuitBreaker注解开启“熔断器”,这样的话前面的配置才能生效
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class HystrixPaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentMain8001.class, args);
}
}
除了上述可以自定义超时时间的异常,出现其他运行时异常也会调用兜底方案返回
5.2.1.3 服务调用方实现服务降级
第一步: 使用配置文件开启hystrix
feign:
hystrix:
enabled: true
第二步: 服务调用方的controller加强,新建方法对其进行兜底,使用方式与服务提供方一样,主要就是服务调用方和服务提供方两套方案,可以实现两边定制化。
@GetMapping("/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfoTimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "服务调用方兜底方案!我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
第三步: 服务调用方的主程序类上使用@EnableHystrix注解开启hystrix
5.2.1.4 服务降级优化
解决冗余问题
前面的每一个方法都对应一个兜底方案,这样的话会显得代码十分臃肿,实际上很多的接口都可以使用一个兜底方案,于是我们就可以配置默认的兜底方案,在没有使用@HystrixCommand注解指定的时候,类中的所有接口都会走默认兜底方案
@DefaultProperties注解的defaultFallback属性指定默认兜底方法,如果类中存在@HystrixCommand注解中不使用属性指定特定兜底方案的情况,就说明这个接口使用是默认兜底方案
@RestController
@RequestMapping("consumer")
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@HystrixCommand
@GetMapping("/payment/hystrix/ex/{id}")
public String paymentInfoException(@PathVariable("id") Integer id) {
int age = 10/0;
String result = paymentHystrixService.paymentInfoException(id);
return result;
}
/**
* hystrix 全局fallback方法
* @return
*/
public String payment_Global_FallbackMethod() {
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
解决耦合问题
上述操作中,原方案和兜底方案都在controller中定义,想要解决这个耦合问题可以使用一个类实现service接口,然后重写所有的接口方法,然后使用service接口上@FeignClient注解的fallback属性指定接口的实现类为兜底类
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfoOK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfoTimeOut(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/ex/{id}")
String paymentInfoException(Integer id);
}
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfoOK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}
@Override
public String paymentInfoTimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
@Override
public String paymentInfoException(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfoException ,o(╥﹏╥)o";
}
}
5.2.2 服务熔断(break)
5.2.2.1 break是什么?
微服务链路中某个微服务的请求达到最大访问之后,直接拒绝访问,然后调用服务降级的方法返回友好的提示;当检测到该节点微服务调用响应正常之后,还可以恢复链路的正常调用。hystrix会默认在服务5秒内调用失败20次后触发熔断机制,但是也可以使用配置对其进行修改。
5.2.2.2 服务提供方实现服务熔断
首先service层使用注解配置服务熔断的相关值和熔断时的备选方案,就以下代码为例:在10S的时间窗口期中,10次请求中失败率达到60%就会触发熔断启动备选方案。
如果在10S的时间窗口期中,请求次数不足10次,那么根本就不可能触发熔断器。当熔断器开启后所有的请求都不会被转发,一段时间窗口期之后(默认5秒,可以自定义配置)断路器切换为半开状态,此时会让其中一个请求进行转发,成功则关闭断路器,反之继续开启
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),/* 是否开启断路器*/
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreakerFallback(Integer id) {
return Thread.currentThread().getName() + "\t" + "id 不能负数或超时或自身错误,请稍后再试,/(ㄒoㄒ)/~~ id: " + id;
}
controller中调用service方法
@GetMapping("/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: " + result);
return result;
}
5.2.3 服务限流(flowlimit)
flowlimit是什么?
在秒杀等高并发的操作下,为了防止大量请求一块发送过来,采用排队的方式,把请求按照顺序排队发送过来。由于hystrix已经停止更新,而且Alibaba的sentinel在进行服务限流的处理时比hystrix更加优秀,所以说这一部分知识可以放在后面进行学习。
5.3 Hystrix图形化监控
第一步: 新建一个模块用于监控,导入相关依赖
<dependencies>
<!--最主要的依赖,用于引入图形化dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.xiaochen</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
第一步: 配置文件配置端口号
server:
port: 9001
第三步: 主启动类开启HystrixDashboard,
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
此外,所有的服务提供方也就是被监控的服务都要引入spring-boot-starter-actuator依赖,表示自己可以受监控。然后就是在服务的主启动类上要使用以下代码中的addUrlMappings配置受监控的路径
/**
* 注意:新版本Hystrix需要在主启动类中指定监控路径
* 此配置是为了服务监控而配置,与服务容错本身无关,spring cloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*
* @return ServletRegistrationBean
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
// 一启动就加载
registrationBean.setLoadOnStartup(1);
// 添加url
registrationBean.addUrlMappings("/hystrix.stream");
// 设置名称
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
启动监控模块、eureka模块、受监控模块之后,访问以下路径可以通过路径监控指定的服务,设置后点击下面的Monitor Stream按钮即可进入图形化监控界面
图形化界面各处代表的含义如下:
六、服务网关——Gateway
6.1 初识Gateway
6.1.1 什么是Gateway?
由于Netflix的zuul发生问题,spring公司自己研发了一套网关框架Gateway用于取代zuul的使用。什么是gateway呢?spring cloud Gateway是使用Webflux中的reactor-netty响应式编程组件,底层使用的是netty通讯框架。
6.1.2 什么是api网关?
API gateway 处于客户端与各个微服务之间,它担任了反向代理的角色,将不同的请求路由到相对应的微服务中去。与此同时,它还有以下功能:安全,限流,缓存,日志,监控,重试,熔断等。网关就是所有项目的一个统一入口,也可以说是进入系统的唯一节点。
6.1.3 网关的三个核心概念
路由(Route)
路由是构建网关的基本模块,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为真则匹配该路由。就是根据断言和过滤器提供的某些规则,将请求发送到指定服务上
断言(Predicate)
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由,就是定义匹配规则,只有满足断言的请求才会继续进行路由
过滤(Filter)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。当请求进行断言之前,或者满足断言后会继续进行路由,但是由于过滤的存在请求会再次被过滤条件进行指定的修改操作
6.2 gateway的工作流程
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
6.3 如何使用Gateway
6.3.1 gateway路由转发
6.3.1.1 使用配置文件
第一步: 创建一个子模块用于配置gateway,导入相关依赖,其中最重要的就是gateway的启动器。==一定不能引入web场景启动器依赖==,否则gateway模块将无法启动
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
第二步: 配置文件
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://localhost:7001/eureka
第三步: 现在我们想在支付微服务8001之前使用gateway网关,让请求在访问8001微服务之前先经过gateway网关。于是我们需要先配置配置文件
spring:
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/create/** # 断言,路径相匹配的进行路由
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
经过如上操作,即可实现如果通过9527端口也可以访问8001的接口,也就是说请求先是通过9527网关,将符合yml配置文件中配置的请求进行路由转发至8001端口
6.3.1.2 使用代码配置
使用代码配置的话就需要使用到自定义配置类了,将上面的第三步由配置文件配置修改为自定义配置类,前两步要保持一致。其中route方法的第一个参数相当于id配置,r.path相当于predicates断言,r.path.uri就是请求将要路由到的地址
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocatorBuilder(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei"))
.build();
return routes.build();
}
}
6.3.1.3 路由实现负载均衡
之前都是通过ribbon来实现请求的负载均衡,其实gateway网关也可以通过注册中心的微服务名来实现负载均衡,也就是动态路由的功能。这里我们先通过以下配置开启从注册中心动态创建路由的功能
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
然后将原先写死的路径改为微服务名,通过这个实现对微服务的轮询
spring:
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/create/** # 断言,路径相匹配的进行路由
6.3.2 gateway九种断言
gateway的断言实际上就是配置文件中的predicates配置项,既然这个单词是复数,那就意味着它不仅仅可以配置一种断言,实际上断言的类型有九种,它们的意思以及使用方式我都写在了下面
predicates:
- Path=/payment/create/** # 路径断言,路径相匹配的进行路由
- Before=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] # 时间断言,时间Before、After、Between指定时间的请求进行路由
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
- Between=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] , 2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
- Cookie=username,zzyy # cookie断言,携带名为username且值为zzyy的请求进行路由(zzyy可以替换为正则)
- Header=X-Request-Id, \d+ # header断言,请求头携带X-Request-Id且值满足正则"\d+"的请求进行路由
- Host=**.atguigu.com, **.atg.com # host断言,请求格式符合的请求进行路由
- Method=GET # method断言,请求方式为get的请求进行路由
- Query=username, \d+ # query断言,有参数名为username且值满足正则"\d+"的请求进行路由
6.3.3 gateway过滤修改
使用配置文件实现filter过滤修改很简单,类似于断言的使用,在配置文件直接配置即可
filters:
- AddRequestParameter=X-Request-Id,1024
但是一般不这么使用filter,我们都是自定义一个全局GlobalFilter,重点就是@Component注解,类实现GlobalFilter, Ordered接口重写方法,设置过滤规则和优先级
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/**
* 判断请求中包含uname参数且它的值不为空,否则拒绝请求
*/
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 加载过滤器顺序,数字越小优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
七、配置中心——Config
7.1 什么是springcloud Config?
简单来说,Spring Cloud Config就是我们通常意义上的配置中心,也就是微服务项目中,每一个微服务都需要配置相应的配置,如果不同服务的配置文件有相同的配置,如果这些相同配置需要修改的话就要全都修改一遍。如果使用springcloud Config的话就可以把原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。
Config分为服务端和客户端,服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访接口。客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容
7.2 config服务端的配置使用
第一步: 外部config需要结合git使用,于是需要在github或者gitee创建一个仓库,仓库里全都是各种配置文件,git远程仓库的创建这里就不多加赘述,创建完成之后需要获取到仓库的链接
第二步: 引入相关依赖
<!--Config服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
第三步: 配置文件配置远程仓库地址
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/mereign/springcloud-config.git #GitHub或者Gitee上面的git仓库名字
search-paths: #搜索目录
- springcloud-config
label: master #读取分支
#启动成功后访问的路径 http://ip:3344/{label}/{application}-{profile}.yml 能访问的配置文件 就表示成功了
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
启动配置中心微服务模块,但是有些小伙伴会遇到这个异常Authentication is required but no CredentialsProvider has been registered,原因是你的远程仓库创建的私有的未开源,解决方案有两种,一来就是把仓库修改为开源仓库,还有就是使用下面的方法配置你github或者Gitee的用户名和密码
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/mereign/springcloud-config.git #GitHub或者Gitee上面的git仓库名字
username: 自己的github或者Gitee用户名
password: 自己的github或者Gitee密码
第四步: 主程序类使用@EnableConfigServer注解标注Config服务端
配置好了之后启动微服务,可以使用url链接直接读取文件里的内容
7.3 config客户端的相关问题
7.3.1 config客户端的配置使用
第一步: 引入相关依赖,这里客户端的依赖与服务端有所不同
<!--Config服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
第二步: 配置文件,这里的客户端的配置文件与之前的有所不同,命名为bootstrap.yml,主要区别为applicaiton.yml是用户级的资源配置项,而bootstrap.yml是系统级的,优先级更加高
server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:3344 #配置中心的地址
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述三个综合http://localhost:3344/master/config-dev.yml
#服务注册到eureka地址
eureka:
client:
service-url:
#设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://localhost:7001/eureka #单机版
经过上述配置,等服务启动开的时候,会3355服务端会到3344服务端获取config-dev的配置内容,3344服务端再来连接配置的GitHub或者Gitee仓库从而获取config-dev的配置内容
第三步: 主程序类只需要标注eureka客户端即可,无需标注config客户端
第四步: 服务端的url链接形式获取内容,而客户端需要使用REST接口的形式获取指定配置信息
@RestController
public class ConfigController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
启动主程序类(要先启动注册中心eureka再启动config服务端,最后才能启动config客户端),然后访问controller中的接口即可获得相应的配置信息
7.3.2 动态刷新问题
当远程仓库的配置修改之后,服务端通过url链接的形式获取内容是更新之后的,但是客户端使用REST接口的形式获取到的配置信息则是更新之前的,只有重启config服务端微服务才能通过客户端接口访问到更新之后的配置信息。
为了解决上述的问题,可以使用动态刷新配置服务端微服务,避免每次配置更新都需要重启微服务才能获取最新配置的痛点
第一步: 引入actuator监控技术依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
第二步: bootstrap.yml文件中暴露服务端点,可以让这个服务处于可被监控状态
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
第三步: 在controller类上加@RefreshScope注解
第四步: 发送post请求,手动刷新3355服务端的监控,这样才能刷新config服务端获取的配置信息
curl -X POST "http://localhost:3355/actuator/refresh
第五步: 到此为止就config服务端就可以访问到远程仓库中最新的配置信息了
7.4 config客户端的遗留问题
config客户端动态刷新解决了微服务每次都要重启才能获取最新配置信息的问题,但是,如果config客户端有很多呢?每次远程仓库配置修改都需要使用post请求手动刷新所有的服务吗?是不是也很麻烦,那么是否可以使用广播的形式,一次通知处处生效,大范围的自动刷新,定制化的指定服务刷新?那就用到了下一篇博客学习的消息总线——springcloud Bus
八、消息总线——Bus
8.1 什么是springcloud Bus?
上一章的springcloud Bus是对分布式微服务的远程配置,但是有一个遗留的问题就是,Config客户端对远程配置的刷新需要手动使用post请求来完成,这就使得Config客户端动态刷新变得十分麻烦。于是消息总线springcloud Bus就是来解决这个问题的
8.1.1 什么是消息总线?
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都链接上来。由于该主题中产生的消息都会被所有实例监听和消费,所以称它为消息总线。
8.1.2 Bus实现自动刷新的原理
所有的Config客户端都监听MQ中同一个topic(默认是SpringCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其他监听同一Topic的服务就能得到通知,然后去更新自身的配置。
由于springcloud Bus支持两种消息的代理(RabbitMQ和kafka),于是接下来的demo选择使用RabbitMQ 3.7.14,其他的消息中间件Bus暂不支持
8.2 RabbitMQ的下载配置
8.2.1 Erlang
第一步: 下载安装Erlang,地址:http://erlang.org/download/otp_win64_21.3.exe,安装除了自定义安装路径外,一路next即可
第二步: 配置环境变量,新建一个环境变量命名为ERLANG_HOME,值为Erlang的安装路径
path环境中添加 %ERLANG_HOME%\bin
第三步: win + R 输入cmd回车,键入命令erl测试配置是否成功
8.2.2 RabbitMQ
第一步: 这里以RabbitMQ 3.7.14为例,地址:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.14,页面的最下方有下载的地方,安装除了自定义安装路径外,一路next即可
第二步: 配置环境变量,新建一个环境变量命名为RABBITMQ_SERVER,值为RabbitMQ的安装路径
path环境中添加 %RABBITMQ_SERVER%\sbin
第三步: 安装管理工具RabbitMQ-Plugins,win + R 输入cmd回车,键入命令
rabbitmq-plugins enable rabbitmq_management
第四步: 第三部安装的管理工具会安装几个RabbitMQ的管理快捷方式,可以点击win键查看,点击即可执行对应操作
第五步: 测试是否安装成功,浏览器==http://localhost:15672==(默认账号:guest,密码:guest),登入RabbitMQ的管理页就表示已经安装成功
8.3 Bus动态刷新
Bus动态刷新有两种设计思想,第一种利用消息总线触发一个客户端的bus/refresh,从而刷新这条总线上的所有客户端配置;第二种利用消息总线触发一个服务端ConfigServer的bus/refresh,从而刷新这个配置中心(服务端)上注册的所有客户端配置。
以上两种思想并不是都有应用,而是选择了第二种,因为第一种设计思想会使得被选中的服务端节点在配置获取职责之外新增配置刷新的职责,这会打破微服务的职责单一性和各节点之间的对等性。且一旦被选中的节点失效,配置刷新也将会随之失效,这条总线上的服务端依旧无法实时获取到最新的配置。
8.3.1 全局广播通知代码实现
此时配置相关微服务一共有三个,它们是配置中心服务端3344、客户端3355、客户端3366,全局广播的代码实现需要对三个微服务都进行配置
配置中心3344: pom文件和配置文件
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
spring:
#rabbitmq相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
##rabbitmq相关配置,暴露bus刷新配置的端点 SpringCloud Bus动态刷新全局广播
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
客户端3355、3366: pom文件和配置文件
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
spring:
#rabbitmq相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
以3366为例,配置文件的缩进如下
server:
port: 3366
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:3344 #配置中心的地址
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述三个综合http://localhost:3344/master/config-dev.yml
#rabbitmq相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
#设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://localhost:7001/eureka #单机版
# 暴露监控端点 否则 curl -X POST "http://localhost:3355/actuator/refresh" 不可使用
management:
endpoints:
web:
exposure:
include: "*"
🚩值得注意的是,之前rabbitmq的web访问端口是15672,但是在代码中连接rabbitmq使用的端口是5672,这里很容易踩坑
完成前面的配置,使用以下链接访问配置中心服务端3344、客户端3355、客户端3366的配置信息
http://localhost:3344/master/config-dev.yml
http://localhost:3355/configInfo
http://localhost:3366/configInfo
然后修改gitee上的远程配置,仍然使用上面链接访问,会发现只有配置中心服务端获取的配置是最新的,但是配置客户端的配置还是修改前的,需要使用win+R输入命令刷新,再访问的话配置客户端就是最新的配置了
curl -X POST "http://localhost:3344/actuator/bus-refresh"
8.3.2 定点通知代码实现
bus除了可以一个命令刷新总线上的所有微服务之外,还可以支持定点刷新,命令如下
定点通知一个
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
定点通知多个
curl -X POST "http://localhost:3344/actuator/bus-refresh/{config-client:3355,config-client:3366}"
九、消息驱动——Stream
9.1 什么是springcloud Stream?
现在市面上有很多的消息中间件,每一个公司使用的都有所不同,为了减少学习的成本,springcloud Stream可以让我们不再关注消息中间件MQ的具体细节,我们只需要通过适配绑定的方式即可实现不同MQ之间的切换,但是遗憾的是springcloud Stream目前只支持RabbitMQ和Kafka。
SpringCloud Stream是一个构建消息驱动微服务的框架,应用程序通过inputs或者 outputs来与SpringCloud Stream中的binder进行交互,我们可以通过配置来binding ,而 SpringCloud Stream 的binder负责与中间件交互,所以我们只需要搞清楚如何与Stream交互就可以很方便的使用消息驱动了!
9.1.1 什么是Binder?
Binder是SpringCloud Stream的一个抽象概念,是应用与消息中间件之间的粘合剂,通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离,可以动态的改变消息的destinations(对应于 Kafka的topic,RabbitMQ的exchanges),这些都可以通过外部配置项来做到,甚至可以任意的改变中间件的类型但是不需要修改一行代码
9.1.2 为什么使用Stream?
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同像RabbitMQ有exchange,kafka有Topic和Partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移;这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这衬候springcloud Stream给我们提供了一种解耦合的方式。
9.2 Stream使用案例
9.2.1 Stream处理消息的架构
Source、Sink: 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。Channel: 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介。Binder: 消息的生产者和消费者中间层,实现了应用程序与消息中间件细节之间的隔离
通过以上两张图片可知,消息的处理流向是:消息生产者处理完业务逻辑之后消息到达source中,接着前往Channel通道进行排队,然后通过binder绑定器将消息数据发送到底层mq,然后又通过binder绑定器接收到底层mq发送来的消息数据,接着前往Channel通道进行排队,由Sink接收到消息数据,消息消费者拿到消息数据执行相应的业务逻辑
9.2.2 Stream常用注解
9.3 消息生产者8801模块搭建
第一步: 创建一个maven模块,引入相关依赖,最主要的就是stream整合rabbitmq的依赖
<!--stream的rabbitmq依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
第二步: 配置文件的编写
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
第三步: 主程序类
@SpringBootApplication
public class CloudStreamRabbitmqProvider8801Application {
public static void main(String[] args) {
SpringApplication.run(CloudStreamRabbitmqProvider8801Application.class, args);
System.out.println("启动成功");
}
}
第四步: 业务层service代码编写,注意:这里实现类注入的对象由之前的dao层对象换成了channel通道对象,详细的发送由实现类的第12完成
public interface IMessageProviderService {
/**
* 定义消息的推送管道
*
* @return
*/
String send();
}
@EnableBinding(Source.class)
public class MessageProviderServiceImpl implements IMessageProviderService {
/**
* 消息发送管道/信道
*/
@Resource
private MessageChannel output;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " + serial);
return serial;
}
}
第五步: controller接口
@RestController
public class SendMessageController {
@Resource
private IMessageProviderService messageProviderService;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProviderService.send();
}
}
9.4 消息消费者8802模块搭建
第一步: 创建一个maven模块,引入相关依赖,最主要的就是stream整合rabbitmq的依赖
<!--stream的rabbitmq依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
第二步: 配置文件的编写,与生产者的区别就在于bindings下的是input而不是output
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
第三步: 主程序类
@SpringBootApplication
public class CloudStreamRabbitmqConsumer8802Application {
public static void main(String[] args) {
SpringApplication.run(CloudStreamRabbitmqConsumer8802Application.class, args);
System.out.println("启动成功");
}
}
第四步: controller接口,使用url请求生产者8801,即可在消费者8802端接收到8801发送的消息
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号 ----> port:" + serverPort + "\t从8801接受到的消息是:" + message.getPayload());
}
}
两个模块搭建完成进行测试,首先启动注册中心7001,然后分别启动消息生产者8801和消息消费者8802,通过url请求访问8001的发送消息请求,会向指定管道中发送一条消息,如果此时这个管道中有消费者即可接收到这条消息。而如何指定消息的管道归属呢,就是通过配置文件中的indings.input.destination来指定,命名相同的服务就会处在同一条管道中
9.5 重复消费问题
按照之前的使用,会带来重复消费问题: 也就是说一个通道上有不止一个消息消费者,stream上默认每一个消费者都属于不同的组,这样的话就会导致这个消息被多个组的消费者重复消费
知道了问题出现的原因就很容易解决了,只要我们自定义配置分组,将这些消费者都分配到同一个组中就能避免重复消费的问题出现了(同一个组间的消费者是竞争关系,不管组间有多少的消费者都只会消费一次)
自定义分组
只需要在配置文件修改一处配置即可实现自定义组名并且自定义分组,组名相同的服务会被分配到同一组,通道内的消息数据会被该组中的所有消费者轮询消费
9.6 持久化问题
上面自定义分组使用的group配置除了可以自定义分组和分组名之外,还可以实现消息的持久化,也就是说使用group配置自定义分组和分组名的消息消费者,就算在消息生产者发送消息的时候挂掉了,等这个消费者重启之后依然是能够消费之前发送的消息
这里一个生产者和两个消费者存在以下十三种情况(生产者发送四次消息):
1、都使用group分组的两个不同组成员,在生产者生产的时候
- 都没挂(各消费四次)
- 挂了其中一个(各消费四次)
- 都挂了(各消费四次)
2、都使用group分组的两个同组成员,在生产者生产的时候
- 都没挂(各消费两次)
- 挂了其中一个(没挂的把四次消费完)
- 都挂了(各消费两次)
3、其中一个使用group分组的两个成员,在生产者生产的时候
- 都挂了(都不消费)
- group的挂了(各消费四次)
- 没group的挂了(没挂的消费四次,挂的由于没有持久化所以不消费)
- 都没挂(各消费四次)
4、都不使用group分组的两个成员,在生产者生产的时候
- 都挂了(都不消费)
- 挂了其中一个(没挂的消费四次,挂的由于没有持久化所以不消费)
- 都没挂(各消费四次)
总之一句话,通道里的消息会持久化给使用group配置的消息消费者(每一组都有一份),就算发送消息的时候这些消费者挂了,如果同组的消费者有没挂的就会把这些消息竞争消费完;如果同组没有消费者,等他重启之后还是会消费这些消息
十、注册中心、配置中心———Nacos
10.1 初识nacos
10.1.1 什么是SpringCloud Alibaba
前面已经学习过了SpringCloud为什么还要学习SpringCloud Alibaba?这是因为SpringCloud Netflix项目进入了维护模式,意味着 SpringCloud Netflix 将不再开发新的组件,维护中的组件将通过平行组件所替代。于是SpringCloud Alibaba应运而生,是目前比较主流的分布式微服务开发框架,它是在SpringCloud的基础上建立的
10.1.2 什么是Nacos?
Nacos是SpringCloud Alibaba中的一个十分重要的组件,它的名字取自Nameing Configuration Service,这也就意味着Nacos承担着服务注册和配置中心的责任,也就相当于SpringCloud中erueka+config+bus的组合版。springcloud官网如是介绍到Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
10.1.3 Nacos下载安装
下载地址:https://github.com/alibaba/nacos/tags
下载完成之后解压缩zip压缩包,即可双击bin目录下的startup.cmd文件启动nacos的单机版进行使用,启动之后的nacos可以在浏览器通过url链接http://localhost:8848/nacos访问图形化界面,默认的用户名和密码都是nacos
10.2 Nacos的使用
10.2.1 Nacos服务注册与发现
首先在使用SpringCloud Alibaba之前,必须在父项目的pom文件中引入SpringCloud Alibaba的依赖
<!--spring cloud 阿里巴巴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
服务提供方9001、9002模块
这里以9001支付模块为例,9002参考9001模块进行创建
第一步: 创建maven项目并引入nacos依赖
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第二步: 配置文件
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
#端口暴露
management:
endpoints:
web:
exposure:
include: '*'
第三步: 主启动类使用@EnableDiscoveryClient注解标注开启注册功能
@EnableDiscoveryClient
@SpringBootApplication
public class CloudAlibabaPayment9001 {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaPayment9001.class, args);
System.out.println("启动成功");
}
}
第三步: controller接口
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: " + serverPort + "\t id" + id;
}
}
服务消费者83模块
第一步: 创建maven项目并引入nacos依赖
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第二步: 配置文件
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
service-url:
nacos-user-service: http://nacos-payment-provider
第三步: 主启动类使用@EnableDiscoveryClient注解标注开启注册功能
@EnableDiscoveryClient
@SpringBootApplication
public class CloudAlibabaConsumeOrder83 {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaConsumeOrder83.class, args);
System.out.println("启动成功");
}
}
第四步: 由于使用到了ribbon的rpc调用,于是需要使用代码进行相关配置
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
第五步: controller接口
@RestController
@RequestMapping("consumer")
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
/**
* 从application.yml配置文件中读取该配置的值
*/
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
}
}
经过上述的配置,目前注册中心中一共有三个服务,分别是服务提供者9001和9002,、服务消费者83。由于nacos的依赖里整合了ribbon依赖,所以说导入nacos依赖后可以实现服务访问的负载均衡操作,在服务消费方进行RestTemplate配置后,即可rpc远程轮询两个服务提供方。于是通过反复发送 http://localhost:83/consumer/payment/nacos/23请求,即可发现数据是依次从9001和9002返回
nacos作注册中心与其他的区别
之前学习过Eureka和Zookeeper作服务的注册与发现,在CAP原则方面,Eureka选择使用AP原则,而Zookeeper选择使用CP原则,但是Nacos却可以在AP和CP原则之间进行切换,只需要使用一行PUT请求即可实现
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
10.2.2 Nacos作配置中心
基础配置
第一步: 创建maven项目并引入nacos依赖
<!--nacos-config 配置中心-自带动态刷新-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery 注册中心-服务发现与注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第二步: 配置文件,按加载顺序配置
bootstrap.yml
spring:
profiles:
active: dev
application.yml
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
config:
server-addr: localhost:8848 #配置中心地址
file-extension: yaml #指定yaml格式的配置
第三步: 主启动类使用@EnableDiscoveryClient注解标注开启注册功能
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigNacosClient3377 {
public static void main(String[] args) {
SpringApplication.run(ConfigNacosClient3377.class, args);
System.out.println("启动成功");
}
}
第四步: controller接口
@RestController
@RefreshScope // nacos的动态刷新
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
第五步: 创建远程配置文件, 之前的springcloud远程配置使用的是GitHub或者Gitee实现,nacos则可以在它的图形化界面上直接创建配置文件,配置文件名的匹配规则如下
也就是说,以本次项目为例,nacos上配置文件的命名应该是nacos-config-client-dev.yaml
启动配置服务3377的主启动类,浏览器访问 http://localhost:3377/config/info
因为nacos支持动态刷新,所以当nacos上的配置文件值进行修改后,直接刷新链接即可刷新配置的值,反观springcloud Config还需要发送post请求手动刷新才能获取到最新的配置信息
进阶配置
为什么需要进阶配置?
- 在实际开发中,通常—个系统会准备dev开发环境、test测试环境、prod生产环境...进阶配置可以保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件。
- 一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境..…那怎么对这些微服务配置进行管理呢?
nacos的配置文件有Namespace命名空间(用于区分环境)、Group分组、Data lD文件名,它使用Namespace+Group+Data lD唯一定位一个在nacos上的远程配置文件。其中如果不进行配置指定的话,Namespace默认为public、Group默认为DEFAULT_GROUP,Data lD必须通过配置文件指定,匹配规则参考基础配置篇
分组group配置
在默认public命名空间里有两个同名配置文件,但是它们的分组不同
命名空间namespace配置
配置文件bootstra.yml中配置的namespace值是命名空间ID,而不是名字
10.3 Nacos集群和持久化配置
若想要实现Nacos集群的集群配置,需要三样东西:一个Nginx集群作为VIP进行负载均衡,一个nacos集群进行远程配置,一个MySQL进行集群配置的集中式存储,也就是以下这张官方给出的Nacos集群架构图(下面的所有配置操作都在Linux中完成,官方推荐在Linux中搭建nacos集群)
10.3.1 持久化数据库切换
nacos的远程配置将配置数据存储在内置的derby中(apache的一种内嵌式数据库),如果想要部署nacos集群,肯定不能让配置数据存储在集群成员各自的derby中,不然就会造成各成员间的数据不一致的现象。为了解决这一问题,我们需要一个集中式存储的方式来支持集群化部署,目前nacos只支持MySQL的存储,于是我们只能选择使用MySQL
第一步: 找到nacos安装目录下的conf文件夹里的nacos-mysql.sql,在Navicat中创建一个新的数据库并运行该sql文件,或者是使用命令运行,运行成功就会生成11张表,这些表就存储着nacos的各种信息数据
第二步: 修改conf文件下的application.properties文件配置你的MySQL数据库连接,如果你的nacos之前配置过其他ip的数据库就注释掉,如果没有就直接把以下配置配到该文件的最下面
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://==数据库ip地址,本地就是localhost==:3306/==上一步创建的数据库名称==?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user===数据库的用户名==
db.password===数据库的密码==
重启nacos服务重新访问nacos的图形化界面url就会得到一个干干净净的没有任何配置的nacos,而且这个nacos的所有新增配置都将会存储在application.properties文件配置的MySQL数据库中
10.3.2 nacos集群配置
第一步: 复制conf文件夹下的cluster.conf.example命名为cluster.conf,修改其中配置,先使用命令hostname -i来获取Linux的ip,然后把得到的ip加自定义端口号配置到cluster.conf文件中,配置到这个文件上的三个端口号在启动后会被认为是同一个nacos集群上的成员
第二步: 复制bin文件夹下的start.sh文件命名为start.sh.bk作为备用,修改start.sh文件以达到可以通过-p命令的方式启动不同端口的nacos
第三步: 修改start.sh文件最下面的
当修改完成之后就可以通过命令./startup.sh -p XXXX来以不同的端口号启动nacos了
10.3.3 nginx负载均衡
复制nginx的conf文件夹下的nginx.conf命名为nginx.conf.bk,修改nginx.conf.中的配置
启动Nacos集群:
进入nacos的bin目录下
  **./startup.sh -p 3333**
  **./startup.sh -p 4444**
  **./startup.sh -p 5555**
启动nginx(-c指定配置文件的路径):
进入nginx的sbin目录下
./nginx -c /usr/local/nginx/conf/nginx.conf
访问192.168.159.121:1111 如果可以进入nacos的web界面,就证明安装成功了
注册进nacos集群
将服务提供者微服务注册进nacos集群(此时nacos扮演的角色是注册中心,而不是配置中心),可以通过配置文件来完成
以上操作,以9002模块视角分析就是如下情况
十一、流量控制、熔断降级——Sentinel
11.1 什么是Sentinel
官网上如是介绍到:随着微服务的流行,服务和服务之间的稳定性变得越来越重要,Sentinel以流星为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。说白了Sentinel就是hystrix(豪猪哥)的阿里翻版,他能实现和豪猪哥一样的功能,但是比之前者它还提供了图形化的操作界面,操作起来更加的方便快捷
Sentinel应用场景十分重要且丰富: 它承接了阿里巴巴近年的双十—大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
11.2 Sentinel下载安装配置
第一步: 下载Sentinel监控页面jar:https://github.com/alibaba/Sentinel/tags
第二步: 使用命令运行jar,访问管理界面(用户名密码默认为sentinel),此时要保证8080端口没有被占用,否则就会产生端口冲突
java -jar
11.3 Sentinel项目实战
初始化监控服务
该项目中会创建一个新的子模块,然后通过配置文件将其注册进nacos注册中心,且通过sentinel进行实时监控
第一步: 创建一个子模块,并引入相关依赖
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续sentinel做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
第二步: 配置文件
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
第三步: 主程序类
@EnableDiscoveryClient
@SpringBootApplication
public class CloudAlibabaSentinelService8401 {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaSentinelService8401.class, args);
System.out.println("启动成功");
}
}
第四步: controller接口,访问浏览器显示相应字符串而已,没有任何联动,主要是为了后面的
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
先启动nacos,然后启动sentinel的监控jar包,最后启动主程序类
此时sentinel测监控页面并不会有任何的服务,因为sentinel使用的是懒加载机制,只有被监控服务有被访问时图形界面才会显示相应的监控图表信息,该服务一段时间没有被访问的话之前的图表数据也会消失
11.3.1 流控规则配置
流控配置就是对微服务里接口的流量控制配置,该配置精准到微服务的具体接口,如果该接口的访问超出了手动配置的阈值,就会执行相应的结果(这里阈值和结果都是自己在页面上配置的)
上面弹窗里所有配置项的意思都在下面进行解释,当需要对一个接口的访问进行限流时,即可参照规则进行流控配置
使用QPS进行阈值设置的请求,会在一秒内达到阈值之后的所有请求选择流控效果默认都直接失败,页面返回失败信息,这些失败的请求甚至没有到请求业务就直接失败了
而使用线程数进行阈值设置的请求,在请求线程数达到阈值之后的所有请求无法选择流控效果,这些请求会打进业务但是无法执行就页面返回失败信息
流控模式: 关联
通过关联配置,关联的接口资源出现问题被限流,对应的接口资源也会被限流,比如说/testA在流控配置的时候关联了/testB,那么一旦/testB被限流/testA也同样会被限流
流控效果: Warm Up
预热,sentinel默认选择该流控效果后需要填写预热时长(只有QPS阈值才可选),配置之后,预热时长范围内阈值都是QPS对3取模,经过预热时长后阈值才恢复预定阈值
比如说使用QPS阈值,单机阈值为13,选择Warm Up效果,预热时长为5,那就意味着前5S(预热时长),接口每秒内最多可接受4次请求(13%3=4),等到过了预热时间5S之后,该接口每秒内最多可接受13(QPS)次请求
流控效果: 排队等待
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第—秒直接拒绝多余的请求。
11.3.2 熔断规则配置
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断。Sentinel和hystrix的熔断降级是非常像的,但是Sentinel并没有像hystrix一样的半开状态(也就是缓冲状态一段时间正常之后就结束熔断)
降级策略: RT
统计时长(1000ms)内,请求数超过最小请求数(5)并且在最大RT时间(200ms)内没有处理完本次任务的话,就会在未来的熔断时长(1s)内对该服务进行熔断,断路器打开微服务不可用
降级策略: 异常比例
当资源的每秒请求量大于最小请求数(5),并且每秒异常总数占通过量的比值超过比例阈值(0.2)之后,资源进入降级状态,即在接下的统计时长(1000ms)内,对该服务进行熔断,断路器打开微服务不可用,对这个方法的调用都会自动地返回。
降级策略: 异常数
当统计时间(1000ms)内的请求数量超过最小请求数(5),并且这1s中的异常数目超过异常数(10)之后会进行熔断,即在接下的统计时长(1000ms)内,对该服务进行熔断,断路器打开微服务不可用,对这个方法的调用都会自动地返回。
11.3.3 热点key配置
每统计窗口时长(1s)内,如果该接口接收到的请求中 参数列表包含索引为0的参数(也就是参数列表中的第一个参数),且数量超过单机阈值(1)的话就会对该服务进行熔断降级,对这个方法的调用都会自动地返回。
根据上面的描述,接口有两个参数p1、p2,如果请求为http://localhost:8401/testHotKey?p1=a或者http://localhost:8401/testHotKey?p1=a&p2=b不符合热点规则,将会触发熔断降级,但是http://localhost:8401/testHotKey?p2=b将不会触发降级
高级选项
想要使用热点规则的高级选项的话,必须从热点规则处新增,不能通过簇点链路新增。通过下面的配置,就可以实现p1有热点key限制阈值为1,但是如果p1旳值为abc的话阈值就是200、值为qaz的话阈值就是23.
11.3.4 系统规则配置
上面涉及到的配置都是具体到接口级的,对单个接口的流控、熔断、热点key监控配置,但是Sentinel系统规则配置是针对整个系统而言的监控配置
入口QPS
入口QPS就是对整个系统的请求数进行限制,并不指定接口请求,只要系统内所有请求的请求数到达阈值就会降级
Load自适应
该设置仅对Linux/Unix-like机器生效,系统的 load1作为启发指标,进行自适应系统保护。当系统load1超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR阶段)。系统容量由系统的 maxQps minRt估算得出。设定参考值一般是cPu cores 2.5。
CPU使用率
当系统CPU使用率超过阈值即触发系统保护(取值范围0.0-1.0) ,比较灵敏。
平均RT
当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。
并发线程数
当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
11.4 @SentinelResource注解
之前有关Sentinel的案例中,一旦违反限流规则后,都是用sentinel系统默认的提示: Blocked by Sentinel (flow limiting),我们还可以使用@SentinelResource注解来实现像hystrix一样的兜底降级方法
11.4.1 如何使用@SentinelResource
只需要使用@SentinelResource注解,value属性为保证唯一使用映射名,blockHandler使用的是兜底方法的名,兜底方法的参数列表上也要加上BlockException对象
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
@SentinelResource注解只能对不符合配置规则的异常进行熔断降级,但是如果出现其他异常(运行时异常...)还是会直接返回错误页面
此时,超出阈值之后就会调用兜底方案
==规则配置的资源名称:如果是映射路径的话降级就会返回默认的页面,就算下面 @SentinelResource注解配置的有兜底方案也不会执行;如果是@SentinelResource注解的value值,有兜底方案就会执行兜底,没有的话返回默认==
11.4.2 代码优化
上述的代码编辑像极了hystrix,每一个方法都要对应一个兜底方案,显得接口和兜底方案耦合度很高
定义一个公共的兜底方案类CustomerBlockHandler,里面有很多的兜底方案,兜底方案定义的时候要使用static修饰
public class CustomerBlockHandler {
public static CommonResult handlerException1(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----2");
}
}
controller中的接口如果想使用该类中的某个方法作为兜底方法的话,就可以使用@SentinelResource注解配置(以handlerException2为例)
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003"));
}
11.5 进阶属性配置
11.5.1 fallback和blockHandler
学习完sentinel的流控、熔断、热点配置之后,项目中的异常大概就能被分为两种,一种是业务代码出现的异常诸如运行时异常等,还有一种就是不符合sentinel规则的BlockException。对应这两种异常,@SentinelResource注解有fallback和blockHandler两种属性可以分别指定兜底方案
如果@SentinelResource注解中设置哪个属性的话,哪种异常就会走配置的兜底方案方法。都不设置的话就是业务异常走默认的Whitelabel Error Page,或者sentinel规则异常的默认提示: Blocked by Sentinel (flow limiting)。都设置的话,两种异常都满足的情况下走blockHandler的兜底方案
异常忽略属性
异常忽略属性exceptionsToIgnore可以配置可忽略的异常类,也就是说当程序出现该异常时就算配置了fallback也不会走对应的兜底方案。完整的@SentinelResource注解参考如下代码
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class, IndexOutOfBoundsException.class})
11.5.2 OpenFeign熔断
在学习如何使用sentinel进行OpenFeign的熔断之前,先让我们复习一下OpenFeign服务调用的使用流程:
(1)引入相关依赖
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)配置文件激活Sentinel对OpenFeign的支持(只有使用sentinel进行OpenFeign的熔断才需配置,其他情况下使用OpenFeign配置文件无需进行特有配置)
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
(3)主启动类开启OpenFeign支持
@EnableFeignClients
(4)service接口使用@FeignClient注解的value指定调用接口所在微服务名,fallback 指定兜底方案, @GetMapping注解指定调用接口的映射路径,接口定义和 @GetMapping注解直接从被调用接口controller方法上注解复制即可
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
(5)fallback 指定的兜底方案,@Component注解注册进spring容器,实现原接口后重写方法为兜底方案
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
(6)controller接口正常调用service接口的方法
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
11.6 配置持久化
sentinel中配置的流控、熔断、热点key规则,一旦应用进行重启之后就会消失,但是生产开发的时候需要将这些配置规则进行持久化,避免很多的配置规则再重启后重新配置。这又使用到了Nacos
将sentinel规则持久化进nacos
第一步: 引入相关依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
第二步: 配置文件配置数据库相关
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
第三步: 根据配置文件的dataId值在nacos新建一个配置,配置的值为sentinel规则对应的值
[
{
"resource": "/rateLimit/byUrl",
"limitApp" : "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior" : 0,
"clusterMode" : false
}
]
resource: sentinel的资源名;
limitApp: 来源应用;
grade: 阈值类型,0表示线程数,1表示QPS;
count: 单机阈值;
strategy: 流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior: 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
clusterMode: 是否集群。
这样配置上之后就可以实现sentinel规则的持久化,也就是说重启服务之后规则还在,但是只限配置之后的sentinel的规则,未配置的规则依然会在重启之后消失
十二、分布式事务处理——Seata
12.1 Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
12.2 一加三概念
所谓的一加三就是一个唯一ID加三个组件模型。
一个ID
- Transaction lD XID,也就是全局唯一的事务ID,XID相同的所有操作被认为是同一个事务
三个组件
- Transaction Coordinator(TC)——事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务提交或回滚。
- Transaction Manager(TM)——事务管理器,定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- Resource Manager(RM)——资源管理器,管理分支事务工作的资源,与 TC 对话以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
一加三模式的运作流程图如下
12.3 Seata的下载安装
下载地址:https://github.com/seata/seata/releases找到合适的版本点击binary下载即可
解压缩zip压缩包,修改conf目录下的file.conf配置文件,主要是三个地方:service、store的mode、store的db配置
修改配置之后根据配置信息去数据库创建相应的数据库seata,执行conf文件下的db_store.sql文件生成表数据。然后修改conf目录下的registry.conf文件,让seata注册进nacos
12.4 官网案例上手
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
12.4.1 数据库数据准备
三个数据库六张表,每个数据库中都有一张业务表和回滚日志表
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE t_order(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
count INT(11) DEFAULT NULL COMMENT '数量',
money DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
status INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE t_storage(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
total INT(11) DEFAULT NULL COMMENT '总库存',
used INT(11) DEFAULT NULL COMMENT '已用库存',
residue INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
12.4.2 订单模块
第一步: 引入相关依赖
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
第二步: 配置文件
application.yml(最重要的就是tx-service-group配置)
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group #自定义事务组名称需要与seata-server中的对应
nacos:
discovery:
server-addr: localhost:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
file.conf(service和db模块的修改)
service {
#transaction service group mapping
vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称,vgroup_mapping.fsp_tx_group保证和seatafile.conf中的my_test_tx_group名一致
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "数据库"
user = "数据库名"
password = "密码"
}
}
registry.conf(注册进nacos)
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
第三步: 实体类domain(根据order订单表创建即可)
第四步: dao接口定义业务方法,resources的mapper下创建对应的映射文件和sql语句
@Mapper
public interface OrderDao {
/**
* 新建订单
*
*/
void create(Order order);
/**
* 修改订单状态,从零改为1
*
*/
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
第五步: service定义接口和实现类
OrderService 接口
public interface OrderService {
void create(Order order);
}
StorageService接口,使用openfeign远程调用
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
AccountService 接口,使用openfeign远程调用
@FeignClient(value = "seata-account-service")
public interface AccountService {
/**
* @param userId
* @param money
* @return
*/
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
OrderService 接口实现类
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
* 注释掉 @GlobalTransactional 的时候,需要注意下方这个方法里面手动模拟了延时,也需要注释掉
* com.atguigu.springcloud.alibaba.service.impl.AccountServiceImpl#decrease(java.lang.Long, java.math.BigDecimal)
*/
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
第六步: controller接口
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "订单创建成功");
}
}
第七步: 主启动类
/**
* exclude = DataSourceAutoConfiguration.class 取消数据源的自动创建,
* 读取自定义的DataSourceProxyConfig.class类,使用Seata对数据源进行代理
* @EnableDiscoveryClient 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
*/
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderService2001Application {
public static void main(String[] args) {
SpringApplication.run(SeataOrderService2001Application.class, args);
System.out.println("启动成功");
}
}
上述代码可知OrderService 接口实现类中涉及到了多个业务操作,于是就要进行事务管理,使用@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)注解,指定业该方法中遇到Exception异常就对多个事务进行回滚操作