微服务架构
微服务架构概述
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于 HTTP 协议的 RESTfulAPI)。每个服务都围绕着具本业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应当尽量避免统一的、集中式的服务管理机制,对具体的个服务而言应根据业务上下文,选择合适的语言、工具对其进行构建
基于分布式的微服务架构

- 服务注册与发现
- 服务调用
- 服务熔断
- 负载均衡
- 服务消息队列
- 配置中心管理
- 服务网关
- 服务监控
- 全链路追踪
- 自动化构建部署
- 服务定时任务操作
- ...
SpringCloud
版本选择

详细查看 spring boot 与 spring cloud 版本依赖关系
https://start.spring.io/actuator/info

在线工具 - 你的工具箱 (tool.lu) 使用 json 转换工具



Spring Cloud 简介
分布式微服务架构一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
官网:Spring Cloud
Spring Cloud 组件

| 服务治理条目 | 落地技术 |
|---|---|
| 服务注册与发现 | Netflix Eureka、Consul、Etcd、Zookeeper、Alibaba Nacos 等 |
| 服务调用 | Netflix Feign、Spring Cloud OpenFeign |
| 负载均衡 | Netflix Ribbon、Spring Cloud LoadBalancer 等 |
| 分布式事务 | Seata、LCN、Hmily 等 |
| 服务配置与管理 | Netflix Archaius、Alibaba Diamond 等 |
| 服务熔断与降级 | Netflix Hystrix、Envoy 、Circuit Breaker 、Alibaba Sentinel 等 |
| 服务配置中心管理 | Spring Cloud Config、Chef 、Consul、Alibaba Nacos 等 |
| 服务路由(API 网关) | Netflix Zuul 、Spring Cloud Gateway 等 |
| 服务监控 | Zabbix、Nagios、Metrics、Specatator 等 |
| 服务链路追踪 | Zipkin、Brave、Dapper 等 |
| 数据流操作开发包 | SpringCloud Stream(封装与 Redis,Rabbit,Kafka 等发送接收消息) |
| 事件消息总线 | SpringCloud Bus |
| 消息队列 | Kafka、RabbitMQ、ActiveMQ 等 |
| 服务部署 | Docker、OpenStack、Kubernetes 等 |
组件升级/更替
基于 Nextflix OSS 的落地

在 2018 年前后 Netflix 公司宣布其核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。
这直接影响了 Spring Cloud 项目的发展路线,Spring 官方不得不采取了应对措施,在 2019 年的在 SpringOne 2019 大会中,SpringCloud 宣布 Spring Cloud Netflix 项目进入维护模式,并在 2020 年移除相关的 Netflix OSS 组件。
截止 25 年,官方微服务组件的更替
- 服务注册与发现:Netflix Eureka ---> Alibaba Nacos
- 负载均衡:Netflix Ribbon ---> Spring Cloud LoadBalancer
- 服务调用:Netflix Feign ---> OpenFeign
- 分布式事务:Alibaba Seata
- 服务熔断和降级:Hystrix ---> Alibaba Sentinel
- 服务网关:Netflix Zuul ---> Spring Cloud Gateway
- 分布式配置管理:Spring Cloud Config ---> Alibaba Nacos
趋势为向云原生、高性能且由活跃社区驱动的现代组件演进
常见企业方案
Spring Cloud 系列:
- 远程调用:OpenFeign
- 网关:Gateway
Spring Cloud Alibaba 系列:
- 注册中心/配置中心:Nacos
- 服务保护:Sentinel
- 分布式事务:Seata

2025 实战
微服务方案
Spring Cloud 系列:
- 远程调用:OpenFeign
- 网关:Gateway
Spring Cloud Alibaba 系列:
- 注册中心/配置中心:Nacos
- 服务保护:Sentinel
- 分布式事务:Seata
工程结构

springcloud-demo 版本锁定
通常会在一个组织或者项目的最顶层的父 POM 中看到 dependencyManagement 元素。
使用 pom.xml 中的 dependencyManagement 元素能让所有在子项目中引用一个依赖而不用显式的列出版本号。 dependencyManagement 里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。
我们看到,这里多了一个 <scope>import</scope>,它的意思是将 spring-cloud-dependencies 和 spring-cloud-alibaba-dependencies 中 dependencyManagement 的 dependencies,全部引入到当前工程的 dependencyManagement 中。
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>org.example</groupId>
<artifactId>springcloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springcloud-demo</name>
<description>springcloud-demo</description>
<modules>
<module>services</module>
</modules>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2025.1.2</spring-cloud.version>
<spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>services 管理公共依赖
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>springcloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>services</artifactId>
<packaging>pom</packaging>
<modules>
<module>service-product</module>
<module>service-order</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>Spring Cloud Alibaba Nacos 注册中心
注册中心的两大功能就是服务注册与服务发现。

Nacos /nɑ: kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
基本路径

Nacos 服务注册
下载安装 Nacos 服务端,启动命令: startup.cmd -m standalone
引入 nacos 注册/发现中心依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>配置 Nacos 地址
spring:
application:
name: service-order
cloud:
nacos:
server-addr: 127.0.0.1:8848
server:
port: 8001spring:
application:
name: service-product
cloud:
nacos:
server-addr: 127.0.0.1:8848
server:
port: 9000启动微服务 order 和 product

注册成功

复制实例模拟集群

Nacos 服务发现
启动类添加注解 @EnableDiscoveryClient
@EnableDiscoveryClient // 开启服务发现
@SpringBootApplication
public class OrderMainApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMainApplication.class, args);
}
}测试使用 API ,Spring Cloud API DiscoveryClient 或者 Nacos API NacosServiceDiscovery
@SpringBootTest
public class DiscoveryTest {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
NacosServiceDiscovery nacosServiceDiscovery;
@Test
public void testDiscovery() {
for(String service : discoveryClient.getServices()){
System.out.println("service: " + service);
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for(ServiceInstance instance : instances){
System.out.println("host: " + instance.getHost());
System.out.println("port: " + instance.getPort());
System.out.println("uri: " + instance.getUri());
}
}
}
@Test
public void testNacosDiscovery() throws NacosException {
List<String> services = nacosServiceDiscovery.getServices();
for(String service : services){
System.out.println("service: " + service);
List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);
for(ServiceInstance instance : instances){
System.out.println("host: " + instance.getHost());
System.out.println("port: " + instance.getPort());
System.out.println("uri: " + instance.getUri());
}
}
}
}手动远程调用

定义模型子工程 model
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>springcloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>model</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
</dependencies>统一记录所有微服务的实体类

微服务中引入模型层依赖
<dependency>
<groupId>org.example</groupId>
<artifactId>model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>配置一个 RestTemplate
@Configuration
public class ProductServiceConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}RestTemplate 提供了多种便捷访问远程 Http 服务的方法, 是一种简单便捷的访问 restful 服务模板类,是 Spring 提供的用于访问 Rest 服务的客户端模板工具集。
使用 restTemplate 访问 restful 接口非常的简单粗暴无脑。 (url, requestMap, ResponseBean.class)这三个参数分别代表 REST 请求地址、请求参数、HTTP 响应转换被转换成的对象类型。
创建订单时远程调用查看商品信息
@Override
public Order createOrder(Long userId, Long productId) {
Order order = new Order();
order.setId(1L);
order.setAddress("address");
order.setUserId(userId);
order.setNickName("tintin");
// 远程获取商品列表
Product product = getProductFromRemote(productId);
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(product.getNum())));
order.setProductList(Arrays.asList(product));
return order;
}
private Product getProductFromRemote(Long productId) {
// 发现商品服务并获取调用地址
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
String host = instances.get(0).getHost();
int port = instances.get(0).getPort();
String url = "http://" + host + ":" + port + "/product/" + productId;
// 发送请求获取商品信息
Product product = restTemplate.getForObject(url, Product.class);
return product;
}这样即使有某个商品服务挂了,其他服务还能接着响应请求。
负载均衡改造
引入 loadbalancer 负载均衡依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>测试 API LoadBalancerClient
@Test
void test() {
ServiceInstance choose = loadBalancerClient.choose("service-product");
System.out.println(choose.getServiceId() + "\t" + choose.getHost() + "\t" + choose.getPort() + "\t" + choose.getUri());
choose = loadBalancerClient.choose("service-product");
System.out.println(choose.getServiceId() + "\t" + choose.getHost() + "\t" + choose.getPort() + "\t" + choose.getUri());
choose = loadBalancerClient.choose("service-product");
System.out.println(choose.getServiceId() + "\t" + choose.getHost() + "\t" + choose.getPort() + "\t" + choose.getUri());
choose = loadBalancerClient.choose("service-product");
System.out.println(choose.getServiceId() + "\t" + choose.getHost() + "\t" + choose.getPort() + "\t" + choose.getUri());
choose = loadBalancerClient.choose("service-product");
System.out.println(choose.getServiceId() + "\t" + choose.getHost() + "\t" + choose.getPort() + "\t" + choose.getUri());
}默认算法——轮询

负载均衡获取地址
@Autowired
private LoadBalancerClient loadBalancerClient;
// 基于负载均衡的远程调用
private Product getProductFromRemote2(Long productId) {
// 负载均衡获取调用地址
ServiceInstance instances = loadBalancerClient.choose("service-product");
String host = instances.getHost();
int port = instances.getPort();
String url = "http://" + host + ":" + port + "/product/" + productId;
// 发送请求获取商品信息
Product product = restTemplate.getForObject(url, Product.class);
System.out.println("发送远程请求:" + url);
return product;
}注解方式负载均衡
@Configuration
public class ProductServiceConfig {
@LoadBalanced // RestTemplate 组件增加注解
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}// 基于注解的负载均衡
private Product getProductFromRemote3(Long productId) {
String url = "http://service-product" + "/product/" + productId;
// 发送请求获取商品信息
Product product = restTemplate.getForObject(url, Product.class);
System.out.println("发送远程请求:" + url);
return product;
}最终负载均衡调用方式如图

