- SpringCloud Alibaba
- 挂载到本地
- 不挂载到本地
- 进入挂载的本地目录或者容器
- 进入配置文件所在目录
- 打开配置文件文件添加主机公网IP
- 3.5.4、整合Java实现消息发送与消费
- 3.6、Seata
- 四、进阶整合
SpringCloud Alibaba
一、微服务简介
1.1、架构演变
1.1.1、单体架构
- 互联网早期,一般的网站应用流量较小,只需一个应用,将所有功能代码都部署在一起就可以,这 样可以减少开发、部署和维护的成本。
- 比如说一个电商系统,里面会包含很多用户管理,商品管理,订单管理,物流管理等等很多模块, 我们会把它们做成一个web项目,然后部署到一台tomcat服务器上
- 优点
- 项目架构简单,小型项目的话, 开发成本低
- 项目部署在一个节点上, 维护方便
- 缺点
- 全部功能集成在一个工程中,对于大型项目来讲不易开发和维护
- 项目模块之间紧密耦合,单点容错率低
- 无法针对不同模块进行针对性优化和水平扩展
1.1.2、垂直应用架构
- 随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块 都会有比较大的访问量.
- 所谓垂直应用架构,就是将原来的一个应用拆成互不相干的几个应用,以提升效率。
- 比如我们可 以将上面电商的单体应用拆分成:
- 电商系统(用户管理 商品管理 订单管理)
- 后台系统(用户管理 订单管理 客户管理)
- CMS系统(广告管理 营销管理)
- 垂直拆分后,一旦用户访问量变大,只需要增加电商系统的节点就可以了,而无需增加后台 和CMS的节点
- 优点:
- 系统拆分实现了流量分担,解决了并发问题,而且可以针对不同模块进行优化和水平扩展
- 一个系统的问题不会影响到其他系统,提高容错率
- 缺点:
- 系统之间相互独立, 无法进行相互调用
- 系统之间相互独立, 会有重复的开发任务
1.1.3、分布式架构
- 当垂直架构拆分越来越多时,重复的业务代码就会越来越多,为了降低耦合,分布式架构诞生了
- 分布式架构将把工程拆分成表现层和服务层两个部分,服务层中包含业务逻辑。
- 表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。
- 优点:
- 抽取公共的功能为服务层,提高代码复用性
- 缺点:
- 系统间耦合度变高,调用关系错综复杂,难以维护
1.1.4、SOA架构
- 面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。
- 接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。
- 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。
- 服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。
- SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。
- SOA可以看作是B/S模型、XML,Web Service技术之后的自然延伸。
- SOA系统是一种企业通用性架构。
- 优点:
- 使用注册中心解决了服务间调用关系的自动调节
- 缺点:
- 服务间会有依赖关系,一旦某个环节出错会影响较大( 服务雪崩 )
- 服务关心复杂,运维、测试部署困难
1.2、认识微服务
1.2.1、什么是微服务
- 微服务是一种架构风格,严格来说微服务并不是一项技术,而是spring技术生态整合的集合,即将单体应用划分为小型的服务单元,达到模块化开发的目的,微服务之间使用 HTTP 的 API 进行资源访问与操作。
1.2.2、微服务优点及缺点
- 优点
- 服务的独立部署:每个服务都是一个独立的项目,可以独立部署,不依赖于其他服务,耦合性低。
- 服务的快速启动:拆分之后服务启动的速度必然要比拆分之前快很多,因为依赖的库少了,代码量也少了。
更加适合敏捷开发:敏捷开发以用户的需求进化为核心,采用迭代、循序渐进的方法进行。模块化发布。
职责专一:每个模块独立开发,提高了开发人员对业务的专注性
服务动态按需扩容:当某个服务的访问量较大时,我们只需要将这个服务扩容即可。
代码的复用:每个服务都提供 REST API,所有的基础服务都必须抽出来,很多的底层实现都可以以接口方式提供。
- 缺点
- 分布式部署,调用复杂性高:在微服务中,每个模块都是独立部署的,通过 HTTP 来进行通信,这当中会产生很多问题,比如网络问题、容错问题、调用关系等。
- 独立数据库,分布式事务挑战:每个微服务都有自己的数据库,这就是所谓的去中心化的数据管理。这种模式的优点在于不同的服务,可以选择适合自身业务的数据,比如订单服务可以用 MySQL、评论服务可以用 Mongodb、商品搜索服务可以用 Elasticsearch。
测试的难度提升:服务和服务之间通过接口来交互,当接口有改变的时候,对所有的调用方都是有影响的,同时就强调了 API 文档的管理尤为重要。
运维难度的提升:在微服务架构下模块化部署,会让业务增加时,服务也将越来越多,服务的部署、监控将变得非常复杂。
1.3、SpringCloud
- SpringCloud是一系列框架的有序集合。它利用 SpringBoot 的开发便利性,巧妙地简化了分布式系统基础设施的开发,如服务注册、服务发现、配置中心、消息总线、负载均衡、断路器、数据监控等,这些都可以用 Spring Boot 的开发风格做到一键启动和部署。
- 通俗地讲,Spring Cloud 就是用于构建微服务开发和治理的框架集合,并不是具体的一个框架。
- 作为新一代的服务框架,Spring Cloud 提出的口号是开发“面向云的应用程序”,它为微服务架构提供了更加全面的技术支持。
- SpringCloud与dubbo的功能区别
- SpringCloud的优势
- Spring Cloud 来源于 Spring,质量、稳定性、持续性都可以得到保证。
- Spirng Cloud 天然支持 Spring Boot,更加便于业务落地。
- Spring Cloud 是 Java领域最适合做微服务的框架。
1.4、SpringCloud Alibaba
1.4.1、什么是SpringCloud Alibaba
- SpringCloud Alibaba是阿里针对SpringCloud优化开源的国产技术,致力于一站式解决方案
- 依托 Spring Cloud Alibaba,只需要添加部分注解和少量配置就可将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
1.4.2、SpringCloud Alibaba功能介绍
- 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
1.4.3、SpringCloud Alibaba优势
- 得益于SpringCloud Alibaba强大的组件生态
- Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时、高可靠的消息发布与订阅服务。
- Dubbo:Apache Dubbo是一款高性能 Java RPC 框架。
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
- Alibaba Cloud OSS: 阿里云对象存储服务,是阿里云提供的海量、安全、低成本、高可靠的云存储服务。可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
二、版本控制与环境构建
- 开发遵循约定 > 配置 > 编码,各框架组件之间的版本必须一一对应。
2.1、框架主体版本
毕业版本依赖关系(推荐使用)
| Spring Cloud Version | Spring Cloud Alibaba Version | Spring Boot Version | | —- | —- | —- | | Spring Cloud 2020.0.0 | 2021.1 | 2.4.2 | | Spring Cloud Hoxton.SR9 | 2.2.6.RELEASE | 2.3.2.RELEASE | | Spring Cloud Greenwich.SR6 | 2.1.4.RELEASE | 2.1.13.RELEASE | | Spring Cloud Hoxton.SR3 | 2.2.1.RELEASE | 2.2.5.RELEASE | | Spring Cloud Hoxton.RELEASE | 2.2.0.RELEASE | 2.2.X.RELEASE | | Spring Cloud Greenwich | 2.1.2.RELEASE | 2.1.X.RELEASE | | Spring Cloud Finchley | 2.0.4.RELEASE(停止维护,建议升级) | 2.0.X.RELEASE | | Spring Cloud Edgware | 1.5.1.RELEASE(停止维护,建议升级) | 1.5.X.RELEASE |使用直接添加依赖
#添加Spring Cloud Alibaba 2021.1
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2.2、组件版本
组件版本关系
| Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | RocketMQ Version | Dubbo Version | Seata Version | | —- | —- | —- | —- | —- | —- | | 2.2.6.RELEASE | 1.8.1 | 1.4.2 | 4.4.0 | 2.7.8 | 1.3.0 | | 2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE | 1.8.0 | 1.4.1 | 4.4.0 | 2.7.8 | 1.3.0 | | 2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE | 1.8.0 | 1.3.3 | 4.4.0 | 2.7.8 | 1.3.0 | | 2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE | 1.7.1 | 1.2.1 | 4.4.0 | 2.7.6 | 1.2.0 | | 2.2.0.RELEASE | 1.7.1 | 1.1.4 | 4.4.0 | 2.7.4.1 | 1.0.0 | | 2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE | 1.7.0 | 1.1.4 | 4.4.0 | 2.7.3 | 0.9.0 | | 2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE | 1.6.3 | 1.1.1 | 4.4.0 | 2.7.3 | 0.7.1 |使用直接添加依赖
#添加Nacos 1.4.2
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.3、第一个SPringCloud案例
2.3.1、技术选型及版本确认
1、目录结构
2、整体框架
- Springboot 2.1.3 RELEASE
- SpringCloud Greenwich.RELEASE
- SpringCloud alibaba 2.1.0
- Maven 3.6.1
- mysql 5.7
springData jpa 2.5.2
<?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.hguo</groupId>
<artifactId>SpringCloud_01</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!--pom父工程总包-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<!--依赖版本锁定-->
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</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>
</project>
2.3.2、模块设计
- springcloud_01 父工程
- shop-common 公共模块【实体类】
- shop-user 用户微服务 【端口: 807x】
- shop-product 商品微服务【端口: 808x】
- shop-order 订单微服务 【端口: 809x】
2.3.3、创建基础模块
1、创建子模块shop-common并配置
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hguo</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
<build>
<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>
</plugins>
</build>
</project>
2、创建实体类
- 用户类 ```java package com.hguo.entity;
import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;
/**
- @author hguo
- @date2021/8/5 17:30
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
- 用户
*/
@Entity(name = “shop_user”)
@Data
public class User {
} ```@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer uid;//主键
private String username;//用户名
private String password;//密码
private String telephone;//手机号
- 商品类 ```java package com.hguo.entity;
import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;
/**
- @author hguo
- @date2021/8/5 17:32
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
- 商品
*/
@Entity(name = “shop_product”)
@Data
public class Product {
} ```@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
- 订单类 ```java package com.hguo.entity;
import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;
/**
- @author hguo
- @date2021/8/5 17:33
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
- 订单 */ @Entity(name = “shop_order”) @Data public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long oid;//订单id private Integer uid;//用户id private String username;//用户名 private Integer pid;//商品id private String pname;//商品名称 private Double pprice;//商品单价 private Integer number;//购买数量 } ```
2.3.4、创建微服务
1、创建用户模块
- 目录结构
- dao层 ```java package com.hguo.dao;
import com.hguo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; /**
- @author hguo
- @date2021/8/5 23:55
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface UserDao extends JpaRepository
{ } ```
- controller层 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.User; import com.hguo.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; /**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class UserController { @Autowired private UserService userService; @GetMapping(“/user/{uid}”) public User user(@PathVariable(“uid”) Integer uid){ User user = userService.findByuid(uid); log.info(“查询用户:”+ JSON.toJSONString(user)); return user; } }
- **service层**
```java
package com.hguo.service;
import com.hguo.entity.User;
/**
* @author hguo
* @date2021/8/6 0:02
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface UserService {
User findByuid(Integer uid);
}
- serviceImpl ```java package com.hguo.service.impl;
import com.hguo.dao.UserDao; import com.hguo.entity.User; import com.hguo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
/**
- @author hguo
- @date2021/8/5 23:58
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao;
public User findByUid(Integer uid){
return userDao.findById(uid).get();
}
@Override public User findByuid(Integer uid) {
return userDao.findById(uid).get();
} } ```
shop_user/pom.xml ```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0 org.hguo SpringCloud_01 1.0-SNAPSHOT com.hguo shop_user 0.0.1-SNAPSHOT <java.version>1.8</java.version>
<dependency>
<groupId>org.hguo</groupId>
<artifactId>shop_common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
org.springframework.boot spring-boot-maven-plugin
- **yaml配置文件**
```yaml
server:
port: 8071
spring:
application:
name: service-user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?
serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa:
properties:
hibernates:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
2、创建商品模块
- 目录结构
- dao层 ```java package com.hguo.dao;
import com.hguo.entity.Product; import org.springframework.data.jpa.repository.JpaRepository;
/**
- @author hguo
- @date2021/8/5 23:55
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface ProductDao extends JpaRepository
{ } ```
- controller层 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Product; import com.hguo.service.ProductService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class ProductController { @Autowired private ProductService productService; @GetMapping(“/product/{pid}”) public Product product(@PathVariable(“pid”) Integer pid){ Product product = productService.findBypid(pid); log.info(“查询商品:”+ JSON.toJSONString(product)); return product; } }
- **service层**
```java
package com.hguo.service;
import com.hguo.entity.Product;
/**
* @author hguo
* @date2021/8/6 0:02
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface ProductService {
Product findBypid(Integer pid);
}
- serviceImpl ```java package com.hguo.service.impl;
import com.hguo.dao.ProductDao; import com.hguo.entity.Product; import com.hguo.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
/**
- @author hguo
- @date2021/8/5 23:58
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Service public class ProductServiceImpl implements ProductService { @Autowired private ProductDao productDao;
@Override public Product findBypid(Integer pid) {
return productDao.findById(pid).get();
} } ```
shop_product/pom.xml ```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0 org.hguo SpringCloud_01 1.0-SNAPSHOT shop_product <java.version>1.8</java.version>
<dependency>
<groupId>org.hguo</groupId>
<artifactId>shop_common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- **yaml配置文件**
```yaml
server:
port: 8081
spring:
application:
name: service-product
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
3、创建订单模块
- 目录结构
- dao层 ```java package com.hguo.dao;
import com.hguo.entity.Order; import org.springframework.data.jpa.repository.JpaRepository;
/**
- @author hguo
- @date2021/8/5 23:55
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface OrderDao extends JpaRepository
{ } ```
- controller层
- order调用product模块,实现微服务之间的相互调用 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Order; import com.hguo.entity.Product; import com.hguo.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class OrderController { @Autowired private OrderService orderService;
@Autowired
private RestTemplate restTemplate;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
//通过restTemplate调用微服务
Product product = restTemplate.getForObject(
"http://localhost:8081/product/"+pid,Product.class
);
log.info(">>商品详情:{}",pid,JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
}
- **service层**
```java
package com.hguo.service;
import com.hguo.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author hguo
* @date2021/8/6 0:02
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public interface OrderService {
//创建订单
void createOrder(Order order);
}
- serviceImpl ```java package com.hguo.service.impl;
import com.hguo.dao.OrderDao; import com.hguo.entity.Order; import com.hguo.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
/**
- @author hguo
- @date2021/8/5 23:58
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao;
@Override public void createOrder(Order order) {
orderDao.save(order);
} } ```
- shop_order/pom.xml
```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0 org.hguo SpringCloud_01 1.0-SNAPSHOT
<artifactId>shop_order</artifactId>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.hguo</groupId>
<artifactId>shop_common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- **yaml配置文件**
```yaml
server:
port: 8091
spring:
application:
name: service-order
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
2.3.5、微服务调用
1、消费者和服务者
- 服务者:服务声明被调用的模块一方,上述案例中的Product
- 消费者:服务主动调用其他模块的一方,上述案例中的Order
- 在Nacos Discovery—服务治理中会详细说明
2、逻辑层的调用关系
- 自底向上的顺序
- entity层
- 定义与数据库对象应的属性,提供get/set方法,tostring方法,有参无参构造函数。
- dao层
- 创建dao接口,接着就可以在配置文件中定义该接口的实现类;
- 在模块中调用dao的接口进行数据业务的处理,dao层的数据源和数据库连接的参数都是在配置文件中进行配置
- service层
- 业务模块的逻辑应用设计,和dao层一样都是先设计接口,再创建要实现的类,然后在配置文件中进行配置其实现的关联。然后service层调用接口进行业务逻辑应用的处理。
- controller层
- 具体的业务模块流程的控制,controller层主要调用Service层里面的接口控制具体的业务流程,控制的配置也要在配置文件中进行。
- Controller和Service的区别
- Controller负责具体的业务模块流程的控制;Service层负责业务模块的逻辑应用设计
- entity层
- 调用逻辑顺序
- Controller —> Service层接口方法 —> Dao层方法 —> 数据库
三、组件详解
3.1、Nacos
3.1.1、服务治理
1、什么是服务治理
- 服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现
- 服务注册
- 在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息,并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务
- 服务发现
- 服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问
- 注册中心
- 服务发现:
- 服务注册:保存服务提供者和服务调用者的信息
- 服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
- 服务配置:
- 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
- 配置下发:主动将配置推送给服务提供者和服务调用者
- 服务健康检测:
- 检测服务提供者的健康情况,如果发现异常,执行服务剔除
2、常见的注册中心
- Nacos
- Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
- 它是 Spring Cloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
- Zookeeper
- zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式 应用中经常遇到的一些数据管理问题
- 如:统一命名服务、状态同步服务、集群管理、分布式应用 配置项的管理等
- Eureka
- Eureka是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现,但是现在已经闭源
- Eureka
- Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现 和配置管理的功能。
- Consul的功能都很实用,其中包括:
- 服务注册/发现
- 健康检查
- Key/Value 存储
- 多数据中心和分布式一致性保证等特性。
- Consul本身只是一个二进制的可执行文件,所以 安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可
3.1.2、Nacos简介
- Nacos 致力于帮助您发现、配置和管理微服务
- Nacos 提供了一组简单易用的特性集,帮助您快速 实现动态服务发现、服务配置、服务元数据及流量管理
- Nacos 支持基于 DNS 和基于 RPC 的服务发现
- 服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务
- Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求
- Nacos特性分析
- 特性大图:要从功能特性,非功能特性,全面介绍我们要解的问题域的特性诉求
- 架构大图:通过清晰架构,让您快速进入 Nacos 世界
- 业务大图:利用当前特性可以支持的业务场景,及其最佳实践
- 生态大图:系统梳理 Nacos 和主流技术生态的关系
- 优势大图:展示 Nacos 核心竞争力
- 战略大图:要从战略到战术层面讲 Nacos 的宏观优势
- Nacos生态
3.1.3、Nacos实战整合
1、搭建Nacos环境
1.1、安装Nacos
- 下载地址:https://github.com/alibaba/nacos/releases
- 下载zip版本,走解压版安装
- 安装
1.2、启动Nacos
- 启动方式一:双击startup.cmd
- 启动方式二:命令行启动
#切换目录
cd nacos/bin
#启动命令
startup cmd -mstandalone
- 启动结果
- 默认访问地址为:192.168.137.1或localhost
- 默认监听端口为:8848
1.3、访问Nacos
- 再浏览器中访问:http://192.168.137.1:8848/nacos,账号:nacos 密码:nacos
2、将服务注册到Nacos中
2.1、将商品微服务加入Nacos中
- 在shop_product/pom.xml中添加Nacos依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在启动类上添加Nacos客户端启动注解
//添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication
- 在shop_product/application.yaml中添加访问地址
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.137.1:8848 #locahost:8848
- 启动访问,查看nacos中微服务的注册情况
2.2、将订单微服务加入Nacos中
- 在shop_order/pom.xml中添加Nacos依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在启动类上添加Nacos客户端启动注解
//添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication
- 修改OrderController ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Order; import com.hguo.entity.Product; import com.hguo.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class OrderController { @Autowired private OrderService orderService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
/*通过restTemplate调用微服务
Product product = restTemplate.getForObject(
"http://localhost:8081/product/"+pid,Product.class
);
log.info(">>商品详情:{}",pid,JSON.toJSONString(product));
*/
//从nacos中获取服务地址
ServiceInstance serviceInstance = discoveryClient.getInstances("service-product").get(0);
String url = serviceInstance.getHost()+":" +serviceInstance.getPort();
log.info(">>从nacos中获取的微服务地址为:"+url);
//通过restTemplate调用微服务
Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
log.info(">>商品详细,查询结果:"+JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
}
- 在**shop_order/application.yaml**中添加访问地址
```yaml
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.137.1:8848 #locahost:8848
- 启动访问,查看nacos中微服务的注册情况
3.1.4、实现负载均衡
1、负载均衡
- 通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行
- 在访问请求到达一定数量时,增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,已到达降低服务器压力的效果,也就是负载均衡。
- 根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡
- 服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡
- 客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求
- 在高性能的请求和响应中,一般要求在客户端做到负载均衡
2、自定义负载均衡
2.1、启动新的微服并设置端口
- 新建微服务
- 启动服务
2.2、查看Nacos注册中心
2.3、修改逻辑请求实现负载均衡
package com.hguo.controller;
import com.alibaba.fastjson.JSON;
import com.hguo.entity.Order;
import com.hguo.entity.Product;
import com.hguo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Random;
/**
* @author hguo
* @date2021/8/6 0:09
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
//3、随机挑选服务,伪负载
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
//创建0-2之间的随机数
int index = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(index);
String url = serviceInstance.getHost()+":"+serviceInstance.getPort();
log.info(">>从nacos中获取的微服务地址:"+url);
//通过restTemplate调用微服务
//通过restTemplate调用微服务
Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
log.info(">>商品详细,查询结果:"+JSON.toJSONString(product));
//订单业务处理
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
}
2.4、负载均衡效果
- 两次随机请求
- 负载均衡效果
3、基于Ribbon实现负载均衡
3.1、ribbon == ?
- Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。
- SpringCloud Ribbon是一种默认轮询的负载均衡策略,在Springboot中添加ribbon组件即可使用。
- 通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
- Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。
- 因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。
- Spring Cloud Ribbon在微服务架构中使用客户端负载均衡调用只需两步:
- 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
- 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
- 这样可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现
3.2、Ribbon实现负载均衡的流程
- 在RestTemplate 的生成方法上添加@LoadBalanced 注解
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
- 修改服务调用的方法 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Order; import com.hguo.entity.Product; import com.hguo.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
import java.util.List; import java.util.Random;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class OrderController { @Autowired private OrderService orderService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
//4、基于ribbon实现负载均衡
//直接使用服务站名称,从nacos中获取
String url = "service-product";
Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
log.info(">>商品详细,查询结果:"+JSON.toJSONString(product));
//订单业务处理
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
}
<a name="39e0110e"></a>
###### 3.3、负载均衡效果
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210807201337676.png#crop=0&crop=0&crop=1&crop=1&id=uZaDO&originHeight=370&originWidth=1528&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="ac8b44ad"></a>
###### 3.4、Ribbon支持的负载均衡策略
- Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为 **com.netflix.loadbalancer.策略**
| 策略名 | 策略描述 | 实现说明 |
| --- | --- | --- |
| BestAvailableRule | 选择一个最小的并发 请求的server | 逐个考察Server,如果Server被 tripped了,则忽略,在选择其中 ActiveRequestsCount最小的server |
| AvailabilityFilteringRule | 过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值 | 使用一个AvailabilityPredicate来包含 过滤server的逻辑,其实就就是检查 status里记录的各个server的运行状 态 |
| WeightedResponseTimeRule | 过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值 | 一个后台线程定期的从status里面读 取评价响应时间,为每个server计算 一个weight。Weight的计算也比较简 单responsetime 减去每个server自己 平均的responsetime是server的权 重。当刚开始运行,没有形成statas 时,使用roubine策略选择server。 |
| RetryRule | 过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值 | 在一个配置时间段内当选择server不 成功,则一直尝试使用subRule的方 式选择一个可用的server |
| RoundRobinRule | 轮询方式轮询选择 server | 轮询index,选择index对应位置的 server |
| RandomRule | 随机选择一个server | 在index上随机,选择index对应位置 的server |
| ZoneAvoidanceRule | 复合判断server所在 区域的性能和server 的可用性选择server | 使用ZoneAvoidancePredicate和 AvailabilityPredicate来判断是否选择 某个server,前一个判断判定一个 zone的运行性能是否可用,剔除不可 用的zone(的所有server), AvailabilityPredicate用于过滤掉连接 数过多的Server。 |
- 通过修改配置来调整Ribbon的负载均衡策略
```yaml
service-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4、基于Feign实现服务调用
4.1、Feign == ?
- Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务 一样简单, 只需要创建一个接口并添加一个注解即可。
- Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负 载均衡的效果。
4.2、Feign的使用
- 加入Feign依赖
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 在主类上添加注解
@SpringBootApplication
//开启nacos客户端
@EnableDiscoveryClient
//开启Fegin
@EnableFeignClients
public class OrderApplication {}
- 创建service并使用Feign实现微服务调用
- 创建FeignSerivce
- 驱动编辑 ```java package com.hguo.service;
import com.hguo.entity.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping;
/**
- @author hguo
- @date2021/8/7 23:59
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */ //用于指定nacos下的微服务 @FeignClient(value = “service-product”) public interface FeignService { //@FeignClient+@GetMapping 就是一个完整的请求路径 http://serviceproduct/product/{pid} @RequestMapping(“/product/{pid}”) Product findByPid(@PathVariable(“pid”) Integer pid); } ```
- 修改controller并启动验证 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Order; import com.hguo.entity.Product; import com.hguo.service.FeignService; import com.hguo.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class OrderController { @Autowired private OrderService orderService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private FeignService feignService;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
//5、通过fegin调用商品微服务
Product product = feignService.findByPid(pid);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
//订单业务处理
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
}
<a name="f66d9d9c"></a>
#### 3.1.5、Nacsos配置解析
<a name="9e4eae89"></a>
##### 1、连接失败问题
<a name="750b1066"></a>
###### 1.1、失败原因
- 下载时,nacos-server使用的是cluster集群模式,
- 这个模式是要求使用mysql进行连接
- 事先需要配置好数据库并连接成功
<a name="05596829"></a>
###### 1.2、解决方案
- 将连接模式更改为单例模式即可 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808144024875.png#crop=0&crop=0&crop=1&crop=1&id=YvoPR&originHeight=207&originWidth=804&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- cluster —> standalone ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808144146157.png#crop=0&crop=0&crop=1&crop=1&id=t6xJE&originHeight=303&originWidth=881&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="d3997a03"></a>
##### 2、数据库连接
<a name="3be11212"></a>
###### 2.1、问题描述
- nacos内置了一套数据持久化处理,属于文件集成,不便于后期维护、升级和数据校验,因此需要配置独立的数据库以达到数据持久化管理
<a name="3f8e1d16"></a>
###### 2.2、解决方案
- Nacos设计之初就已经做好了业务拓展的准备
- nacos的配置文件中包含了数据库连接并同步数据的配置文件
- 修改配置文件,启动连接即可
<a name="9ff8fb02"></a>
###### 2.3、解决流程
- 找到application.properties文件 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808141014633.png#crop=0&crop=0&crop=1&crop=1&id=EfyIk&originHeight=456&originWidth=1045&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 修改数据库连接配置<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808141318573.png#crop=0&crop=0&crop=1&crop=1&id=AzVkx&originHeight=286&originWidth=1792&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 将nacos-mysql.sql脚本在本地数据库运行 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808142743044.png#crop=0&crop=0&crop=1&crop=1&id=OVKg8&originHeight=587&originWidth=1198&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 登陆Nacos官网 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808144525365.png#crop=0&crop=0&crop=1&crop=1&id=vVNa1&originHeight=654&originWidth=1913&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 数据存储在数据库中 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808144541331.png#crop=0&crop=0&crop=1&crop=1&id=h9giz&originHeight=220&originWidth=883&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 总结
- 在Nacos中产生的数据都会存储到数据库中
<a name="377017df"></a>
#### 3.1.6、Nacos config
<a name="2f268480"></a>
##### 1、服务配置中心
<a name="ac885ca8"></a>
###### 1.1、微服务带来的配置问题
- 配置文件相对分散。
- 在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
- 配置文件无法区分环境。
- 微服务项目可能会有多个环境
- 例如:测试环境、预发布环境、生产环 境,每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要去各个微服务下手动维护,这比较困难。
- 配置文件无法实时更新。
- 修改配置文件之后,必须重新启动微服务才能使配置生效,这对一 个正在运行的项目来说是非常不友好的。
<a name="714914b8"></a>
###### 1.2、配置中心的思路
- 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。
- 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置。
- 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210813154634648.png#crop=0&crop=0&crop=1&crop=1&id=xbei8&originHeight=424&originWidth=833&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="f27057da"></a>
###### 1.3、常用的配置中心
- Apollo
- Apollo是由携程开源的分布式配置中心。
- 特点有很多:配置更新之后可以实时生效,支持灰 度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API。
- Disconf
- Disconf是由百度开源的分布式配置中心。
- 它是基于Zookeeper来实现配置变更后实时通知和生效 的。
- SpringCloud Config
- 是Spring Cloud中带的配置中心组件。
- 它和Spring是无缝集成,使用起来非常方便,并且它的配 置存储支持Git。
- 没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。
- Nacos
- 这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心。
- 集成了服务配置的功能,可以直接使用它作为服务配置中心。
<a name="a6fce9f4"></a>
##### 2、Nacos config
<a name="163719aa"></a>
###### 2.1、Nacos config入门
- 使用nacos作为配置中心,其实就是将nacos当做一个服务端,将各个微服务看成是客户端,将各个微服务的配置文件统一存放在nacos上,然后各个微服务从nacos上拉取配置即可。
- 开启Nacos环境
- 双击打开nacos中的startup.cmd文件启动nacos
- 引入依赖
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在微服务中添加nacos config配置
- 新建一个bootstrap.yml作为配置文件
- 配置文件优先级:bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 环境标识
- 在nacos中添加配置
- 点击配置列表,点击右边+号,新建配置。在新建配置过程中,要注意下面的细节:
- Data ID不能随便写,要跟配置文件中的对应,对应关系如图所示
- 配置文件格式要跟配置文件的格式对应,且目前仅仅支持YAML和Properties
- 配置内容按照上面选定的格式书写
- 注释本地的application.yam中的内容, 启动程序进行测试
2.2、Nacos config深入
- 配置动态刷新
- 在入门案例中,我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是无法读取到 的,因此,我们需要开启配置的动态刷新功能。
- 在nacos中的service-product-dev.yaml配置项中添加下面配置
config:
appName: product
方式一:硬编码格式
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@GetMapping("/nacos-config-test1")
public String nacosConfingTest1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
}
方式二:注解方式
@RestController
@RefreshScope//只需要在需要动态读取配置的类上添加此注解就可以
public class NacosConfigController {
@Value("${config.appName}")
private String appName;
//2 注解方式
@GetMapping("/nacos-config-test2")
public String nacosConfingTest2() {
return appName;
}
}
2.3、配置共享
- spring.application.name 命名的配置文件,然后将其所有环境的公共配置放在里面即可。
- 不同微服务之间的配置共享
- 不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引 入。
- 在nacos中定义一个DataID为all-service.yaml的配置,用于所有微服务共享
spring:
zipkin:
base-url: http://192.168.137.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
application:
name: service-product
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
- 修改bootstrap.yaml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
shared-dataids: all-service.yaml # 配置要引入的配置
refreshable-dataids: all-service.yaml # 配置要实现动态配置刷新的配置
profiles:
active: dev # 环境标识
3、Nacos配置介绍
- 命名空间(Namespace)
命名空间可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间 - 配置分组(Group)
配置分组用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组 - 配置集(Data ID)
在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集
3.2、Sentinel
3.2.1、服务雪崩效应
1、高并发问题
1.1、高并发带来的问题
- 在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络 原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会 出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
1.2、模拟高并发场景
- 代码及配置的编写
- 新建OrderControllerTest类 ```java package com.hguo.controller;
import com.alibaba.fastjson.JSON; import com.hguo.entity.Order; import com.hguo.entity.Product; import com.hguo.service.FeignService; import com.hguo.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
/**
- @author hguo
- @date2021/8/6 0:09
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@RestController @Slf4j public class OrderControllerTest { @Autowired private OrderService orderService;
@Autowired
private FeignService feignService;
//买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid){
log.info(">>客户已下单,调用微服务查询订单信息");
//5、通过fegin调用商品微服务
Product product = feignService.findByPid(pid);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
//模拟网络延时出现的卡顿
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//订单业务处理
Order order = new Order();
order.setUid(1);
order.setUsername("用户测试");
order.setPid(product.getPid());
order.setUsername(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("订单创建成功:{}",JSON.toJSONString(order));
return order;
}
@RequestMapping("/order/message")
public String message(){
return "高并发环境下的测试";
}
}
- application.yaml文件配置tomcat高并发数
```yaml
server:
port: 8091
tomcat:
max-threads: 10
- 使用工具进行压力测试
- 下载
- 安装并启动
- 进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN,然后点击jmeter.bat 启动软件
- 双击Jemter.bat启动
- 添加线程组
- 配置线程并发数
- 添加http取样
- 开启查看结果树
- 配置取样并启动
- 总结
- 当并发启动后, 由于order方法囤积了大量请求, 导致message方法的访问出现了问题,这就是服务雪崩的雏形
2、雪崩效应
2.1、雪崩效应
- 在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了 问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等 待,进而导致服务瘫痪。
- 由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是 服务故障的 “雪崩效应” 。
- 雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某 台机器的资源耗尽。
- 无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问 题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
2.2、容错策略
- 要防止雪崩的扩散,就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施,在Springcoud中有对应的组件可以解决
- 常见的容错思路有隔离、超时、限流、熔断、降级这几种
- 隔离
- 它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。
- 当有故 障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。
- 常见的隔离方式有:线程池隔离和信号量隔离
- 超时
- 在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应, 就断开请求,释放掉线程。
- 限流
- 限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到 的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
- 熔断
- 熔断 在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整 体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
- 服务熔断一般有三种状态:
- 熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制 - 熔断开启状态(Open)
后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法 - 半熔断状态(Half-Open)
尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。
如果成功率达到预 期,则说明服务已恢复,进入熔断关闭状态。
如果成功率仍旧很低,则重新进入熔断关闭状 态。
- 熔断关闭状态(Closed)
- 降级
- 降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
2.3、容错组件
- Sentinel
- Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定
- Hystrix
- Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止 级联失败,从而提升系统的可用性与容错性。
- Resilience4J
- Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推 荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和 prometheus等多款主流产品进行整合。
- 容错组件对比
3.2.2、Sentinel入门
1、Sentinel == ?
- Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
- Sentinel 的特征:
- 丰富的应用场景:
Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 - 完备的实时监控:
- 丰富的应用场景:
Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:
Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
- 完善的SPI扩展点:
Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 - Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。 - Sentinel 分为两个部分:
- 核心库(Java 客户端)
不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 - 控制台(Dashboard)
- 核心库(Java 客户端)
基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等 应用容器。
2、Sentinel概念与功能
2.1、基础概念
- 资源
- 资源就是Sentinel要保护的东西,资源是 Sentinel 的关键概念。
- 资源可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是 一个方法,甚至可以是一段代码
- 案例中的message1方法就可以认为是一个资源
- 规则
- 规则就是用来定义如何进行保护资源的
- 作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则
- 门案例中就是为message1资源设置了一种流控规则,限制了进入message1的流量
2.2、重要功能
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808152125345.png#crop=0&crop=0&crop=1&crop=1&id=DyuMG&originHeight=309&originWidth=714&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
Sentinel的主要功能就是容错
- 流量控制
- 流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。
- 任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。
- 需要根据系统的处理能力对流量进行控制。
- Sentinel作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
熔断降级
- 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则 对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
- Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源 出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的 线程完成任务后才开始继续接收请求。 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的 时间窗口之后才重新恢复。
Sentinel 和 Hystrix的区别
- 两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务 但是在限制的手段上, 确采取了完全不一样的方法:
- Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程切换的成本。
- Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
- 两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务 但是在限制的手段上, 确采取了完全不一样的方法:
- 系统负载保护
- Sentinel 同时提供系统维度的自适应保护能力。
- 当系统负载较高的时候,如果还持续让 请求进入可能会导致系统崩溃,无法响应。
- 在集群环境下,会把本应这台机器承载的流量转发到其它的机器上去。这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保 护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
- 总之需要手动做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能
- 流量控制
3、微服务集成Sentinel
- 为微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可
3.1、在对应的微服务文件pom.xml中添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3.2、编写controller类
4、安装Sentinel控制台
4.1、下载jar包资源
- Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
- 下载地址: https://github.com/alibaba/Sentinel/releases
4.2、启动控制台
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
4.3、修改shop_order并加入控制台配置
spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
4.4、访问控制台
- 访问网址:localhost:8080/192.168.137.1:8080
- 用户名/密码:sentinel/sentinel
4.5、补充
- Sentinel的控制台其实就是一个SpringBoot编写的程序。
- 需要将自定义的微服务程序注册到控制台上,即在微服务中指定控制台的地址,并且还要开启一个跟控制台传递数据的端口,控制台也可以通过此端口 调用微服务中的监控程序获取微服务的各种信息。
5、实现一个接口限流
5.1、在控制台中为message1添加流控规则
- 以QPS类型进行限制,每秒访问刷新次数为2
5.2、通过控制台快速频繁访问, 观察效果
3.2.3、Sentinel规则
1、流控规则
- 流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
- 点击簇点链路,我们就可以看到访问过的接口地址,然后点击对应的流控按钮,进入流控规则配置页面。
- 资源名:唯一名称,默认是请求路径,可自定义
- 针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
- 阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流
- 是否集群:暂不需要集群
1.1、流控配置
- 做一个基础配置,设置阈值类型为QPS,单机阈值为3,也就是请求大于3的时候就开始限流
- 访问设置网址及端口,查看响应效果,当QPS > 3时,服务就不能正常响应,返回Blocked by Sentinel (flow limiting)
1.2、配置流控模式
- 在编辑中点击高级选项,可以查看流控模式
- 流控模式
- 直接(默认):接口达到限流条件时,开启限流
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流。上面案例使用的就是直接流控模式 - 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流- 配置限流规则, 将流控模式设置为关联,关联资源设置为/order/message2
- 通过JMeter向/order/message2连续发送请求,注意QPS一定要大于3
- 访问/order/message1,会发现已经被限流
- 链路:当从某个接口过来的资源达到限流条件时,开启限流
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项
区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细- 新增service,在里面添加一个方法message
@Service
public class OrderServiceImpl3 {
@SentinelResource("message")
public void message() {
System.out.println("message");
}
}
- 新增service,在里面添加一个方法message
- 直接(默认):接口达到限流条件时,开启限流
- 在Controller中声明两个方法,分别调用service中的方法message1/2
@RestController
@Slf4j
public class OrderController3 {
@Autowired
private OrderServiceImpl3 orderServiceImpl3;
@RequestMapping("/order/message1")
public String message1() {
orderServiceImpl3.message();
return "message1";
}
@RequestMapping("/order/message2")
public String message2() {
orderServiceImpl3.message();
return "message2";
}
}
- 禁止收敛URL的入口 context
- 暂时将SpringCloud Alibaba的版本调整为2.1.1.RELEASE
<spring-cloud-alibaba.version>2.1.1.RELEASE</spring-cloud-alibaba.version>
- 配置文件中关闭sentinel的CommonFilter实例化
spring:
cloud:
sentinel:
filter:
enabled: false
- ) 添加一个配置类,自己构建CommonFilter实例
package com.itheima.config;
import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterContextConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
- 控制台配置限流规则 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210808224110621.png#crop=0&crop=0&crop=1&crop=1&id=CBlO3&originHeight=383&originWidth=740&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 分别通过 /order/message1 和 /order/message2 访问, 发现2没问题, 1的被限流了
1.3、流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
- 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待, 它还会设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
2、降级规则
2.1、规则说明
- 降级规则就是设置当满足什么条件的时候,对服务进行降级。
- Sentinel提供了三个衡量条件:
- 平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。 当接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,在接下的时间窗口 (以 s 为单位)之内,就会对这个方法进行服务降级。
- 异常比例:当资源每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回,异常比率的阈值范围是 [0.0, 1.0]。
- 异常数 :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级, 统计时间窗口是分钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。
2.2、场景模拟
- 平均响应时间
- 异常比例
- 首先模拟一个异常
int i = 0;
@RequestMapping("/order/message2")
public String message2() {
i++;
//异常比例为0.333
if (i % 3 == 0){
throw new RuntimeException();
}
return "message2";
}
- 首先模拟一个异常
- 设置异常比例为0.25
- 异常数
3、热点规则
- 热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
- 热点规则简单使用
- 编写代码
@RequestMapping("/order/message3")
@SentinelResource("message3")//注意这里必须使用这个注解标识,热点规则不生效
public String message3(String name, Integer age) {
return name + age;
}
- 编写代码
- 配置热点规则
- 分别用两个参数访问,会发现只对第一个参数限流了
- 热点规则增强使用
- 参数例外项允许对一个参数的具体值进行流控编辑定义的规则,增加参数例外项
4、授权规则
- 在很多场景下,需要要根据调用来源判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。
- 当多路请求同时访问,可以对访问来源做简单判断,然后对其进行放行,达到安全访问的效果
- 来源访问控制根据资源的请求来源(origin)限制资源是否通过:
- 若配置白名单,则只有请求来源位于白名单内时才可通过;
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过
- 流控应用的填写
- Sentinel提供了 RequestOriginParser 接口来处理来源
- 只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析 访问来源。
- 自定义来源处理规则
@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
- 授权规则设置
- 访问地址: http://localhost:8091/order/message1?serviceName=pc 观察结果
5、系统规则
- 系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效
- Load(仅对 Linux/Unix-like 机器生效):
- 当系统 load1(1分钟加载) 超过阈值,且系统当前的并发线程数超过 系统容量时才会触发系统保护。
- 系统容量由系统的 maxQps minRt 计算得出,设定参考值一般 是 CPU cores 2.5。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护
3.2.4、规则定义使用及持久化
1、自定义异常返回
//异常处理页面
@Component
public class ExceptionHandlerPage implements UrlBlockHandler {
// BlockException 异常接口,包含Sentinel的五个异常
// FlowException 限流异常
// DegradeException 降级异常
// ParamFlowException 参数限流异常
// AuthorityException 授权异常
// SystemBlockException 系统负载异常
@Override
public void blocked(HttpServletRequest request, HttpServletResponse
response, BlockException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "接口被降级了...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
class ResponseData {
private int code;
private String message;
}
2、@SentinelResource注解使用
2.1、注解介绍
- 在定义了资源点之后,通过Dashboard来设置限流和降级策略来对资源点进行保护,同时还能通过@SentinelResource来指定出现异常时的处理策略
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项
| 属性 | 作用 | | —- | —- | | value | 资源名称 | | entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT | | blockHandler | 处理BlockException的函数名称,函数要求:
1. 必须是 public
2.返回类型 参数与原方法一致
3. 默认需和原方法在同一个类中4.
若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。 | | blockHandlerClass | 存放blockHandler的类,对应的处理函数必须static修饰。 | | fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所 有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进 行处理。函数要求:
1. 返回类型与原方法一致
2. 参数类型需要和原方法相匹配
3. 默认需和原方法在同一个类中
4.若希望使用其他类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法。 | | fallbackClass | 存放fallback的类。对应的处理函数必须static修饰。 | | defaultFallback | 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进 行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函 数要求:
1. 返回类型与原方法一致
2. 方法参数列表为空,或者有一个 Throwable 类型的参数。 3. 默认需要和原方法在同一个类中
4.若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。 | | exceptionsToIgnore | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入 fallback逻辑,而是原样抛出。 | | exceptionsToTrace | 需要trace的异常 |接口实现方法 ```java package com.hguo.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;
/**
- @author hguo
- @date2021/8/9 10:50
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
@Service @Slf4j public class SentinelServiceImpl {
@SentinelResource(
value = "message",
//指定发生BlockException时进入的方法
blockHandler = "blockHandler",
//指定发生Throwable时进入的方法
fallback = "fallback"
)
public String message(String name) {
return "message";
}
//BlockException时进入的方法
public String blockHandler(String name,BlockException ex) {
log.error("触发异常BlockException", ex);
return "接口被限流或者降级了...";
}
//Throwable时进入的方法
public String fallback(String name,Throwable throwable) {
log.error("触发异常Throwable", throwable);
return "接口发生异常了...";
}
}
- Ordercontroller请求编写
```java
package com.hguo.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.hguo.service.impl.SentinelServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author hguo
* @date2021/8/8 18:35
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
@RestController
@Slf4j
public class OrderControllerTest2 {
@Autowired
private SentinelServiceImpl sentinelService
@RequestMapping("/order/message2")
public String message2() {
return "message2";
}
}
2.2、限流方法一
直接将限流和降级方法定义在方法中
@Service
@Slf4j
public class SentinelServiceImpl {
int i = 0;
@SentinelResource(
value = "message",
//指定发生BlockException时进入的方法
blockHandler = "blockHandler",
//指定发生Throwable时进入的方法
fallback = "fallback"
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message";
}
//BlockException时进入的方法
public String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
//Throwable时进入的方法
public String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
2.3、限流方法二
将限流和降级方法外置到单独的类中 ```java @Service @Slf4j public class OrderServiceImpl3 {
int i = 0;
@SentinelResource(
value = "message",
blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class,
blockHandler = "blockHandler",
fallbackClass = OrderServiceImpl3FallbackClass.class,
fallback = "fallback"
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
} }
@Slf4j public class OrderServiceImpl3BlockHandlerClass { //注意这里必须使用static修饰方法 public static String blockHandler(BlockException ex) { log.error(“{}”, ex); return “接口被限流或者降级了…”; } }
@Slf4j public class OrderServiceImpl3FallbackClass { //注意这里必须使用static修饰方法 public static String fallback(Throwable throwable) { log.error(“{}”, throwable); return “接口发生异常了…”; } }
<a name="dcd34f38"></a>
##### 3、Sentinel规则持久化
<a name="16d7b37c"></a>
##### 3.1、限流控制的非持久性
- 通过Dashboard来为每个Sentinel客户端设置各种各样的规则,但是这里有一个问题,就是这些规则默认是存放在内存中,断电即失,重启即无,极不稳定,这就导致每次开机都得重新配置流控规则,麻烦得一批。
- 本地文件数据源会定时轮询文件的变更,读取规则。可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210809141811604.png#crop=0&crop=0&crop=1&crop=1&id=NEa5K&originHeight=206&originWidth=766&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。
<a name="a8f25aed"></a>
##### 3.2、流控持久性操作
- 编写处理类
```java
package com.hguo.config;
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* @author hguo
* @date2021/8/9 14:22
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
//规则持久化
public class FilePersistence implements InitFunc {
@Value("spring.application:name")
private String appcationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinel-rules/"+appcationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new
FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new
FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new
FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new
FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new
FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new
FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new
FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new
FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new
FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new
FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source ->
JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source
-> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source ->
JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
- 添加配置文件
- 再当前微服务下的resources下创建META-INF/services配置目录
- 在该目录下添加文件com.alibaba.csp.sentinel.init.InitFunc
- 在配置文件中添加持久化配置类的全路径
com.hguo.config.FilePersistence
- 配置流控规制
- 查看流控结果
- 重启微服务再次查看流控结果
3.2.5、Feign整合Sentinel
1、Feign实现服务容错
1.1、引入依赖
<!--sentinel客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1.2、开启Feign对Sentinel支持
feign:
sentinel:
enabled: true
1.3、创建容错类
//容错类要求必须实现被容错的接口,并为每个方法实现容错方案
@Component
@Slf4j
public class ProductServiceFallBack implements ProductService {
@Override
public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-1);
return product;
}
}
1.4、编辑容错接口对应实现类
//value用于指定调用nacos下哪个微服务
//fallback用于指定容错类
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)
public interface ProductService {
@RequestMapping("/product/{pid}")//指定请求的URI部分
Product findByPid(@PathVariable Integer pid);
}
1.5、修改controller
//判断Feign远程调用是否失败
if(product.getPid() == -100){
Order order = new Order();
order.setOid(-100L);
order.setPname("下单失败");
return order;
}
1.6、启动并查看运行情况
1.7、制造异常(关闭商品服务)再次查看运行情况
1.8、Feign容错不能打印错误日志
2、Fabackactory容错获取
- 拿到容错日志
- 实现FallbackFactory类 ```java package com.hguo.service.fallback;
import com.hguo.entity.Product; import com.hguo.service.FeignService; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;
/**
- @author hguo
- @date2021/8/9 15:30
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
//将类注入到容器的两中注解 @Component &@Service
@Component
@Slf4j
public class FeignServiceFallBackFactory implements FallbackFactory
@Override
//Feign产生异常
public FeignService create(Throwable throwable) {
return new FeignService() {
@Override
public Product findByPid(Integer pid) {
//异常打印
log.error("{}",throwable);
//容错逻辑
Product product = new Product();
product.setPid(-100);
product.setPname("远程调用商品微服务异常,进入容错处理");
return product;
}
};
}
}
- 修改Feign容错指定类
```java
//用于指定nacos下的微服务
@FeignClient(
value = "service-product",
//指定feign接口的容错类
// fallback = FeignServiceFallBack.class,
fallbackFactory = FeignServiceFallBackFactory.class
)
- 异常打印
3.3、Gateway
3.3.1、服务网关
1、网关介绍
- 服务网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等
- 目前比较流行的网关
- Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用 lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本 - Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题: 只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。 - Zuul
Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配 置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx - Spring Cloud Gateway
- Ngnix+lua
Spring公司为了替换Zuul而开发的网关服务
- SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway 来做网关,因此本教程就以Spring Cloud Gateway为例
2、Gateway介绍
- Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
- 它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
- 优秀
- 性能强劲:是第一代网关Zuul的1.6倍
- 功能强大:内置了很多实用的功能,例如转发、监控、限流等
- 设计优雅,容易扩展
- 缺点
- 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
- 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
- 需要Spring Boot 2.0及以上的版本,才支持
3.3.2、Gateway入门分析
- 要求: 通过浏览器访问api网关,然后通过网关将请求转发到商品微服务
1、Gateway快速入门
1.1、基础版
- 基础配置转发路径和数据来源都是写死的
创建一个 api-gateway 的模块,导入相关依赖 ```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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0 <groupId>org.hguo</groupId>
<artifactId>SpringCloud_01</artifactId>
<version>1.0-SNAPSHOT</version>
com.hguo api-gateway 1.0-SNAPSHOT 1.8 org.springframework.cloud spring-cloud-starter-gateway
- 添加配置文件
```yaml
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的标识, 要求唯一
uri: http://192.168.137.1:8081 # 请求要转发到的地址
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix = 1 # 转发之前去掉1层路径
- 启动项目, 并通过网关去访问微服务
1.2、增强版
- 从服务中心获取转发路径的地址
- 加入nacos
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在主类上添加注解 ```java package com.hguo;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
- **修改配置文件**
```yaml
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.137.1:8848
gateway:
discovery:
locator:
enabled: true #让gateway可以发现nacos中的微服务
routes:
- id: product_route
uri: lb://service-product # 让gateway可以发现nacos中的微服务
predicates:
- Path=/product/**
filters:
- StripPrefix=1
- nacos注册情况
- 访问测试
1.3、简约版
- 去掉路由网关配置 ```yaml server: port: 7000
spring: application: name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
- **启动项目并访问**<br />按照**网关地址/微服务/接口/id**的格式访问,就可以得到成功响应 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210809181056858.png#crop=0&crop=0&crop=1&crop=1&id=ZoCnU&originHeight=286&originWidth=995&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="659a2ddb"></a>
##### 2、Gateway核心框架
<a name="0eaba60c"></a>
###### 2.1、基本概念
- 路由(Route)是gateway中最基本的组件之一,表示一个具体的路由信息载体
- **id**:路由标识符,区别于其他 Route
- **uri:**路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
- **order:**用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
- **predicate:**断言的用是进行条件判断,只有断言都返回真,才会真正的执行路由。
- **filter:**过滤器用于修改请求和响应信息。
<a name="8111fe03"></a>
###### 2.2、执行流程
- 执行流程 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210810091100516.png#crop=0&crop=0&crop=1&crop=1&id=XLFfw&originHeight=827&originWidth=740&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 流程分析
- **Gateway Client **向 **Gateway Server**发送请求
- 请求首先会被**HttpWebHandlerAdapter**进行提取组装成网关上下文
- 然后网关上下文会传递到**DispatcherHandler**,它负责将请求分发给 **RoutePredicateHandlerMapping**
- **RoutePredicateHandlerMapping**负责路由查找,并根据路由断言判断路由是否可用
- 如果断言成功,由**FilteringWebHandler**创建过滤器链并调用
- 请求会一次经过**PreFilter--微服务--PostFilter**的方法,最终返回响应
<a name="eba902ca"></a>
#### 3.3.3、Gateway路由断言
- Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。
- 断言就是: 满足什么条件,才能进行路由转发。
<a name="f2d32ffe"></a>
##### 1、内置路由断言工厂
- SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。
- 基于Datetime类型的断言工厂
- AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
```xml
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
- 基于远程地址的断言工厂RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主 机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
- CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
- 基于Header的断言工厂
- HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
- HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配。
- 基于Host的断言工厂
- HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
- HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
- 基于Method请求方法的断言工厂
- MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
- MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
- 基于Path请求路径的断言工厂
- PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment
- PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
- 基于Query请求参数的断言工厂
- QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配。
Query=baz, ba.
- QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配。
- 基于路由权重的断言工厂
- WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
- WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
- 使用内置路由断言
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
predicates:
- Path=/product-serv/**
- Before=2019-11-28T00:00:00.000+08:00 #限制请求时间在2019-11-28之前
- Method=POST #限制请求方式为POST
filters:
- StripPrefix=1
2、自定义路由断言工厂
- 假设我们的应用仅仅让age在(min,max)之间的人来访问
- 在配置文件中,添加一个Age的断言配置
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.137.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
predicates:
- Path=/product/**
- Age = 18,60 #限制年龄只有在18~60岁之间的人才能访问
filters:
- StripPrefix=1
- 自定义一个断言工厂, 实现断言方法 ```java package com.hguo.predicates;
import com.alibaba.nacos.client.utils.StringUtils; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays; import java.util.List; import java.util.function.Predicate;
/**
- @author hguo
- @date2021/8/10 11:31
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Compoent public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory
{ //构造函数 public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值赋值到配置类中的属性上 @Override public List
shortcutFieldOrder() { //这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑 @Override public Predicate
apply(AgeRoutePredicateFactory.Config config) { return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//从serverWebExchang获取传入参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if(StringUtils.isEmpty(ageStr)){
int age = Integer.parseInt(ageStr);
return age > config.getMinAge()&& age <config.getMaxAge();
}
return true;
}
};
}
@Data @NoArgsConstructor public static class Config{
private int minAge; //18
private int maxAge; //60
} } ```
- 启动测试
#测试发现当age在(20,60)可以访问,其它范围不能访问
http://localhost:7000/product-serv/product/1?age=30
http://localhost:7000/product-serv/product/1?age=10
3.3.4、Gateway过滤器
1、Gateway过滤器介绍
- 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚
- 没做过滤处理
- Filter过滤
- 生命周期: Pre & Post
- Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。
- 客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务
- 比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。
- PRE: 这种过滤器在请求被路由之前调用。利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
- Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
- GatewayFilter:应用到单个路由或者一个分组的路由上。
- GlobalFilter:应用到所有的路由上。
2、Gateway局部过滤器
- 局部过滤器是针对单个路由的过滤器
2.1、内置局部过滤器
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。
| 过滤工厂 | 作用 | 参数 | | —- | —- | —- | | AddRequestHeader | 为原始请求添加Header | Header的名称及值 | | AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 | | AddResponseHeader | 为原始响应添加Header | Header的名称及值 | | DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名 称及去重策略 | | Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand的名称 | | FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 | | PrefixPath | 为原始请求路径添加前缀 | 前缀路径 | | PreserveHostHeader | 为请求添加一个 preserveHostHeader=true的属 性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 | | RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、 rateLimiter、 statusCode、 denyEmptyKey、 emptyKeyStatus | | RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url | | RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header | | RemoveRequestHeader | 为原始请求删除某个Header | Header名称 | | RemoveResponseHeader | 为原始响应删除某个Header | Header名称 | | RewritePath | 重写原始的请求路径 | 原始路径正则表达式以 及重写后路径的正则表 达式 | | RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正 则表达式,重写后的值 | | SaveSession | 在转发请求之前,强制执行 WebSession::save操作 | 无 | | secureHeaders | 为原始响应添加一系列起安全作 用的响应头 | 无,支持修改这些安全 响应头的值 | | SetPath | 修改原始的请求路径 | 修改后的路径 | | SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 | | SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是 数字,也可以是字符串 | | StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的 路径的数量 | | Retry | 针对不同的响应进行重试 | retries、statuses、 methods、series | | RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large | 请求包大小,单位为字 节,默认值为5M | | ModifyRequestBody | 在转发请求之前修改原始请求体内 | 修改后的请求体内容 | | ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |内置局部过滤器的使用
- 修改响应码返回值
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- SetStatus=2000 #修改返回值
- 修改响应码返回值
- 未修改前超过返回值
- 修改后超过返回值
2.2、自定义局部过滤
- 添加Log过滤器到配置文件
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: consumer
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- Log=true,false #修改返回值
- 自定义过滤工厂实现过滤方法 ```java package com.hguo.predicates;
import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;
import java.util.Arrays; import java.util.List;
/**
- @author hguo
- @date2021/8/10 14:45
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory
{ //构造函数 public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中 @Override public List
shortcutFieldOrder() { return Arrays.asList("consoleLog", "cacheLog");
}
//过滤器逻辑 @Override public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog已经开启了....");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog已经开启了....");
}
return chain.filter(exchange);
}
};
} //配置类 接收配置参数 @Data @NoArgsConstructor public static class Config {
private boolean consoleLog;
private boolean cacheLog;
} } ```
- 启动测试
3、Gateway全局过滤器
- 全局过滤器作用于所有路由且无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
3.1、内置全局过滤器
- GatewayFilterAdapter实现类主要目的是为了将GlobalFilter过滤器包装成GatewayFilter类型的对应,是GlobalFilter过滤器的包装类
- GlobalFilter 为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,模式系统初始化时加载,并作用在每个路由上。
3.2、自定义全局过滤器
- 如果内置全局过滤器不能满足企业需求,还可以根据具体需求自定义全局过滤器
- 开发中的鉴权逻辑:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
- 对于验证用户是否已经登录鉴权的过程可以在网关统一检验。
- 检验的标准就是请求中是否携带token凭证以及token的正确性。
- 校验所有请求的请求参数中是否包含“token”,如何不包含请求 参数“token”则不转发路由,否则执行正常的逻辑。 ```java package com.hguo.filter;
import com.alibaba.nacos.client.utils.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;
/**
- @author hguo
- @date2021/8/10 14:58
- @title 现世安稳,岁月静好,佛祖保佑,永无bug! */
//自定义全局过滤器需要实现GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
- 未授权或错误授权 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210810150942126.png#crop=0&crop=0&crop=1&crop=1&id=JLnaA&originHeight=474&originWidth=837&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 授权 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210810151001792.png#crop=0&crop=0&crop=1&crop=1&id=ujFLc&originHeight=314&originWidth=813&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="c29d0da3"></a>
#### 3.3.5、路由限流
<a name="e82ea779"></a>
##### 1、网关限流介绍
- 网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,Sentinel组件来实现网关的限流就是其中一中。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。 ![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210810151316859.png#crop=0&crop=0&crop=1&crop=1&id=y5fvM&originHeight=509&originWidth=834&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
- route维度:即在Spring配置文件中配置的路由条目,资源名为对应的route
- 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
<a name="6ebc6415"></a>
##### 2、路由维度
- **只针对指定路由名称或指定路由id进行限流**
<a name="b74d25e0"></a>
###### 2.1、导入依赖
```xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
2.2、编写控制类
- 基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的 SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。 ```java package com.hguo.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct; import java.util.*;
/**
- @author hguo
- @date2021/8/10 15:23
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Configuration public class GatewayConfiguration { private final List
viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider - > viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数 @PostConstruct public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route") //资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
// 自定义限流异常页面 @PostConstruct public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
} } ```
2.3、启动测试
3、API分组维度
3.1、导入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
3.2、自定义API分组
- 自定义API分组是一种更细粒度的限流规则定义 ```java package com.hguo.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct; import java.util.*;
/**
- @author hguo
- @date2021/8/10 15:23
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ @Configuration public class GatewayConfiguration { private final List
viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider - > viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数 @PostConstruct public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//API分组维度
rules.add(new GatewayFlowRule("product_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
// 自定义限流异常页面 @PostConstruct public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
//自定义API分组 @PostConstruct private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-serv/product/api1 开头的请求全部限流
add(new ApiPathPredicateItem().setPattern("/product/product/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("product_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-serv/product/api2/demo1 完成的url路径匹配 只有此地址访问被限流
add(new ApiPathPredicateItem().setPattern("/product/product/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
} } ```
3.3、启动测试
- api1/demo*分组被限流
- api2/demo*没有设置分组限流,访问不会被限流
3.4、链路追踪技术
3.4.1、链路追踪
- 分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等
- 常见的链路追踪技术
- cat
大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控。集成方案是通过代码埋点的方式来实现监控
比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高,风险较大。 - zipkin
Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题
包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth 使用较为简单, 集成很方便, 但是功能较简单。 - pinpoint
Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。
特点:支持多种插件,UI功能强大,接入端无代码侵入。 - skywalking
SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。
特点:支持多种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。 - Sleuth
- cat
SpringCloud 提供的分布式系统中链路追踪解决方案。
- SpringCloud alibaba技术栈中并没有提供自己的链路追踪技术的,可采用Sleuth + Zinkin来做链路追踪解决方案
3.4.2、Sleuth入门
1、Sleuth介绍
- SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。大量借用了Google Dapper的设计。
- Trace
- 由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的 入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系 统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。可以使用该唯 一标识将所有的请求串联起来,形成一条完整的请求链路。
- Span
- 代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时 候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结 束时间戳,就能统计该span的调用时间,除此之外,还可以获取如事件的名称。请求信息等元数据。
- Annotation 用它记录一段时间内的事件,内部使用的重要注释:
- cs(Client Send)客户端发出请求,开始一个请求的生命
- sr(Server Received)服务端接受到请求开始进行处理, sr-cs = 网络延迟(服务调用的时间)
- ss(Server Send)服务端处理完毕准备发送到客户端,ss - sr = 服务器上的请求处理时间
- cr(Client Reveived)客户端接受到服务端的响应,请求结束。 cr - sr = 请求的总时间
2、Sleuth入门
- 整合Sleuth
- 导入依赖
<!--链路追踪 Sleuth-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
- 导入依赖
- 启动服务查看日志
- 日志分析
INFO中包含四个信息,第一段是微服务名称,第二段是Tracald,第三段是Spanld,第四段是是否加入第三方将链路追踪结果输出到第三方平台
3.4.3、ZipKin
1、ZipKin介绍
- Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
- Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。
- ZipKin基础架构中的4个核心组件
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件, 基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息。
- Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。
- 客户端会 配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth的监听器监听,并生成相应的 Trace 和 Span信息发送给服务端。
2、安装ZipKin–Server
2.1、下载ZipKin的jar包
2.2、启动ZipKin Server
#启动命令
java -jar zipkin-server-2.12.9-exec.jar
2.3、测试访问
3、集成zip–Client
- ZipKin客户端和Sleuth的集成非常简单,只需要在微服务中添加其依赖和配置即可。
3.1、添加依赖
<!--ZipKin Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
3.2、添加配置
3.3、开启服务并查看访问结果
3.4、查看记录
4、ZipKin数据持久化
- Zipkin Server默认会将追踪数据信息保存到内存,关机或重启主机数据就会消失,不适用于生存环境
- Zipkin支持将追踪数据持久化到mysql数据库或elasticsearch中。
4.1、使用MySQL实现数据持久化
- 创建数据库环境
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this
means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs
query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration
and maxDuration query'
) ENGINE=INNODB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`)
COMMENT 'ignore insert on duplicate';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`)
COMMENT 'for joining with zipkin_annotations';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for
getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and
getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces
ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this
means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with
zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or
Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller
than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if
Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL;
Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is
null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint
is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint
is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when
Binary/Annotation.endpoint is null'
) ENGINE=INNODB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`,
`span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`,
`span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`)
COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT
'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`)
COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT
) ENGINE=INNODB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
- 启动ZipKin Server时指定数据保存的MySQL信息
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=123456
- 查询MySQL & 日志分析
4.2、使用elasticsearch实现数据持久化
- 下载elasticsearch
- 开启elasticsearch
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES-HOST=localhost:9200
- 启动ZipKin Server时指定数据保存的elasticsearch的信息
3.4.4、SkyWalking
3.5、RocketMQ
3.5.1、MQ介绍
- MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。
- 一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。
- MQ作用
- 消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
- 解耦:一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。
- 异步:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。
- 削峰:高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。
- MQ缺点
- 系统可用性降低。依赖服务也多,服务越容易挂掉。需要考虑MQ瘫痪的情况
- 系统复杂性提高。需要考虑消息丢失、消息重复消费、消息传递的顺序性
- 业务一致性。主业务和从属业务一致性的处理
- MQ异步场景:场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 ,一是串行的方式 二是并行的方式
- 异步处理
- 串行方式:将注册信息写入数据库后,发送注册邮件,在发送注册短信,以上三个任务全部完成之后才返回给客户端。这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这总做法让客户端等待没有必要等待的东西
- 并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间
- 消息列队:引入消息队列后,把发送邮件,短信不是必要的业务逻辑异步处理
- 流量削峰
- 流量削峰也是消息队列 MQ 的常用场景,一般在秒杀或团队抢购(高并发)活动中使用广泛。
- 在秒杀或团队抢购活动中,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。
- 为解决这些问题,可在应用和下游通知系统之间加入消息队列 MQ。
- 秒杀处理流程
- 用户发起海量秒杀请求到秒杀业务处理系统。
- 秒杀处理系统按照秒杀处理逻辑将满足秒杀条件的请求发送至消息队列 MQ。
- 下游的通知系统订阅消息队列 MQ 的秒杀相关消息,再将秒杀成功的消息发送到相应用户。
- 用户收到秒杀成功的通知。
- 异步处理
3.5.2、常见的MQ
- ZeroMQ
- 号称最快的消息队列系统,尤其针对大吞吐量的需求场景。
- 扩展性好,开发比较灵活,采用C语言 实现,实际上只是一个socket库的重新封装,如果做为消息队列使用,需要开发大量的代码。
- ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。
- RabbitMQ
- 使用erlang语言开发,性能较好,适合于企业级的开发。
- 但是不利于做二次开发和维护。
- ActiveMQ
- 历史悠久的Apache开源项目。
- 已经在很多产品中得到应用,实现了JMS1.1规范,可以和spring-jms轻松融合,实现了多种协议,支持持久化到数据库,对队列数较多的情况支持不好。
- RocketMQ
- 阿里巴巴的MQ中间件,由java语言开发,性能非常好,能够撑住双十一的大流量,而且使用起来 很简单。
- Kafka
- Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统, 相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
3.5.3、RocketMQ入门
- RocketMQ是阿里巴巴开源的分布式消息中间件,现在是Apache的一个顶级项目。在阿里内部使用非常广泛,已经经过了”双11”这种万亿级的消息流转。
1、RocketMQ构架及概念
- RocketMQ整体分为4个部分:NameServer,Broker,Producer,Consumer
- Broker(邮递员)
Broker是RocketMQ的核心,负责消息的接收,存储,投递等功能 - NameServer(邮局)
消息队列的协调者,Broker向它注册路由信息,同时Producer和Consumer向其获取路由信息 - Producer(寄件人)
消息的生产者,需要从NameServer获取Broker信息,然后与Broker建立连接,向Broker发送消息 - Consumer(收件人)
消息的消费者,需要从NameServer获取Broker信息,然后与Broker建立连接,从Broker获取消 息 - Topic(地区)
用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息 - Message Queue(邮件)
为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个Message Queue,这样消息就可以并行往各个Message Queue发送消息,消费者也可以并行的从多个 Message Queue读取消息 - Message
Message 是消息的载体。 - Producer Group
生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组。 - Consumer Group
消费者组,消费同一类消息的多个 consumer 实例组成一个消费者组。
- Broker(邮递员)
2、RocketMQ环境搭建
2.1、下载RocketMQ
- 下载地址: http://rocketmq.apache.org/release_notes/release-notes-4.4.0/
- 环境要求
- linux x64
- JDK1.8+
2.2、普通安装RocketMQ
- 上传RocketMQ消息中间件
[root@hg14150 rocketmq]# ls /usr/local/src/
rocketmq-all-4.4.0-bin-release.zip
- 解压到安装目录
[root@hg14150 src]# unzip rocketmq-all-4.4.0-bin-release.zip
[root@hg14150 src]# mv rocketmq-all-4.4.0-bin-release ../rocketmq
2.3、普通启动RocketMQ
- 切换到安装目录
[root@hg14150 rocketmq]# ls
benchmark bin conf lib LICENSE NOTICE README.md
- 启动NameServer
[root@hg14150 rocketmq]# nohup ./bin/mqnamesrv &
[1] 1467
# 只要进程不报错,就应该是启动成功了,可以查看一下日志
[root@heima rocketmq]# tail -f /root/logs/rocketmqlogs/namesrv.log
- 启动Broker
# 编辑bin/runbroker.sh 和 bin/runserver.sh文件,修改里面的
# JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
# 为JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
[root@hg14150 rocketmq]# nohup bin/mqbroker -n localhost:9876 &
[root@hg14150 rocketmq]# tail -f /root/logs/rocketmqlogs/broker.log
2.4、docker安装RocketMQ
- 拉取最新镜像
#搜索镜像
[root@hg14150 ~]# docker search rocketmq
#拉取镜像
[root@hg14150 ~]# docker pull foxiswho/rocketmq
- 本地创建文件目录用于数据挂载
[root@hg14150 /] cd usr/local; mkdir conf
- 启动nameserver
```shell
挂载到本地
docker run -d -p 9876:9876 -v /usr/local/mq/data/namesrv/logs:/root/logs -v /usr/local/mq/data/namesrv/store:/root/store —name rmqnamesrv -e “MAX_POSSIBLE_HEAP=100000000” foxiswho/rocketmq:server-4.5.1
不挂载到本地
docker run -d -p 9876:9876 —name rmqserver foxiswho/rocketmq:server-4.5.1
- 启动broker
```shell
#挂载到本地
docker run -d -p 10911:10911 -p 10909:10909\
--name rmqbroker --link rmqserver:namesrv\
-e "NAMESRV_ADDR=namesrv:9876" -e "JAVA_OPTS=-Duser.home=/opt"\
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m"\
-v /usr/conf/broker.conf:/etc/rocketmq/broker.conf \
foxiswho/rocketmq:broker-4.5.1
#不挂载到本地
docker run -d -p 10911:10911 -p 10909:10909\
--name rmqbroker --link rmqserver:namesrv\
-e "NAMESRV_ADDR=namesrv:9876" -e "JAVA_OPTS=-Duser.home=/opt"\
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m"\
foxiswho/rocketmq:broker-4.5.1
进入配置文件所在目录
[root@dfaf41bd7d38 rocketmq-4.5.1]# cd /etc/rocketmq/
打开配置文件文件添加主机公网IP
brokerIP1=主机地址
- 启动服务
```shell
docker run -d --name rmqconsole -p 8180:8080 --link rmqserver:namesrv\
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=namesrv:9876\
-Dcom.rocketmq.sendMessageWithVIPChannel=false"\
-t styletang/rocketmq-console-ng
- 重启rockermq server后访问 http://39.101.188.99:8180
2.5、测试RocketMQ
- 测试消息发送
[root@hg14150 rocketmq]# export NAMESRV_ADDR=localhost:9876
[root@hg14150 rocketmq]# bin/tools.shorg.apache.rocketmq.example.quickstart.Producer
- 测试消息接收
[root@hg14150 rocketmq]# export NAMESRV_ADDR=localhost:9876
[root@hg14150 rocketmq]# bin/tools.shorg.apache.rocketmq.example.quickstart.Consumer
2.6、关闭RocketMQ
[root@hg14150 rocketmq]# bin/mqshutdown broker
[root@hg14150 rocketmq]# bin/mqshutdown namesrv
3.5.4、整合Java实现消息发送与消费
1、消息发送与消费
1.1、导入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
1.2、实现发送消息
- 消息发送步骤:
- 创建消息生产者,指定生产者所属组名
- 指定Nameserver地址
- 启动生产者
- 创建消息对象,指定主题,标签和消息体
- 发送消息
- 关闭生产者 ```java package com.hguo.test;
import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message;
/**
- @author hguo
- @date2021/8/12 10:46
@title 现世安稳,岁月静好,佛祖保佑,永无bug! */ public class RocketMQSendTest {
//发送消息 public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("39.101.188.99:9876");
//3. 启动生产者
producer.start();
//4. 创建消息对象,指定主题、标签和消息体
Message msg = new Message("myTopic", "myTag", ("RocketMQ Message").getBytes());
//5. 发送消息
SendResult sendResult = producer.send(msg,10000);
System.out.println(sendResult);
//6. 关闭生产者
producer.shutdown();
} } ```
1.3、实现接收信息
- 消息接收步骤:
- 创建消息消费者,指定消费者所属组名
- 指定Nameserver地址
- 指定消费者订阅主题和标签
- 设置回调函数,编写处理消息的方法
- 启动消息消费者 ```java package com.hguo.test;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
- @author hguo
- @date2021/8/12 11:08
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class RocketMQReceiveTest {
public static void main(String[] args) throws MQClientException {
} } ```//1. 创建消息消费者, 指定消费者所属的组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumergroup");
//2. 指定Nameserver地址
consumer.setNamesrvAddr("192.168.109.131:9876");
//3. 指定消费者订阅的主题和标签
consumer.subscribe("myTopic", "*");
//4. 设置回调函数,编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt>
msgs, ConsumeConcurrentlyContext context) {
System.out.println("Receive New Messages: " + msgs);
//返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5. 启动消息消费者
consumer.start();
System.out.println("Consumer Started.");
2、消息订阅案例
- 模拟下单成功后向用户发送消息通知
2.1、订单微服务发送消息
- 在订单微服务shop-order添加rocketmq依赖
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
- 添加yaml配置
rocketmq:
name-server: 39.101.188.99:9876 #rocketMQ服务的地址
producer:
group: shop-order # 生产者组
编写控制层代码
@RestController
@Slf4j
public class OrderController2 {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
//准备买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//通过fegin调用商品微服务
Product product = productService.findByPid(pid);
if (product == null){
Order order = new Order();
order.setPname("下单失败");
return order;
}
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
//下单成功之后,将消息放到mq中
rocketMQTemplate.convertAndSend("order-topic", order);
return order;
}
}
2.2、用户微服务订阅消息
- 在用户微服务shop-user添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
- 启动类上添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
- 修改配置文件
server:
port: 8071
spring:
application:
name: service-user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
rocketmq:
name-server: 192.168.109.131:9876
- 编写消息接收类
//发送短信的服务
@Slf4j
@Service
@RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic")
public class SmsService implements RocketMQListener<Order> {
@Override
public void onMessage(Order order) {
log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order));
}
}
- 启动服务,查看控制台日志打印情况
3、消息分类
3.1、普通消息
- 可靠同步发送
- 同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。
- 此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。
- 可靠异步发送
- 异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。发送 方通过回调接口接收服务器响应,并对响应结果进行处理。
- 异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如用户视频上传后通知 启动转码服务,转码完成后通知推送转码结果等。
- 单向发送
- 单向发送是指发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不 等待应答。
- 适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
- 导入依赖
<!--依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
编写测试环境
//测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrderApplication.class)
public class MessageTypeTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
//同步消息
@Test
public void testSyncSend() {
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
SendResult sendResult =
rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息");
System.out.println(sendResult);
}
//异步消息
@Test
public void testAsyncSend() throws InterruptedException {
public void testSyncSendMsg() {
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
//参数三: 回调函数, 处理返回结果
rocketMQTemplate.asyncSend("test-topic-1", "这是一条异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
//让线程不要终止
Thread.sleep(30000000);
}
//单向消息
@Test
public void testOneWay() {
rocketMQTemplate.sendOneWay("test-topic-1", "这是一条单向消息");
}
}
- 三种方式的对比
| 发送方式 | 发送TPS | 发送结果反馈 | 可靠性 | | —- | —- | —- | —- | | 同步发送 | 快 | 有 | 不丢失 | | 异步发送 | 快 | 有 | 不丢失 | | 单向发送 | 最快 | 无 | 可能丢失 |
3.2、顺序消息
- 顺序消息是消息队列提供的一种严格按照顺序来发布和消费的消息类型。
//同步顺序消息[异步顺序 单向顺序写法类似]
public void testSyncSendOrderly() {
//第三个参数用于队列的选择
rocketMQTemplate.syncSendOrderly("test-topic-1", "这是一条异步顺序消息",
"xxxx");
}
3.3、事务消息
- RocketMQ提供了事务消息,通过事务消息就能达到分布式事务的最终一致
- 事务消息交互流程:
- 两个概念
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了RocketMQ服务端,但是服务 端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的 消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失, RocketMQ服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该 消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。
- 事务消息发送步骤
- 发送方将半事务消息发送至RocketMQ服务端。
- RocketMQ服务端将消息持久化之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事 务消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息
- 事务消息回查步骤
- 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时 间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息 进行操作。 ```java //事物日志 @Entity(name = “shop_txlog”) @Data public class TxLog { @Id private String txLogId; private String content; private Date date; }
@Service public class OrderServiceImpl4 {
@Autowired private OrderDao orderDao; @Autowired private TxLogDao txLogDao; @Autowired private RocketMQTemplate rocketMQTemplate;
public void createOrderBefore(Order order) {
String txId = UUID.randomUUID().toString();
//发送半事务消息
rocketMQTemplate.sendMessageInTransaction(
"tx_producer_group",
"tx_topic",
MessageBuilder.withPayload(order).setHeader("txId",
txId).build(), order ); }
//本地事物
@Transactional
public void createOrder(String txId, Order order) {
//本地事物代码
orderDao.save(order);
//记录日志到数据库,回查使用
TxLog txLog = new TxLog();
txLog.setTxLogId(txId);
txLog.setContent("事物测试");
txLog.setDate(new Date());
txLogDao.save(txLog);
}
} @RocketMQTransactionListener(txProducerGroup = “tx_producer_group”) public class OrderServiceImpl4Listener implements RocketMQLocalTransactionListener {
@Autowired private TxLogDao txLogDao;
@Autowired private OrderServiceImpl4 orderServiceImpl4;
//执行本地事物 @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { //本地事物 orderServiceImpl4.createOrder((String) msg.getHeaders().get(“txId”), (Order) arg); return RocketMQLocalTransactionState.COMMIT; } catch (Exception e) { return RocketMQLocalTransactionState.ROLLBACK; } }
//消息回查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
//查询日志记录
TxLog txLog = txLogDao.findById((String)
msg.getHeaders().get(“txId”)).get();
if (txLog == null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
<a name="6cd39a01"></a>
###### 3.4、消息拓展
- RocketMQ支持的两种消息模式
- 广播消费: 每个消费者实例都会收到消息,也就是一条消息可以被每个消费者实例处理;
- 集群消费: 一条消息只能被一个消费者实例消费;
```java
@RocketMQMessageListener(
consumerGroup = "shop",//消费者分组
topic = "order-topic",//要消费的主题
consumeMode = ConsumeMode.CONCURRENTLY, //消费模式:无序和有序
messageModel = MessageModel.CLUSTERING, //消息模式:广播和集群,默认是集群
)
public class SmsService implements RocketMQListener<Order> {}
3.6、Seata
3.6.1、分布式事务基础
1、事务的基本特质
- 事务
- 事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。
- 简单地说,事务提供一种“事情开始就要求要么成功,要么失败”机制。
- 事务的四大特性
- A:原子性(Atomicity),一个事务中的所有操作,要么全部成功,要么全部失败
- C:一致性(Consistency),在一个事务执行之前和执行之后数据库都必须处于一致性状态
- I:隔离性(Isolation),在并发环境中,当不同的事务同时操作相同的数据时,事务之间互不影响
- D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须永久的保存下来
- 分布式事务
- 分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作。
- 这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。
- 如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
2、事务的一致性
- 强一致性
- 系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值;
- 原子一致性(Atomic Consistency)线性一致性(Linearizable Consistency)
- 满足的两个前提
- 任何一次读都能读到某个数据的最近一次写的数据。
- 系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。
- 弱一致性
- 系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。
- 即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值。
- 所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
- 最终一致性
- 当缓存和数据库数据一直时即为最终一致性,这也是最终需要达到的目标
3.6.2、分布式事务解决方案
1、全局事务
- 全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型X/Open Distributed Transaction Processing Reference Model。
- 规定了要实现分布式事务,需要三种角色:
- AP: Application 应用系统 (微服务)
- TM: Transaction Manager 事务管理器 (全局事务管理)
- RM: Resource Manager 资源管理器 (数据库)
- 全局事务可分为两个阶段
- 阶段一: 表决阶段,所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者。
- 阶段二: 执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚。
- 参与角色
- 一个协调者:事务的发起者
- 多个参与者:事务的执行者
- 优点:
- 提高了数据一致性的概率,实现成本较低
- 缺点:
- 单点问题: 事务协调者宕机
- 同步阻塞: 延迟了提交时间,加长了资源阻塞时间
- 数据不一致: 提交第二阶段,依然存在commit结果未知的情况,有可能导致数据不一致
2、可靠消息服务
- 基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。
- 假设有A和B两个系统,分别可以处理任务A和任务B,此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理,就可以使用消息中间件来实现这种分布式事务。
- 消息由系统A投递到中间件
- 在系统A处理任务A前,首先向消息中间件发送一条消息
- 消息中间件收到后将该条消息持久化,但并不投递。持久化成功后,向A回复一个确认应答
- 系统A收到确认应答后,则可以开始处理任务A
- 任务A处理完成后,向消息中间件发送Commit或者Rollback请求。该请求发送完成后,对系统A而 言,该事务的处理过程就结束了
- 如果消息中间件收到Commit,则向B系统投递消息;如果收到Rollback,则直接丢弃消息。如果消息中间件收不到Commit和Rollback指令,那么就要依靠”超时询问机制”。
- 超时询问机制
- 系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。
- 当消息中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用 系统A提供的事务询问接口询问该系统目前的状态。
- 该接口会返回三种结果:
- 提交:将该消息投递给系统B
- 回滚:直接将条消息丢弃
- 处理中:继续等待
- 消息由中间件投递到系统B
- 消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务 处理完成后便向消息中间件返回应答。
- 当消息中间件收到确认应答后便认为该事务处理完毕,如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。
- 一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。
- 这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题。
- 基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。
3、定期校对
- 最大努力通知也被称为定期校对,其实是对第二种解决方案的进一步优化。
- 它引入了本地消息表来 记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游系统消费。
- 消息由系统A投递到中间件
- 处理业务的同一事务中,向本地消息表中写入一条记录
- 准备专门的消息发送者不断地发送本地消息表中的消息到消息中间件,如果发送失败则重试
- 消息由中间件投递到系统B
- 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行
- 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该 事务完成
- 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表 4
- 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费
- 优点: 一种非常经典的实现,实现了最终一致性。
- 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
4、TCC事务
- TCC即为Try Confirm Cancel,它属于补偿型分布式事务。
- TCC实现分布式事务一共有三个步骤:
- Try:尝试待执行的业务这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
- Confirm:确认执行业务确认执行业务操作,不做任何业务检查,只使用Try阶段预留的业务资源。
- 通常情况下,采用TCC 则认为 Confirm阶段是不会出错的。
- 即:只要Try成功,Confirm一定成功。若Confirm阶段真的 出错了,需引入重试机制或人工处理。
- Cancel:取消待执行的业务 取消Try阶段预留的业务资源。
- 通常情况下,采用TCC则认为Cancel阶段也是一定成功的。
- 若 Cancel阶段真的出错了,需引入重试机制或人工处理。
- TCC两阶段提交与XA两阶段提交的区别是:
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
- 优点:把数据库层的二阶段提交上提到了应用层来实现,规避了数据库层的2PC性能低下问题。
- 缺点:TCC的Try、Confirm和Cancel操作功能需业务提供,开发成本高。
5、CAP原则
- CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer’s theorem),指的是在一个分布式系统中,不可能同时满足以下三点。
- 一致性(Consistency):指强一致性,在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果。
- 可用性(Availability):可用性是指,每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)
- 分区容忍性(Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,服务器A和B发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。
- CAP解决办法
- 假设可以同时满足一致性、可用性、分区容错这三个特性,由于满足分区容错,可以切断 A/B 的连线
- 可见,根本完成不了,只要出现了网络分区,A 就无法满足,因为节点 A 根本连接不上节点 B。
- 当强行满足 C 原子性,就必须停止服务运行,从而放弃可用性 C。
- 若要保证一致性:则必须进行节点间数据同步,同步期间数据锁定,导致期间的读取失败或超时,破坏了可用性。
- 若要保证可用性:则不允许节点间同步期间锁定,这又破坏了一致性。
3.6.3、Seata入门
1、Seata介绍
- Seata是Alibaba中间件开发团队在2019年发布的事务型中间件,以高效并且对业务 0 侵入的方式,解决微服务场景下面临的分布式事务问题。
- 初衷就是要让分布式微服务事务跟本地单体事务一样简单。
- Seata事务的框架组成
- 解决分布式事务的设计初衷
- 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
- 高性能:减少分布式事务解决方案所带来的性能消耗
- Seata的两种分布式事务解决方案
- AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
- TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
2、Seata核心组件
- TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。
- TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务。
- RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分 支事务的状态,接受TC的命令来提交或者回滚分支事务。
3、Seata执行流程
- A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
- A服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖
- A服务执行分支事务,向数据库做操作
- A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
- B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
- B服务执行分支事务,向数据库做操作
- 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
- TC协调其管辖之下的所有分支事务, 决定是否回滚
4、2PC的不同实现
- 架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM本质上就是数据库自身,通过XA协 议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。
- 两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保 持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2 持锁的时间,整体提高效率
3.6.4、Seata分布式事务控制
1、Seata案例流程分析
2、模拟下单—库存事务案例
3、Seata的使用
四、进阶整合
基于微服务实现企业级业务管理系统方案