SpringCloud 是微服务一站式服务解决方案,微服务全家桶。它是微服务开发的主流技术栈。它采用了名称,而非数字版本号。
s方法gCloud 和 springCloud Alibaba 目前是最主流的微服务框架组合。
版本选择:
选用 springboot 和 springCloud 版本有约束,不按照它的约束会有冲突。
本次学习的各种软件的版本:
Cloud Release Train | Boot Version |
---|---|
Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
查看版本对应关系:https://start.spring.io/actuator/info
"spring-cloud": {
"Finchley.M2": "Spring Boot >=2.0.0.M3 and <2.0.0.M5",
"Finchley.M3": "Spring Boot >=2.0.0.M5 and <=2.0.0.M5",
"Finchley.M4": "Spring Boot >=2.0.0.M6 and <=2.0.0.M6",
"Finchley.M5": "Spring Boot >=2.0.0.M7 and <=2.0.0.M7",
"Finchley.M6": "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1",
"Finchley.M7": "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2",
"Finchley.M9": "Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE",
"Finchley.RC1": "Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE",
"Finchley.RC2": "Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE",
"Finchley.SR4": "Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT",
"Finchley.BUILD-SNAPSHOT": "Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3",
"Greenwich.M1": "Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE",
"Greenwich.SR6": "Spring Boot >=2.1.0.RELEASE and <2.1.18.BUILD-SNAPSHOT",
"Greenwich.BUILD-SNAPSHOT": "Spring Boot >=2.1.18.BUILD-SNAPSHOT and <2.2.0.M4",
"Hoxton.SR8": "Spring Boot >=2.2.0.M4 and <2.3.5.BUILD-SNAPSHOT",
"Hoxton.BUILD-SNAPSHOT": "Spring Boot >=2.3.5.BUILD-SNAPSHOT and <2.4.0.M1",
"2020.0.0-M3": "Spring Boot >=2.4.0.M1 and <=2.4.0.M1",
"2020.0.0-SNAPSHOT": "Spring Boot >=2.4.0.M2"
},
尚硅谷阳哥教程版本:
cloud | Hoxton.SR1 |
boot | 2.2.2.RELEASE |
cloud alibaba | 2.1.0.RELEASE |
java | java8 |
maven | 3.5及以上 |
mysql | 5.7及以上 |
cloud版本决定了boot版本
1,Eureka停用,可以使用zk作为服务注册中心
2,服务调用,Ribbon准备停更,代替为LoadBalance
3,Feign改为OpenFeign
4,Hystrix停更,改为resilence4j
或者阿里巴巴的sentienl
5.Zuul改为gateway
6,服务配置Config改为 Nacos
7,服务总线Bus改为Nacos
参考资料,尽量去官网
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/
写一个下图的Hello World
构建父工程,后面的项目模块都在此工程中:
设置编码:Settings -> File Encodings
注解激活:
Java版本确定:
<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.0modelVersion>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud2020artifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<junit.version>4.12junit.version>
<log4j.version>1.2.17log4j.version>
<lombok.version>1.16.18lombok.version>
<mysql.version>5.1.47mysql.version>
<druid.version>1.1.16druid.version>
<mybatis.spring.boot.version>1.3.0mybatis.spring.boot.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-project-info-reports-pluginartifactId>
<version>3.0.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.2.2.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR1version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.spring.boot.version}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>${junit.version}version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>${log4j.version}version>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
project>
上面配置的解释:
首先要加
这个。
pom 为了让项目顺利的运行,我们必须使用统一的版本号;
1、dependencyment
(1)在我们项目中,我们会发现在父模块的pom文件中常常会出现dependencyMent元素,这是因为我们可以通过其来管理子模块的版本号,也就是说我们在父模块中声明号依赖的版本,但是并不实现引入;
2、dependencies
(1)上面说到dependencyment只是声明一个依赖,而不实现引入,故我们在子模块中也需要对依赖进行声明,倘若不声明子模块自己的依赖,是不会从父模块中继承的;只有子模块中也声明了依赖。并且没有写对应的版本号它才会从父类中继承;并且version和scope都是取自父类;此外要是子模块中自己定义了自己的版本号,是不会继承自父类的。
3、总结
dependencyment只是用来管理依赖,规定未添加版本号的子模块依赖继承自它,dependencies是用来声明子模块自己的依赖,可以在其中来写自己需要的版本号聚合版本依赖,dependencyManagement 只声明依赖,并不实现引入,所以子项目还需要写要引入的依赖。
可以统一版本
父工程创建完成执行mvn:install将父工程发布到仓库方便子工程继承
创建一个module后(只能改a),在父工程的pom里多了个
,
在模块的pom里没有gv,只有a。
模块里的
里的依赖只有ga,没有v
cloud-provider-payment8001 子工程的pom文件:
这里面的 lombok 这个包,引入以后,实体类不用再写set 和 get
可以如下写实体类:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor public class Payment implements Serializable { private Integer id; private String serial; }
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020artifactId>
<groupId>com.dkf.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-provider-payment8001artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
cloud-provider-payment8001 子工程的yml文件:
server:
port: 8001
spring:
application:
name: cloud-provider-payment8001
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/cloud2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.dkf.springcloud.entities # 所有Entity 别名类所在包
cloud-provider-payment8001 子工程的主启动类:
package com.dkf.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args){
SpringApplication.run(PaymentMain8001.class, args);
}
}
下面的常规操作:
①建表SQL
create table `payment`(
`id` bigint(20) not null auto_increment comment 'ID',
`serial` varchar(200) default '',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
select * from payment;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data //set/get方法
@AllArgsConstructor //有参构造器
@NoArgsConstructor //无参构造器
public class Payment implements Serializable {
private long id;//数据库是bigint
private String serial;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//返回给前端的通用json数据串
@Data //set/get方法
@AllArgsConstructor //有参构造器
@NoArgsConstructor //无参构造器
public class CommonResult<T> {
private Integer code;
private String message;
private T data; //泛型,对应类型的json数据
//自定义两个参数的构造方法
public CommonResult(Integer code, String message){
this(code, message, null);
}
}
@Mapper // 是ibatis下面的注解 //@Repositoty有时候会有问题
public interface PaymentDao {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
resource下创建mapper文件夹,新建PaymentMapper.xml。在yml里有所有entity别名类所在包,所有payment不用写全类名
<mapper namespace="com.xzq.springcloud.dao.PaymentDao">
<resultMap id="BaseResultMap" type="com.xzq.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
resultMap>
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values (#{serial})
insert>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id = #{id}
select>
mapper>
@Param注解:https://blog.csdn.net/qq_39505065/article/details/90550705
import com.xzq.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Param;
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
import com.xzq.springcloud.dao.PaymentDao;
import com.xzq.springcloud.entities.Payment;
import com.xzq.springcloud.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PaymentServiceImpl implements PaymentService {
@Autowired
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
package com.dkf.springcloud.controller;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import com.dkf.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController //必须是这个注解,因为是模拟前后端分离的restful风格的请求,要求每个方法返回 json
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping(value = "/payment/create")
// 注意这里的 @RequestBody 是必须要写的,虽然 MVC可以自动封装参数成为对象,
// 但是当消费者项目调用,它传参是 payment 整个实例对象传过来的, 即Json数据,因此需要写这个注解
// https://blog.csdn.net/weixin_38004638/article/details/99655322
public CommonResult create(@RequestBody Payment payment){
int result = paymentService.create(payment);
log.info("****插入结果:" + result);
if(result > 0){
return new CommonResult(200, "插入数据库成功", result);
}
return new CommonResult(444, "插入数据库失败", null);
}
@GetMapping(value = "/payment/{id}")
public CommonResult getPaymentById(@PathVariable("id")Long id){
Payment result = paymentService.getPaymentById(id);
log.info("****查询结果:" + result);
if(result != null){
return new CommonResult(200, "查询成功", result);
}
return new CommonResult(444, "没有对应id的记录", null);
}
}
对应POST方式的请求,要学会用POSTMAN
工具
微服务多了之后就使用run dashboard
不但编译有个别地方会报错,启动也会报错,但是测试两个接口都是没问题的,推测启动报错是因为引入了下面才会引入的jar包,目前不影响。
代码改动后希望自动生效
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
重启IDEA
热部署只允许在开发阶段使用
新建模块cloud-consumer-order80
消费者现在只模拟调用提供者的Controller方法,没有持久层配置,只有Controller和实体类
当然也要配置主启动类和启动端口
pom文件:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020artifactId>
<groupId>com.dkf.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-customer-order80artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
把CommonResult 和 Payment 两个 实体类也创建出来
application.yml
server:
port:80 # 80端口就可以省略了
OrderMain80.java
@SpringbootApplication
public class OrderMain80{
public static void main(String[] args){
SpringApplication.run(OrderMain80.class,args);
}
}
entites包中的类也拷贝到本项目中
ApplicationContextConfig 内容:
package com.dkf.springcloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;//网络客户端
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
/*
RestTemplate提供了多种便捷访问远程http服务的方法,
是一种简单便捷的访问restful服务模板类,是spring提供的用于rest服务的客户端模板工具集
*/
}
}
package com.dkf.springcloud.controller;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderController {
//远程调用的 地址
public static final String PAYMENY_URL = "http://localhost:8001";
@Resource
private RestTemplate restTemplate;
@PostMapping("customer/payment/create")
public CommonResult<Payment> create (Payment payment){
return restTemplate.postForObject(PAYMENY_URL + "/payment/create",//请求地址
payment,//请求参数
CommonResult.class);//返回类型
}
@GetMapping("customer/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
return restTemplate.getForObject(PAYMENY_URL + "/payment/" + id,//请求地址
CommonResult.class);//返回类型
}
}
如果 runDashboard 控制台没有出来,右上角搜索 即可
运用spring cloud框架基于spring boot构建微服务,一般需要启动多个应用程序,在idea开发工具中,多个同时启动的应用
需要在RunDashboard运行仪表盘中可以更好的管理,但有时候idea中的RunDashboard窗口没有显示出来,也找不到直接的开启按钮
idea中打开Run Dashboard的方法如下
view > Tool Windows > Run Dashboard
如果上述列表找不到Run Dashboard,则可以在工程目录下找到.idea文件夹下的workspace.xml,在其中相应位置加入以下代码(替换)即可:
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType"/>
set>
option>
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule"/>
RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule"/>
RuleState>
list>
option>
component>
上面 两个子项目,有多次重复的 导入 jar,和重复的 Entity 实体类。可以把 多余的部分,加入到一个独立的模块中,将这个模块打包,并提供给需要使用的 module
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020artifactId>
<groupId>com.dkf.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-api-commonsartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.1.0version>
dependency>
dependencies>
project>
mvn跳过test,mvc clean,mvn install
将此项目打包 install 到 maven仓库。
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
如果是上面只有两个微服务,通过 RestTemplate ,是可以相互调用的,但是当微服务项目的数量增大,就需要服务注册中心。目前没有学习服务调用相关技术,使用 SpringCloud 自带的 RestTemplate 来实现RPC
什么是服务治理:
SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
什么是服务注册与发现:
Eureka采用了CS的设计结构,Eureka Server服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。这点和zookeeper很相似
在服务注册与发现中,有一个注册中心。当服务器启却时候,会把当前自己服务器的信息比如服务地址適讯地址等以别名方式注册到注册中心上。另一方(消费者服务提供者),以该别名的方式去注册中心上获取到实际的服务適讯地址然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为便用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
官方停更不停用,以后可能用的越来越少。
Eureka 是 Netflix 开发的,一个基于 REST 服务的,服务注册与发现的组件,以实现中间层服务器的负载平衡和故障转移。
Eureka 分为 Eureka Server 和 Eureka Client及服务端和客户端。Eureka Server为注册中心,是服务端,而服务提供者和消费者即为客户端,消费者也可以是服务者,服务者也可以是消费者。同时Eureka Server在启动时默认会注册自己,成为一个服务,所以Eureka Server也是一个客户端,这是搭建Eureka集群的基础。
- Eureka Client:一个Java客户端,用于简化与 Eureka Server 的交互(通常就是微服务中的客户端和服务端)。通过注册中心进行访问。是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(roundrobin)负载算氵去的负载均衡器
在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)- Eureka Server:提供服务注册服务,各个微服务节,通过配置启动后,会在Eureka Serverc中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点信息,服务节点的信息可以在界面中直观看到。
服务在Eureka上注册,然后每隔30秒发送心跳来更新它们的租约。如果客户端不能多次续订租约,那么它将在大约90秒内从服务器注册表中剔除。注册信息和更新被复制到集群中的所有eureka节点。来自任何区域的客户端都可以查找注册表信息(每30秒发生一次)来定位它们的服务(可能在任何区域)并进行远程调用
服务提供者向注册中心注册服务,并每隔30秒发送一次心跳,就如同人还活着存在的信号一样,如果Eureka在90秒后还未收到服务提供者发来的心跳时,那么它就会认定该服务已经死亡就会注销这个服务。这里注销并不是立即注销,而是会在60秒以后对在这个之间段内“死亡”的服务集中注销,如果立即注销,势必会对Eureka造成极大的负担。这些时间参数都可以人为配置。
Eureka还有自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,所以不会再接收心跳,也不会删除服务。
客户端消费者会向注册中心拉取服务列表,因为一个服务器的承载量是有限的,所以同一个服务会部署在多个服务器上,每个服务器上的服务都会去注册中心注册服务,他们会有相同的服务名称但有不同的实例id,所以拉取的是服务列表。我们最终通过负载均衡来获取一个服务,这样可以均衡各个服务器上的服务。
消费者端口80,提供者端口8001。
Eureka端口7001
pom
<artifactId>cloud-eureka-server7001artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
版本说明:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
dependency>
application.yml
server:
port: 7001
eureka:
instance:
hostname: localhost # eureka 服务端的实例名称
client:
# false 代表不向服务注册中心注册自己,因为它本身就是服务中心
register-with-eureka: false
# false 代表自己就是服务注册中心,自己的作用就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与 Eureka Server 交互的地址,查询服务 和 注册服务都依赖这个地址
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
@EnableEurekaServer
最后写主启动类,如果启动报错,说没有配置 DataSource ,就在 主启动类的注解加上 这样的配置:
// exclude :启动时不启用 DataSource的自动配置检查
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaServer // 表示它是服务注册中心
public class EurekaServerMain7001 {
public static void main(String[] args){
SpringApplication.run(EurekaServerMain7001.class, args);
}
}
启动测试,访问 7001 端口
这里的提供者,还是使用 上面的 cloud-provider-payment8001 模块,做如下修改:
<artifactId>cloud-provider-payment8001artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
@EnableEurekaClient
spring:
application:
name: cloud-payment-service # 项目名,也是注册的名字
eureka:
client:
# 注册进 Eureka 的服务中心
register-with-eureka: true
# 检索 服务中心 的其它服务
fetch-registry: true
service-url:
# 设置与 Eureka Server 交互的地址
defaultZone: http://localhost:7001/eureka/
这里的消费者 也是上面 的 cloud-customer-order80 模块
<artifactId>cloud-customer-order80artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
@EnableEurekaClient
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka/
spring:
application:
name: cloud-order-service
Eureka Server在设计的时候就考虑了高可用设计,在Eureka服务治理设计中,所有节点既是服务的提供方,也是服务的消费方,服务注册中心也不例外。
Eureka Server的高可用实际上就是将自己做为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。
Eureka Server的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。可以采用两两注册的方式实现集群中节点完全对等的效果,实现最高可用性集群,任何一台注册中心故障都不会影响服务的注册与发现。
问题:微服务RPC远程服务调用最核心的是什么:
高可用,试想你的注册中心只有一个。onlyone,它出故障了那就呵呵了,会导致整个为服务环境不可用,所以要搭建Eureka注册中心集群,实现负载均衡+故障容错
Eureka 集群的原理:相互注册,互相守望。每台Eureka服务器都有集群里其他Eureka服务器地址的信息
开始构建Eureka集群:
现在创建 cloud-eureka-server7002 ,也就是第二个 Eureka 服务注册中心,pom 文件和 主启动类,与第一个Server一致。
模拟多个 为了不用输出C:\Windows\System32\drivers\etc\hosts 添加如下:
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
现在修改这两个 Server 的 yml 配置:
7001 端口的Server yml文件:
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com # eureka 服务器的实例地址
client:
register-with-eureka: false
fetch-registry: false
service-url:
## 一定要注意这里的地址,这是搭建集群的关键。反过来写,写的是集群中其他Eureka服务器的地址
defaultZone: http://eureka7002.com:7002/eureka/
7002 端口的Server yml文件:
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com # eureka 服务器的实例地址
client:
register-with-eureka: false
fetch-registry: false
service-url:
## 一定要注意这里的地址 这是搭建集群的关键
defaultZone: http://eureka7001.com:7001/eureka/
eureka.instance.hostname 才是启动以后 本 Server 的注册地址,而 service-url 是 map 类型,只要保证 key:value 格式就行,它代表 本Server 指向了那些 其它Server 。利用这个,就可以实现Eureka Server 相互之间的注册,从而实现集群的搭建。
上面配置了多个Eureka作为集群,接下来要配置的是提供者集群,让提供者高可用
为提供者cloud-provider-payment8001
模块创建集群,新建模块名为 cloud-provider-payment8002
即两个提供者8001和8002
其余配置都一致,需要配置集群的配置如下:
配置区别要点:
spring:application:name:
要一致@EnableDiscoveryClient
注解是基于spring-cloud-commons
依赖,并且在classpath中实现;@EnableEurekaClient
注解是基于spring-cloud-netflix
依赖,只能为eureka作用;消费者(一般需要连接其他微服务的服务或者gateway/zuul)
# 提供者
server:
port: 8001 # 端口号不一样
spring:
application:
name: cloud-provider-service # 这次重点是这里,两个要写的一样,这是这个集群的关键
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/cloud2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.dkf.springcloud.entities
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
注意在 Controller 返回不同的消息,从而区分者两个提供者的工作状态。(只是为了学习测试才这么做,生产环境直接复制即可)
在提供者的controller中
@Value("${server.port}")
private String serverPort;
此时消费者一旦消费完之后,他以后访问的还是那台提供者。明显不对,原因在于消费者并没有去Rureka里找服务,而是自己找的
就是消费者如何访问 由这两个提供者组成的集群?
Eureka Server 上的提供者的服务名称如下:
@RestController
@Slf4j
public class OrderController {
// 重点是这里,改成 提供者在Eureka 上的名称,而且无需写端口号
public static final String PAYMENY_URL = "http://CLOUD-PROVIDER-SERVICE";//取决于我们在提供者出配置的name,CLOUD-PAYMENY-SERVICE,//同时要注意使用@LoadBalanced注解赋予RestTemplate负载均衡能力
@Resource
private RestTemplate restTemplate;
@PostMapping("customer/payment/create")
public CommonResult<Payment> create (Payment payment){
return restTemplate.postForObject(PAYMENY_URL + "/payment/create", payment, CommonResult.class);
}
@GetMapping("customer/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
return restTemplate.getForObject(PAYMENY_URL + "/payment/" + id, CommonResult.class);
}
}
server:
port: 80
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: false
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机
#defaultZone: http://localhost:7001/eureka
# 集群
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
@LoadBalanced
还有,消费者里面对RestTemplate配置的config文件,需要更改成如下:(就是加一个注解 @LoadBalanced
)
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //这个注解,就赋予了RestTemplate 负载均衡的能力
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
这时候,消费者消费的提供者多次访问就会变化了(这就是Ribbon的负载平衡功能)
为了在微服务Eureka控制台能看到我们的某个具体服务是在哪台服务器上部署的,我们需要配置一些内容。
修改 提供者在Eureka 注册中心显示的 主机名:即修改eureka:instance:instance-id:
和eureka:instance:prefer-ip-address:
# 提供者
server:
port: 8001 # 端口号不一样
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
instance: #重点,和client平行
instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
prefer-ip-address: true # 可以显示ip地址
@EnableDiscoveryClient
对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。(即我们前面可视化页面的信息)
@EnableDiscoveryClient
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/customer/discovery")
public Object discovery(){
//获得服务清单列表
List<String> services = discoveryClient.getServices();
for(String service: services){
log.info("*****service: " + service);
}
// 根据具体服务进一步获得该微服务的信息
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-ORDER-SERVICE");
for(ServiceInstance serviceInstance:instances){
log.info(serviceInstance.getServiceId() + "\t" + serviceInstance.getHost()
+ "\t" + serviceInstance.getPort() + "\t" + serviceInstance.getUri());
}
return this.discoveryClient;
}
某时刻某一个微服务不可用了,Eureka不会立即清理,依旧会对该微服务的信息进行保存。属于CAP里的AP分支
保护模式主要用于一组客户和Eureka Server之间存在网络分区场景下保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中固定信息,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段提示,则说明Eureka讲入了保护模式:
EMERGENCY!EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE
为什么会产生Eureka自我保护机制?
为了防止Eureka Client可以正常运行但是与Eureka Server网络不通情况下,Eureka Server不会立刻将Eureka Client服务剔除
什么是自我保护模式?
默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了—因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过"自我保护模式"来解决这个问题—当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
自我保护机制:默认情况下Eureka CIient定时向Eureka Server端发送心跳包。
如畀Eureka在server端在一定时间内(默认90秒)没有收到Eureka Client发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间(90秒中)内丢矢了大量的服务实例心跳,这时Eureka Server会开启目我保护机制,不令剔除该服务(该现象可能出现如果网络不通但是Eureka Client出现宕机,此时如果别的注册中心如果一定时间内没有收到心跳会将剔除该服务这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问韙而产生的
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注册任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
禁止自我保护:(如果想)
在 Eureka Server 的模块中的 yml 文件进行配置:
server: port: 7001 eureka: instance: hostname: eureka7001.com client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://eureka7002.com:7002/eureka/ server: # 与client平行 # 关闭自我保护机制,保证不可用该服务被及时剔除 enable-self-preservation: false eviction-interval-timer-in-ms: 2000
修改 Eureka Client 模块的 心跳间隔时间:
# 提供者
server:
port: 8001 # 端口号不一样
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url: # 集群
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
instance: #重点,和client平行
instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
prefer-ip-address: true # 可以显示ip地址
# Eureka客户端像服务端发送心跳的时间间隔,单位s,默认30s
least-renewal-interval-in-seconds: 1
# Rureka服务端在收到最后一次心跳后等待时间上线,单位为s,默认90s,超时将剔除服务
least-expiration-duration-in-seconds: 2
在注册服务之后,服务提供者会维护一个心跳用来持续高速Eureka Server,“我还在持续提供服务”,否则Eureka Server的剔除任务会将该服务实例从服务列表中排除出去。我们称之为服务续约。
面是服务续约的两个重要属性:
(1)eureka.instance.lease-expiration-duration-in-seconds
leaseExpirationDurationInSeconds,表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance。
默认为90秒
如果该值太大,则很可能将流量转发过去的时候,该instance已经不存活了。
如果该值设置太小了,则instance则很可能因为临时的网络抖动而被摘除掉。
该值至少应该大于leaseRenewalIntervalInSeconds
(2)eureka.instance.lease-renewal-interval-in-seconds
leaseRenewalIntervalInSeconds,表示eureka client发送心跳给server端的频率。如果在leaseExpirationDurationInSeconds后,server端没有收到client的心跳,则将摘除该instance。除此之外,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。
默认30秒
*eureka.client.registry-fetch-interval-seconds* :表示eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
*eureka.server.enable-self-preservation*
是否开启自我保护模式,默认为true。
默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
*eureka.server.eviction-interval-timer-in-ms*
eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒
Eureka停更说明:
2.0后停更了。
springCloud 整合 zookeeper
新建模块cloud-provider-payment8004
pom文件如下:
<artifactId>cloud-provider-payment8004artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yaml
server:
port: 8004
spring:
application:
name: cloud-provider-service
cloud:
zookeeper:
connect-string: 192.168.40.100:2181 # zk地址
主启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 以后用这个就可以了,不用eureka了
public class PaymentMain8004 {
public static void main(String[] args){
SpringApplication.run(PaymentMain8004.class, args);
}
}
Controller 打印信息:
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@RequestMapping("/payment/zk")
public String paymentzk(){
return "springcloud with zookeeper :" + serverPort + "\t" + UUID.randomUUID().toString();
}
}
如果 zookeeper 的版本和导入的jar包版本不一致,启动就会报错,由zk-discovery和zk之间的jar包冲突的问题。
解决这种冲突,需要在 pom 文件中,排除掉引起冲突的jar包,添加和服务器zookeeper版本一致的 jar 包,
但是新导入的 zookeeper jar包 又有 slf4j 冲突问题,于是再次排除引起冲突的jar包
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
启动测试:
# 在zk客户端
ls /services # 输入
[cloud-provider-service] #输出
# 继续向下查看
ls /services/cloud-provider-service
# 继续向下,然后get,返回了个json
我们在zk上注册的node是临时节点,当我们的服务一定时间内没有发送心跳,那么zk就会将这个服务的znode删除了。没有自我保护机制。重新建立连接后znode-id号也会变
创建测试zookeeper作为服务注册中心的 消费者 模块 cloud-customerzk-order80
主启动类、pom文件、yml文件和提供者的类似
config类,注入 RestTemplate
@SpringBootConfiguration
public class ApplicationContextConfig {
@Bean
@LoadBalanced // 继续加上这个
public RestTemplate getTemplate(){
return new RestTemplate();
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class OrderZKMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderZKMain80.class, args);
}
}
controller层也是和之前类似:
@RestController
@Slf4j
public class CustomerZkController {
public static final String INVOKE_URL="http://cloud-provider-service"; //和原来一样
@Resource
private RestTemplate restTemplate;
@RequestMapping("/customer/payment/zk")
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk",String.class);
return result;
}
}
然后就在zk里查到consumer信息了。
关于 zookeeper 的集群搭建,目前使用较少,而且在 yml 文件中的配置也是类似,以列表形式写入 zookeeper 的多个地址即可,而且zookeeper 集群,在 hadoop的笔记中也有记录。总而言之,只要配合zookeeper集群,以及yml文件的配置就能完成集群搭建
后面会用ribbon代替RestTemplate
consul也是服务注册中心的一个实现,是由go语言写的。官网地址: https://www.consul.io/intro 中文地址: https://www.springcloud.cc/spring-cloud-consul.html
Consul是一套开源的分布式服务发现和配置管理系统。
提供了微服务系统中的服务治理,配置中心,控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网络。
下载地址:https://www.consul.io/downloads.html
打开下载的压缩包,只有一个exe文件,实际上是不用安装的,在exe文件所在目录打开dos窗口使用即可。
使用开发模式启动:consul agent -dev
访问8500端口,即可访问首页
新建提供者模块:cloud-providerconsul-service8006
pom 文件:
<artifactId>cloud-providerconsul-service8006artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml 文件:
server:
port: 8006
spring:
application:
name: consul-provider-service
cloud:
consul:
host: localhost
port: 8500
discovery: # 指定注册对外暴露的服务名称
service-name: ${
spring.application.name}
主启动类:
@SpringBootApplication
@EnableDiscoveryClient // 提供者
public class ConsulProviderMain8006 {
public static void main(String[] args) {
SpringApplication.run(ConsulProviderMain8006.class,args);
}
}
controller也是简单的写一下就行。
新建 一个 在82端口的 消费者模块。pom和yml和提供者的类似,主启动类不用说,记得注入RestTemplate
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class OrderConsulMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
controller层:
@RestController
public class CustomerConsulController {
public static final String INVOKE_URL="http://consul-provider-service";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/customer/payment/consul")
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul",String.class);
return result;
}
}
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集合 |
---|---|---|---|---|---|
Eureka | java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | java | CP | 支持 | 客户端 | 已集成 |
CAP:
CAP理论关注粒度是数据,而不是整体系统设计的
RestTemplate不是给我们提供远程调用了吗,那还要学其他的做什么。答案是负载均衡,在发送请求时通过负载均衡算法从提供者列表中选择一个。
我们这里提前启动好之前在搭建的 eureka Server 集群(5个模块)
SpringCloud Ribbon是基于NetfIixRibbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Neix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出LoadBalancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
LB负载均衡(LoadBalance)是什么?
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
常见的负载均衡有软件Nginx,LVS,硬件F5等。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别:
Nginx是服务器负载均衡(集中式LB),客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon是本地负载均衡(进程内LB),在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器!
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Ribbon在工作时分成两步:
上面在eureka时,确实实现了负载均衡机制,那是因为 eureka-client包里面自带着ribbon:
一句话,Ribbon 就是 负载均衡 + RestTemplate 调用。实际上不止eureka的jar包有,zookeeper的jar包,还有consul的jar包都包含了他,就是上面使用的服务调用。
如果自己添加,在 模块的 pom 文件中引入:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
对于RestTemplate 的一些说明:
有两种请求方式:post和get ,还有两种返回类型:object 和 Entity
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.lb.LoadBalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.net.URI;
import java.util.List;
@RestController
@Slf4j
public class OrderController{
//public static final String PAYMENT_URL = "http://localhost:8001";
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Resource
private RestTemplate restTemplate;
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENT_URL +"/payment/create",payment,CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
// ResponseEntity是spring中的类
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();//获取json
}else{
return new CommonResult<>(444,"操作失败");
}
}
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() <= 0){
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
// ====================> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin(){
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
}
IRule:根据特定算法从服务列表中选择一个要访问的服务
Ribbon 负载均衡规则类型:
配置负载均衡规则:
官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
注意上面说的,而Springboot主启动类上的 @SpringBootApplication 注解,相当于加了@ComponentScan注解,会自动扫描当前包及子包,所以注意不要放在SpringBoot主启动类的包内。
创建包:
在这个包下新建 MySelfRule类:
package com.dkf.myrule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MySelfRule {
@Bean
public IRule myrule(){
return new RandomRule(); //负载均衡规则定义为随机
}
}
然后在主启动类上添加如下注解 @RibbonClient:
package com.dkf.springcloud;
import com.dkf.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name="CLOUD-PROVIDER-SERVICE", configuration = MySelfRule.class)//指定该负载均衡规则对哪个提供者服务使用 , 加载自定义规则的配置类
public class OrderMain80 {
public static void main(String[] args){
SpringApplication.run(OrderMain80.class, args);
}
}
负载均衡轮询算法 :
rest接口第几次请求次数 % 服务器集群总数量 = 实际调用服务器位置下标
每次服务器重启后,rest接口计数从1开始。
ribbon源码:
private AtomicInteger nextServerCyclicCounter;
public Server choose(ILoadBalancer lb, Object key) {
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();//获取原子的值
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next)) //CAS
return next;
}
}
手写负载算法:cas+自旋
首先8001、8002服务controller层加上
@GetMapping("/payment/lb")
public String getPaymentLB() {
return SERVER_PORT;
}
LoadBalancer接口:
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
实现
import org.springframework.cloud.client.ServiceInstance;
import java.sql.SQLOutput;
@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
private final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!atomicInteger.compareAndSet(current, next));
System.out.println("第几次访问,次数next:" + next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
controller类中添加:
@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");//获得总的提供者数
if (instances == null || instances.size() <= 0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);//传入总的实例数
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
这里和之前学的dubbo很像,例如消费者的controller 可以调用提供者的 service层方法,但是不一样,它貌似只能调用提供者的 controller,即写一个提供者项目的controller的接口,消费者来调用这个接口方法,就还是相当于是调用提供者的 controller ,和RestTemplate 没有本质区别
Feign能干什么:
Feign旨在使编写JavaHttp客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接囗会被多处调用,所以通常都会针对每个微服务自行封装些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可,即可完成对服务提供方的接口绑定,简化了使用Springcloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并目通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口目以声明式的方法,优雅而简单的实现了服务调用
Feign | OpenFeign |
---|---|
Feign是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign是SpringCloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的下的接囗,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 |
org.springframework.cloud spring-cloud-starter-feign | org.springframework.cloud spring-cloud-starter-openfeign |
新建cloud-consumer-feign-order80模块
feign用在消费端,feign自带负载均衡配置,所以不用手动配置
pom :
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
yaml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
@EnableFeignClients
主启动类:
@SpringBootApplication
@EnableFeignClients //关键注解
public class CustomerFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(CustomerFeignMain80.class, args);
}
}
@FeignClient
新建一个service
这个service还是 customer 模块的接口,和提供者没有任何关系,不需要包类名一致。它使用起来就相当于是普通的service。
推测大致原理,对于这个service 接口,读取它某个方法的注解(GET或者POST注解不写报错),知道了请求方式和请求地址,而抽象方法,只是对于我们来讲,调用该方法时,可以进行传参等。
@Component //别忘了添加这个
@FeignClient(value = "CLOUD-PROVIDER-SERVICE") //服务名称,要和eureka上面的一致才行
public interface PaymentFeignService
{
//这个就是provider 的controller层的方法定义。
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
}
/*
在提供端有:
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id)
{
Payment payment = paymentService.getPaymentById(id);
if(payment != null)
{
return new CommonResult(200,"查询成功,serverPort: "+serverPort,payment);
}else{
return new CommonResult(444,"没有对应记录,查询ID: "+id,null);
}
}
*/
Controller层:
//使用起来就相当于是普通的service。
@RestController
public class CustomerFeignController {
@Resource
private PaymentFeignService paymentFeignService;//动态代理
@GetMapping("/customer/feign/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}
超时设置,故意设置超时演示出错情况:
Openfeign默认超时等待为一秒,在消费者里面配置超时时间
//8001提供方
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout(){
// 业务逻辑处理正确,但是需要耗费3秒钟
try {
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
e.printStackTrace(); }
return serverPort;
}
//消费方80
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
// OpenFeign客户端一般默认等待1秒钟
return paymentFeignService.paymentFeignTimeout();
}
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
Feign共了日志打印功能,我们可以诵过配置来调整日志级别,从而了解Feign中Tttp请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出。
日志级别:
首先写一个config配置类:
package com.atguigu.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig{
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
然后在yml文件中开启日志打印配置:
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
主要是服务降级、服务熔断、服务限流的开发思想和框架实现
官方地址:https://github.com/Netflix/Hystrix/wiki/How-To-Use
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出"
如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,
谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器“本身是一种开关装置,当某个服务单元发生故障之后,涌过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
Hystrix停止更新,进入维护阶段:https://github.com/Netflix/Hystrix
https://github.com/Netflix/Hystrix/wiki/How-To-Use
fallback
服务器忙碌或者网络拥堵时,不让客户端等待并立刻返回一个友好提示,fallback。
对方系统不可用了,你需要给我一个兜底的方法,不要耗死。
向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
降低发生的情况:
break
flowlimit
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟几个,有序进行
可见,上面的技术不论是消费者还是提供者,根据真实环境都是可以加入配置的。
首先构建一个eureka作为服务中心的单机版微服务架构 ,这里使用之前eureka Server 7001模块,作为服务中心
新建 提供者 cloud-provider-hystrix-payment8001
模块:
pom 文件:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
下面主启动类、service、和controller代码都很简单普通。
主启动类:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
service层:
@Service
public class PaymentService {
/* 可以正常访问的方法*/
public String paymentinfo_Ok(Integer id){
return "线程池:" + Thread.currentThread().getName() + "--paymentInfo_OK,id:" + id;
}
/* 超时访问的方法 */
public String paymentinfo_Timeout(Integer id){
int interTime = 3;
try{
TimeUnit.SECONDS.sleep(interTime);//模拟超时
}catch (Exception e){
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + "--paymentInfo_Timeout,id:" + id +
"耗时" + interTime + "秒钟--";
}
}
controller层:
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/{id}")
public String paymentInfo_OK(@PathVariable("id")Integer id){
log.info("paymentInfo_OKKKKOKKK");
return paymentService.paymentinfo_Ok(id);
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id")Integer id){
log.info("paymentInfo_timeout");
return paymentService.paymentinfo_Timeout(id);
}
}
JMeter
这里使用一个新东西 JMeter 压力测试器,模拟多个请求
下载压缩包,解压,双击 /bin/ 下的 jmeter.bat 即可启动
ctrl + S 保存后,输入请求地址开始压测。
从测试可以看出,当模拟的超长请求被高并发以后,访问普通的小请求速率也会被拉低。
tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理。
上面还是服务8001自己测试,加入此时外部的消费者80页来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死。
测试可见,当启动高并发测试时,消费者访问也会变得很慢,甚至出现超时报错。
演示问题:8001端口自身已经被打满了,80还要访问8001,80也响应慢了。
解决思路:
新建消费者 cloud-customer-feign-hystrix-order80
模块:以feign为服务调用,eureka为服务中心的模块,
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
主启动类
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80{
//消费端
public static void main(String[] args)
{
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
一般服务降级放在客户端,即 消费者端 ,但是提供者端一样能使用。
首先提供者,即8001 先从自身找问题,设置自身调用超时的峰值,峰值内正常运行,超出峰值需要有兜底的方法处理,作服务降级fallback
@HystrixCommand
首先 对 8001
的service进行配置(对容易超时的方法进行配置) :
降级配置:@HystrixCommand
,可以在里面指定超时/出错的回调方法,作为兜底方法
提供方 service方法:演示超时
// 服务端service
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",//超时后回调方法
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
value="3000")})//超时时间
public String paymentInfo_TimeOut(Integer id){
try {
TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) {
e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){
// 回调函数向调用方返回一个符合预期的、可处理的备选响应
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
提供方 主启动类:
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//加上这个
public class PaymentHystrixMain8001{
//提供者
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
上面演示的是超时,下面演示上面service出错:同样走一样的回调方法
// 服务端service
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",//超时后回调方法,出错后也走
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
value="3000")})//超时时间
public String paymentInfo_TimeOut(Integer id){
int age = 10/0;
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){
// 回调函数向调用方返回一个符合预期的、可处理的备选响应
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
上面的案例是服务端降级,现在我们服务端处理3s,然后返回。但是消费端等1s就等不住了,这时候就需要消费端也有降级方法
80
的降级。原理是一样的,上面的@HystrixCommand降级可以放在服务端,也可以放在消费端。但一般放在客户端。
注意:我们自己配置过的热部署方式对java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务。
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
@SpringBootApplication
@EnableFeignClients
@EnableHystrix // 加上这个 //注意区别我们在提供端添加的注解是@EnableCircuitBreaker
public class OrderHystrixMain80{
public static void main(String[] args){
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
然后对 80 进行服务降级:很明显 service 层是接口,所以我们对消费者,在它的 controller 层进行降级。继续使用@HystrixCommand
注解指定方法超时后的回调方法
controller
@RestController
@Slf4j
public class OrderHystirxController{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
value="1500") })//只等1.5s
//@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
// int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o";
}
}
//然后我们把上面的 int age=10/0;打开,出错后也会调用它的兜底方法
service
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,fallback = PaymentFallbackService.class)
public interface PaymentHystrixService{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
目前问题:
我们定义一个全局的兜底方法,这样就不用每个方法都得写兜底方法了。
@DefaultProperties
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") //添加了这个
public class OrderHystirxController{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
value="1500") })//只等1.5s
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
// int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o";
}
// 下面是全局fallback方法
public String payment_Global_FallbackMethod(){
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
// 自己指定了兜底方法的话就走自己的兜底方法,否则走全局的兜底方法
@FeignClient
下面解决业务逻辑混在一起的问题(解耦):我们改在service层进行服务降级
服务降级,客户端去调用服务端,碰上服务端宕机或关闭。
本次案例降级处理是在客户端80实现完成的,与服务端8001没有关系。只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。
在这种方式一般是在客户端,即消费者端,首先上面再controller中添加的 @HystrixCommand 和 @DefaultProperties 两个注解去掉。就是保持原来的controller
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,
fallback = PaymentFallbackService.class)//指定的是回调的class类
public interface PaymentHystrixService{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
service回调方法:是service接口的实现类。此时就不需要在controller上写fallback方法了
@Component
public class PaymentFallbackService implements PaymentHystrixService{
// 统一为该接口异常处理
@Override
public String paymentInfo_OK(Integer id){
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}
@Override //兜底方法,根据上述配置,程序内发生异常、或者运行超时,都会执行该兜底方法
public String paymentInfo_TimeOut(Integer id){
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
}
此时的yml文件配置
server:
port: 80
spring:
application:
name: cloud-customer-feign-hystrix-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
# 用于服务降级 在注解@FeignClient 中添加 fallback 属性值
feign:
hystrix:
enabled: true # 在feign中开启 hystrix
@Component // 这里是重点
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = OrderFallbackService.class)
public interface OrderService {
@GetMapping("/payment/hystrix/{id}")
public String paymentInfo_OK(@PathVariable("id")Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id")Integer id);
}
package com.dkf.springcloud.service;
import org.springframework.stereotype.Component;
@Component //注意这里,它实现了service接口
public class OrderFallbackService implements OrderService{
@Override
public String paymentInfo_OK(Integer id) {
return "OrderFallbackService --发生异常";
}
@Override
public String paymentInfo_Timeout(Integer id) {
return "OrderFallbackService --发生异常--paymentInfo_Timeout";
}
}
新问题,这样配置如何设置超时时间?
首先要知道 下面两个 yml 配置项:
hystrix.command.default.execution.timeout.enable=true ## 默认值 ## 为false则超时控制有ribbon控制,为true则hystrix超时和ribbon超时都是用,但是谁小谁生效,默认为true hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000 ## 默认值 ## 熔断器的超时时长默认1秒,最常修改的参数
看懂以后,所以:
只需要在yml配置里面配置 Ribbon 的 超时时长即可。注意:hystrix 默认自带 ribbon包。
ribbon: ReadTimeout: xxxx ConnectTimeout: xxx
实际上服务熔断 和 服务降级 没有任何关系,就像 java 和 javaScript
服务熔断,有点自我恢复的味道
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后对应服务降级的方法并返回友好提示。
就是保险丝:服务的降级->进而熔断->恢复调用链路。
熔断机制概述:
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该微服务的调用,快速返回错误的响应信息。
当检查到该结点微服务调用响应正常后,恢复调用链路。
在SpringCloud框架里,熔断机制通过Hystrix
实现。Hystrix会监控微服务间调用的情况,
当失败的调用到一定阈值(缺省是5秒内20次调用失败),就会启动熔断机制。熔断机制的注解是@HystrixCommand
修改cloud-provider-hystrix-payment8001模块
package com.atguigu.springcloud.service;
import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
@Service
public class PaymentService{
//=====服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",
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"),// 失败率达到多少后跳闸
}) // 在10s内10次请求有60%失败 // 先看次数,再看百分比
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();// 等价于UUID.randomUUID().toString(); //pom中有hutool-all
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
//服务降级
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
}
涉及到断路器的三个重要参数快照时间窗、请求总数阀值、错误百分比阀值
The precise way that the circuit opening and closing occurs is as follows:
- Assuming the volume across a circuit meets a certain threshold (
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)…- And assuming that the error percentage exceeds the threshold error percentage (
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)…- Then the circuit-breaker transitions from
CLOSED
toOPEN
.- While it is open, it short-circuits all requests made against that circuit-breaker.
- After some amount of time (
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
), the next single request is let through (this is theHALF-OPEN
state). If the request fails, the circuit-breaker returns to theOPEN
state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions toCLOSED
and the logic in 1. takes over again.经过一段时间后,如果有1个尝试成功了,就慢慢尝试恢复参考:https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker
controller:
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id")Integer id){
return paymentService.paymentCircuitBreaker(id);
}
实验效果为,多次出错调用fallback后,调用正常的也出错调用fallback。过了一会又自己恢复了。
展望:以后用的是Sentinel代替。
关于解耦以后的全局配置说明:
例如上面提到的全局服务降级,并且是feign+hystrix整合,即 service 实现类的方式,如何做全局配置?
上面有 做全局配置时,设置超时时间的方式,我们可以从中获得灵感,即在yml文件中 进行熔断配置:
hystrix: command: default: circuitBreaker: enabled: true requestVolumeThreshold: 10 sleepWindowInMilliseconds: 10000 errorThresholdPercentage: 60
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s08qjlCg-1602605634458)(images\1597556913055.png)]除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(HystrixDashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream实现了对以上指标的监控。SpringCloud也提供了HystrixDashboard的整合,对监控内容转化成可视化界面。
新建模块 cloud-hystrix-dashboard9001 :
pom 文件:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml文件只需要配置端口号,主启动类加上这样注解:@EnableHystrixDashboard
启动测试:访问 http://ocalhost:9001/hystrix
下面使用上面 9001 Hystrix Dashboard 项目,来监控 8001 项目 Hystrix 的实时情况:
注意8001被监控的时候pom里要actuator依赖,此外还要有
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9UT1qhv-1602605634460)(images\1597557620438.png)]
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(streamServlet);
servletRegistrationBean.setLoadOnStartup(1);
servletRegistrationBean.addUrlMappings("/hystrix.stream");
servletRegistrationBean.setName("HystrixMetricsStreamServlet");
return servletRegistrationBean;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jf1q8jOi-1602605634461)(images\1597558323714.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pq9A9S1j-1602605634463)(images\1597558486510.png)]
内容过多,开发可参考 https://docs.spring.io/ 官网文档
SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring5.O+Springboot 2.0和ProjectReactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
SpringCloudGateway作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版本的zuul2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
springCloudGateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
一方面因为Zuul 1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的是亲儿子产品,值得信赖。而且很多功能比zull用起来都简单便捷。
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
多方面综合考虑Gateway是很理想的网关选择。
SpringCloudGateway与Zuul的区别:
在SpringCloudFinchley正式版之前,SpringCloud推荐的网关是Netflix提供的Zuul:
Zuull.x
springcloud中所集成的zuul版本,采用的是tomcat容器,使用的是传统的servlet IO处理模型。
学过尚硅谷web中期课程都知道一个题目,Servlet的生命周期,servlet由servlet container进行生命周期管理
上述模式的缺点:
servlete—个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。
在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势
所以Zuul 1.x是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以springcloudzuul无法摆脱servlet模型的弊端。
传统的Web框架比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。
但是,在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使
用java8)
SpringWebFlux是Spring5.0引入的新的响应式框架区别于SpringMVC,它不需要依赖ServletAPI,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。
Gateway特性:
GateWay的三大核心概念:
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;
而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
客户端向Spring Cloud Gateway发出请求。然后在Gateway HandlerMapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler
Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(post)执行业务逻辑。
Filter在pre类型的过滤器可以做参数校验,权限校验,流量监听,日志输出,协议转换等,
在post类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
新建模块 cloud-gateway-gateway9527
现在实现,通过Gateway (网关) 来访问其它项目,这里选择之前8001项目,要求注册进Eureka Server 。其它没要求。
pom文件:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml文件:
server:
port: 9527
spring:
application:
name: cloud-gateway
## GateWay配置
cloud:
gateway:
routes: #多个路由
- id: payment_routh # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址 #uri+predicates # 要访问这个路径得先经过9527处理
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
# 注册进 eureka Server # 网关他本身也是一个微服务,也要注册进注册主中心
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
register-with-eureka: true
fetch-registry: true
如果IDEA出现了bug,yml配置文件没有变为小叶子,就去structure里设置下模块的spring,选择该yml
主启动类
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class,args);
}
}
8001看看controller的访问地址,我们目前不想暴露8001端口,希望在8001外面套一层9527。这样别人就攻击不了8001,有网关挡着
访问测试:
可见,当我们访问 http://localhost:9527/payment/get/1 时,即访问网关地址时,会给我们转发到 8001 项目的请求地址,以此作出响应。
加入网关前:http://localhost:8001/payment/get/1
加入网关后:http://localhost:9527/payment/get/1
上面是以 yml 文件配置的路由,也有使用config类配置的方式:
@Configuration
public class GateWayConfig{
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
// 分别是id,本地址,转发到的地址
routes.route("path_route_atguigu",
r -> r.path("/guonei").uri("http://news.baidu.com/guonei")
).build();//JDK8新特性
return routes.build();
}
}
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: "+new Date());
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);
}
@Override
public int getOrder() {
return 0;
}
}
这里所谓的动态配置就是利用服务注册中心,来实现 负载均衡 的调用 多个微服务。
默认情况下gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
注意,这是GateWay 的负载均衡
对yml进行配置:让其先通过gateway,再通过gateway去注册中心找提供者
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 # 匹配后提供服务的路由地址
uri: lb://CLOUD-PROVIDER-SERVICE # lb 属于GateWay 的关键字,代表是动态uri,即代表使用的是服务注册中心的微服务名,它默认开启使用负载均衡机制
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 # 匹配后提供服务的路由地址
uri: lb://CLOUD-PROVIDER-SERVICE
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
# uri: lb://CLOUD-PROVIDER-SERVICE 解释:lb 属于GateWay 的关键字,代表是动态uri,即代表使用的是服务注册中心的微服务名,它默认开启使用负载均衡机制
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
下面可以开启 8002 模块,并将它与8001同微服务名,注册到 Eureka Server 进行测试。
注意到上面yml配置中,有个predicates 属性值。
具体使用:
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
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/lb/** # 断言,路径相匹配的进行路由
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
predicates下面可以有多个属性,表示多个属性与操作为true这个路由才生效
predicates属性下的After属性
predicates:
- Path=/payment/lb/**
- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] # 会在这个时间之后这个路由才生效
predicates属性下的Cookie属性
predicates:
- Cookie=username,zzyy,ch.p # 需要有这个Cookie值才生效 最后一个是正则表达式
# curl 地址 --cookie "a=b"
predicates属性下的Header属性
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org # Host Route Predicate Factory接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则
# 放爬虫思路,前后端分离的话,只限定前端项目主机访问,这样可以屏蔽大量爬虫。
例如我加上: - Host=localhost:** ** 代表允许任何端口
就只能是主机来访
更多属性可以参考:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#configuring-route-predicate-factories-and-gateway-filter-factories
需要注意的是value部分写的是正则表达式
高并发工具:jmeter、postman、curl
配置错误页面:
注意,springboot默认/static/error/ 下错误代码命名的页面为错误页面,即 404.html
而且不需要导入额外的包,Gateway 里面都有。
生命周期:
种类:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue # 添加了个请求头X-Request-red
主要是配置全局自定义过滤器,其它的小配置具体看官网吧
2个主要接口:implements GlobalFilter,Ordered
场景:
自定义全局过滤器配置类:
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
log.info("********** come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//合法性检验
if(uname == null) {
log.info("*******用户名为null,非法用户,o(╥﹏╥)o,请求不被接受");
//设置 response 状态码 因为在请求之前过滤的,so就算是返回NOT_FOUND 也不会返回错误页面
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
//完成请求调用
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);//过滤链放行
}
// 返回值是加载顺序,一般全局的都是第一位加载
@Override
public int getOrder() {
return 0;
}
}
SpringCloud Config 分布式配置中心
微服务意味着要将单应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信户.能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
springCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理。比如数据库的信息,我们可以写到一个统一的地方。
是什么:
SpringCloudConfig为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中
心化的外部配置。
怎么玩:
SpringCloudConfig分为服务端和客户端两部分。
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容
能干嘛:
首先在github上新建一个仓库 springcloud-config
[email protected]:名字/项目.git
然后使用git命令克隆到本地,命令:git clone https://github.com/LZXYF/springcloud-config
注意上面的操作不是必须的,只要github上有就可以,克隆到本地只是修改文件。
新建 cloud-config-center3344 模块:
pom文件:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml 配置:
server:
port: 3344
spring:
application:
name: cloud-config-center # 注册进eureka Server 的微服务名
cloud:
config:
server:
git:
uri: https://github.com/LZXYF/springcloud-config # github 仓库位置
## 搜索目录
search-paths:
- springcloud-config
# 读取的分支
label: master
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
主启动类:
@SpringBootApplication
@EnableConfigServer //关键注解
public class ConfigCenterMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class,args);
}
}
添加模拟映射:【C:\Windows\System32\drivers\etc\hosts】文件中添加: 127.0.0.1 config-3344.com
启动微服务3344,访问http://config-3344.com:3344/master/config-dev.yml 文件(注意,要提前在git上弄一个这文件)
文件命名和访问的规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHkwyddB-1602605634467)(images\1597646186970.png)]
不加分支名默认是master:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWprQb1X-1602605634469)(images\1597646308915.png)]
这里的客户端指的是,使用 Config Server 统一配置文件的项目。既有之前说的消费者,又有提供者
新建 cloud-config-client-3355 模块:
pom文件:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
**** bootstrap.yml文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsVbqpwh-1602605634470)(images\1597646809831.png)]
内容:
server:
port: 3355
spring:
application:
name: config-client
cloud:
# config 客户端配置
config:
label: master # 分支名称
name: config # 配置文件名称,文件也可以是client-config-dev.yml这种格式的,这里就写 client-config
profile: dev # 使用配置环境
uri: http://config-3344.com:3344 # config Server 地址
# 综合上面四个 即读取配置文件地址为: http://config-3344.com:3344/master/config-dev.yml
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
主启动类,极其普通:
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
controller层,测试读取配置信息
package com.dkf.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
启动测试完成!如果报错,注意github上的 yml 格式有没有写错!
问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-747vScMB-1602605634472)(images\1597649646383.png)]
就是github上面配置更新了,config Server 项目上是动态更新的,但是,client端的项目中的配置,目前还是之前的,它不能动态更新,必须重启才行。
解决:
client端一定要有如下依赖:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jwUHlqqA-1602605634475)(images\1597649915331.png)]
client 端增加 yml 配置如下,即在 bootstrap.yml 文件中:
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNLqCQxE-1602605634475)(images\1597650236798.png)]
到此为止,配置已经完成,但是测试仍然不能动态刷新,需要下一步。
如 curl -X POST “http://localhost:3355/actuator/refresh”
两个必须:1.必须是 POST 请求,2.请求地址:http://localhost:3355/actuator/refresh
成功!
但是又有一个问题,就是要向每个微服务发送一次POST请求,当微服务数量庞大,又是一个新的问题。
就有下面的消息总线!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvAk9QNt-1602605634477)(images\1597651086179.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JjdzMfYx-1602605634480)(images\1597651183100.png)]
在windows 上安装RabbitMQ
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Wxuhyip-1602605634483)(images\1597723463350.png)]
还是按照之前的 3344(config Server)和 3355(config client)两个项目来增进。
首先给 config Server 和 config client 都添加如下依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
config Server 的yml文件增加如下配置:
# rabbitMq的相关配置
rabbitmq:
host: localhost
port: 5672 # 这里没错,虽然rabbitMQ网页是 15672
username: guest
password: guest
# rabbitmq 的相关配置2 暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
config Client 的yml文件修改成如下配置:(注意对齐方式,和config Server不一样)
spring:
application:
name: config-client
cloud:
# config 客户端配置
config:
label: master # 分支名称
name: client-config # 配置文件名称
profile: test # 使用配置环境
uri: http://config-3344.com:3344 # config Server 地址
# 综合上面四个 即读取配置文件地址为: http://config-3344.com:3344/master/config-dev.yml
# rabbitMq的相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
可在github上修改yml文件进行测试,修改完文件,向 config server 发送 请求:
【curl -X POST “http://localhost:3344/actuator/bus-refresh”】
注意,之前是向config client 一个个发送请求,但是这次是向 config Server 发送请求,而所有的config client 的配置也都全部更新。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s1XEgLNs-1602605634484)(images\1597725204568.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzgcAIgF-1602605634487)(images\1597725365978.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdU8jD0L-1602605634488)(images\1597725567239.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1mYtU5k-1602605634492)(images\1597725597884.png)]
就像 JDBC 形成一种规范,统一不同数据库的接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uN2cuHW-1602605634494)(images\1597725703365.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlntUr64-1602605634495)(images\1597726446212.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YEOdO1ur-1602605634498)(images\1597730581088.png)]
新建模块 cloud-stream-rabbitmq-provider8801
pom依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
yml 配置:
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: # 表示是生产者,向rabbitMQ发送消息
destination: studyExchange # 表示要使用的Exchange名称
content-type: application/json # 设置消息类型,本次是json,文本是 "text/plain"
binder: defaultRabbit # 设置要绑定的消息服务的具体配置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳时间,默认是30秒
lease-expiration-duration-in-seconds: 5 # 最大心跳间隔不能超过5秒,默认90秒
instance-id: send-8801.com # 在信息列表显示主机名称
prefer-ip-address: true # 访问路径变为ip地址
主启动类没什么特殊的注解。
业务类:(此业务类不是以前的service,而实负责推送消息的服务类)
package com.dkf.springcloud.service;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import javax.annotation.Resource;
import java.util.UUID;
@EnableBinding(Source.class) // 不是和controller打交道的service,而是发送消息的推送服务类
public class IMessageProviderImpl implements IMessageProvider {
//上面是自定义的接口
@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 null;
}
}
controller:
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage(){
return messageProvider.send();
}
}
启动Eureka Server 7001,再启动8801,进行测试,看是否rabbitMQ中有我们发送的消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xyb4EeOG-1602605634501)(images\1597730112088.png)]
新建模块 cloud-stream-rabbitmq-consumer8802
pom依赖和生产者一样。
yml配置: 在 stream的配置上,和生产者只有一处不同的地方,output 改成 input
server:
port: 8802
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: # 服务的整合处理
input: # 表示是消费者,这里是唯一和生产者不同的地方,向rabbitMQ发送消息
destination: studyExchange # 表示要使用的Exchange名称
content-type: application/json # 设置消息类型,本次是json,文本是 "text/plain"
binder: defaultRabbit # 设置要绑定的消息服务的具体配置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳时间,默认是30秒
lease-expiration-duration-in-seconds: 5 # 最大心跳间隔不能超过5秒,默认90秒
instance-id: receive-8802.com # 在信息列表显示主机名称
prefer-ip-address: true # 访问路径变为ip地址
接收消息的业务类:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
public class ConsumerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
System.out.println("消费者1号,serverport: " + serverPort + ",接受到的消息:" + message.getPayload());
}
}
新建 cloud-stream-rabbitmq-consumer8802 模块:
8803 就是 8802 clone出来的。
当运行时,会有两个问题。
第一个问题,两个消费者都接收到了消息,这属于重复消费。例如,消费者进行订单创建,这样就创建了两份订单,会造成系统错误。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oBY5GgB-1602605634503)(images\1597731525531.png)]
Stream默认不同的微服务是不同的组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tpEcPiZI-1602605634507)(images\1597731630685.png)]
对于重复消费这种问题,导致的原因是默认每个微服务是不同的group,组流水号不一样,所以被认为是不同组,两个都可以消费。
解决的办法就是自定义配置分组:
消费者 yml 文件配置:
# 8802 的消费者
bindings:
input:
destination: studyExchange
content-type: application/json
binder: defaultRabbit
group: dkfA # 自定义分组配置
# 8803 的消费者
bindings:
input:
destination: studyExchange
content-type: application/json
binder: defaultRabbit
group: dkfB # 自定义分组配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLYA3GGu-1602605634509)(images\1597732035990.png)]
当两个消费者配置的 group 都为 dkfA 时,就属于同一组,就不会被重复消费。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnJjGMZn-1602605634510)(images\1597732238270.png)]
加上group配置,就已经实现了消息的持久化。
分布式请求链路跟踪,超大型系统。需要在微服务模块极其多的情况下,比如80调用8001的,8001调用8002的,这样就形成了一个链路,如果链路中某环节出现了故障,我们可以使用Sleuth进行链路跟踪,从而找到出现故障的环节。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IucfN4kz-1602605634512)(images\1597732562780.png)]
sleuth 负责跟踪,而zipkin负责展示。
zipkin 下载地址: http://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/2.12.9/zipkin-server-2.12.9-exec.jar
使用 【java -jar】 命令运行下载的jar包,访问地址:【 http://localhost:9411/zipkin/ 】
使用之前的 提供者8001 和 消费者80
分别给他们引入依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
yml增加配置:
spring:
zipkin:
base-url: http://localhost:9411 # zipkin 地址
sleuth:
sampler:
# 采样率值 介于0-1之间 ,1表示全部采集
probability: 1
alibaba 的 github上有中文文档
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QID2y2Fv-1602605634517)(images\1597735215211.png)]
Nacos = Eureka + Config + Bus
github地址: https://github.com/alibaba/Nacos
Nacos 地址: https://nacos.io/zh-cn/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqnUVTx4-1602605634520)(images\1597755893534.png)]
nacos可以切换 AP 和 CP ,可使用如下命令切换成CP模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNGLyEqi-1602605634522)(images\1597756097369.png)]
下载地址: https://github.com/alibaba/nacos/releases/tag/1.1.4
直接下载网址: https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.zip
下载压缩包以后解压,进入bin目录,打开dos窗口,执行startup命令启动它。
可访问 : 【 http://192.168.101.105:8848/nacos/index.html】地址,默认账号密码都是nacos
新建模块 cloudalibaba-provider-payment9001
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml 配置:
server:
port: 9001
spring:
application:
name: nacos-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
主启动类没有特殊的注解。
Nacos 自带负载均衡机制,下面创建第二个提供者。
新建 cloudalibaba-provider-payment9003 提供者模块,clone 9001 就可以
新建消费者 模块: cloudalibaba-customer-order80
pom依赖和主启动类没有好说的,和提供者一致,yml依赖也是类似配置,作为消费者注册进nacos服务中心。
nacos底层也是ribbon,注入ReatTemplate
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller :
@RestController
public class OrderController {
//在yml里面写的提供者服务路径, 值为:http://nacos-provider
@Value("${service-url.nacos-user-service}")
private String nacos_user_service;
@Resource
private RestTemplate restTemplate;
@GetMapping("customer/nacos/{id}")
public String orderId(@PathVariable("id")Integer id){
return restTemplate.getForObject(nacos_user_service + "/payment/nacos/" + id, String.class);
}
}
nacos 还可以作为服务配置中心,下面是案例,创建一个模块,从nacos上读取配置信息。
nacos 作为配置中心,不需要像springcloud config 一样做一个Server端模块。
新建模块 cloudalibaba-nacos-config3377
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
主启动类也是极其普通:
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabaConfigMain3377 {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaConfigMain3377.class,args);
}
}
***bootstrap.yml 配置:
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos作为服务注册中心
config:
server-addr: localhost:8848 # nacos作为服务配置中心
file-extension: yaml # 指定yaml 格式的配置
controller 层进行读取配置测试:
@RestController
@RefreshScope //支持Nacos的动态刷新
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("configclient/getconfiginfo")
public String getConfigInfo(){
return configInfo;
}
}
下面在 Nacos 中添加配置文件,需要遵循如下规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hb9gcZBP-1602605634523)(images\1597757341383.png)]
从上面可以看到重要的一点,配置文件的名称第二项,spring.profiles.active 是依据当前环境的profile属性值的,也就是这个值如果是 dev,即开发环境,它就会读取 dev 的配置信息,如果是test,测试环境,它就会读取test的配置信息,就是从 spring.profile.active 值获取当前应该读取哪个环境下的配置信息。
所以要配置spring.profiles.active,新建application.yml文件,添加如下配置:
spring:
profiles:
active: dev # 表示开发环境
综合以上说明,和下面的截图,Nacos 的dataid(类似文件名)应为: nacos-config-client-dev.yaml (必须是yaml)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUB2YDIO-1602605634525)(images\1597757775084.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sgRNbeHf-1602605634529)(images\1597757747480.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ddr77Hy4-1602605634530)(images\1597758227367.png)]
当修改配置值,会发现 3377 上也已经修改,Nacos自带自动刷新功能!
其它说明:
Nacos 的 Group ,默认创建的配置文件,都是在DEFAULT_GROUP中,可以在创建配置文件时,给文件指定分组。
yml 配置如下,当修改开发环境时,只会从同一group中进行切换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9K5Umy1V-1602605634531)(images\1597807821441.png)]
Nacos 的namespace ,默认的命名空间是public ,这个是不允许删除的,可以创建一个新的命名空间,会自动给创建的命名空间一个流水号。
在yml配置中,指定命名空间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LLp0NLVh-1602605634533)(images\1597808181104.png)]
最后,dataid、group、namespace 三者关系如下:(不同的dataid,是相互独立的,不同的group是相互隔离的,不同的namespace也是相互独立的)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8Bb1JyG-1602605634535)(images\1597808385154.png)]
上面只是小打小闹,下面才是真正的高级操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDNiwcOF-1602605634538)(images\1597808678658.png)]
搭建集群必须持久化,不然多台机器上的nacos的配置信息不同,造成系统错乱。它不同于单个springcloud config,没有集群一说,而且数据保存在github上,也不同于eureka,配置集群就完事了,没有需要保存的配置信息。
Nacos默认使用它自带的嵌入式数据库derby:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u8zno8tc-1602605634542)(images\1597809107487.png)]
Nacos持久化配置:
在 nacos的 conf目录下,有个nacos-mysql.sql 的sql文件,创建一个名为【nacos_config】的数据库,执行里面内容,在nacos_config数据库里面创建数据表。
找到conf/application.properties 文件,尾部追加如下内容:
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf-8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=123456
重启nacos,即完成持久化配置。
现在进行企业中真正需要的nacos集群配置,而不是上面的单机模式,需要准备如下:
一台linux虚拟机:nginx服务器,3个nacos服务,一个mysql数据库。
nginx的安装参考之前学,使用 ContOs7 至少需要安装gcc库,不然无法编译安装【yum install gcc】
nacos下载linux版本的 tar.gz 包:https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.tar.gz
mysql root用户密码为 Dkf!!2020
Nacos集群配置
首先对 nacos 进行持久化操作,操作如上面一致。
修改 nacos/conf 下的cluster文件,最好先复制一份,添加如下内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nV6PgwvB-1602605634543)(images\1597812518508.png)]
模拟三台nacos服务,编辑nacos的startup启动脚本,使他能够支持不同的端口启动多次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IjKxZeP3-1602605634545)(images\1597812716080.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwF0tFkb-1602605634550)(images\1597812799242.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FP7JCzF-1602605634553)(images\1597813020494.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhnnjQHC-1602605634556)(images\1597815675706.png)]
nginx配置负载均衡:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFI68atZ-1602605634571)(images\1597813917440.png)]
测试完成!
使用 9003 模块注册进Nacos Server 并获取它上面配置文件的信息,进行测试。
sentinel在 springcloud Alibaba 中的作用是实现熔断和限流
下载地址: https://github.com/alibaba/Sentinel/releases/download/1.7.1/sentinel-dashboard-1.7.1.jar
下载jar包以后,使用【java -jar】命令启动即可。
它使用 8080 端口,用户名和密码都为 : sentinel
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmGHPc4i-1602605634586)(images\1597817544663.png)]
新建模块 cloudalibaba-sentinel-service8401 ,使用nacos作为服务注册中心,来测试Sentinel的功能。
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml 配置:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
# 服务注册中心
server-addr: localhost:8848
sentinel:
transport:
# 配置 Sentinel Dashboard 的地址
dashboard: localhost:8080
# 默认8719 ,如果端口被占用,端口号会自动 +1,提供给 sentinel 的监控端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
写一个简单的主启动类,再写一个简单的controller测试sentinel的监控。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5tzZVnw-1602605634589)(images\1597819388174.png)]
流控模式–直接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeqwzZgm-1602605634592)(images\1597819546992.png)]
限流表现:当超过阀值,就会被降级。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LlboWlwy-1602605634595)(images\1597819613273.png)]
流控模式–关联:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaF8Zz8q-1602605634596)(images\1597819976569.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7w8WdSsC-1602605634599)(images\1597820015308.png)]
流控效果–预热:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VyEIQkXM-1602605634607)(images\1597820393038.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-42Z2u3MH-1602605634608)(images\1597820534168.png)]
流控效果–排队等待:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoQH1QoM-1602605634609)(images\1597820662461.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A1vzy2Bi-1602605634612)(images\1597820926895.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L6KmNoHf-1602605634621)(images\1597820943934.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8mvTCf4-1602605634623)(images\1597820987861.png)]
降级策略–RT:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5RpVZdE-1602605634625)(images\1597821156100.png)]
降级策略–异常比例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exCylZyj-1602605634629)(images\1597821313361.png)]
降级测录–异常数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RuPik2u-1602605634633)(images\1597821525073.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsVtHckW-1602605634636)(images\1597821548056.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zweU8Oxs-1602605634638)(images\1597821618735.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQbpwmVE-1602605634641)(images\1597821753416.png)]
controller层写一个demo:
@GetMapping("/testhotkey")
@SentinelResource(value = "testhotkey", blockHandler = "deal_testhotkey")
//这个value是随意的值,并不和请求路径必须一致
//在填写热点限流的 资源名 这一项时,可以填 /testhotkey 或者是 @SentinelResource的value的值
public String testHotKey(
@RequestParam(value="p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2
){
return "testHotKey__success";
}
//类似Hystrix 的兜底方法
public String deal_testhotkey(String p1, String p2, BlockException e){
return "testhotkey__fail";
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01KJBNfk-1602605634645)(images\1597822501876.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMi0HoR8-1602605634646)(images\1597822772165.png)]
说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLAbC1Sf-1602605634648)(images\1597822972448.png)]
一般配置在网关或者入口应用中,但是这个东西有点危险,不但值不合适,就相当于系统瘫痪。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FShhs1zB-1602605634650)(images\1597900777242.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RSLcMMs-1602605634652)(images\1597900843477.png)]
@SentinelResource 注解,主要是指定资源名(也可以用请求路径作为资源名),和指定降级处理方法的。
例如:
package com.dkf.springcloud.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RateLimitController {
@GetMapping("/byResource") //处理降级的方法名
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource(){
return new CommonResult(200, "按照资源名限流测试0K", new Payment(2020L,"serial001"));
}
//降级方法
public CommonResult handleException(BlockException e){
return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r7tooa6i-1602605634657)(images\1597901945492.png)]
很明显,上面虽然自定义了兜底方法,但是耦合度太高,下面要解决这个问题。
写一个 CustomerBlockHandler 自定义限流处理类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLgYZIya-1602605634658)(images\1597903188558.png)]
之前有 open-feign 和 hystrix 的整合,现在来实现sentinel 整合 ribbon + open-feign + fallback 进行服务熔断。
新建三个模块,两个提供者 9004、9005,和一个消费者 84
目的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMS5NFkg-1602605634660)(images\1597905761214.png)]
上面使用sentinel有一个很明显的问题,就是sentinel,对程序内部异常(各种异常,包括超时)这种捕捉,显得很乏力,它主要是针对流量控制,系统吞吐量,或者是异常比例这种,会发生降级或熔断,但是当程序内部发生异常,直接返回给用户错误页面,根本不会触发异常比例这种降级。所以才需要整合open-feign 来解决程序内部异常时,配置相应的兜底方法
-----------------------------------------------------------两个提供者模块一致,如下:
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml配置:
server:
port: 9005 # / 9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
主启动类只是启动,没有其它注解。
controller :
package com.dkf.sprIngcloud.controller;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
//模拟sql查询
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "xcxcxcxcxcxcxcxcxcxcxcxcxc11111111"));
hashMap.put(2L, new Payment(2L, "xcxcxcxcggggggggg2222222222222222"));
hashMap.put(3L, new Payment(3L, "xcxcxcxccxxcxcfafdgdgdsgdsgds33333"));
}
@GetMapping("/payment/get/{id}")
public CommonResult paymentSql(@PathVariable("id")Long id){
Payment payment = hashMap.get(id);
CommonResult result = new CommonResult(200, "from mysql, server port : " + serverPort + " ,查询成功", payment);
return result;
}
}
---------------------------------------------------------------------------------消费者:
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml配置:
server:
port: 84
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
application:
name: nacos-order-consumer
主启动类不用说了。
config类里面注入 Resttemplate:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller 层:
@RestController
public class OrderController {
private static final String PAYMENT_URL="http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@GetMapping("/consutomer/payment/get/{id}")
public CommonResult getPayment(@PathVariable("id")Long id){
if(id >= 4){
throw new IllegalArgumentException("非法参数异常...");
}else {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
}
上面只实现了 以nacos 作为服务注册中心,消费者使用ribbon 实现负载均衡调用提供者的效果。
只配置 fallback:
@GetMapping("/consutomer/payment/get/{id}")
@SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
public CommonResult getPayment(@PathVariable("id")Long id){
if(id >= 4){
throw new IllegalArgumentException("非法参数异常...");
}else {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
//兜底方法
public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
return new CommonResult(414, "---非法参数异常--", e);
}
业务异常会被 fallback 处理,返回我们自定义的提示信息,而如果给它加上流控,并触发阈值,只能返回sentinel默认的提示信息。
只配置blockHandler:
//@SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
@GetMapping("/consutomer/payment/get/{id}")
@SentinelResource(value = "fallback", blockHandler = "handleblockHandler")
public CommonResult getPayment(@PathVariable("id")Long id){
if(id >= 4){
throw new IllegalArgumentException("非法参数异常...");
}else {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
// //====fallback
// public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
// return new CommonResult(414, "---非法参数异常--", e);
// }
//====blockHandler blockHandler的方法必须有这个参数
public CommonResult handleblockHandler(@PathVariable("id")Long id, BlockException e){
return new CommonResult(414, "---非法参数异常--", e);
}
这时候的效果就是,运行异常直接报错错误页面。在sentinel上添加一个降级规则,设置2s内触发异常2次,触发阈值以后,返回的是我们自定义的 blockhanlder 方法返回的内容。
两者都配置:
//@SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
@GetMapping("/consutomer/payment/get/{id}")
@SentinelResource(value = "fallback", blockHandler = "handleblockHandler", fallback = "handleFallback")
public CommonResult getPayment(@PathVariable("id")Long id){
if(id >= 4){
throw new IllegalArgumentException("非法参数异常...");
}else {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
//====fallback
public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
return new CommonResult(414, "---非法参数异常--form fallback的提示", e);
}
//====blockHandler blockHandler的方法必须有这个参数
public CommonResult handleblockHandler(@PathVariable("id")Long id, BlockException e){
return new CommonResult(414, "---非法参数异常--", e);
}
明显两者都是有效的,可以同时配置。
上面是单个进行 fallback 和 blockhandler 的测试,下面是整合 openfeign 实现把降级方法解耦。和Hystrix 几乎一摸一样!
还是使用上面 84 这个消费者做测试:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
@FeignClient(value = "nacos-payment-provider", fallback = PaymentServiceImpl.class)
public interface PaymentService {
@GetMapping("/payment/get/{id}")
public CommonResult paymentSql(@PathVariable("id")Long id);
}
@Component
public class PaymentServiceImpl implements PaymentService {
@Override
public CommonResult paymentSql(Long id) {
return new CommonResult(414, "open-feign 整合 sentinel 实现的全局服务降级策略",null);
}
}
fallback 和 blockHandler 肤浅的区别:
F : 不需要指定规则,程序内部异常均可触发(超时异常需要配置超时时间)
B : 配上也没用,必须去 Sentinel 上指定规则才会被触发。
这是 @SentinelResource 注解的一个值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WFx9qlT6-1602605634665)(images\1597909285814.png)]
目前的sentinel 当重启以后,数据都会丢失,和 nacos 类似原理。需要持久化。它可以被持久化到 nacos 的数据库中。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${
spring.application.name}
group: DEFAULT_GROUP
data-type: json
rule-type: flow
[
{
"resource": "/testA",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTprHOyH-1602605634667)(images\1597913169207.png)]
Seate 处理分布式事务。
微服务模块,连接多个数据库,多个数据源,而数据库之间的数据一致性需要被保证。
官网: http://seata.io/zh-cn/
Seata术语: 一 + 三
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h40hED1Q-1602605634668)(images\1597982738615.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p7Ld2ME0-1602605634670)(images\1597982788659.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Q3eOl3s-1602605634673)(images\1597982858545.png)]
下载地址 : https://github.com/seata/seata/releases/download/v1.0.0/seata-server-1.0.0.zip
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mnEZhrCr-1602605634674)(images\1597984908755.png)]
主要修改自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWFhB75u-1602605634676)(images\1597990431346.png)]
创建名和 file.conf 指定一致的数据库。
在新建的数据库里面创建数据表,db_store.sql文件在 conf 目录下(1.0.0有坑,没有sql文件,下载0.9.0的,使用它的sql文件即可)
修改 conf/registry.conf 文件内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oREBm9DE-1602605634679)(images\1597986227251.png)]
先启动 nacos Server 服务,再启动seata Server 。
启动 Seata Server 报错,在bin目录创建 /logs/seata_gc.log 文件。再次双击 bat文件启动。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tQjz5wY3-1602605634680)(images\1597987564813.png)]
创建三个数据库: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQYkTDbU-1602605634683)(images\1597987722338.png)]
每个数据库创建数据表:
order 库:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzKF3f3L-1602605634685)(images\1597988061545.png)]
account 库:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DhRRzakl-1602605634686)(images\1597988768251.png)]
storage 库:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jiIz0sK-1602605634690)(images\1597988262441.png)]
三个数据库都创建一个回滚日志表,seata/conf/ 有相应的sql文件(1.0.0没有,依然使用0.9.0中的)。
最终效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfct2q9Y-1602605634695)(images\1597989003349.png)]
实现 下订单-> 减库存 -> 扣余额 -> 改(订单)状态
需要注意的是,下面做了 seata 与 mybatis 的整合,所以注意一下,和以往的mybatis的使用不太一样。
新建模块 cloudalibaba-seata-order2001 :
pom依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.0.0version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dkf.cloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
yml配置:
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组,需要和当时在 seata/conf/file.conf 中的一致
tx-service-group: dkf_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 123456
# 注意,这是自定义的,原来的是mapper_locations
mybatis:
mapperLocations: classpath:mapper/*.xml
logging:
level:
io:
seata: info
将 seata/conf/ 下的 file.conf 和 registry.cong 两个文件拷贝到 resource 目录下。
创建 domain 实体类 : Order 和 CommonResult 两个实体类。
dao :
package com.dkf.springcloud.dao;
import org.apache.ibatis.annotations.Mapper;
import com.dkf.springcloud.domain.Order;
import org.apache.ibatis.annotations.Param;
@Mapper
public class OrderDao {
//创建订单
public void create(Order order);
//修改订单状态
public void update(@Param("userId") Long userId, @Param("status") Integer status);
}
Mapper文件:
<mapper namespace="com.dkf.springcloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.dkf.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT">id>
<result column="user_id" property="userId" jdbcType="BIGINT">result>
<result column="product_id" property="productId" jdbcType="BIGINT">result>
<result column="count" property="count" jdbcType="INTEGER">result>
<result column="money" property="money" jdbcType="DECIMAL">result>
<result column="status" property="status" jdbcType="INTEGER">result>
resultMap>
<insert id="create">
insert into t_order(id, user_id, product_id, count, money, status)
values (null, #{userId},#{productId},#{count},#{money},0)
insert>
<update id="update">
update t_order set status = 1 where user_id=#{userId} and status=#{status}
update>
mapper>
创建service :
注意,红框标记的是通过 open-feign 远程调用微服务的service
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ms1fGdF9-1602605634700)(images\1597992817318.png)]
serviceImpl :
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
@Override
public void create(Order order) {
log.info("--------》 开始创建订单");
orderDao.create(order);
log.info("--------》 订单微服务开始调用库存,做扣减---Count-");
storageService.decrease(order.getProductId(), order.getCount());
log.info("--------》 订单微服务开始调用库存,库存扣减完成!!");
log.info("--------》 订单微服务开始调用账户,账户扣减---money-");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("--------》 订单微服务开始调用账户,账户扣减完成!!");
//修改订单状态,从0到1
log.info("--------》 订单微服务修改订单状态,start");
orderDao.update(order.getUserId(),0);
log.info("--------》 订单微服务修改订单状态,end");
log.info("--订单结束--");
}
@Override
public void update(Long userId, Integer status) {
}
}
config (特殊点):
//下面是两个配置类,这个是和mybatis整合需要的配置
@Configuration
@MapperScan({
"com.dkf.springcloud.alibaba.dao"})
public class MybatisConfig {
}
//这个是配置使用 seata 管理数据源,所以必须配置
package com.dkf.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource){
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
主启动类:
//这里必须排除数据源自动配置,因为写了配置类,让 seata 管理数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class,args);
}
}
controller 层调用 orderService 方法即可。
先启动 nacos --》 再启动 seata --> 再启动此order服务,测试,可以启动。
仿照上面 创建 cloudalibaba-seata-storage2002 和 cloudalibaba-seata-account2003 两个模块,唯一大的区别就是这两个不需要导入 open-feign 远程调用其它模块。
操,累死老子啦,测试可以正常使用!
@Override
//只需要在业务类的方法上加上该注解,name值自定义唯一即可。
@GlobalTransactional(name = "dkf-create-order", rollbackFor = Exception.class)
public void create(Order order) {
log.info("--------》 开始创建订单");
orderDao.create(order);
log.info("--------》 订单微服务开始调用库存,做扣减---Count-");
storageService.decrease(order.getProductId(), order.getCount());
log.info("--------》 订单微服务开始调用库存,库存扣减完成!!");
log.info("--------》 订单微服务开始调用账户,账户扣减---money-");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("--------》 订单微服务开始调用账户,账户扣减完成!!");
//修改订单状态,从0到1
log.info("--------》 订单微服务修改订单状态,start");
orderDao.update(order.getUserId(),0);
log.info("--------》 订单微服务修改订单状态,end");
log.info("--订单结束--");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-134sSDLT-1602605634703)(images\1597998982271.png)]
原理三个阶段:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKNQR0Tq-1602605634704)(images\1597999156669.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y790OOPN-1602605634705)(images\1597999227092.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJhfb9mH-1602605634705)(G:\学习课件\大学\java框架\springCloud\images\1597999292492.png)]