注册中心宕机?
根据上面的代码示例,发送请求应该是完成下面两个步骤
- 请求注册中心获取微服务地址列表
- 给对方服务的某个地址发送请求
这导致依赖注册中心与浪费性能。
但 Nacos 和 loadbalancer 实际上在发出了第一次请求,就缓存了微服务地址列表,注册中心宕机,远程调用在一段时间内还能成功。
Spring Cloud Alibaba Nacos 配置中心

Nacos 配置中心
引入 nacos 配置中心依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency设置配置的导入路径
spring.cloud.nacos.server-addr=127.0.0.1:8848
spring.config.import=nacos:service-order.properties在 Nacos 实例中写入配置列表

测试获取
@Value("${order.timeout}")
String orderTimeout;
@Value("${order.auto-confirm}")
String orderAutoConfirm;
@GetMapping("/config")
public String getConfig() {
return "order timeout: " + orderTimeout + ", order auto confirm: " + orderAutoConfirm;
}
自动刷新,添加注解
@RefreshScope
@RestController
public class OrderController {其他微服务可能用不到配置中心,但是启动报错

关闭配置导入检查
spring.cloud.nacos.config.import-check.enabled=false或者设置配置可选
spring.config.import=optional:nacos:配置无感自动刷新
创建配置类进行批量导入
@Component
@ConfigurationProperties(prefix = "order")
@Data
public class OrderProperties {
private int timeout;
private boolean autoConfirm;
}获取配置类的配置变量
@Autowired
private OrderProperties orderProperties;
@GetMapping("/config")
public String getConfig() {
return "order timeout: " + orderProperties.getTimeout() + ", order auto confirm: " + orderProperties.isAutoConfirm();
}通过 @ConfigurationProperties 导入配置中心的信息是无感自动刷新额
监听配置变化
@EnableDiscoveryClient // 开启服务发现
@SpringBootApplication
public class OrderMainApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMainApplication.class, args);
}
// 1、项目启动就监听配置文件变化
// 2、发生变化后拿到变化值
// 3、发送邮件
@Bean
ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager) {
return args -> {
System.out.println("ApplicationRunner ready to work.");
ConfigService configService = nacosConfigManager.getConfigService();
configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() {
@Override
public Executor getExecutor() {
return Executors.newFixedThreadPool(4);
}
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("Changed config info: " + configInfo);
System.out.println("Send email...");
}
});
};
}
}配置优先级?
Nacos 中的数据集 和 application.properties 有相同的配置项,哪个生效?
如果导入多个,谁优先
spring.config.import=nacos:service-order.properties,nacos:common.properties
数据隔离
每个微服务,同一种配置,在每套环境(dev, test, prod)的值都不一样。
- 多套环境——通过 Namespace 名称空间区分
- 多种微服务——通过 Group 分组区分
- 多种配置——通过 Data-id 数据集区分
- 按需加载配置

在 Nacos 实例中定义多种配置进行区分

根据微服务需求导入不同配置,这种配置需要手动切换 namespace
spring:
application:
name: service-order
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
import-check:
enabled: false
namespace: dev # 表示使用 Nacos dev 命名空间的配置
config:
import:
# - nacos:service-order.properties
- nacos:common.properties?group=order
- nacos:database.properties?group=order
server:
port: 7000配合 spring.config.activate.on-profile 实现配置环境隔离,此时会根据 spring.profiles.active 进行动态激活
spring:
# profiles:
# active: dev
application:
name: service-order
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
import-check:
enabled: false
namespace: ${spring.profiles.active:public} # 不配置环境则使用默认 public 命名空间
server:
port: 7000
---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
- nacos:dev.properties?group=order
activate:
on-profile: dev
---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
- nacos:test.properties?group=order
activate:
on-profile: test
---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
- nacos:prod.properties?group=order
activate:
on-profile: prod测试启动效果

Spring Cloud OpenFeign

OpenFeign 声明式 REST 客户端
声明式 REST 客户端 Feign 代替编程是 REST 客户端(RestTemplate)
由注解驱动
- 指定远程地址:@FeignCliènt
- 指定请求方式:@GetMapping、@PostMapping、@DeleteMapping..
- 指定携带数据:@RequestHeader、@RequestParam、@RequestBody..
- 指定结果返回:响应模型
引入 openfeign 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>开启 Feign 远程调用功能
@EnableFeignClients // 开启 Feign 远程调用功能
@EnableDiscoveryClient // 开启服务发现
@SpringBootApplication
public class OrderMainApplication {定义 Feign 客户端,mvc 注解的里另一种使用逻辑
@FeignClient(name = "service-product") // 服务名称
public interface ProductFeignClient {
@GetMapping("/product/{id}") // 服务接口地址
Product getProductById(@PathVariable("id") Long id);
}基本使用
@Autowired
private ProductFeignClient productFeignClient;
// 远程获取商品列表
// Product product = getProductFromRemote3(productId);
Product product = productFeignClient.getProductById(productId);OpenFeign 自带负载均衡
如果是调用自己写的服务,直接将对应项目服务的 controller 方法复制到当前项目的 client 接口中
OpenFeign 调用第三方 API
声明第三方服务的客户端
// 接入 UApiPro 免费服务 https://uapis.cn/ 测试
@FeignClient(value = "uapi-pro", url = "https://uapis.cn/api/v1")
public interface UApiProFeignClient {
@GetMapping("/misc/weather")
public String getWeather(@RequestParam(value = "adcode", required = false) String adcode,
@RequestParam(value = "city", required = false) String city,
@RequestParam(value = "forecast", required = false) String forecast);
}客户端负载均衡 VS 服务端负载均衡

OpenFeign 开启日志
增加配置
logging:
level:
org.example.order.feign: debug定义组件
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}控制台效果

OpenFeign 超时控制

OpenFeign 引入两种超时机制
- connectTimeOut 默认 10s
- readTimeout 默认 60s
配置客户端超时机制等参数
spring:
cloud:
openfeign:
client:
config:
default: # 默认配置
logger-level: full
connect-timeout: 1000 # 连接等待
read-timeout: 2000 # 读取等待
retryer: feign.retryer.Default # 默认重试器 间隔100ms 最大间隔1s 最多尝试5次
service-product: # 指定客户端配置
logger-level: full
connect-timeout: 3000
read-timeout: 5000效果

OpenFeign 重试
远程调用超时失败后,还可以进行多次尝试,如果某次成功返回 ok,如果多次依然失败则结束调用,返回错误
配置方式
spring.cloud.openfeign.client.config.default.retryer=feign.retryer.Default注册组件方式
@Bean
Retryer retryer() {
return new Retryer.Default(); // 默认重试器 不传参数则默认间隔100ms 最大间隔1s 最多尝试5次
}效果:由于超时失败,一共尝试调用了 5 次商品服务
OpenFeign 拦截器
Rest 客户端拦截器类似于前端 Vue 项目的 Axios 拦截器,区别于请求进入 Spring MVC Dispatcher Servlet 后的拦截器。

请求拦截器适合做请求头统一处理、参数加密等。
响应拦截器适合做全局错误处理、登录失效跳转等。
为容器添加拦截器,例如为请求添加头部参数
@Component
public class XTokenInterceptor implements RequestInterceptor {
@Override
public void apply(feign.RequestTemplate template) {
System.out.println("XTokenInterceptor working...");
template.header("X-TOKEN", "123456");
// template.body("test");
// template.query("name", "test");
}
}OpenFeign Fallback
Fallback 兜底返回功能需要整合 Sentinel 才能实现

引入 sentinel 依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>开启 sentinel 熔断功能
feign:
sentinel:
enabled: true编写回调类
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public Product getProductById(Long id) {
System.out.println("兜底回调....");
Product product = new Product();
product.setId(id);
product.setPrice(new BigDecimal("0"));
product.setProductName("未知商品");
product.setNum(0);
return product;
}
}客户端指定回调类
@FeignClient(name = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}效果:商品服务宕机后,请求 getProductById 后返回兜底的回调方法
Spring Cloud Alibaba Sentinel 服务保护
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从 流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

Sentinel 工作原理

Sentinel 资源&规则
- 定义资源:
- 主流框架自动适配(WebServlet、Dubbo、SpringCloud、gRPC、SpringWebFlux、Reactor);所有 Web 接口均为资源
- 编程式:SphU API
- 声明式:
@SentinelResource
- 定义规则:
- 流量控制(FlowRule)
- 熔断降级(DegradeRule)
- 系统保护(SystemRule)
- 来源访问控制(AuthorityRule)
- 热点参数(ParamFlowRule)
工作流程

Sentinel 整合使用

下载安装 Nacos 服务端,启动命令: java -jar sentinel-dashboard-1.8.10.jar --server.port=8081
默认账号密码:sentinel/sentinel
引入 sentinel 依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>配置控制面板地址
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8081
eager: true # 懒加载启动微服务,连接成功

注解为方法添加服务保护
@SentinelResource(value = "createOrder")
@Override
public Order createOrder(Long userId, Long productId) {
Order order = new Order();
order.setId(1L);
order.setAddress("address");
order.setUserId(userId);
order.setNickName("tintin");
// 远程获取商品列表
// Product product = getProductFromRemote3(productId);
Product product = productFeignClient.getProductById(productId);
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(product.getNum())));
order.setProductList(Arrays.asList(product));
return order;
}发送一次请求,sentinel 会进行监控

添加流控规则

每秒请求数超过 1 则出现阻塞提示

