本项目使用 jdk17
涉及技术: consul,LoadBalancer,OpenFeign,CircuitBreaker,Resilience4J,Sleuth,ZipKin,GateWay,SpringCloud Alibaba,nacos,sentinel,seata.......
一、创建父工程Project: zx-spring-cloud
zx-spring-cloud POM文件:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <hutool.version>5.8.22</hutool.version> <lombok.version>1.18.26</lombok.version> <druid.version>1.1.20</druid.version> <mybatis.springboot.version>3.0.2</mybatis.springboot.version> <mysql.version>8.0.11</mysql.version> <swagger3.version>2.2.0</swagger3.version> <mapper.version>4.2.3</mapper.version> <fastjson2.version>2.0.40</fastjson2.version> <persistence-api.version>1.0.2</persistence-api.version> <spring.boot.test.version>3.1.5</spring.boot.test.version> <spring.boot.version>3.2.0</spring.boot.version> <spring.cloud.version>2023.0.0</spring.cloud.version> <spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version></properties><dependencyManagement> <dependencies> <!--springboot 3.2.0--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--springcloud 2023.0.0--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--springcloud alibaba 2022.0.0.0-RC2--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringBoot集成mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.springboot.version}</version> </dependency> <!--Mysql数据库驱动8 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--SpringBoot集成druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!--通用Mapper4之tk.mybatis--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>${mapper.version}</version> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>${persistence-api.version}</version> </dependency> <!-- fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>${fastjson2.version}</version> </dependency> <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${swagger3.version}</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <optional>true</optional> </dependency> <!-- spring-boot-starter-test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring.boot.test.version}</version> <scope>test</scope> </dependency> </dependencies></dependencyManagement>
二、MYSQL8 创建数据库
DROP TABLE IF EXISTS `t_pay`;CREATE TABLE `t_pay` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号', `order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号', `user_id` INT(10) DEFAULT '1' COMMENT '用户账号ID', `amount` DECIMAL(8,2) NOT NULL DEFAULT '9.9' COMMENT '交易金额', `deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`)) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='支付交易表';INSERT INTO t_pay(pay_no,order_no) VALUES('pay17203699','6544bafb424a');SELECT * FROM t_pay;
三、创建Maven工程 mybatis_generator
mybatis_generator工程 POM文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zx</groupId>
<artifactId>zx_cloud</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 普通maven工程 与springBoot和springCloud无关 -->
<artifactId>mybatis_generator</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Mybatis Generator 自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql8.0-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
mybatis_generator工程 在src\main\resource下 创建配置文件: config.properties
配置如下
#t_pay表包名
package.name=com.zx
# mysql8.0 把url,user,password换成自己的
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/zxcloud?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =root
mybatis_generator工程 创建generatorConfig.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_pay" domainObjectName="Pay">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
双击执行 mybatis-generator:generate插件生成entity+mapper接口+mapper.xml
成功示例
warning不用管 出现 success即生成成功
四、创建cloud-provider-payment-8001工程
微服务提供者支付Module模块
流程概述:
1.创建Module
2.修改POM
3.编辑yml
4.编写主启动类
5.编写业务类
实现:
1.创建 module
2. 修改POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zx</groupId>
<artifactId>zx_cloud</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud_provider_payment8001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.创建application.yml
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zxcloud?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.zx.entities
configuration:
map-underscore-to-camel-case: true
4.编写主启动类
将默认的Main类名修改为Main8001
package com.zx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
@SpringBootApplication
@MapperScan("com.zx.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class,args);
}
}
5.编写业务类
将之前生成的代码拷贝到8001中
在enties包下新建PayDTO实体类用于请求实体
package com.zx.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 14:30
* @Version 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayDTO implements Serializable {
private Integer id;
//支付流水号
private String payNo;
//订单流水号
private String orderNo;
//用户账号ID
private Integer userId;
//交易金额
private BigDecimal amount;
}
6.在src\main\resources路径下 新建文件夹 mapper 将PayMapper.xml拷贝到此mapper文件夹中
7.创建sevice包合impl包
PayService:
package com.zx.service;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 14:39
* @Version 1.0
**/
import com.zx.entities.Pay;
import java.util.List;
/**
* @auther zzyy
* @create 2023-11-03 18:40
*/
public interface PayService {
public int add(Pay pay);
public int delete(Integer id);
public int update(Pay pay);
public Pay getById(Integer id);
public List<Pay> getAll();
}
PayServiceImpl
package com.zx.service.impl;
import com.zx.entities.Pay;
import com.zx.mapper.PayMapper;
import com.zx.service.PayService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 14:40
* @Version 1.0
**/
@Service
public class PayServiceImpl implements PayService {
@Resource
PayMapper payMapper;
@Override
public int add(Pay pay){
return payMapper.insertSelective(pay);
}
@Override
public int delete(Integer id){
return payMapper.deleteByPrimaryKey(id);
}
@Override
public int update(Pay pay){
return payMapper.updateByPrimaryKeySelective(pay);
}
@Override
public Pay getById(Integer id){
return payMapper.selectByPrimaryKey(id);
}
@Override
public List<Pay> getAll(){
return payMapper.selectAll();
}
}
8.创建controller
package com.zx.controller;
import com.zx.entities.Pay;
import com.zx.entities.PayDTO;
import com.zx.service.PayService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 14:42
* @Version 1.0
**/
@RestController
public class PayController{
@Resource PayService payService;
@PostMapping(value = "/pay/add")
public String addPay(@RequestBody Pay pay){
System.out.println(pay.toString());
int i = payService.add(pay);
return "成功插入记录,返回值:"+i;
}
@DeleteMapping(value = "/pay/del/{id}")
public Integer deletePay(@PathVariable("id") Integer id) {
return payService.delete(id);
}
@PutMapping(value = "/pay/update")
public String updatePay(@RequestBody PayDTO payDTO){
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);
int i = payService.update(pay);
return "成功修改记录,返回值:"+i;
}
@GetMapping(value = "/pay/get/{id}")
public Pay getById(@PathVariable("id") Integer id){
return payService.getById(id);
}
}
9.测试: 启动Main8001
测试swagger3调用接口
访问路径
localhost:8081/swagger-ui/index.html
本章只测了添加和查询,修改和删除未测,想测自己测一下简单的crud
关于上面返回时间格式问题可以在实体类中加上@JsonFormat字段
10.统一返回值
定义返回标准格式3大标配 code,message,data;
新建枚举类ReturnCodeEnum
package com.zx.resp;
import lombok.Getter;
import java.util.Arrays;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 15:08
* @Version 1.0
**/
@Getter
public enum ReturnCodeEnum
{
/**操作失败**/
RC999("999","操作XXX失败"),
/**操作成功**/
RC200("200","success"),
/**服务降级**/
RC201("201","服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202("202","热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203("203","系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204("204","授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403("403","无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401("401","匿名用户访问无权限资源时的异常"),
RC404("404","404页面找不到的异常"),
/**服务异常**/
RC500("500","系统异常,请稍后重试"),
RC375("375","数学运算异常,请稍后重试"),
INVALID_TOKEN("2001","访问令牌不合法"),
ACCESS_DENIED("2003","没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED("1001","客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR("1002","用户名或密码错误"),
BUSINESS_ERROR("1004","业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");
/**自定义状态码**/
private final String code;
/**自定义描述**/
private final String message;
ReturnCodeEnum(String code, String message){
this.code = code;
this.message = message;
}
//遍历枚举V1
public static ReturnCodeEnum getReturnCodeEnum(String code)
{
for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
if(element.getCode().equalsIgnoreCase(code))
{
return element;
}
}
return null;
}
//遍历枚举V2
public static ReturnCodeEnum getReturnCodeEnumV2(String code)
{
return Arrays.stream(ReturnCodeEnum.values()).filter(x -> x.getCode().equalsIgnoreCase(code)).findFirst().orElse(null);
}
}
新建统一返回定义对象ResultData
package com.zx.resp;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 15:10
* @Version 1.0
**/
@Data
@Accessors(chain = true)
public class ResultData<T> {
private String code;/** 结果状态 ,具体状态码参见枚举类ReturnCodeEnum.java*/
private String message;
private T data;
private long timestamp ;
public ResultData (){
this.timestamp = System.currentTimeMillis();
}
public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(ReturnCodeEnum.RC200.getCode());
resultData.setMessage(ReturnCodeEnum.RC200.getMessage());
resultData.setData(data);
return resultData;
}
public static <T> ResultData<T> fail(String code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(code);
resultData.setMessage(message);
return resultData;
}
}
11.返回修改PayController
修改后的controller
package com.zx.controller;
import com.zx.entities.Pay;
import com.zx.entities.PayDTO;
import com.zx.resp.ResultData;
import com.zx.service.PayService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 14:42
* @Version 1.0
**/
@RestController
@Tag(name = "支付微服务模块",description = "支付CRUD")
public class PayController
{
@Resource
PayService payService;
@PostMapping(value = "/pay/add")
@Operation(summary = "新增",description = "新增支付流水方法,json串做参数")
public ResultData<String> addPay(@RequestBody Pay pay)
{
System.out.println(pay.toString());
int i = payService.add(pay);
return ResultData.success("成功插入记录,返回值:"+i);
}
@DeleteMapping(value = "/pay/del/{id}")
@Operation(summary = "删除",description = "删除支付流水方法")
public ResultData<Integer> deletePay(@PathVariable("id") Integer id) {
int i = payService.delete(id);
return ResultData.success(i);
}
@PutMapping(value = "/pay/update")
@Operation(summary = "修改",description = "修改支付流水方法")
public ResultData<String> updatePay(@RequestBody PayDTO payDTO)
{
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);
int i = payService.update(pay);
return ResultData.success("成功修改记录,返回值:"+i);
}
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "按照ID查流水",description = "查询支付流水方法")
public ResultData<Pay> getById(@PathVariable("id") Integer id)
{
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
}
12.使用webservice测试一下
重启8081项目
访问: localhost:8001/pay/get/1
13.全局异常处理:
查询方法写个bug
if(id == -4) throw new RuntimeException("id不能为负数")
访问: localhost:8001/pay/get/-4
图片
返回格式很乱,不清晰 没达到一目了然
14.创建全局异常处理器 GlobalExceptionHandler
package com.zx.exp;
import com.zx.resp.ResultData;
import com.zx.resp.ReturnCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 15:19
* @Version 1.0
**/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 默认全局异常处理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e) {
System.out.println("----come in GlobalExceptionHandler");
log.error("全局异常信息exception:{}", e.getMessage(), e);
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),e.getMessage());
}
}
在PayController中新增接口 getPayError 代码如下:
@RequestMapping(value = "/pay/error",method = RequestMethod.GET)
public ResultData<Integer> getPayError()
{
Integer i = Integer.valueOf(200);
try
{
System.out.println("--------come here");
int data = 10/0;
}catch (Exception e){
e.printStackTrace();
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),e.getMessage());
}
return ResultData.success(i);
}
再访问 localhost:8001/pay/get/-4
一目了然 多好 很清晰. GO ON MENFOLK!
做到现在能全部跑通 那你就往下看
15.目前目录结构:
整理工程,重构一下
最后会有很多的微服务模块,将返回类和DTO请求实体类封装到一个Module中
五、创建cloud-api-commons微服务模块
不啰嗦直接贴代码 最后放目录结构
1.POM:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zx</groupId>
<artifactId>zx_cloud</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-api-commons</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>
2.将8001模块中的entity里的DTO类和全局返回类和枚举类分别放到此模块中
全局异常可加可不加
3.maven命令- clean install
install之后会将模块打成jar包, 将打成的jar包放入8001模块POM内
将8001模块原有过得entities和统一返回体删除
4.修改 POM 加入以下依赖
<!-- 自己定义的api通用包 -->
<dependency>
<groupId>com.zx</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
添加完之后一定要右上角刷新maven然后重新导包
测试一下就ok了
六、开始引入微服务理念
1.创建cloud-consumer-order80模块
如果有一个订单微服务80模块,要如何才能调用到支付微服务8001模块?
1.创建cloud-consumer-order80模块
2.修改POM
3.编写YML
4.编写主启动类(修改类名为Main80)
5.编写业务类
6.测试
2.修改POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zx</groupId>
<artifactId>zx_cloud</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-consumer-order80</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.编写YML
server:
port: 80
4.主启动类
package com.zx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
@SpringBootApplication
public class Main80
{
public static void main(String[] args)
{
SpringApplication.run(Main80.class,args);
}
}
5.编写业务类
服务调用方法 'RestTemplate'
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是spring提供的用于访问Rest服务的客户端末班工具集
RestTemplate官网地址
https://docs.spring.io/spring-framework/docs/6.0.11/javadoc-api/org/springframework/web/client/RestTemplate.html
使用restTemplate访问restful接口非常的简单粗暴无脑。
(url, requestMap, ResponseBean.class)这三个参数分别代表
REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
创建RestTemplateConfig 配置类
package com.zx.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 16:32
* @Version 1.0
**/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate()
{
return new RestTemplate();
}
}
创建OrderController类
package com.zx.controller;
import com.zx.entities.PayDTO;
import com.zx.resp.ResultData;
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 XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 16:33
* @Version 1.0
**/
@RestController
public class OrderController {
public static final String PaymentSrv_URL = "http://localhost:8001";//先写死,硬编码
@Autowired
private RestTemplate restTemplate;
/**
* 一般情况下,通过浏览器的地址栏输入url,发送的只能是get请求
* 我们底层调用的是post方法,模拟消费者发送get请求,客户端消费者
* 参数可以不添加@RequestBody
*
* @param payDTO
* @return
*/
@GetMapping("/consumer/pay/add")
public ResultData addOrder(PayDTO payDTO) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
}
@GetMapping("/consumer/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id) {
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/" + id, ResultData.class, id);
}
}
6.测试
启动80微服务
访问http://localhost/consumer/pay/get/1
可以看到 当我们访问80的接口时,实际返回的是8001接口返回的数据,这就是RestTemplate调用.
到此为止,也还是没有引入任何的SpringCloud相关内容
所以为什么要引入微服务?这样不也能调用其他模块的接口吗?
答: 上述代码硬编码写死问题.
public static final String PaymentSrv_URL = "http://localhost:8001";
(1)如果订单微服务和支付微服务的IP地址或者端口号发生了变化,则支付微服务将变得不可用,需要同步修改订单微服务中调用支付微服务的IP地址和端口号。
(2)如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。
(3)如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护会变得异常复杂。
七、正式进入SpringCloud微服务! ! ! ! ! ! !
1.引入Consul作为此微服务的注册中心
不使用Eureka的原因:
1.停更 在Eureka2.0以后就不更新了 不好维护
2.Eureka对初学者不友好(自我保护机制)
3.注册中心独立且和微服务功能解耦,目前主流服务中心希望单独隔离出来,而不是作为一个独立的微服务嵌入到系统中 按照NetFlix的之前思路,注册中心Eureka也是作为一个微服务且需要程序员自己开发部署的. 实际情况是希望微服务和注册中心分离解耦,注册中心是和业务无关的,不要混为一谈,提供类似tomcat一样独立的组件,微服务注册上去使用,是个成品才对.
4.最重要的是阿里巴巴Nacos的崛起
2.Consul
官网地址
https://www.consul.io/
介绍Consul:
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。它具有很多优点。包括:基于 raft 协议,比较简洁;支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
spirng-cloud集成consul官网地址
https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/
下载地址
https://developer.hashicorp.com/consul/downloads
下载完成之后 只有一个consul.exe文件 对应全路径下查看版本号信息
启动consul 输入命令 开发模式启动
consul agent -dev
成功启动后 浏览器输入 localhost:8500 进入consul管理界面
3.将支付模块cloud-provider-payment8001注册进consul 引入依赖
<!--SpringCloud consul discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
4.配置yml
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
5.在主启动类上添加注解 @EnableDiscoveryClient 开启服务发现
@SpringBootApplication
@MapperScan("com.zx.mapper")
@EnableDiscoveryClient
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class,args);
}
}
启动8001并查看consul控制台
启动8001并查看consul控制台
6.将cloud-consumer-order80也注册进consul
服务消费者80成功注入consul
7.修改RestTemplate配置类 添加@LoadBalanced注解开启负载均衡功能
8.常见的3个注册中心
Eureka Consul Zookeeper
注册中心的CAP C:Consistency(强一致性) A:Availability(可用性) P:Partition tolerance(分区容错性)
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
Eureka=AP
Zookeeper/Consul=CP
八、consul的服务配置与刷新
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。
当下我们每一个微服务自己带着一个application.yml,上百个配置文件的管理......
1.修改cloud-provider-payment8001 添加依赖
<!--SpringCloud consul config-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2.新增配置文件bootstrap.yml
解释:
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的`Application Context`的父上下文。初始化的时候,`Bootstrap Context`负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的`Environment`。
`Bootstrap`属性有高优先级,默认情况下,它们不会被本地配置覆盖。`Bootstrap context`和`Application Context`有着不同的约定,所以新增了一个`bootstrap.yml`文件,保证`Bootstrap Context`和`Application Context`配置的分离。
application.yml文件改为bootstrap.yml,这是很关键的或者两者共存
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
# config/cloud-payment-service/data
# /cloud-payment-service-dev/data
# /cloud-payment-service-prod/data
bootstrap.yml将application.yml中属于springboot的配置全部抽取出去了, bootstrap优先级高于application. 所以application.yml只留自己用的东西. 如下:
server:
port: 8001
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zxcloud?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: root
profiles:
active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.zx.entities
configuration:
map-underscore-to-camel-case: true
3.consul服务器key/value配置填写
config文件夹下分别创建其他3个文件夹 以/结尾
cloud-payment-service
cloud-payment-service-dev
cloud-payment-service-prod
分别在3个文件夹下常见data内容,data不再是文件夹
三个都配置好了之后 如图:
4.在PayController中新增getPayInfoByConsulConfig
@Value("${server.port}") // 80
private String port;
@GetMapping(value = "/pay/get/info")
public String getPayInfoByConsulConfig(@Value("${com.zx}") String zx){
return "zx: " + zx + "\t" + "port: "+ port;
}
server.port 拿自己模块的端口
请求参数的@Value("${com.zx}") 获取的是在consul中创建config文件里的data内容
上面创建了3个文件夹 分别是 xx xx-dev xx-prod
获取哪个文件夹中data的数据就将application.yml中的profiles:atvice修改 假设需要获取dev中data的内容:
profiles:
atvice: dev
5.重启8001提供者 浏览器访问 http://localhost:8001/pay/get/info
这样就实现了 '一处修改 处处生效'的需求. 不管是10个微服务模块还是几十个甚至上百个微服务模块 都可以只修改consul中的data数据就可以解决大量服务的问题.
consul configuration is not complete !
此时还会有一个问题, 你会发现 在consul里config中的data数据发生变化并save之后 刷新接口其实并没有任何变化!
我在这里已经添加了一段数字.
但是刷新页面重新访问接口会发现 还是之前的数据.
在spring官网中 默认会有一个配置叫
spring.cloud.consul.config.watch.wait-time
默认是55秒刷新一次. 在测试阶段如果想立马看到效果,可以在bootstrap.yml修改此配置 如下:
改为1秒. 并且在主启动类上添加注解@RefreshScope 代表动态刷新
配置好了之后重启就会发现 修改完了consul之后刷新页面就会即时更新.
实际开发的时候建议不改
6.ConSul持久化
现在如果把命令行关掉之后,回来再启动consul会发现之前配置的config文件夹里所有的数据全部没有了!
配置consul数据持久化并将consul注册为Windows服务.
在安装consul路径下创建mydata文件夹和consul_start.bat文件,后缀为.bat
consul_start.bat文件np++打开 加入以下内容 路径改为自己的
@echo.服务启动......
@echo off
@sc create Consul binpath= "D:\install\consul\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect 1 -data-dir D:\install\consul\mydata "
@net start Consul
@sc config Consul start= AUTO
@echo.Consul start is OK......success
@pause
右键以管理员身份运行
出现success 即启动成功
查看Windows后台
配置consul持久化完成
九、LoadBalancer负载均衡服务调用
随着Ribbon进入维护模式, Ribbon官网提供的未来替换方案就是spring-cloud-loadbalance
https://github.com/Netflix/ribbon
图片
Ribbon不多说,直接进入spring-cloud-loadbalance
spring-cloud-loadbalance官网:
https://spring.io/guides/gs/spring-cloud-loadbalancer
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WeClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)
LodanBalance与Nginx负载均衡有什么区别?
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务器实现的.
loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
逼话不多说直接开干
1.将8001拷贝一份新建8002微服务
将拷贝后的8002微服务端口改为8002
其他配置与8001微服务一样
启动8002微服务注册入consul中
2.修改80订单微服务模块 添加以下依赖
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
在RestTemplate上添加注解
@LoadBalanced//使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
controller新增接口
@GetMapping(value = "/consumer/pay/get/info")
private String getInfoByConsul()
{
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/info", String.class);
}
替换之前硬编码写死代码;
启动80并注册进consul
浏览器访问 http://localhost/consumer/pay/get/info
交替访问到了8001/8002
3.使用DiscoverClient动态获取所有上线的服务列表
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/discovery")
public String discovery()
{
List<String> services = discoveryClient.getServices();
for (String element : services) {
System.out.println(element);
}
System.out.println("===================================");
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
for (ServiceInstance element : instances) {
System.out.println(element.getServiceId()+"\t"+element.getHost()+"\t"+element.getPort()+"\t"+element.getUri());
}
return instances.get(0).getServiceId()+":"+instances.get(0).getPort();
}
访问http://localhost/consumer/discovery
4.总结:
LoadBalancer默认算法有两种 1.轮询 2.随机
public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer
public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer
默认是1 轮询算法
玩一下随机算法
修改RestTemplateConfig
package com.zx.config;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.client.RestTemplate;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 16:32
* @Version 1.0
**/
@Configuration
@LoadBalancerClient(
//下面的value值大小写一定要和consul里面的名字一样,必须一样
value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig {
@Bean
@LoadBalanced//使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
重启访问http://localhost/consumer/pay/get/info 会发现 8001/8002随机切换
十、OpenFeign服务接口调用
为什么有了LoadBalancer还要使用OpenFeign 以后用哪个?
官网说话!
OpenFeign是一个声明式的Web服务客户端,并且也支持并包含了LoadBalance
逼话不多说 开搞就是
1.新建cloud-consumer-feign-order-80微服务模块
修改POM文件:
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud consul discovery-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.zx</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
2.编写application.yml
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
3.启动类: 修改类名为: MainOpenFeign80
启动类上面添加@EnableFeignClients注解表示激活openfeign功能
修改cloud-api-commons模块
引入openfeign依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
新建服务接口PayFeignApi, 类名上添加@FeignClient注解
value必须与服务接口注册到consul上的服务名相同
将8001的controller代码拿过来放到此接口里 方法体去掉 作为方法实现;
package com.zx.apis;
import com.zx.entities.PayDTO;
import com.zx.resp.ResultData;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/19 14:49
* @Version 1.0
**/
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
/**
* 新增支付相关流水记录
* @param pay 支付实体
* @return com.zx.resp.ResultData<T>
*/
@PostMapping(value = "/pay/add")
ResultData<String> addPay(@RequestBody PayDTO pay);
/**
* 按照主键记录查询支付流水
* @param id id
* @return com.zx.resp.ResultData<T>
*/
@GetMapping(value = "/pay/get/{id}")
ResultData<?> getPayInfo(@PathVariable("id") Integer id);
/**
* openfeign天然支持负载均衡演示
* @return java.lang.String
*/
@GetMapping(value = "/pay/get/info")
String mylb();
}
4.拷贝之前的80工程到cloud-consumer-feign-order80, 去掉部分代码和LoadBalancer不相关特性
修改controller层的调用
package com.zx.controller;
import com.zx.apis.PayFeignApi;
import com.zx.entities.PayDTO;
import com.zx.resp.ResultData;
import jakarta.annotation.Resource;
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.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/18 16:33
* @Version 1.0
**/
@RestController
public class OrderController {
@Resource
private PayFeignApi payFeignApi;
@PostMapping("/feign/pay/add")
public ResultData<?> addOrder(@RequestBody PayDTO payDTO)
{
System.out.println("第一步:模拟本地addOrder新增订单成功(省略sql操作),第二步:再开启addPay支付微服务远程调用");
ResultData<?> resultData = payFeignApi.addPay(payDTO);
return resultData;
}
@GetMapping("/feign/pay/get/{id}")
public ResultData<?> getPayInfo(@PathVariable("id") Integer id)
{
System.out.println("-------支付微服务远程调用,按照id查询订单支付流水信息");
ResultData<?> resultData = payFeignApi.getPayInfo(id);
return resultData;
}
/**
* openfeign天然支持负载均衡演示
*
* @return
*/
@GetMapping(value = "/feign/pay/mylb")
public String mylb()
{
return payFeignApi.mylb();
}
}
5.测试:
启动consul服务器.
再启动微服务8001
再启动cloud-consumer-feign-order80
postman测试
新增 http://localhost/feign/pay/add
JSON请求体:
{
"payNo": "payfeign-test1",
"orderNo": "consumer-feign-test1",
"userId": "2",
"amount": "9.99"
}
查询 http://localhost/feign/pay/get/7
测试负载均衡,启动8002 访问接口 http://localhost/feign/pay/mylb
测试发现
测试可得 openfeign天生具备负载均衡功能
5.OpenFeign高级特性
1.超时控制
写累了 不想写格式了 能看懂就看 看不懂拉倒吧
首先openFeign默认等待60返回,超过60秒之后 openfeign会报错
演示一下
在cloud-provider-payment8001模块中 加上休眠62秒程序
重启8001,调用 http://localhost/feign/pay/get/7
如遇到不能调用到8001接口的问题. 请清理idea缓存,或使用maven-compile
调用完成之后可以看到,开始是45:52秒,结束时46:52秒, 其中间隔是60s. 但咱们明明休眠了62s,为什么到60s的时候就返回并抛出异常了?
解释:
openfeign默认等待请求超时时间为60s, 超过之后 就会抛出 read time out....exception
官网地文档地址:https://docs.spring.io/spring-cloud-openfeign/reference/configprops.html
如果有特殊要求,这个timeout支持修改
配置yml:
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
# default: #默认
cloud-payment-service: #可以指定服务进行超时配置
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
重启openfeign80模块 请求http://localhost/feign/pay/get/7
发现读取时间从60s变为了3s 这样就实现了超时控制.
2.重试机制
1. 因为Openfeign默认使用jdk自带的HttpURLConnection发送HTTP请求
性能比较垃圾所以我们换到Apache HttpClient 5
openfeign80模块修改 POM依赖:
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
配置yml:
httpclient:
hc5:
enabled: true
重启80模块继续请求看报错
2.OpenFeign 默认是不走重试机制的. 默认值是default
在feign中 有一个Retryer的接口 默认是 NEVER_RETRY 不走重试策略
将他设置为自定义
80模块 新增FeignConfig配置类
并设置OpenFeign的打印功能
@Configuration
public class FeignConfig {
@Bean
public Retryer myRetryer() {
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100,1,3);
}
/*@Bean
public Retryer myRetryer() {
return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
}*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
日志级别
图片
NONE: 默认的,不显示任何日志
BASIC: 仅记录请求方法,URL,响应状态码及执行时间
HEADERS: 除了BASIC中定义的信息之外,还有请求的和响应的头信息
FULL: 除了HEADERS中定义的信息之外,还有请求和响应的正文及其元数据
方便观察,建议使用FULL
配置完成之后,重启80服务,访问http://localhost/feign/pay/get/7
看日志能看到 第一次是正常请求 是没有RETRYING字眼的
而第二次请求回出现RETRYING
代表重试请求,包括第三次也会出现.
说明他的机制是 你的Retryer配置类中的maxAttempts参数 代表最终请求总次数. 最大次数3次 1次正常请求+2次重试请求.
OpenFeign和Sentinel集成实现fallback服务降级 见后续.
十一、CircuitBreaker断路器
众所周知服务熔断和服务降级在微服务中起到了非常重要的作用,用于提高系统稳定性和可用性的两种重要机制.
说到降级可能会联想到大名鼎鼎的'豪猪' Hystrix, 是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟,异常引起资源耗尽导致系统不可用的解决方案. 主要有三个功能: 1.服务降级 2.服务熔断 3.服务限流
啰嗦一下 主要是不太熟悉,想记录一下以后自己方便回顾
1.服务降级
首先 SpringCloud是通过HTTP Rest的方式在微服务之间进行调用的.所谓的每一个微服务模块都是一个web项目. 既然是web项目 那它就有可能会发生错误,这个错误有可能是内存不足或者客户端传参错误,再或者是网络问题等.也就是说会因为一些原因从而不能给调用者返回正确的信息.
对于目前的单个Springboot项目来说,我们会使用Ajax等方式来做限制.当调用服务接口时,如果服务发生错误,在前端就会对这个错误进行处理.有可能是重试调用接口,或者给用户一个友好的提示,比如"服务繁忙,稍后再试".
但是在分布式系统中,同样也会发生一些“错误”,而且在多个服务之间调用时,如果不能对这些“错误”进行友好的处理,就会导致我们整个项目瘫痪,这是万万不能发生的。所以Hystrix利用服务降级来很好的解决了这个问题。这个其实就类似于我们的try-catch这样的机制,发生错误了,我就执行catch中的代码。
通过服务降级,能保证在某个或某些服务出问题的时间,不会导致整个项目出现问题,避免级联故障,从而来提高分布式系统的弹性。
2.服务熔断
Hystrix意为"断路器",就和我们生活中的保险丝开关一个道理
当我们给整个服务配置了服务降级后,如果A服务发生了错误,就会调用降级后的方法来保证程序的运行.
但是有一个问题,调用者并不知道它调用的这个A服务出错了,没有做任何处理,就会导致业务发生时会一直调用,导致提供业务的A服务会一直报错,然后去调用降级的方法.
举个例子:
client: 我要调用你的方法A
server:不行,我报错了.你调用降级方法吧.
client: 哦,那我去调降级方法
过了一会儿..
client又发生了业务,然后又去调用server了
client: 我要调用你的方法A
server: 刚才不是说了吗?我报错了,你调用降级方法.
client: 哦,那我调降级方法
又过了一会儿..
client: 我要调用你的方法A
server: 没完了是吧?都告诉你我报错了,你去调降级方法啊,非让我再运行一次再报一次错?
client: 哦,我去调降级方法.
例子说明 当服务端发生错误时,客户端就会调用降级方法,但是当有一个新的访问时,客户端会一直调用服务端,让服务端运行一段明知会报错的代码. 这能不能避免? 我知道我错了,你访问我的时候,就直接去访问降级方法,不要让我在执行错误的代码了.
这就是服务熔断,就好比我们家中的保险丝,当检测到家中用电负荷过大时,便会自动跳闸来保证安全.在分布式中,在一定时间内,他这个服务端发生的错误数达到一定值时,我们就打开这个断路器,不让调用,而是让他直接去调用降级方法. 当过一段时间后,再一次调用,发现这个服务通了,就将这个断路器改为"半开"状态,让调用一个个的慢慢过去,如果一直没有发生错误,九江这个断路器关闭,让所有的服务全部通过. 反之如果在半开状态时,陆续进去的调用又出现了错误,那断路器将会变成开启状态, 调用还是要去调降级方法.
3.服务限流
服务限流就容易理解多了,顾名思义,这是对访问的流量进行限制,就比如上边的场景,我们还可能通过服务限流的方法来解决高并发以及秒杀等问题。主流的限流算法主要有:漏桶算法和令牌算法
而Hystrix官网官宣 停更,进入维护阶段.官网中表明了Hystrix未来替换方案为 resilience4j "瑞栽拎死forjava"
在2023年影响极大的真实生产故障:
1. 2023.10.23 语雀崩了
2. 11.12 阿里产品也崩了
在分布式中,应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的发生错误.
假如有5个微服务分别为 a,b,c,d,e,f
调用顺序为
a->b->c->d->e->f
当一切顺利的时候 a会顺利拿到f返回的数据.但是如果f服务超时会出现什么情况?
那就是服务雪崩. a到f全部崩溃
在微服务之间 假设a服务调用微服务b和微服务c, 微服务b又和微服务c调用其他的微服务,这就是所谓的"扇出",如果扇出的链路上某个微服务调用响应时间过长或者不可用,那对微服务a的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的"雪崩效应"
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
相信很多程序员都听说过雪崩吧, 比如redis雪崩. 我们的需求就是 禁止服务雪崩.
如何解决?
将有问题的节点快速熔断,不让请求继续执行.
服务降级,提示服务器忙,请稍后再试,不让客户端等待,迅速返回一个友好提示,然后fallback 或调用降级方法
3.做服务限流,秒杀等高并发操作,严禁一窝蜂的过来拥挤. 做限流排队,一秒钟进N个,有序进行.
** 4.服务限时,限制请求时间,比如秒杀入口只开启一分钟.**
总之一句话 出故障了,那你就跳闸 别把整个家给烧了.
既然"豪猪"已经停止更新进入维护阶段了, 那就直接使用替代品
Spring Cloud Circuit Breaker
Spring Cloud Circuit Breaker是什么?
官网:
https://spring.io/projects/spring-cloud-circuitbreaker#overview
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
4.服务隔离
舱壁隔离(以船为例).
官网: RateLimiter
gitHub: https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules-reatlimiter.md
主要作用是依赖隔离和负载保护,用来限制对于下游服务的最大并发数量的限制
Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量.
5.熔断和降级实战开始
Circuit Breaker只是一套规范和接口, 落地实现者是Resilience4j
断路器有三个状态 1.open 2.half_open 3.closed
1.开启断路器 2.半开断路器 3.关闭断路器
还有两个特殊状态: 1.DISABLED(禁用) 2.FORCED_OPEN(强制开启)
当熔断器处于closed状态时,所有请求都会通过熔断器,当服务端失败率超过设定的阈值后,熔断器就会从closed状态转换到open状态,这时所有请求都会被拒绝
当经过一段时间后,熔断器会从open状态转为half_open状态,这时仅有一定数量的请求会被放入,并重新计算失败率 当失败率超过阈值,则继续转为open状态,当失败率低于阈值 就会转为closed状态.
断路器配置参数文档参考
https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/CircuitBreaker.md
其中 sliding-window-type可以基于次数也可以基于时间来进行熔断.默认是次数
开干!!
1.修改cloud-provider-payment-8001 新建PayCircuitController
按次数类型做熔断:
@RestController
public class PayCircuitController {
@GetMapping("/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id){
if(id == -4) throw new RuntimeException("----circuit id 不能负数");
if(id == 9999){
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
}
return "Hello, circuit! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
}
2.修改公共微服务模块 PayFeignApi 新增接口
@GetMapping(value = "/pay/circuit/{id}")
String myCircuit(@PathVariable("id") Integer id);
3.引入依赖 在cloud-consumer-feign-order80模块:
<!--resilience4j-circuitbreaker-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4.修改yml 下附整体yml文件
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default: #默认
# cloud-payment-service: #可以指定服务进行超时配置
#连接超时时间
connectTimeout: 2000
#读取超时时间
readTimeout: 2000
httpclient:
hc5:
enabled: true
# 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
#feign日志
logging:
level:
com:
zx:
apis:
PayFeignApi: debug
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slidingWindowType: COUNT_BASED # 滑动窗口的类型
slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
5.新建OrderCircuitController
package com.zx.controller;
import com.zx.apis.PayFeignApi;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author XiaTianDa
* @ClassName zx_cloud
* @Description //TODO
* @Date 2024/6/27 9:06
* @Version 1.0
**/
@RestController
public class OrderCircuitController{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
public String myCircuitBreaker(@PathVariable("id") Integer id)
{
return payFeignApi.myCircuit(id);
}
//myCircuitFallback就是服务降级后的兜底处理方法
public String myCircuitFallback(Integer id,Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
6.先启动8001 再启动openfeign80服务
访问 http://localhost/feign/pay/circuit/111
正常请求和返回.
再次请求http://localhost/feign/pay/circuit/-4
8001抛出runtime异常. 代码报错啦 那上面怎么说的, 我报错了 client你去调用降级方法吧
请求 http://localhost/feign/pay/circuit/9999
也会报这个. 因为配置文件里配置的openfeign请求等待时间为2秒,而业务类里面 如果id为9999时 会休眠5秒, openfeign 2秒内没接收到返回数据会报timeout异常 被circuitBreaker捕获到,走myCircuitFallback降级方法
连续请求3次http://localhost/feign/pay/circuit/-4 让接口报3次错
然后在请求一次正确数据 http://localhost/feign/pay/circuit/111
会发现 连续3次报错之后 就算请求111正常请求也会走降级方法.
因为配置文件里配置了
当6个请求内有50%失败了也就是 失败了3次 那断路器会立马从closed转换为open,在5s内不管是不是正确请求,都会被断路器拦截并调用降级方法.
5s之后,断路器会自动转换为half_open状态,此时只能会过滤两个请求, 只要这两个请求有任意一个调用接口时接口报错,断路器就会继续转换为open状态 这就是所谓的半开状态.
circuitBreaker有两种类型来处理熔断和降级. 一是上面说的次数类型,以次数为条件,超过多少次之后进行熔断和降级. 二是下面要说的时间类型,以多长时间内失败了
时间类型:
yml
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
将这里修改时间长一点,之前是2秒 现在是20秒 为了方便看到效果,不改20秒也可以 一定要改到5秒以上
将重试机制关闭
重启80端口服务
请求一次http://localhost/feign/pay/circuit/9999
在请求一次http://localhost/feign/pay/circuit/111
会发现 9999上面那个虽然慢 但是过了5秒 也正确返回了,而111更没问题 直接返回正确数据.
但是记住 实际 9999已经是慢调用了,配置文件里配置的是超过2s就算慢调用一次
然后搞4个9999请求 依次从1-4全部请求, 此时再执行一遍 正确的111这时,111的正确请求也会被熔断和调用降级方法
6.隔离(BulkHead)实战
1.信号量舱壁隔离(SemaphoreBulkhead)原理 当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。 当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器, 如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。 若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
1.修改cloud-provider-payment8001微服务中PayCircuitController新增如下接口
/**
* circuitBreaker服务熔断 Resilience4j实现
*/
@GetMapping("/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id) {
if (id == -4) throw new RuntimeException("----circuit id 不能负数");
if (id == 9999) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Hello, circuit! inputId: " + id + " \t " + IdUtil.simpleUUID();
}
2.修改PayFeignApi接口新增舱壁api方法
/**
* Resilience4j Bulkhead 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/bulkhead/{id}")
String myBulkhead(@PathVariable("id") Integer id);
3.修改cloud-consumer-feign-order80
1.新增依赖:
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
2.yml修改
#----------舱壁隔离 信号量隔离----------
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
cloud-payment-service:
baseConfig: default
3.业务类修改orderCircuitController
/**
*(船的)舱壁,隔离
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id) {
return payFeignApi.myBulkhead(id);
}
public String myBulkheadFallback(Throwable t)
{
return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
4.测试
http://localhost/feign/pay/bulkhead/9999
http://localhost/feign/pay/bulkhead/3
5秒之后返回成功
2.固定线程池舱壁隔离
固定线程池舱壁(FixedThreadPoolBulkhead) FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。 当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。 当线程池中无空闲时时,接下来的请求将进入等待队列, 若等待队列仍然无剩余空间时接下来的请求将直接被拒绝, 在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。 另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
1.修改cloud-consumer-feign-order80 pom
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
2.yml
#----------舱壁隔离 信号量隔离----------
# bulkhead:
# configs:
# default:
# maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
# maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
thread-pool-bulkhead: #固定线程池隔离
configs:
default:
#最大线程数=max-thread-poll-size+queue-capacity 超出则报错
core-thread-pool-size: 1
max-thread-pool-size: 1
queue-capacity: 1
instances:
cloud-payment-service:
baseConfig: default
3.修改controller
/**
*(船的)舱壁,隔离 THREADPOOL机制 返回 CompletableFuture<T>
* @param id
* @return
* FixedThreadPoolBulkhead 类运行实现
* FixedThreadPoolBulkhead中的submit方法提交请求返回CompletableFuture;
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkhead(@PathVariable("id") Integer id) {
System.out.println(Thread.currentThread().getName() +"\t" + "开始执行" );
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() +"\t" + "结束执行" );
return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" +" Bulkhead.Type.THREADPOOL");
}
public CompletableFuture<String> myBulkheadFallback(Throwable t)
{
return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
4.测试
依次访问:
http://localhost/feign/pay/bulkhead/1
http://localhost/feign/pay/bulkhead/2
http://localhost/feign/pay/bulkhead/3
代码中 休眠了3秒,在3秒之内没有处理完数据,那线程会被一直占用,而配置文件里最大线程池为1,包括等待队列1,加一起一共是2个线程,请求1-2的时候已经被占满,到3的时候直接拦截走fallback方法降级.
7.限流(RateLimiter)实战
POM:
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
YML:
resilience4j:
#----------默认限流算法----------
ratelimiter:
configs:
default:
limitForPeriod: 10 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 5s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
cloud-provider-payment8001
PayCircuitController新增myRateLimit接口
/**
* circuitBreaker限流(RateLimiter) Resilience4j实现
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRateLimit(@PathVariable("id") Integer id){
return "hello,myRateLimit 欢迎来到 inputId: "+ id +"\t" +IdUtil.simpleUUID();
}
cloud-api-commons
PayFeignApi新增接口
/**
* Resilience4j ratelimiter接口 调用8001服务
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
String myRateLimit(@PathVariable("id") Integer id);
cloud-consumer-feign-order80
OrderCircuitController新增接口
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRateLimitFallback")
@GetMapping(value = "/feign/pay/ratelimit/{id}")
public String myRateLimit(@PathVariable("id")Integer id){
return payFeignApi.myRateLimit(id);
}
public String myRateLimitFallback(Integer id,Throwable t){
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
启动80和8001
访问 http://localhost/feign/pay/ratelimit/111
狂刷10次F5
CircuitBreaker断路器到此结束,下一步分布式链路追踪
版权归原作者 LaoRenYuHai- 所有, 如有侵权,请联系我们删除。