Sentinel 异常处理 Web 接口
如何自定义违反规则后返回的错误提示页面?
例如返回 JSON 提示数据
{
"code": 500,
"msg": "流量超出限制",
"data": ""
}Sentinel 检查请求是否违反规则,是则抛出 BlockException 异常,不同的规则对应不同的异常(流控、热点参数异常、熔断降级异常、权限控制异常、系统阻塞异常)

由于资源的不同,针对 BlockException 的异常处理机制也不同

针对 Web 接口(控制器)的异常护理机制,自定义 BlockExceptionHandler
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {
// Return 429 (Too Many Requests) by default.
response.setStatus(429);
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("utf-8");
R fail = R.fail("Blocked by Sentinel (flow limiting)", null, 429);
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(fail));
}
}效果

Sentinel 异常处理 Web @SentinelResource
@SentinelResource(value = "createOrder")
public Order createOrder(Long userId, Long productId) {为 @SentinelResource 方法添加流控规则

Sentinel 通过切面对 SentinelResource 方法进行保护,由于 @SentinelResource 注解未指定,blockHandler 或者 fallback,异常由 SpringBoot 的全局异常处理器处理,因此违反规则返回白页。

指定 blockHandler
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
@Override
public Order createOrder(Long userId, Long productId) {}
// 兜底回调
public Order createOrderFallback(Long userId, Long productId, BlockException blockException) {
Order order = new Order();
order.setId(1L);
order.setAddress("");
order.setUserId(userId);
order.setNickName("");
return order;
}违反规则后有方法回调

Sentinel 异常处理 远程调用
添加远程调用资源的流控规则

最终调用了 OpenFeign 的兜底返回
@FeignClient(name = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
Sentinel 流量控制
流量控制限制多余请求,从而保护系统资源不被耗尽。


阈值类型:
- QPS:统计每秒请求数
- 并发线程数:统计并发线程数
流控模式:直接策略、链路策略、关联策略
调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系;有了调用链路的统计信息,我们可以衍生出多种流量控制手段。
只有快速失败支持流控模式的设置

流控效果:快速失败、warm up、匀速排队(露桶算法)

Sentinel 熔断降级

熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
工作原理

熔断策略:
慢调用比例:【统计时长】内,发送了至少【最小请求数】个请求,其中 【比例阈值 * 100%】 的请求是慢调用请求(请求时长小于 【最大 RT】),则熔断(熔断时长)

异常比例:

异常数

熔断降级是否存在时,fallback 的区别
- 无熔断,每次都要等请求超时返回错误后才 fallback
- 有熔断,在熔断时长内可以快速返回 fallback 无需等待请求。

Sentinel 热点参数
需求 1:每个用户秒杀 QPS 不得超过 1(秒杀下单 userld 级别)
效果:携带某参数(如 userld )的参与流控,不携带不流控
需求 2:6 号用户是 vvip,不限制 QPS(例外情况)
需求 3:666 号是下架商品,不允许访问


Spring Cloud Gateway 网关
Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到API,并为它们提供交叉关注点,如:安全性、监控/度量和弹性。

创建网关
需求
- 客户端发送
/api/order/**转到 service-order - 客户端发送
/api/product/**转到 service-product - 以上转发有负载均衡效果
引入 gateway 网关依赖和 nacos 发现依赖 loadbalancer 负载均衡依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>配置 nacos 地址
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
server:
port: 80网关规则配置
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: order-route
uri: lb:service-order
predicates:
- Path=/api/order/**
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**调整微服务调用地址
@RequestMapping("/api/order")
@FeignClient(name = "service-product",path = "/api/product", fallback = ProductFeignClientFallback.class)
@RequestMapping("/api/product")效果:

Gateway 工作原理

路由优先级 order
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- Path=/**
order: 10
- id: order-route
uri: lb://service-order
predicates:
- Path=/api/order/**
order: 0 # 优先级,如果相同优先级按照声明顺序
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
order: 0Gateway 断言
断言短写法
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]断言长写法
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: path
args:
patterns: /api/order/**
matchTrailingSlash: true断言工厂

过滤器类型

query 断言
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha自定义断言
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
public VipRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return (GatewayPredicate) serverWebExchange -> {
ServerHttpRequest request = serverWebExchange.getRequest();
String first = request.getQueryParams().getFirst(config.param);
return (StringUtils.hasText(first) && first.equals(config.value));
};
}
public static class Config {
@NotEmpty
private @Nullable String param;
@NotEmpty
private @Nullable String value;
public @Nullable String getParam() {
return param;
}
public @Nullable String getValue() {
return value;
}
public void setParam(@Nullable String param) {
this.param = param;
}
public void setValue(@Nullable String value) {
this.value = value;
}
}
} - name: Vip
args:
param: user
value: 666效果


Gateway 过滤器

过滤器工厂

过滤器类型



路径重写 rewirtePath

先去掉微服务控制器和 Feign 客户端的前缀,再将 /api/xxx/*** 转发至 xxx 服务
- id: order-route
uri: lb://service-order
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
order: 0 # 优先级,如果相同优先级按照声明顺序
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/product/?(?<segment>.*), /$\{segment}
order: 0添加响应头
filters:
- AddResponseHeader=X-Response-Abc, 123
默认过滤器
spring:
cloud:
gateway:
server:
webflux:
routes:
...
default-filters:
- AddResponseHeader=X-Response-Abc, 123全局过滤器
@Component
public class RtGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("custom global filter");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}自定义过滤器
@Component
public class OnceTokenHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 每次响应之前添加令牌 根据提供的格式 支持 uuid jwt 等格式
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
String name = Objects.requireNonNull(config.getName(), "name must not be null");
if ("uuid".equals(config.getValue())) {
value = UUID.randomUUID().toString();
}
if ("jwt".equals(config.getValue())) {
value = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
}
exchange.getResponse().getHeaders().set(name, value);
}));
}
@Override
public String toString() {
String name = config.getName();
String value = config.getValue();
return filterToStringCreator(OnceTokenHeaderGatewayFilterFactory.this)
.append(name != null ? name : "", value != null ? value : "")
.toString();
}
};
}
} - id: order-route
uri: lb://service-order
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- OnceTokenHeader=X-Response-Token,uuid效果

解决跨域问题
- 单体项目解决方案
- 在 Controller 上
@CrossOrigin注解 - controller 过多则注入跨域过滤器
- 在 Controller 上
- 分布式项目
- 微服务过多,直接在网关配置跨域过滤器
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: ["*"]
allowed-methods: ["*"]
allowed-headers: ["*"]效果

微服务经过网关调用
原先微服务之间直接调用

如果要经过网关,将原调用 order 服务 改为调用网关服务即可
@FeignClient(value = "gateway",fallback = ProductFeignClientFallback.class)
// 加基础路径前缀
@GetMapping("/api/product/product/{id}") 1implementation Ify
实际架构不建议这样调用
Spring Cloud Alibaba Seata 分布式事务
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

环境搭建



引入 seata 分布式事务依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>本地事务
@EnableTransactionManagement
@Transactional在保存订单时发生异常
@Transactional
@Override
public OrderTbl create(String userId, String commodityCode, int orderCount) {
//1、计算订单价格
int orderMoney = calculate(commodityCode, orderCount);
//2、扣减账户余额
accountFeignClient.debit(userId, orderMoney);
//3、保存订单
OrderTbl orderTbl = new OrderTbl();
orderTbl.setUserId(userId);
orderTbl.setCommodityCode(commodityCode);
orderTbl.setCount(orderCount);
orderTbl.setMoney(orderMoney);
//3、保存订单
orderTblMapper.insert(orderTbl);
int i = 10/0;
return orderTbl;
}此时虽然库存扣减,余额扣减,但是订单并没有创建,因为只有创建订单的 方法回滚了。、
Seata 架构原理

Seata 整合
下载 seata,并启动服务 seata-server.bat
登录 默认账号密码 seata/seata

分组可以不配 默认 default_tx_group
seata:
tx-service-group: default_tx_group配置文件 file.conf ,seata 服务器地址(不是web地址)
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}注解 @GlobalTransactional 开启分布式全局事务
@GlobalTransactional
@Override
public void purchase(String userId, String commodityCode, int orderCount) {
//1. 扣减库存
storageFeignClient.deduct(commodityCode, orderCount);
//2. 创建订单
orderFeignClient.create(userId, commodityCode, orderCount);
}Seata 二阶提交协议
提交阶段
- 一阶提交:业务数据 + undo_log
- 二阶提交:
- 成功:所有服务删除 undo_log
- 失败:所有服务拿到前后镜像进行恢复,最后删除 undo_log
全局锁:一阶提交每个事务提交之前要申请一个数据级别的全局锁

Seata 四种模式
- Seata AT 模式:默认,性能较高
- Seata TCC 模式
- Seata Saga 模式
- Seata XA 模式:
实战构建
约定 > 配置 > 代码
业务说明

总父工程 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springcloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.2</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<swagger3.version>2.2.0</swagger3.version>
<mapper.version>4.2.3</mapper.version>
<fastjson2.version>2.0.40</fastjson2.version>
<persistence-api.version>1.0.2</persistence-api.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springboot 3.2.0-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springCloud 2023.0.0-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springCloud alibaba 2022.0.0.0-RC2-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--通用Mapper4之tk.mybatis-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>${persistence-api.version}</version>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>通常会在一个组织或者项目的最顶层的父 POM 中看到 dependencyManagement 元素。
使用 pom.xml 中的 dependencyManagement 元素能让所有在子项目中引用一个依赖而不用显式的列出版本号。
dependencyManagement 里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。
我们看到,这里多了一个 <scope>import</scope>,它的意思是将 spring-boot-dependencies 中 dependencyManagement 的 dependencies,全部引入到当前工程的 dependencyManagement 中。
REST 微服务工程构建


微服务提供者
子工程 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>springcloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>module1</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Mybatis Generator 自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql8.0-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>服务端口和名称
server:
port: 8001
spring:
application:
name: cloud-payment-service数据源和 mybatis
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: tintin
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package:主启动类
@SpringBootApplication
public class CloudProviderPayment8001Main {
public static void main(String[] args) {
SpringApplication.run(CloudProviderPayment8001Main.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
entities
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult <T>{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code,message,null);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}dao
@Mapper
public interface PaymentDao {
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.tintin.springcloud.dao.PaymentDao">
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
INSERT INTO payment(SERIAL) VALUES(#{serial});
</insert>
<resultMap id="BaseResultMap" type="com.tintin.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
SELECT * FROM payment WHERE id = #{id};
</select>
</mapper>service
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}controller
@RestController
@RequestMapping("payment/payment")
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping("/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("***插入结果***"+ result);
if (result > 0) {
return new CommonResult(200,"插入数据库成功",result);
} else {
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping("/info/{id}")
public CommonResult<Payment> info(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("***插入结果***"+ payment);
if (payment != null) {
return new CommonResult(200,"查询成功",payment);
} else {
return new CommonResult(444,"没有对应记录,查询id:"+id,null);
}
}
}客户端消费者
配置、pom、entities 同上
RestTemplate
RestTemplate 提供了多种便捷访问远程 Http 服务的方法, 是一种简单便捷的访问 restful 服务模板类,是 Spring 提供的用于访问 Rest 服务的客户端模板工具集
使用 使用 restTemplate 访问 restful 接口非常的简单粗暴无脑。 (url, requestMap, ResponseBean.class)这三个参数分别代表 REST 请求地址、请求参数、HTTP 响应转换被转换成的对象类型。
添加 RestTemplate 组件
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}@RestController
@RequestMapping("/consumer/payment/")
@Slf4j
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@GetMapping("create")
public CommonResult create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL + "/payment/payment/create",payment,CommonResult.class);
}
@GetMapping("info/{id}")
public CommonResult<Payment> info(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL + "/payment/payment/info/" + id,CommonResult.class);
}
}工程重构
提取公共类

公共依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud</artifactId>
<groupId>com.tintin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-api-commons</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
</project>SpringCloud Netflix Eureka 注册与发现
Eureka 基础
服务治理 Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理
在传统的 rpc 远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
服务注册与发现
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息 比如 服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地 RPC 调用 RPC 远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何 rpc 远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
Eureka 采用了 CS 的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。
Eureka 两组件
Eureka Server 提供服务注册服务 各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
EurekaClient 通过注册中心进行访问 是一个 Java 客户端,用于简化 Eureka Server 的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向 Eureka Server 发送心跳(默认周期为 30 秒)。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,EurekaServer 将会从服务注册表中把这个服务节点移除(默认 90 秒)

EurekaServer 端
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud</artifactId>
<groupId>com.tintin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-eureka-server7001</artifactId>
<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.tintin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>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/@SpringBootApplication
@EnableEurekaServer
public class CloudEurekaServer7001Main {
public static void main(String[] args) {
SpringApplication.run(CloudEurekaServer7001Main.class,args);
}
}开启后暂无注册

EurekaClient 端-服务提供者
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
spring:
application:
name: cloud-payment-service # 微服务注册名称@SpringBootApplication
@EnableEurekaClient
public class CloudProviderPayment8001Main {
public static void main(String[] args) {
SpringApplication.run(CloudProviderPayment8001Main.class,args);
}
}
EurekaClient 端-服务消费者
同上
集群构建
Eureka 集群环境构建
微服务 RPC 远程服务调用最核心的是什么 高可用,试想你的注册中心只有一个 only one, 它出故障了,会导致整个为服务环境不可用,所以
解决办法:搭建 Eureka 注册中心集群 ,实现负载均衡+故障容错
修改映射配置

集群配置
#集群配置
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7002.com:7002/eureka/
#集群配置
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
注册配置
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# defaultZone: http://localhost:7001/eureka 单机版测试

提供者服务集群环境构建

消费者访问的地址不能写死,而关注服务名
public static final String PAYMENT_URL = "http://cloud-payment-service";开启负载均衡
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate() {
return new RestTemplate();
}
}actuator 微服务信息完善
eureka:
instance:
instance-id: payment8002 # 主机名称:服务名称修改
prefer-ip-address: true #访问路径可以显示IP地址
Eureka 服务发现
主启动类@EnableDiscoveryClient //服务发现
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/discovery")
public Object discovery() {
//所有微服务的名称
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("service:" + service);
}
//微服务下的所有实例
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort());
}
return discoveryClient;
}

Eureka 自我保护
概述
保护模式主要用于一组客户端和 Eureka Server 之间存在网络分区场景下的保护。一旦进入保护模式, Eureka Server 将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
一句话:某时刻某一个微服务不可用了,Eureka 不会立刻清理,依旧会对该微服务的信息进行保存
产生原因
为了防止 EurekaClient 可以正常运行,但是 与 EurekaServer 网络不通情况下,EurekaServer 不会立刻将 EurekaClient 服务剔除

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让 Eureka 集群更加的健壮、稳定。
禁止自我保护
Eureka 服务端
eureka:
server:
#关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
# 服务定端没收到客户端发送心跳包(默认90秒)时间间隔后,超时将剔除服务
eviction-interval-timer-in-ms: 2000Eureka 客户端
eureka:
instance:
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2Zookeeper 注册与发现
Eureka 停止更新

注册中心 Zookeeper
zookeeper 是一个分布式协调工具,可以实现注册中心功能
关闭 Linux 服务器防火墙后启动 zookeeper 服务器
zookeeper 服务器取代 Eureka 服务器,zk 作为服务注册中心
微服务提供者
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!--若出现版本问题 则排除旧版本并引入新版本-->
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper3.5.3-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.4.9版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 8004
#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.10.102:2181@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class CloudProviderPayment8004Main {
public static void main(String[] args) {
SpringApplication.run(CloudProviderPayment8004Main.class,args);
}
}
@RestController
@RequestMapping("/payment/payment")
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/zk")
public String zk() {
return "springcloud zookeeper" + serverPort + "uuid:" + UUID.randomUUID().toString().substring(0,3);
}
}开启服务后



该服务节点为临时节点

微服务消费者
同服务提供者
@Configuration
public class ApplicationContextCOnfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}Consul 注册与发现
Consul
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之 Consul 提供了 一种完整的服务网格解决方案。

中文文档:https://www.springcloud.cc/spring-cloud-consul.html
Consul 安装运行
https://learn.hashicorp.com/consul/getting-started/install.html
在 consul.exe 文件路径下运行 cmd

开发者模式运行 consul agent -dev
通过以下地址可以访问 Consul 的首页:Services - Consul

微服务提供者
同 Zookeeper
###consul服务端口号
server:
port: 8006
spring:
application:
name: consul-provider-payment
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
微服务消费者
###consul服务端口号
server:
port: 80
spring:
application:
name: cloud-consumer-order
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}三个注册中心的异同
CAP
C: Consistency(强一致性)
A: Availability(可用性)
P: Partition tolerance(分区容错性)
CAP 理论关注粒度是数据,而不是整体系统设计的策略
最多只能同时较好的满足两个。 CAP 理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求, 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。 CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。 AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

异同

AP 架构 当网络分区出现后,为了保证可用性,系统 B 可以返回旧值,保证系统的可用性。 结论:违背了一致性 C 的要求,只满足可用性和分区容错,即 AP

CP 架构 当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性 结论:违背了可用性 A 的要求,只满足一致性和分区容错,即 CP

SpringCloud Netflix Ribbon 负载均衡与服务调用
Ribbon
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端 负载均衡的工具。
简单的说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出 Load Balancer(简称 LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。 我们很容易使用 Ribbon 实现自定义的负载均衡算法。
负载均衡
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用)。 常见的负载均衡有软件 Nginx,LVS,硬件 F5 等。
Ribbon 本地负载均衡客户端 VS Nginx 服务端负载均衡区别 Nginx 是服务器负载均衡,客户端所有请求都会交给 nginx,然后 由 nginx 实现转发请求。即负载均衡是由服务端实现的。
Ribbon 本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到 JVM 本地,从而在 本地实现 RPC 远程服务调用技术。
集中式 LB VS 进程式 LB
集中式 LB 即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,如 F5, 也可以是软件,如 nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内 LB 将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Ribbon = 负载均衡+RestTemplate 调用
Restemplate 说明
getForObject 返回对象为响应体中数据转化成的对象,基本上可以理解为 Json
getForEntities 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等

Ribbon 负载均衡架构
整合 Eureka 架构

Ribbon 在工作时分成两步 第一步先选择 EurekaServer , 它优先选择在同一个区域内负载较少的 server. 第二步再根据用户指定的策略,在从 server 取到的服务注册列表中选择一个地址。 其中 Ribbon 提供了多种策略:比如轮询、随机和根据响应时间加权。
spring-cloud-starter-netflix-eureka-client 确实引入了 Ribbon

Ribbon 核心组件 IRule
负载均衡算法

public interface IRule {
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}落地实现

自定义负载均衡算法
特殊化前提
这个自定义配置类不能放在@ComponentScan 所扫描的当前包下以及子包下, 否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。

添加自定义算法组件
@Configuration
public class MyLoadBalanceRule
{
@Bean
public IRule myRule()
{
return new RandomRule();//定义为随机
}
}主启动类添加注解
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(value = "cloud-payment-service",configuration = MyLoadBalanceRule.class)
public class CloudConsumerOrder80Main {
public static void main(String[] args) {
SpringApplication.run(CloudConsumerOrder80Main.class,args);
}
}负载均衡算法原理
轮询算法:rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后 rest 接口计数从 1 开始。
//RoundRobinRule.class
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
while(true) {
if (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if (upCount != 0 && serverCount != 0) {
int nextServerIndex = this.incrementAndGetModulo(serverCount);
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}手写负载均衡算法

//取消RestTemplate负载均衡的能力
// @LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
//手写负载均衡算法
@Component
public class MyLoadBalancerImpl implements MyLoadBalancer{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = atomicInteger.get();
next =current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!atomicInteger.compareAndSet(current,next));
System.out.println("第" + next + "次访问");
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
//初始为0 调用一次即为1
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);//某一次访问的服务实例
}
}public class OrderController {
public static final String PAYMENT_URL = "http://cloud-payment-service";
@Resource
private RestTemplate restTemplate;
@Resource
private MyLoadBalancer myLoadBalancer;//用于服务负载均衡
@Resource
private DiscoveryClient discoveryClient;//用于获取服务实例
@GetMapping("/lb")
public CommonResult lb() {
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
if (instances == null || instances.size() <= 0){
return null;
} else {
//通过负载均衡得到了某一个的服务实例
ServiceInstance instance = myLoadBalancer.instances(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/payment/info/1",CommonResult.class);
}
}
}SpringCloud Netflix OpenFeign 服务接口调用
概述
Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写 Web Service 客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。
Feign 也支持可拔插式的编码器和解码器。Spring Cloud 对 Feign 进行了封装,使其支持了 Spring MVC 标准注解和 HttpMessageConverters。Feign 可以与 Eureka 和 Ribbon 组合使用以支持负载均衡
功能
Feign 旨在使编写 Java Http 客户端变得更容易。
问题
前面在使用 Ribbon+RestTemplate 时,利用 RestTemplate 对 http 请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。
解决
Feign 在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在 Feign 的实现下,我们 只需创建一个接口 并使用注解的方式来配置它(以前是 Dao 接口上面标注 Mapper 注解, 现在是一个微服务接口上面标注一个 Feign 注解即可),即可完成对服务提供方的接口绑定,简化了使用 Spring cloud Ribbon 时,自动封装服务调用客户端的开发量。
Feign 集成了 Ribbon 利用 Ribbon 维护了 Payment 的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与 Ribbon 不同的是,通过 feign 只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
Feign
Feign 是 Spring Cloud 组件中的一个轻量级 RESTful 的 HTTP 服务客户端 Feign 内置了 Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign 的使用方式是:使用 Feign 的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
OpenFeign 是 Spring Cloud 在 Feign 的基础上支持了 SpringMVC 的注解
使用
<!--openfiegn-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>server:
port: 82
eureka:
client:
register-with-eureka: false # 不注册进eureka 但抓取注册中心的服务信息
service-url:
defaultZone: http:@SpringBootApplication
@EnableFeignClients//开启OpenFeign功能
public class CloudConsumerFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(CloudConsumerFeignMain80.class, args);
}
}业务逻辑接口 + 注解
新建 PaymentFeignService 接口并新增注解@FeignClient
@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")//调用的服务的名称
public interface PaymentFeignService {
@GetMapping("/payment/payment/info/{id}")//路径必须正确完整
public CommonResult<Payment> info(@PathVariable("id") Long id);
}超时控制
默认 Feign 客户端只等待一秒钟,但是服务端处理需要超过 1 秒钟,导致 Feign 客户端不想等待了,直接返回报错。 为了避免这样的情况,有时候我们需要设置 Feign 客户端的超时控制。
yml 文件中开启配置
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000日志打印
Feign 提供了日志打印功能,我们可以通过配置来 调整日志级别,从而了解 Feign 中 Http 请求的细节。
日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。配置
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}logging:
level:
# feign日志以什么级别监控哪个接口
com.tintin.springcloud.service.PaymentFeignService: debugSpringCloud Netflix Hystrix 断路器
概述
问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的 “扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
Hystrix
是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
原理
当某个服务单元发生故障时,向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
功能概念
服务降级(fallback)
为客户端返回一个 友好提示。例如下列情况,程序运行异常、超时、服务熔断触发服务降级、线程池或信号量满也会导致服务降级。
服务熔断(break)
当服务失败达到最大次数或服务访问最大时,直接拒绝访问,通过服务降级的方式并返回友好提示
服务限流(limit)
对于高并发的操作,限制流量。
接近实时的监控
测试案例
构建
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka@Service
public class PaymentServiceImpl implements PaymentService {
/**
* 正常访问
* @param id
* @return
*/
@Override
public String paymentInfoOk(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " paymentInfoOk,id: " + id;
}
/**
* 异常访问 超时
* @param id
* @return
*/
@Override
public String paymentInfoTimeout(Integer id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfoTimeout,id: " + id + " 耗时三秒钟";
}
}
@RestController
@RequestMapping("/payment/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/InfoOk/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id ) {
return paymentService.paymentInfoOk(id);
}
@GetMapping("/InfoTimeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
return paymentService.paymentInfoTimeout(id);
}
}高并发测试
Jmeter 20000 个并发 访问 paymentInfoTimeout 
导致两个服务方法的访问都被拖慢
原因:tomcat 的默认的工作线程数被打满 了,没有多余的线程来分解压力和处理。
开启消费者端 80 调用服务 就会出现访问慢的问题
故障原因
tomcat 的默认的工作线程数被打满 了,没有多余的线程来分解压力和处理。
开启消费者端 80 调用服务 就会出现访问慢的问题
解决

服务降级
服务端 fallback
服务方法添加注释@HystrixCommand
@Override
@HystrixCommand(fallbackMethod = "paymentInfoTimeOutHandler", commandProperties = {
//超时三秒即触发服务降级
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfoTimeout(Integer id) {
// int a = 10/0; 程序异常 服务降级
Integer time = 6;
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfoTimeout,id: " + id + " 耗时"+ time + "秒钟";
}
public String paymentInfoTimeOutHandler(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " paymentInfoTimeOutHandler,id: " + id ;
}主启动类启动
@EnableCircuitBreaker//回路客户端 fallback
80 订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护
feign:
hystrix:
enabled: true@GetMapping("/info/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeoutFallbackMethod", commandProperties = {
//客户端1.5秒则服务降级
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
public String orderInfoTimeout(@PathVariable Integer id) {
return paymentHystrixService.paymentInfoTimeout(id);
}
public String paymentTimeoutFallbackMethod(Integer id) {
return Thread.currentThread().getName() + "对方系统繁忙,请稍后重试";
}问题
每个业务方法对应一个兜底的方法,代码膨胀
全局配置和自定义配置分开
解决
为服务接口配置全局降级方法,全局与自定义各自分开
@RestController
@RequestMapping("/payment/payment")
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/info/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id ) {
return paymentService.paymentInfoOk(id);
}
@GetMapping("/info/timeout/{id}")
// @HystrixCommand//无特殊指明 使用全局降级方法
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
return paymentService.paymentInfoTimeout(id);
}
//全局降级
public String paymentGlobalFallbackMethod() {
return "线程池:" + Thread.currentThread().getName() + " paymentInfoTimeOutHandler 全局 系统繁忙或运行错误,请稍后再试" ;
}
}由于与降级方法与业务逻辑代码写在一起 耦合度依然高
假如 服务降级,客户端去调用服务端,碰上服务端宕机或关闭
本次案例服务降级处理是在客户端 80 实现完成的,与服务端 8001 没有关系只需要为 Feign 客户端定义的接口添加一个服务降级处理的实现类即可实现解耦
根据 cloud-consumer-feign-hystrix-order80 已经有的 PaymentHystrixService 接口, 重新新建一个类(PaymentFallbackService)实现该接口统一为接口里面的方法进行异常处理
# 用于服务降级 在注解@FeignClient中添加fallbackFactory属性值
feign:
hystrix:
enabled: true #在Feign中开启Hystrix@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentHystrixFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/payment/info/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id );
@GetMapping("/payment/payment/info/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id);
}//客户端service中所有的降级方法
public class PaymentHystrixFallbackService implements PaymentHystrixService{
@Override
public String paymentInfoOk(Integer id) {
return "服务调用失败,提示来自:cloud-consumer-feign-order80";
}
@Override
public String paymentInfoTimeout(Integer id) {
return "服务调用失败,提示来自:cloud-consumer-feign-order80";
}
}服务熔断
熔断机制是应对 雪崩效应 的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时, 会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。 当检测到该节点微服务调用响应正常后,恢复调用链路。

在 Spring Cloud 框架里,熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用的状况, 当失败的调用到一定阈值,缺省是 5 秒内 20 次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
涉及到断路器的三个重要参数: 快照时间窗、请求总数阀值、错误百分比阀值。 1:快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒。
2:请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10 秒内,如果该 hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3:错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了 30 次调用,如果在这 30 次调用中,有 15 次发生了超时异常,也就是超过 50%的错误百分比,在默认设定 50%阀值情况下,这时候就会将断路器打开。
//服务熔断部分
@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"),//错误百分比阀值
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
if(id < 0)
{
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id)
{
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}效果:
多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行
熔断类型

熔断流程
- 服务正常情况下:熔断关闭
- 一个快照时间窗后,熔断器统计请求总数达到阈值,且请求错误的百分比超过阈值:熔断打开
- 一个 MTTR(平均故障处理时间)后:熔断半开
- 允许部分请求根据规则调用服务,若请求成功后:熔断关闭,若请求失败后:熔断打开
服务限流
[在 Sentinel 中讲解](#19 SpringCloud Alibaba Sentinel实现熔断与限流)
Hystrix 工作流程

服务监控 DashBoard(web 界面)
除了隔离依赖服务的调用以外,Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)
<!--hystrix-dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>server:
port: 9001@EnableHystrixDashboard所有 Privider 微服务提供类都需要监控依赖配置
<!-- actuator监控信息完善 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>监控演示
主启动类指定监控路径
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//回路
public class CloudProviderHystrixPayment8001 {
public static void main(String[] args) {
SpringApplication.run(CloudProviderHystrixPayment8001.class,args);
}
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
SpringCloud Netflix zuul 路由网关
zuul2 正在更新
Cloud 全家桶中有个很重要的组件就是网关,在 1.x 版本中都是采用的 Zuul 网关;但在 2.x 版本中,zuul 的升级一直跳票,
GateWay 新一代网关
概述
SpringCloud 自己研发了一个网关替代 Zuul,那就是 SpringCloud Gateway 一句话:gateway 是原 zuul1.x 版的替代
SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在 Spring Cloud 2.0 以上版本中,没有对新版本的 Zuul 2.0 以上最新高性能版本进行集成,仍然还是使用的 Zuul 1.x 非 Reactor 模式的老版本。而为了提升网关的性能,SpringCloud Gateway 是 基于 WebFlux 框架 实现的,而 WebFlux 框架底层则使用了 高性能的 Reactor 模式通信框架 Netty。
Spring Cloud Gateway 的目标提供统一的路由方式且 基于 Filter 链 的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
源码架构

功能
反向代理 鉴权 流量控制 熔断 日志监控 ...
微服务架构中的位置

特性
基于 Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建; 动态 路由:能够匹配任何请求属性; 可以对路由指定 Predicate(断言)和 Filter(过滤器); 集成 Hystrix 的断路器功能; 集成 Spring Cloud 服务发现功能; 易于编写的 Predicate(断言)和 Filter(过滤器); 请求限流功能; 支持路径重写。
与 zuul 对比
Zuul 1.x 基于 Servlet 2. 5 使用阻塞架构 它 不支持任何长连接(如 WebSocket) Zuul 的设计模式和 Nginx 较像,每次 I/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是 Nginx 用 C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差。
Zuul 1.x l 缺点:servlet 是一个简单的网络 IO 模型,当请求进入 servlet container 时,servlet container 就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用 jemeter 压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个 request 分配一个线程,只需要 1 个或几个线程就能应对极大并发的请求,这种业务场景下 servlet 模型没有优势
spring Cloud Gateway 建立 在 Spring Framework 5、 Project Reactor 和 Spring Boot 2 之上, 使用非阻塞 API。Spring Cloud Gateway 还 支持 WebSocket, 并且与 Spring 紧密集成拥有更好的开发体验
webflux 是什么
传统的 Web 框架,比如说:struts2,springmvc 等都是基于 Servlet API 与 Servlet 容器基础之上运行的。
但是在 Servlet3.1 之后有了 异步非阻塞 的支持。而 WebFlux 是一个典型非阻塞异步的框架,它的核心是基于 Reactor 的相关 API 实现的。相对于传统的 web 框架来说,它可以运行在诸如 Netty,Undertow 及支持 Servlet3.1 的容器上。非阻塞式+函数式编程(Spring5 必须让你使用 java8)
Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖 Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
核心概念
路由 route
构建网关的基本模块,由 id,目标 uri,一系列断言和过滤器组成,如果断言为 true 则匹配该路由
断言 predicate
参考 Java8 的 java.util.function.Predicate
开发人员可以匹配的 HTTP 请求中所有内容,例如请求头或请求参数,如果请求与断言相匹配则进行路由
过滤 filter
指 Spring 框架中的 GatewayFilter 实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

web 请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。 predicate 就是我们的匹配条件; 而 filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标 uri,就可以实现一个具体的路由了
工作流程
路由转发+执行过滤链
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
Filter 在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等, 在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
入门配置
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
deaultZone: http://eureka7001.com:7001/eureka
register-with-eureka: true
fetch-registry: true在 8001 服务套一层 9527 网关
# 网关配置
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由效果

代码配置方式
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_rout_tintin", p -> p.path("/guonei").uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}通过服务名实现动态路由
默认情况下 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/payment/info/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb//cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/payment/lb/** # 断言,路径相匹配的进行路由predicate 的使用

spring:
cloud:
routes:
predicates:
- Path=/payment/payment/info/**
- After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai]
- Cookie=username,zzyy
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Host=**.atguigu.com
- Method=GET
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由Predicate 就是为了实现一组匹配规则 让请求过来找到对应的 Route 进行处理。11.7 filter 的使用
filter 的使用
路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用。
Spring Cloud Gateway 内置了多种路由过滤器,他们都由 GatewayFilter 的工厂类来产生
yml 方式
spring:
cloud:
routes:
filters:
- AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024自定义全局 filter
@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");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}SpringCloud Config
概述
分布式系统的配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以 一套集中式的、动态的配置管理设施 是必不可少的。
SpringCloud Config
SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

功能
集中管理配置文件
不同环境不同配置 dev/test/prod/beta/release
运行期间动态调整配置,服务会想配置中心拉取自己的信息
当配置发生改动时,无需重启即可感知配置变化并应用新的配置
将配置信息以 rest 接口的形式暴露
Config 服务端配置与测试

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>server:
port: 3344
spring:
application:
name: cloud-config-center #注册进eureka的微服务名
cloud:
config:
server:
git:
uri: git@github.com:TintinLY/springcloud-config.git # github上的仓库名称
search-paths:
- springcloud-config # 搜索仓库的目录
default-label: main # 读取某一分支
#服务注册到eureka
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka@EnableConfigServer


Config 客户端配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>bootstrap.yml
applicaiton.yml 是用户级的资源配置项 bootstrap.yml 是系统级的,优先级更加高
Spring Cloud 会创建一个“Bootstrap Context”,作为 Spring 应用的 Application Context 的父上下文。初始化的时候,Bootstrap Context 负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的 Environment。
Bootstrap 属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context 和 Application Context 有着不同的约定,所以新增了一个 bootstrap.yml 文件,保证 Bootstrap Context 和 Application Context 配置的分离。
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eurekaconfig 客户端动态刷新(无需重启获取配置)

引入监控模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency># 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"@RestController
@RequestMapping("/config/client")
@RefreshScope
public class ConfigClientController {运维人员发送 post 请求刷新 3355

如何广播,一次通知,每个客户端都生效
总线
SpringCloud Bus 消息总线
概述
SpringCloudBus
SpringCloud Bus 配合 SprigCloud Config 使用能实现配置的动态刷新

Spring Cloud Bus 是用来 将分布式系统的节点与轻量级消息系统链接起来 的框架, 它整合了 Java 的事件处理机制和消息中间件的功能。 Spring Clud Bus 目前支持 RabbitMQ 和 Kafka。
总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient 实例都监 听 MQ 中同一个 topic(默认是 springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到 Topic 中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置。
RabbitMQ 环境配置
安装 Erlang 并配置环境变量
安装 RabbitMQ
启动管理功能
rabbitmq-plugins enable rabbitmq_management
出现可视化插件

默认访问地址 RabbitMQ Management
登录 RabbitMq
./rabbitmqctl add_user admin admin
./rabbitmqctl set_user_tags admin administratorSpringCloud Bus 动态刷新全局广播
以 cloud-config-client3355 为模板 负责 cloud-config-client3377
@RestController
@RequestMapping("/config/client")
@Slf4j
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@Value("${server.port}")
private String serverPort;
@GetMapping("/configInfo")
public String configInfo() {
log.info(configInfo);
System.out.println(serverPort);
return "serverPort:" + serverPort + ", configInfo:" + configInfo;
}
}1)利用消息总线触发一个客户端/bus/refresh, 而刷新所有客户端配置
这种设计实现打破了微服务的单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。 破坏了微服务各节点的对等性。 有一定的局限性。例如,微服务在迁移时,会发生变化,此时如果想要做到自动刷新,那就多改
2)利用消息总线触发一个服务端 ConfigServer 的/bus/refresh 端点,而刷新所有客户
给 cloud-config-center3344 配置中心添加消息总线支持
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency># 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
#rabbitmq相关配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest给 cloud-config-client3355 和 cloud-config-client3377 配置中心添加消息总线支持
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency># 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
效果:一次修改,广播通知,处处生效
SpringCloud Bus 动态刷新定点通知
定点通知 3355

总结

SpringCloud Stream 消息驱动
SpringCloud Stream 概述
一个屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中 binder 对象交互。 通过我们配置来 binding(绑定) ,而 Spring Cloud Stream 的 binder 对象负责与消息中间件交互。 所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
SpringCloud Stream 设计思想
标准 MQ
生产者与消费者之间靠消息媒介传递信息内容:Message
消息必须走特定通道:MessageChannel
由消息通道里的子接口 SubMessageChannel,由 MessageHandler 消息处理器订阅
使用 SpringCloud Stream 屏蔽不同中间件的差异性
像 RabbitMQ 有 exchange,kafka 有 Topic 和 Partitions 分区,
当项目往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候 springcloud Stream 给我们提供了一种解耦合的方式。
原理
由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的 Channel 通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
Binder 绑定器
Binder 可以生成 Binding,Binding 用来绑定消息容器的生产者和消费者,它有两种类型,INPUT 和 OUTPUT,INPUT 对应于消费者,OUTPUT 对应于生产者。

发布-订阅模式

标准编码流程

binder 连接中间件,屏蔽差异
Channel 通道,是队列 Queue 的一种抽象,在消息通讯系统中存储和转发的媒介,通过 Channel 对队列进行配置
Source 和 Sink,简单理解为 SpringCloud Stream 本身,接受消息或发布消息。
API 与注解
| 组成 | 说明 |
|---|---|
| MiddleWare | 中间件,目前只支持 RabbitMQ 和 Kafka |
| Binder | Binder 是应用与消息中间件之间的封装,目前实行了 Kafka 和 RabbitMQ 的 Binder,通过 Binder Binder 可以很方便的连接中间件,可以动态的改变消息类型(对应于 Kaika 的 topic,RabbitMQ 的 exchange),这些都可以通过配置文件来实现 |
| @lnput | 注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
| @Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
| @StreamListener | 监听队列,用于消费者的队列的消息接收 |
| @EnableBinding | 指信道 channel 和 exchange 绑定在一起 |
测试案例

消息驱动之生产者
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
fetch-registry: true
register-with-eureka: true
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地址@EnableBinding(Source.class)//定义消息推送的管道
public class MessageProviderImpl implements MessageProvider {
@Resource
private MessageChannel output;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
System.out.println("***serial: "+serial);
return serial;
}
}开启 7001 rabbitmq 8801

消息驱动之消费者
pom.xml 同上
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
register-with-eureka: true
fetch-registry: true
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地址@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message)
{
System.out.println("消费者1号,------->接收到的消息:" + message.getPayload()+"\t port: "+serverPort);
}
}
分组消费与持久化
分组消费
根据消费者 8802 复制出消费者 8803

运行后的两个问题:
重复消费与消息持久化问题
重复消费:8802 与 8803 都收到了消息

解决:将 8802 8803 设置在同一个组
多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,
但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在 Spring Cloud Stream 中提供了 消费组 的概念。
注意在 Stream 中处于同一个 group 中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。
不同组是可以全面消费的(重复消费),
同一组内会发生竞争关系,只有其中一个可以消费。
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: tintinA # 分组消费若不同分组,出现重复消费

若同一分组,解决重复消费
持久化

SpringCloud Sleuth 分析请求链路追踪
概述
问题 :
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
Spring Cloud Sleuth 为此提供了一套完整的服务跟踪的解决方案 在分布式系统中提供追踪解决方案并且兼容支持了 zipkin

搭建链路监控
Zipkin
下载 jar 包 Maven Central Repository Search 并运行

表示一请求链路,一条链路通过 Trace Id 唯一标识,Span 标识发起的请求信息,各 span 通过 parent id 关联起来

Trace: 类似于树结构的 Span 集合,表示一条调用链路,存在唯一标识 span: 表示调用链路来源,通俗的理解 span 就是一次请求信息
服务提供者
以 cloud-provider-payment8001 为例
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency> zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1服务消费者(调用方)
同上
@GetMapping("/zipkin")
public CommonResult zipkin() {
ResponseEntity<CommonResult> responseEntity = restTemplate.getForEntity(PAYMENT_URL + "/payment/payment/zipkin", CommonResult.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
return responseEntity.getBody();
} else {
return new CommonResult(444,"操作失败");
}
}由 80 调用 8001 打开浏览器访问: http://localhost: 9411

SpringCloudAlibaba 入门简介
SpringCloud Netflix 项目进入维护模式

官网: https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
诞生: 2018.10.31,Spring Cloud Alibaba 正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本。
功能:
服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
SPringCloud Alibaba Nacos 服务注册与配置中心
Nacos 简介
Dynamic Naming and Configuration Service
Nacos = Eureka + Config + Bus (注册中心和配置中心的组合)

安装并运行
最新版本默认为集群方式运行,
使用如下命令进行单机版运行
$ startup.cmd -m standalone
账号 nacos 密码 nacos

Nacos 作为服务注册中心
基于 Nacos 服务提供者
cloudalibaba-provider-payment9001
父 pom
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyMagement>子 pom
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>@EnableDiscoveryClient@RestController
@RequestMapping("/payment/payment")
public class PaymentController {
@Value("server.port")
private String serverPort;
@GetMapping("nacos/{id}")
private String nacos(@PathVariable("id") Integer id) {
return "nocos registry, serverPort:" + serverPort + ", id:" +id;
}
}启动 9001

复制 9001 配置启动 9002


基于 Nacos 服务消费者
cloudalibaba-consumernacos-order81
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.tintin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider开启 resttemplate 服务调用与负载均衡
@Configuration
public class ApplicationContextBean {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}业务类
@RestController
@RequestMapping("/order/order")
public class OrderController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serviceURL;
@GetMapping("nacos/{id}")
public String nacos(@PathVariable("id") Integer id) {
return restTemplate.getForObject(serviceURL + "/payment/payment/nacos/" + id, String.class);
}
}访问 nacos-payment-provider 服务 对两个服务实例进行 轮询 访问
服务注册中心对比


C 是所有节点在同一时间看到的数据是一致的;而 A 的定义是所有的请求都会收到响应。
nacos 支持 AP 和 CP 模式的切换
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'Nacos 作为服务配置中心
基础配置
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency># nacos配置 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: yml #指定yaml格式的配置# nacos配置 application.yml
spring:
profiles:
active: dev # 环境@RestController
@RequestMapping("/config/client")
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/Info")
public String configInfo() {
return configInfo;
}
}在 nacos 中添加配置信息
配置规则:
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

分类配置
问题:
实际开发中,通常一个系统会准备 dev 开发环境,test 测试环境,prod 生产环境。 如何保证指定环境启动时服务能正确读取到 Nacos 上相应环境的配置文件呢?
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境...... 那怎么对这些微服务配置进行管理呢?



命名空间 NameSpace
Nacos 默认的命名空间是 public,Namespace 主要用来实现隔离不同的部署环境。比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个 Namespace,不同的 Namespace 之间是隔离的。
分组 Group**
Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去,同一个分组的微服务将会竞争消费消息。
微服务 Service
Service 就是微服务;一个 Service 可以包含多个 Cluster(集群)
集群 Cluster
Nacos 默认 Cluster 是 DEFAULT,Cluster 是对指定微服务的一个虚拟划分。 比方说为了容灾,将 Service 微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的 Service 微服务起一个集群名称(HZ), 给广州机房的 Service 微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
实例 Instance
最后是 Instance,就是微服务的实例。
解决方案 1:DataID
指定 spring.profile.active 和配置文件件的 DatalD 来使不同环境下读取不同的配置

解决方案 2:Group
在 config 下增加一条 group 配置即可。可配置为 DEV_GROUP 或 TEST_GROUP
spring:
cloud:
nacos:
config:
group: TEST_GROUP # 分组名称 搭配环境可以找到对应配置文件
解决方案 3:NameSpace
spring:
cloud:
nacos:
config:
namespace: 5da1dccc-ee26-49e0-b8e5-7d9559b95ab0 # 命名空间id 搭配环境和分组可以找到对应配置文件
集群与持久化配置
说明
集群架构

默认 Nacos 使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的 Nacos 节点,数据存储是存在一致性问题的。 为了解决这个问题,Nacos 采用了 集中式存储 的方式来支持集群化部署,目前只支持 MySQL 的存储。

持久化配置(mysql)
Nacos 默认自带的嵌入式数据库是 derby
由 derby 切换到 mysql
创建数据库 nacos_config 并执行安装目录 nacos\conf\nacos-mysql.sql 脚本

修改 nacos\conf\application.properties
# derby切换mysql
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
db.user=root
db.password=root启动 Nacos, 可以看到是个全新的空记录界面,以前是记录进 derby
Linux 安装 Nacos 集群+Mysql 持久化配置
1 个 nginx 3 个 nacos 1 个 muysql

mysql 配置 nacos-mysql.sql

备份 application.properties 并修改
# derby切换mysql
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
db.user=root
db.password=123456 # 此数据库为虚拟机中docker的mysql备份 cluster.conf 并修改 配置 ⽂ 件中设置集群中每 ⼀ 个节点的信息
# 集群节点配置
192.168.10.102:3333
192.168.10.102:4444
192.168.10.102:5555编辑 bin/startup.sh 脚本,使 nacos 能以不同的端口启动


nginx 配置,作为负载均衡器

启动三个 nacos(需要不断尝试 确认每个节点是否启动成功)和一个 nginx
bin/startup.sh -P 5555
sbin/nginx插入配置后 mysql 有对应记录

cloud9001 注册金 nacos mysql 有对应记录


SpringCloud Alibaba Sentinel 实现熔断与限流
与 Hystrix 对比

概述
简介
轻量级流量控制、熔断降级 Java 库
功能

Sentinel 分为两个部分: 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo/Spring Cloud 等框架也有较好的支持。 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
安装运行


初始化演示工程
cloudalibaba-sentinal-service8401
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service8401
cloud:
nacos:
discovery:
# Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
# sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口 如果被占用则依次加1扫描,直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'@RestController
@RequestMapping("flow/limit")
public class FlowLimitController {
@GetMapping("testA")
public String testA() {
return "-----testA";
}
@GetMapping("testB")
public String testB() {
return "-----testB";
}
}启动 sentinel 控制面板查看
发现无服务列表
原因:懒加载机制
执行一次控制器方法即可

流量控制规则
基本介绍


流控模式
直接
QPS+直接+快速失败 : 表示 1 秒钟内查询 n 次就是 OK,若超过次数 n,就直接-快速失败,报默认错误
关联
QPS+关联 testB+快速失败:表示资源 B 达到阈值,限制 A
链路
流控效果
快速失败
WarmUp
阈值除以 coldFactor(默认值为 3),经过预热时长才会达到最终的阈值
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过 "冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参 WarmUpFlowDemo。 通常冷启动的过程系统允许通过的 QPS 曲线如下图所

排队等待
匀速排队,让请求以均匀的速度通过,阀值类型必须设成 QPS,否则无效。 设置含义:/testA 每秒 1 次请求,超过的话就排队等待,等待的超时时间为 20000 毫秒。

降级规则
基本介绍
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制, 让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在 接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
无半开状态

RT(平均响应时间,秒级) 平均响应时间 超出阈值 且 在时间窗口内通过的请求 >= 5,两个条件同时满足后触发降级 窗口期过后关闭断路器 RT 最大 4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt = XXXX 才能生效)
异常比列(秒级) QPS >= 5 且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级) 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
降级-RT

降级-异常比例

降级-异常数

热点规则
基本介绍
何为热点 热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的 TopN 数据,并对其访问进行限流或者其它操作

热点参数限流会 统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
@SentinelResource
兜底方法 分为系统默认和客户自定义,两种
之前的 case,限流出问题后,都是用 sentinel 系统默认的提示:Blocked by Sentinel (flow limiting)
我们能不能自定?类似 hystrix,某个方法出问题了,就找对应的兜底降级方法?@SentinelResource
代码测试
@GetMapping("/testHotKey")
// 资源名↓
@SentinelResource(value = "testHotKey", blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2", required = false) String p2 ) {
return "-----testHotKey";
}
public String dealHandler_testHotKey(String p1, String p2, BlockException exception) {
return "-----dealHandler_testHotKey";
}
限流模式只支持 QPS 模式,固定写死了。(这才叫热点) @SentinelResource 注解的方法参数索引,0 代表第一个参数,1 代表第二个参数,以此类推 单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。 上面的抓图就是第一个参数有值的话,1 秒的 QPS 为 1,超过就限流,限流后调用 dealHandler_testHotKey 支持方法。
参数例外项
QPS 超过阈值后,达到阈值后马上被限流:普通 我们期望 p1 参数当它是某个特殊值时,它的限流值和平时不一样 假如当 p1 的值等于 5 时,它的阈值可以达到 200:特例
热点参数的注意点,参数必须是基本类型或者 String
异常处理
@SentinelResource 处理的是 Sentinel 控制台配置的违规情况,有 blockHandler 方法配置的兜底处理;
RuntimeException int age = 10/0, 这个是 java 运行时报出的运行时异常 RunTimeException,@SentinelResource 不管
总结 @SentinelResource 主管配置出错,运行出错该走异常走异常
[@SentinelResource 配置](#19.8 @SentinelResource)
系统规则
Sentinel 系统自适应限流 从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。


@SentinelResource
按资源名限流
cloudalibaba-sentinal-service8401 引入通用包实现支付功能
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service8401
cloud:
nacos:
discovery:
# Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
# sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口 如果被占用则依次加1扫描,直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'@RestController
@RequestMapping("/rate/limit")
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handlerException")
public CommonResult byResource() {
return new CommonResult(200, "资源名称限流测试ok",new Payment(2020L,"serial001"))
}
public CommonResult handlerException(BlockException e) {
return new CommonResult(444,e.getClass().getCanonicalName() + "\t服务不可用")
}
}流控规则

当服务关闭,流控规则等将会清楚
按 url 地址限流

问题
1 系统默认的,没有体现我们自己的业务要求。 2 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。 3 每个业务方法都添加一个兜底的,那代码膨胀加剧。 4 全局统一的处理方法没有体现。
自定义限流处理逻辑
自定义限流处理类
public class CustomerBlockHandler {
public static CommonResult handlerException1(BlockException e) {
return new CommonResult<>(444,"自定义限流处理信息...1" +e.getClass().getCanonicalName());
}
public static CommonResult handlerException2(BlockException e) {
return new CommonResult<>(444,"自定义限流处理信息...2" +e.getClass().getCanonicalName());
}
} @GetMapping("/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200,"按客户自定义限流处理逻辑");
}自定义限流处理只适用于根据资源名称限流
不同 url 可以配相同的资源名 实现统一管理
其他注解

服务熔断
sentinel 整合 ribbon+openFeign+fallback
Ribbon 系列
提供者 9003/9004 模拟数据库查询
@RestController
@RequestMapping("/payment/payment")
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static
{
hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment);
return result;
}
}消费者 84
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>测试

//无fallback 无blockHandler 只返回错误页面对客户不友好
@RequestMapping("/fallback/{id}")
@SentinelResource(value = "fallback")
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(serviceURL + "/payment/payment/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}//只配置fallback 无blockHandler
@SentinelResource(value = "fallback", fallback = "handlerFallback")
public CommonResult<Payment> fallback(@PathVariable Long id)
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}//只配置blockHandler 无fallback
@RequestMapping("/fallback/{id}")
@SentinelResource(value = "fallback",
// fallback = "handlerFallback",
blockHandler = "CommonResult")
public CommonResult<Payment> fallback(@PathVariable Long id)
{
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}//同时配置blockHandler fallback
@RequestMapping("/fallback/{id}")
@SentinelResource(value = "fallback",
fallback = "handlerFallback",
blockHandler = "CommonResult")
public CommonResult<Payment> fallback(@PathVariable Long id)
{若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})//忽略某个异常的属性
public CommonResult<Payment> fallback(@PathVariable Long id)
{openFeign 系列
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency># 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true规则持久化
一旦我们重启应用,sentinel 规则将消失,生产环境需要将配置规则进行持久化
将限流配置规则持久化进 Nacos 保存,只要刷新 8401 某个 rest 地址, sentinel 控制台的流控规则就能看到,只要 Nacos 里面的配置不删除,针对 8401 上 sentinel 上的流控规则持续有效
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow在 nacos 添加对应配置
dataId: cloudalibaba-sentinel-service groupId: DEFAULT_GROUP data-type: json
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]即可实现规则持久化
关闭后重启微服务 规则还在
SpringCloud Alibaba Seata 处理分布式事务
分布式事务问题
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源, 业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是 全局的数据一致性问题没法保证。

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用, 就会产生分布式事务问题
Seata 简介
Seata 是一款开源的分布式事务解决方案,致力在微服务架构下提供高性能和简单易用的分布式事务服务。
分布式处理过程的 ID+三组件模型
全局唯一的事务 ID
事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚; Transaction Coordinator (TC)
控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议; TransactionManager(TM)
控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚 Resource Manager(RM)
处理过程
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

全局@GlobalTransactional

安装运行 (Seata 服务端配置)
配置模板文件地址
seata/script at 1.4.2 · seata/seata (github.com)
备份修改 seata/conf/file.conf
此配置项为 seata 服务器的存储配置,存储方式选择 db,再配置数据库的连接信息,以及处理事务的全局性表(表名使用默认的就可以)。


driverClassName 驱动的配置需要根据 mysql 的版本决定: mysql5.+使用 driverClassName = "com.mysql.jdbc.Driver" mysql8 使用 driverClassName = "com.mysql.cj.jdbc.Driver"
备份修改 seata/conf/registry.conf
需要配置选用的注册中心类型(nacos),注册中心的连接信息;配置中心的类型,配置中心的连接信息。


① 当 nacos 开启安全配置(在 nacos 的 conf/application.properties 中配置 nacos.core.auth.enabled = true)后,对 nacos 的连接信息都要带上用户名、密码等信息
② 在 seata1.4.2 后才可以使用 dataId = "seataServer.properties" 的方式读取配置信息
在 nacos 中配置 seata 事务的相关属性
seata\script\config-center\config.txt
此配置信息是 seata 事务的相关属性,在 nacos 中创建 data id 时,粘贴到文本值的内容,即 seataServer.properties 的配置项,
①.service.vgroupMapping.my_test_tx_group = default 中的 my_test_tx_group 需要与 bootstrap.yml 中配置的 seata.tx-service-group 的值一致。
②.service.vgroupMapping.my_test_tx_group = default 配置的 default 必须要等于 registry.conf 中配置的 cluster = "default"。
③.store.mode = db 配置为 db 的方式,则需要配置 db 数据库方式的连接信息 store.db.url、store.db.user、store.db.password,此数据库存储下存放的表 global_table、branch_table、lock_table,用于记录全局性的事务信息
④.store.db.driverClassName 的配置 mysql5.+使用 driverClassName = "com.mysql.jdbc.Driver" mysql8 使用 driverClassName = "com.mysql.cj.jdbc.Driver"
⑤.service.default.grouplist = ip: port 为访问 seata 服务器的地址和端口(仅注册中心为 file 时使用),8091 是默认端口, 也可以修改启动端口,在启动项目时加上端口: seata-server.bat -p 18091 sh seata-server.sh -p 18091
⑥seata server 需要配置集群时,只需要在启动 seata server 服务时指定不同的端口和节点序号即可,配置 file.conf 和 registry.conf 的内容一致
seata\script\server\db\mysql.sql 进行建库 seata
创建事务表


先启动 nacos 再启动 seata

订单/库存/账户业务数据库准备 (Seata 客户端配置)
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存, 再通过远程调用账户服务来扣减用户账户里面的余额, 最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单》扣库存》减余额
创建业务数据库

创建业务表



seata\script\client\at\db\mysql.sql 创建回滚事务表

DROP TABLE `undo_log`;
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;订单/库存/账户业务微服务准备(Seata 客户端配置)
业务需求
下订单》扣库存》减余额
seata-order-service2001
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>需要与安装的seata版本一致
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>@GlobalTransaction

分布式事务流程
TM 开启分布式事务(TM 向 TC 注册全局事务记录) 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 ) TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务) TC 汇总事务信息,决定分布式事务是提交还是回滚 TC 通知所有 RM 提交/回滚 资源,,事务二阶段结束

AT 模式

在一阶段,Seata 会拦截“业务 SQL”, 1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”, 2 执行“业务 SQL”更新业务数据,在业务数据更新之后, 3 其保存成“after image”,最后生成行锁。 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如是顺利提交的话,
因为“业务 SQL”在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可
二阶段回滚: 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。 回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”, 如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
总结

