🧊SpringCloud学习

一、前言(环境搭建)

视频地址(bilibili):【尚硅谷SpringCloud框架开发教程(SpringCloudAlibaba微服务分布式架构丨Spring Cloud)】https://www.bilibili.com/video/BV18E411x7eT

GitHub代码地址https://github.com/zhangzhixi0305/SpringCloud-Study

软件资源地址https://pan.baidu.com/s/1jaFL7M4et8cCTjftc2UO5Q?pwd=8023

版本选型:

SpringBoot2.x:

源码地址:https://github.com/spring-projects/spring-boot/releases/

SpringBoot2新特性:https://github.com/spring-projects/spring-boot/wiki/spring-Boot-2.0-Release-Notes

SpringCloud H版:

SpringCloud官方文档:https://spring.io/projects/spring-cloud#overview

SpringCloud中文文档:https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md

Spring Boot 与 Spring Cloud 兼容性查看:

官方文档查看:https://spring.io/projects/spring-cloud#overview

JSON对照:https://start.spring.io/actuator/info

开发用到的组件版本:

  • Cloud - Hoxton.SR1
  • Boot - 2.2.2.RELEASE
  • Cloud Alibaba - 2.1.0.RELEASE
  • Java - Java 8+
  • Maven - 3.5及以上
  • MySQL - 5.7及以上

二、初识模块搭建(了解微服务调用)

  以订单模块(provider)和支付模块(consumer)为例

2.1、父模块环境搭建

1、创建一个Maven项目

2、添加依赖 

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!--统一管理jar包版本-->
    <maven.compiler.target>1.8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <log4j.version>1.2.17</log4j.version>
    <lombok.version>1.16.18</lombok.version>
    <mysql.version>8.0.32</mysql.version>
    <druid.version>1.1.16</druid.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    <cloud-api-common.version>1.0-SNAPSHOT</cloud-api-common.version>
</properties>

<!--子模块继承之后,提供作用:
    锁定版本+子modlue不用写groupId和version-->
<dependencyManagement>

    <dependencies>
        <!--公共模块-->
        <dependency>
            <groupId>com.zhixi</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${cloud-api-common.version}</version>
        </dependency>

        <!--spring boot 2.2.2-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.2.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--spring cloud Hoxton.SR1-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--spring cloud alibaba 2.1.0.RELEASE-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</dependencyManagement>

3、设置工程类型  

因为是个聚合项目,在父工程的pom文件的<package>标签中要设置成pom

<packaging>pom</packaging>

2.2、数据库搭建

MySQL中创建数据库:spring-cloud,执行以下SQL

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80015
 Source Host           : localhost:3306
 Source Schema         : db2019

 Target Server Type    : MySQL
 Target Server Version : 80015
 File Encoding         : 65001

 Date: 06/03/2020 10:19:42
*/

SET NAMES utf8mb4;
SET
FOREIGN_KEY_CHECKS = 0;
CREATE
database db2019;
USE
db2019;

-- ----------------------------
-- Table structure for payment
-- ----------------------------
DROP TABLE IF EXISTS `payment`;
CREATE TABLE `payment`
(
    `id`     bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `serial` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '支付流水号',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '支付表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of payment
-- ----------------------------
INSERT INTO `payment`
VALUES (31, '尚硅谷111');
INSERT INTO `payment`
VALUES (32, 'atguigu002');
INSERT INTO `payment`
VALUES (34, 'atguigu002');
INSERT INTO `payment`
VALUES (35, 'atguigu002');

SET
FOREIGN_KEY_CHECKS = 1;

--- seata  分布式事务
-- the table to store GlobalSession data
create
database seata;
USE
seata;
drop table if exists `global_table`;
create table `global_table`
(
    `xid`                       varchar(128) not null,
    `transaction_id`            bigint,
    `status`                    tinyint      not null,
    `application_id`            varchar(32),
    `transaction_service_group` varchar(32),
    `transaction_name`          varchar(128),
    `timeout`                   int,
    `begin_time`                bigint,
    `application_data`          varchar(2000),
    `gmt_create`                datetime,
    `gmt_modified`              datetime,
    primary key (`xid`),
    key                         `idx_gmt_modified_status` (`gmt_modified`, `status`),
    key                         `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table`
(
    `branch_id`         bigint       not null,
    `xid`               varchar(128) not null,
    `transaction_id`    bigint,
    `resource_group_id` varchar(32),
    `resource_id`       varchar(256),
    `lock_key`          varchar(128),
    `branch_type`       varchar(8),
    `status`            tinyint,
    `client_id`         varchar(64),
    `application_data`  varchar(2000),
    `gmt_create`        datetime,
    `gmt_modified`      datetime,
    primary key (`branch_id`),
    key                 `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table`
(
    `row_key`        varchar(128) not null,
    `xid`            varchar(96),
    `transaction_id` long,
    `branch_id`      long,
    `resource_id`    varchar(256),
    `table_name`     varchar(32),
    `pk`             varchar(36),
    `gmt_create`     datetime,
    `gmt_modified`   datetime,
    primary key (`row_key`)
);

-- - seata_order
create
database IF NOT EXISTS seata_order ;
USE
seata_order;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`
(
    `int`        bigint(11) NOT NULL AUTO_INCREMENT,
    `user_id`    bigint(20) DEFAULT NULL COMMENT '用户id',
    `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
    `count`      int(11) DEFAULT NULL COMMENT '数量',
    `money`      decimal(11, 0) DEFAULT NULL COMMENT '金额',
    `status`     int(1) DEFAULT NULL COMMENT '订单状态:  0:创建中 1:已完结',
    PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

create
database IF NOT EXISTS seata_storage;
USE
seata_storage;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`
(
    `int`        bigint(11) NOT NULL AUTO_INCREMENT,
    `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
    `total`      int(11) DEFAULT NULL COMMENT '总库存',
    `used`       int(11) DEFAULT NULL COMMENT '已用库存',
    `residue`    int(11) DEFAULT NULL COMMENT '剩余库存',
    PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage`
VALUES (1, 1, 100, 0, 100);

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE
database IF NOT EXISTS seata_account;
USE
seata_account;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`
(
    `id`      bigint(11) NOT NULL COMMENT 'id',
    `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
    `total`   decimal(10, 0) DEFAULT NULL COMMENT '总额度',
    `used`    decimal(10, 0) DEFAULT NULL COMMENT '已用余额',
    `residue` decimal(10, 0) DEFAULT NULL COMMENT '剩余可用额度',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;

INSERT INTO `t_account`
VALUES (1, 1, 1000, 0, 1000);

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
View Code

2.3、公共模块搭建

创建微服务模块套路:

  1. 建Module
  2. 改POM
  3. 写YML
  4. 主启动
  5. 业务类

1、建模块:cloud-api-common

 2、改pom

  作为一个公共模块,抽取公共的部分,比如工具类、业务的统一返回值、POJO实体类等,其他微服务模块想要使用这些,直接引入公共模块的gav坐标即可

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.15</version>
    </dependency>
</dependencies>

3、写yml

公共模块不需要写yml

4、主启动

公共模块不需要有主启动类

5、POJO:com.zhixi.pojo.Payment

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 支付模块实体类
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    private Long id;
    private String serial;
}

6、统一返回数据对象:com/zhixi/result/CommonResult

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 统一返回结果类
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    /**
     * 返回结果码
     */
    private Integer code;
    /**
     * 返回结果信息
     */
    private String message;
    /**
     * 返回结果数据
     */
    private T data;


    public CommonResult(Integer code, String message) {
        this(code, message, null);
    }
}

7、Maven进行构建即可

2.3、订单支付模块(provider)

├─java
│  └─com
│      └─zhixi
│          │  SpringCloudProviderPayment8001.java
│          │
│          ├─controller
│          │      PaymentController.java
│          │
│          ├─dao
│          │      PaymentDao.java
│          │
│          └─service
│              │  PaymentService.java
│              │
│              └─impl
│                      PaymentServiceImpl.java
│
└─resources
    │  application.yml
    │  rebel.xml
    │
    └─mapper
            PaymentMapper.xml

1、新建名为:cloud-provider-payment-8001 的maven项目

2、改pom

<dependencies>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <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.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、写yml: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/spring-cloud?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: zhixi158
#mybatis配置
mybatis:
  mapperLocations: classpath:mapper/**/*.xml
  type-aliases-package: com.zhixi.pojo
  configuration:
    map-underscore-to-camel-case: true
#日志
logging:
  level:
    com.zhixi: debug

4、主启动:com.zhixi.SpringCloudProviderPayment8001

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 支付服务提供者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
@MapperScan("com.zhixi.dao")
public class SpringCloudProviderPayment8001 {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudProviderPayment8001.class, args);
    }
}

5、dao层:com.zhixi.dao.PaymentDao

import com.zhixi.pojo.Payment;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 支付模块dao层
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@Repository
public interface PaymentDao {
    /**
     * 新增
     *
     * @param payment 实体对象
     * @return 影响行数
     */
    public int create(Payment payment);

    /**
     * 根据id查询
     *
     * @param id 主键
     * @return 实体对象
     */
    public Payment getPaymentById(@Param("id") Long id);
}

6、service层:com.zhixi.service.PaymentService

import com.zhixi.pojo.Payment;
import org.apache.ibatis.annotations.Param;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 支付模块service层
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
public interface PaymentService {
    /**
     * 新增
     *
     * @param payment 实体对象
     * @return 影响行数
     */
    public int create(Payment payment);

    /**
     * 根据id查询
     *
     * @param id 主键
     * @return 实体对象
     */
    public Payment getPaymentById(@Param("id") Long id);
}

7、Service实现类:com.zhixi.service.impl.PaymentServiceImpl

import com.zhixi.dao.PaymentDao;
import com.zhixi.pojo.Payment;
import com.zhixi.service.PaymentService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @ClassName PaymentServiceImpl
 * @Author zhangzhixi
 * @Description 支付模块service层实现类
 * @Date 2023-04-03 11:20
 * @Version 1.0
 */
@Service
public class PaymentServiceImpl implements PaymentService {

    @Resource
    private PaymentDao paymentDao;

    @Override
    public int create(Payment payment) {
        return paymentDao.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
        return paymentDao.getPaymentById(id);
    }
}

8、mapper:src/main/resources/mapper/PaymentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhixi.dao.PaymentDao">
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into `spring-cloud`.payment(serial)
        values (#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select *
        from `spring-cloud`.payment
        where id = #{id};
    </select>
</mapper>

9、Controller:com.zhixi.controller.PaymentController

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import com.zhixi.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @ClassName PaymentController
 * @Author zhangzhixi
 * @Description 支付模块controller层
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/payment")
@SuppressWarnings("all")
public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @PostMapping(value = "/create")
    public CommonResult create(@RequestBody Payment payment) {
        int result = paymentService.create(payment);
        log.info("*****插入结果:" + result);
        if (result > 0) {
            return new CommonResult(200, "插入数据库成功,插入的主键ID是: " + payment.getId(), result);
        } else {
            return new CommonResult(444, "插入数据库失败", null);
        }
    }

    @GetMapping(value = "/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);

        if (payment != null) {
            return new CommonResult(200, "查询成功,端口是:  " + serverPort, payment);
        } else {
            return new CommonResult(444, "没有对应记录,查询ID: " + id, null);
        }
    }
}

10、测试功能

http://localhost:8001/payment/get/31

 2.4、订单消费模块(consumer)

 1、建模块:cloud-consumer-order-80

2、改pom

<dependencies>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、写yml

server:
  port: 80
spring:
  application:
    name: cloud-consumer-order
# 订单服务调用支付服务
order:
  payment:
    service:
      # 服务名
      url: http://localhost:8001

4、主启动:com.zhixi.controller.OrderController  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @ClassName OrderMain80
 * @Author zhangzhixi
 * @Description 订单服务消费者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
/**
 * name: 指定要使用的Ribbon负载均衡策略的服务名称 configuration: 指定自定义的Ribbon负载均衡策略类
 */
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class, args);
    }
}

5、业务类(注入RestTemplate):com.zhixi.config.RestTemplateConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @ClassName RestTemplateConfig
 * @Author zhangzhixi
 * @Description RestTemplate配置类
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@Configuration
public class RestTemplateConfig {
    @Bean
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

6、Controller:com.zhixi.controller.OrderController

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
 * @ClassName OrderController
 * @Author zhangzhixi
 * @Description 订单控制器
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("/consumer/payment")
@SuppressWarnings("all")
public class OrderController {

    @Value("${order.payment.service.url}")
    private String paymentUrl;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/create")
    public CommonResult<Payment> create(Payment payment) {
        return restTemplate.postForObject(paymentUrl + "/payment/create", payment, CommonResult.class);
    }

    @GetMapping("/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
        return restTemplate.getForObject(paymentUrl + "/payment/get/" + id, CommonResult.class);
    }
}

7、测试  

启动微服务:

  SpringCloudProviderPayment8001

  OrderMain80 

测试:

  localhost/consumer/payment/get/41

  localhost/consumer/payment/create

三、服务注册与发现

常见的注册中心异同点

CAP:

  • C:Consistency (强一致性)

  • A:Availability (可用性)

  • P:Partition tolerance (分区容错性)

AP架构(Eureka)

  当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。

结论:违背了一致性C的要求,只满足可用性和分区容错,即AP

CP架构(ZooKeeper/Consul)

  当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。

结论:违背了可用性A的要求,只满足一致性和分区容错,即CP。

3.1、Eureka

前面我们没有服务注册中心,也可以服务间调用,为什么还要服务注册?

当服务很多时,单靠代码手动管理是很麻烦的,需要一个公共组件,统一管理多服务,包括服务是否正常运行等……

Eureka用于服务注册,不过目前官网已经停止更新

3.1.1、什么是服务治理

  Spring Cloud封装了Netflix 公司开发的Eureka模块来实现服务治理

在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂。

所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

3.1.2、什么是服务注册与发现

  Eureka采用了CS的设计架构,Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。

  在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。

  在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息(接口地址)

3.1.3、Eureka的两个组件:Eureka Server和Eureka Client

Eureka Server:提供服务注册服务

  各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。

EurekaClient:通过注册中心进行访问

  它是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。

如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)

3.1.4、Eureka服务端环境搭建

1、新建模块:cloud-eureka-server-7001

2、改pom

<dependencies>
    <!--eureka-server-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!--boot web actuator-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--一般通用配置-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
    </dependency>
</dependencies>

3、写yml  

server:
  port: 7001
spring:
  application:
    name: cloud-eureka-server-7001

eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @ClassName CloudEurekaServer7001
 * @Author zhangzhixi
 * @Description Eureka服务端启动类
 * @Date 2023-04-04 9:49
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaServer
public class CloudEurekaServer7001 {
    public static void main(String[] args) {
        SpringApplication.run(CloudEurekaServer7001.class, args);
    }
}

3.1.5、Eureka服务端环境搭建(集群版本)

  当然如果是测试的话,也不是非必须搭建集群版本,这里只是演示,具体的还是按照自己笔记本实际情况来操作,搭建集群非必须

为什么要搭建集群版本?

  问题:微服务RPC远程服务调用最核心的是什么(高可用,试想你的注册中心只有一个only one,万一它出故障了,会导致整个为服务环境不可用)

解决办法:

  搭建Eureka注册中心集群,实现负载均衡+故障容错。

重复3.1.4Eureka搭建步骤,分别创建Maven项目:

  cloud-eureka-server-7002

  cloud-eureka-server-7003

其他的不用动,只需要修改pom文件的:defaultZone,让Eureka互相守望即可。

1、修改本机hosts文件:为了模拟域名,更真实环境

  • 找到C:\Windows\System32\drivers\etc路径下的hosts文件,修改映射配置添加进hosts文件
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7003.com

2、修改项目的yml

分别修改Eureka集群:cloud-eureka-server-7002、cloud-eureka-server-7003的yml文件:

7002

server:
  port: 7002
spring:
  application:
    name: cloud-eureka-server-7002

eureka:
  instance:
    hostname: eureka7002.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/

7003  

server:
  port: 7003
spring:
  application:
    name: cloud-eureka-server-7003

eureka:
  instance:
    hostname: eureka7003.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

3、启动集群  

分别启动:cloud-eureka-server-7001、cloud-eureka-server-7002、cloud-eureka-server-7003项目

然后访问:

  http://eureka7001.com:7001/

  http://eureka7002.com:7002/

  http://eureka7003.com:7003/

其实有点掩耳盗铃了,用localhost:7001/localhost:7002/localhost:7003也是一样的~

3.1.6、将订单生产者8001和消费者80注册进Eureka

cloud-provider-payment-8001:

eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
  # 服务端的服务发现页面和健康检查地址
  instance:
    instance-id: payment8001
    # 显示IP地址
    prefer-ip-address: true
    #心跳检测与续约时间
    #开发时没置小些,保证服务关闭后注册中心能即使剔除服务
    #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
#    lease-renewal-interval-in-seconds: 2
    #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
#    lease-expiration-duration-in-seconds: 10

cloud-consumer-order-80:  

# Eureka客户端配置
eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka

3.1.7、Eureka停更说明

https://github.com/Netflix/eureka/wiki

Eureka 2.0 (Discontinued)

The existing open source work on eureka 2.0 is discontinued. The code base and artifacts that were released as part of the existing repository of work on the 2.x branch is considered use at your own risk.

Eureka 1.x is a core part of Netflix’s service discovery system and is still an active project.

下面我们用ZooKeeper代替Eureka功能。

 3.2、Zookeeper

zookeeper是一个分布式协调工具,可以实现注册中心功能;

zookeeper服务器取代Eureka服务器,zk作为服务注册中心;

3.2.1、Zookeeper安装(Windows)

参考:https://www.cnblogs.com/zhangzhixi/p/14359694.html#_label0_3

然后启动Zookeeper服务,如下:

 

3.2.2、服务提供者注册进Zookeeper(支付服务)

1、建模块:cloud-provider-payment-zookeeper-8004

2、加pom

<dependencies>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--公共组件-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        <!--先排除自带的zookeeper3.5.3 防止与3.4.9起冲突-->
        <exclusions>
            <exclusion>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--添加zookeeper3.4.9版本-->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.9</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、写yml 

#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
  port: 8004

#服务别名----注册zookeeper到注册中心名称
spring:
  application:
    name: cloud-payment-service
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description 支付服务提供者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 * @deprecated @EnableDiscoveryClient 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
 */
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudProviderPayment8004 {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudProviderPayment8004.class, args);
    }
}

5、Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/zk")
    public String paymentzk() {
        return "springcloud with zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

6、启动8004注册进zookeeper

浏览器验证:

  http://localhost:8004/payment/zk

ZK客户端验证:

  启动zkCli.cmd

[zk: localhost:2181(CONNECTED) 0] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /services/cloud-payment-service
[f4712ac6-f48d-471a-a520-93f85911e9bb]
[zk: localhost:2181(CONNECTED) 3] get /services/cloud-payment-service/f4712ac6-f48d-471a-a520-93f85911e9bb
{"name":"cloud-payment-service","id":"f4712ac6-f48d-471a-a520-93f85911e9bb","address":"zhixi","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-payment-service","metadata":{}},"registrationTimeUTC":1681964767710,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}

json格式化后:  

{
    "name": "cloud-payment-service",
    "id": "f4712ac6-f48d-471a-a520-93f85911e9bb",
    "address": "zhixi",
    "port": 8004,
    "sslPort": null,
    "payload": {
        "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
        "id": "application-1",
        "name": "cloud-payment-service",
        "metadata": {}
    },
    "registrationTimeUTC": 1681964767710,
    "serviceType": "DYNAMIC",
    "uriSpec": {
        "parts": [
            {
                "value": "scheme",
                "variable": true
            },
            {
                "value": "://",
                "variable": false
            },
            {
                "value": "address",
                "variable": true
            },
            {
                "value": ":",
                "variable": false
            },
            {
                "value": "port",
                "variable": true
            }
        ]
    }
}

3.2.3、订单服务注册进zookeeper

1、建模块:cloud-consumer-order-zookeeper-80

2、加pom

<dependencies>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--公共组件-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        <!--先排除自带的zookeeper3.5.3 防止与3.4.9起冲突-->
        <exclusions>
            <exclusion>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--添加zookeeper3.4.9版本-->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.9</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、写yml 

server:
  port: 80

#服务别名----注册zookeeper到注册中心名称
spring:
  application:
    name: cloud-consumer-order
  cloud:
    zookeeper:
      connect-string: 127.0.0.1:2181
      payment:
        service:
          #服务提供者,注册zookeeper到注册中心名称
          name: cloud-payment-service

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName CloudConsumerOrderZookeeper80
 * @Author zhangzhixi
 * @Description
 * @Date 2023-04-06 17:45
 * @Version 1.0
 */
@SpringBootApplication
@EnableDiscoveryClient
public class CloudConsumerOrderZookeeper80 {
    public static void main(String[] args) {
        SpringApplication.run(CloudConsumerOrderZookeeper80.class, args);
    }
}

5、config:com.zhixi.config.CloudConsumerOrderConfig

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @ClassName CloudConsumerOrderConfig
 * @Author zhangzhixi
 * @Description 消费者配置类
 * @Date 2023-04-06 17:46
 * @Version 1.0
 */
@Configuration
public class CloudConsumerOrderConfig {
    @Bean
    @LoadBalanced // 负载均衡
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

6、Controller:com.zhixi.controller.ConsumerController  

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
 * @ClassName ConsumerController
 * @Author zhangzhixi
 * @Description
 * @Date 2023-04-06 17:47
 * @Version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/consumer")
public class ConsumerController {

    @Value("${spring.cloud.zookeeper.payment.service.name}")
    private String paymentServiceName;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/payment/zk")
    public String paymentInfo() {
        return restTemplate.getForObject("http://" + paymentServiceName + "/payment/zk", String.class);
    }
}

7、验证测试  

运行ZooKeeper服务端、cloud-provider-payment-zookeeper-8004、cloud-consumer-order-zookeeper-80

打开ZooKeeper客户端:

[zk: localhost:2181(CONNECTED) 0] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /services
[cloud-consumer-order, cloud-payment-service]

8、访问测试

http://localhost/consumer/payment/zk

四、服务调用

4.1、Ribbon

4.1.1、Ribbon介绍

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。

简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

LB负载均衡(Load Balance)是什么

  简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高可用)。

常见的负载均衡有软件Nginx,LVS,硬件F5等。

Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别

  Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

集中式LB和进程内LB

集中式:

  即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

进程内:

  将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。

Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。


 

一句话:负载均衡 + RestTemplate调用

4.1.2、Ribbon的负载均衡和Rest调用

1、结构说明

Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。

Ribbon在工作时分成两步:

  • 第一步先选择EurekaServer ,它优先选择在同一个区域内负载较少的server。

  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。

其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。


 

在Eureka中集成了Ribbon,可以不引入Ribbon即可使用

2、RestTemplate的使用

  • getForObject() / getForEntity() - GET请求方法
  • getForObject():返回对象为响应体中数据转化成的对象,基本上可以理解为Json。
  • getForEntity():返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id)
{
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

    if(entity.getStatusCode().is2xxSuccessful()){
        return entity.getBody();//getForObject()
    }else{
        return new CommonResult<>(444,"操作失败");
    }
}

4.1.3、Ribbon默认自带的负载规则

lRule:根据特定算法中从服务列表中选取一个要访问的服务

  • RoundRobinRule 轮询
  • RandomRule 随机
  • RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重
  • WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器

4.1.4、Ribbon负载规则替换

对消费者:cloud-consumer-order-80,进行修改

1、添加Ribbon配置类:com.zhixi.config.MyRibbonConfig

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName MyRibbonConfig
 * @Author zhangzhixi
 * @Description 自定义Ribbon负载均衡策略
 * @Date 2023-04-06 22:03
 * @Version 1.0
 */
@Configuration
public class MyRibbonConfig {

    /**
     * 自定义负载均衡策略
     *
     * @return IRule
     */
    @Bean
    public IRule myRule() {
        return new RandomRule();
    }
}

2、主启动类添加注解:@RibbonClient

import com.zhixi.config.MyRibbonConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

/**
 * @ClassName OrderMain80
 * @Author zhangzhixi
 * @Description 订单服务消费者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
/**
 * name: 指定要使用的Ribbon负载均衡策略的服务名称 configuration: 指定自定义的Ribbon负载均衡策略类
 */
@RibbonClient(name = "cloud-payment-service", configuration = MyRibbonConfig.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class, args);
    }
}

3、测试

开启:cloud-eureka-server7001,cloud-provider-payment8001,cloud-provider-payment8002,cloud-consumer-order80

浏览器-输入:http://localhost/consumer/payment/get/31

返回结果中的serverPort在8001与8002两种间反复横跳(随机)

4.2、OpenFeign

4.2.1、OpenFeign介绍

Feign是一个声明式WebService客户端。
使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。
Feign也支持可拔插式的编码器和解码器。
Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。
Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

Feign能干什么?  

Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。
但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。
所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。
在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量

Feign集成Ribbon  

利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

Feign和OpenFeign区别

Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。

Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。

OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

4.2.2、OpenFeign服务调用  

接口+注解:微服务调用接口 + @FeignClient  

1、新建模块:cloud-consumer-feign-order-80  

2、pom

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml  

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @ClassName CloudConsumerFeignOrder80
 * @Author zhangzhixi
 * @Description 开启Feign的功能
 * @Date 2023-04-10 12:51
 * @Version 1.0
 */
@EnableFeignClients
@SpringBootApplication
public class CloudConsumerFeignOrder80 {
    public static void main(String[] args) {
        SpringApplication.run(CloudConsumerFeignOrder80.class, args);
    }
}

5、业务类:com.zhixi.service.PaymentClient

业务逻辑接口+@FeignClient配置调用cloud-provider-payment-8001服务

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;


/**
 * @author zhixi
 * OpenFeign的客户端接口,用于调用服务提供者的接口
 */
@FeignClient(value = "cloud-payment-service")
public interface PaymentClient {

    @GetMapping(value = "/payment/get/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @PostMapping(value = "/payment/create")
    CommonResult create(@RequestBody Payment payment);

    /**
     * 测试OpenFeign的超时控制
     *
     * @return 服务端的返回值
     */
    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout();
}

6、Controller:com.zhixi.controller.FeignController

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import com.zhixi.service.PaymentClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @ClassName FeignController
 * @Author zhangzhixi
 * @Description feign的controller层
 * @Date 2023-04-10 12:57
 * @Version 1.0
 */
@RestController
@RequestMapping("/feign")
@Slf4j
public class FeignController {

    @Resource
    private PaymentClient paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }

    @PostMapping("/consumer/payment/create")
    public CommonResult<Payment> create(@RequestBody Payment payment) {
        return paymentFeignService.create(payment);
    }
    
}

7、测试

先启动2个eureka集群7001/7002

再启动2个微服务8001/8002

启动cloud-consumer-feign-order-80,访问:http://localhost/feign/consumer/payment/get/31

可以看到访问的业务是对8001、8002轮询的。

4.2.3、OpenFeign超时控制

1、服务提供方8001、8002写暂停程序

/**
 * 测试OpenFeign超时控制
 *
 * @return 服务端口
 */
@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeout() {
    // 业务逻辑处理正确,但是需要耗费3秒钟
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return serverPort;
}

2、服务消费方cloud-consumer-feign-order-80添加超时方法进PaymentClient 

/**
 * 测试OpenFeign的超时控制
 *
 * @return 服务端的返回值
 */
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();

3、服务消费方cloud-consumer-feign-order-80:Controller  

/**
 * 测试OpenFeign的超时控制:OpenFeign客户端一般默认等待1秒钟*
 *
 * @return 返回数据
 */
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout() {
    // OpenFeign客户端一般默认等待1秒钟
    return paymentFeignService.paymentFeignTimeout();
}

4、设置feign客户端超时时间  

#设置feign客户端超时时间(OpenFeign默认支持ribbon)(单位:毫秒)
feign:
  client:
    config:
      default:
        connectTimeout: 2000
        readTimeout: 2000
          #feign日志级别
          # NONE:默认的,不显示任何日志;
        # BASIC:仅记录请求方法、URL、响应状态码及执行时间;
        # HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
        # FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
        loggerLevel: full
logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.zhixi.*: debug

5、测试

访问地址:http://localhost/feign/consumer/payment/feign/timeout

我们调用的接口是sleep3秒钟,在yml中设置了如何超过2s没有返回,就报错,如下:

 五、服务降级

分布式系统面临的问题

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务雪崩

  多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.

  对于高流量的应用来说,单一的后避依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

  所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

5.1、Hystrix

  Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack)。

而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

5.1.1、Hystrix概述

能做什么?

  • 服务降级
  • 服务熔断
  • 接近实对的监控

官网资料:https://github.com/Netflix/Hystrix/wiki/How-To-Use

Hystrix停更说明https://github.com/Netflix/Hystrix

  • 被动修bugs
  • 不再接受合并请求
  • 不再发布新版本

服务降级:

  服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback

哪些情况会发生服务降级?

  • 程序运行导常
  • 超时
  • 服务熔断触发服务降级
  • 线程池/信号量打满也会导致服务降级

服务熔断

  类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。  

服务的降级 -> 进而熔断 -> 恢复调用链路

服务限流

  秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

5.1.2、Hystrix支付微服务构建

1、新建模块:cloud-provider-hygtrix-payment-8001

2、pom

<dependencies>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment
# Eureka客户端配置
eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
  # 服务端的服务发现页面和健康检查地址
  instance:
    instance-id: provider-hystrix-payment-8001
    # 显示IP地址
    prefer-ip-address: true

4、主启动

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description Hytrix服务提供者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
// 开启Eureka客户端
@EnableEurekaClient
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

}

5、业务类

/**
 * @ClassName PaymentHystrixService
 * @Author zhangzhixi
 * @Description 服务提供者接口
 * @Date 2023-04-11 18:09
 * @Version 1.0
 */
public interface PaymentHystrixService {


    /**
     * 正常访问
     *
     * @param id id
     * @return result
     */
    String paymentInfoOk(Integer id);


    /**
     * 超时访问
     *
     * @param id id
     * @return result
     */
    String paymentInfoTimeOut(Integer id);
}

 

import com.zhixi.service.PaymentHystrixService;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @ClassName PaymentHystrixServiceImpl
 * @Author zhangzhixi
 * @Description 服务提供者实现类
 * @Date 2023-04-11 18:10
 * @Version 1.0
 */
@Service
public class PaymentHystrixServiceImpl implements PaymentHystrixService {

    @Override
    public String paymentInfoOk(Integer id) {
        return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_OK,id:  " + id + "\t" + "O(∩_∩)O哈哈~";
    }

    @Override
    public String paymentInfoTimeOut(Integer id) {

        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:  " + Thread.currentThread().getName() + " id:  " + id + "\t" + "O(∩_∩)O哈哈~" + "  耗时(秒): 3";
    }

}

6、Controller  

import com.zhixi.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @ClassName PaymentController
 * @Author zhangzhixi
 * @Description 支付服务提供者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@RequestMapping("/payment")
@RestController
@Slf4j
public class PaymentController {

    @Resource
    private PaymentHystrixService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfoOk(id);
        log.info("*****result: " + result);
        return result;
    }

    @GetMapping("/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfoTimeOut(id);
        log.info("*****result: " + result);
        return result;
    }
}

7、测试  

启动eureka7001、启动cloud-provider-hystrix-payment-8001

访问

  success的方法 - http://localhost:8001/payment/hystrix/ok/1
  每次调用耗费5秒钟 - http://localhost:8001/payment/hystrix/timeout/1

上述module均OK

  以上述为根基平台,从正确 -> 错误 -> 降级熔断 -> 恢复。

5.1.3、对Hystrix支付微服务构建进行Jmeter压测

1、测试计划右键-->添加-->线程(用户)-->线程组

2、设置线程数200、循环次数200

3、线程组右键-->添加-->取样器-->HTTP请求

压测结论:

  上面还是服务提供者8001自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖慢。

5.1.4、微服务消费者

1、新建模块:cloud-consumer-feign-hystrix-order-80

2、pom

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 80
  servlet:
    application-display-name: cloud-consumer-feign-hystrix-order-80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @ClassName OrderHystrixMain80
 * @Author zhangzhixi
 * @Description 开启Feign的功能
 * @Date 2023-04-10 12:51
 * @Version 1.0
 */
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}

5、业务类:通过Feign调用微服务接口

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @ClassName PaymentHystrixService
 * @Author zhangzhixi
 * @Description 服务提供者的接口
 * @Date 2023-04-11 18:38
 * @Version 1.0
 */
@FeignClient(
        // 服务提供者的名称
        value = "cloud-provider-hystrix-payment"
)
public interface PaymentHystrixService {
    /**
     * 正常访问
     *
     * @param id id
     * @return result
     */
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfoOk(@PathVariable("id") Integer id);

    /**
     * 超时访问
     *
     * @param id id
     * @return result
     */
    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfoTimeOut(@PathVariable("id") Integer id);
}

6、Controller

import com.zhixi.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;

/**
 * @ClassName OrderHystrixController
 * @Author zhangzhixi
 * @Description 对外暴露的接口,调用服务提供者的接口
 * @Date 2023-04-11 18:39
 * @Version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfoOk(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfoTimeOut(id);
    }
}

7、测试  

正常测试:

  http://localhost/consumer/payment/hystrix/ok/1

并发测试:

  2W个线程压8001

  消费端80微服务再去访问正常的Ok微服务8001地址

  消费者80被拖慢

原因:8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕。

正因为有上述故障或不佳表现才有我们的降级/容错/限流等技术诞生。

5.1.5、需要解决的问题

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级。
  • 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级。
  • 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级。

5.1.6、Hystrix之服务降级支付侧fallback

降级配置 - @HystrixCommand

cloud-provider-hygtrix-payment-8001,先从自身找问题

设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处埋,作服务降级fallback。

1、回滚流程业务代码:com.zhixi.service.impl.PaymentHystrixServiceImpl

业务类启用 - @HystrixCommand报异常后如何处理

—旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法。

这里我们将超时时间由之前的3S,设置为5S

/**
 * @param id id 服务提供者的id
 * @return java.lang.String
 * @Description 服务降级
 */
@HystrixCommand(
        // 服务降级的方法
        fallbackMethod = "paymentInfoTimeOutBak",
        commandProperties = {
                // 设置超时时间为3秒,超过3秒就会调用paymentInfoTimeOutBak方法
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
        })
@Override
public String paymentInfoTimeOut(Integer id) {
    try {
        TimeUnit.MILLISECONDS.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "线程池:  " + Thread.currentThread().getName() + " id:  " + id + "\t" + "O(∩_∩)O哈哈~" + "  耗时(秒): 3";
}
/**
 * @param id id 服务提供者的id
 * @return java.lang.String
 * @Description 服务降级的善后方法
 */
@Override
public String paymentInfoTimeOutBak(Integer id) {
    return "线程池:  " + Thread.currentThread().getName() + "  8001系统繁忙或者运行报错,请稍后再试,id:  " + id + "\t" + "o(╥﹏╥)o";
}

2、主启动添加注解

// 开启服务降级
@EnableCircuitBreaker

3、测试

5.1.7、Hystrix之服务降级订单消费者侧fallback

cloud-consumer-feign-hystrix-order-80,订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护

服务提供者端编(cloud-provider-hygtrix-payment-8001)写Controller

/**
 * 测试消费者端服务降级
 * @param id id
 * @return 返回值
 */
@GetMapping("/hystrix/timeout/fallback/{id}")
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "我是服务提供者8001~~~";
}

消费者端:yml

server:
  port: 80
  servlet:
    application-display-name: cloud-consumer-feign-hystrix-order-80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

# 开启feign的hystrix支持
feign:
  hystrix:
    enabled: true
  # 配置feign超时时间,否则服务降级不生效
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full

消费者端:主启动 

/**
 * @ClassName OrderHystrixMain80
 * @Author zhangzhixi
 * @Description 开启Feign的功能
 * @Date 2023-04-10 12:51
 * @Version 1.0
 */
@SpringBootApplication
@EnableFeignClients
@EnableHystrix//添加到此处
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}

消费者端,用Feign调用服务提供者的接口:com.zhixi.service.PaymentHystrixService

@GetMapping("/hystrix/timeout/fallback/{id}")
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id);

消费者端:Controller

@GetMapping("/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
        commandProperties = {
                // 设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
        })
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
    return paymentHystrixService.paymentInfoTimeOut(id);
}
/**
 * 善后方法,要求:方法的返回值和参数要和原方法一致
 *
 * @param id id
 * @return String
 */
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}

测试  

启动:cloud-eureka-server-7001、cloud-provider-hygtrix-payment-8001、cloud-consumer-feign-hystrix-order-80

访问:http://localhost/consumer/payment/hystrix/timeout/1

5.1.8、Hystrix之全局服务降级DefaultProperties

目前问题1:

  每个业务方法对应一个兜底的方法,代码膨胀

解决方法

  1. 每个方法配置一个服务降级方法,技术上可以,但是不聪明
  2. 除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”)统一跳转到统一处理结果页面

通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量

消费者端(cloud-consumer-feign-hystrix-order-80)Controller

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.zhixi.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;

/**
 * @ClassName OrderHystrixController
 * @Author zhangzhixi
 * @Description 对外暴露的接口,调用服务提供者的接口
 * @Date 2023-04-11 18:39
 * @Version 1.0
 */
@RestController
@Slf4j
@RequestMapping("/consumer")
// 设置全局的服务降级fallback方法
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfoOk(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    /*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
            commandProperties = {
                    // 设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
            })*/
    //用全局的fallback方法
    @HystrixCommand
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfoTimeOut(id);
    }

    /**
     * 善后方法,要求:方法的返回值和参数要和原方法一致
     *
     * @param id id
     * @return String
     */
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

    // 下面是全局fallback方法
    public String paymentGlobalFallbackMethod() {
        return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
    }
}

测试  

启动:cloud-eureka-server-7001、cloud-provider-hygtrix-payment-8001、cloud-consumer-feign-hystrix-order-80

访问:http://localhost/consumer/payment/hystrix/timeout/1

  消费者80调用接口去访问8001,8001的service层接口paymentInfoTimeOut做了限制,超过3s没有结束,就调用善后方法:善后方法,

但是因为服务调用者这边使用了全局的降级方法,所以最后输出的是服务消费者80的全局降级方法:paymentGlobalFallbackMethod

调用Controller接口,实际上调用的是

5.1.9、Hystrix之通配服务降级FeignFallback

目前问题2:统一和自定义的分开,代码混乱

服务降级:客户端去调用服务端,碰上服务端宕机或关闭

  本次案例服务降级处理是在客户端cloud-consumer-feign-hystrix-order-80实现完成的,与服务端8001没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

未来我们要面对的异常:

  • 运行
  • 超时
  • 宕机

1、添加PaymentHystrixService接口实现类:com.zhixi.service.impl.PaymentHystrixServiceImpl

import com.zhixi.service.PaymentHystrixService;
import org.springframework.stereotype.Service;

/**
 * @ClassName PaymentHystrixServiceImpl
 * @Author zhangzhixi
 * @Description
 * @Date 2023-04-11 22:06
 * @Version 1.0
 */
@Service
public class PaymentHystrixServiceImpl implements PaymentHystrixService {
    @Override
    public String paymentInfoOk(Integer id) {
        return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
    }

    @Override
    public String paymentInfoTimeOut(Integer id) {
        return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
    }
}

2、修改PaymentHystrixService接口,添加服务降级代码

import com.zhixi.service.impl.PaymentHystrixServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @ClassName PaymentHystrixService
 * @Author zhangzhixi
 * @Description 服务提供者的接口
 * @Date 2023-04-11 18:38
 * @Version 1.0
 */
@FeignClient(
        // 服务提供者的名称
        value = "cloud-provider-hystrix-payment",
        // 服务降级的类,比如服务提供者挂了,就会调用这个类的方法
        fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {
    /**
     * 正常访问
     *
     * @param id id
     * @return result
     */
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfoOk(@PathVariable("id") Integer id);

    /**
     * 超时访问
     *
     * @param id id
     * @return result
     */
    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfoTimeOut(@PathVariable("id") Integer id);
}

3、测试  

只启动cloud-eureka-server-7001和cloud-consumer-feign-hystrix-order-80,不启动8001,模拟服务提供者宕机情况

访问:http://localhost/consumer/payment/hystrix/ok/1

我们做的服务降级成功~

5.1.10、Hystrix之服务熔断

  熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

当检测到该节点微服务调用响应正常后,恢复调用链路。

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。

 1、服务熔断service层:com.zhixi.service.impl.PaymentHystrixServiceImpl

cloud-provider-hygtrix-payment-8001

//=====服务熔断=======
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback",
        commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
        })
@Override
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    if (id < 0) {
        throw new RuntimeException("你好,id不能为负数");
    }
    String serialNumber = IdUtil.simpleUUID();
    return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
/**
 * 服务熔断的善后方法
 *
 * @param id id
 * @return java.lang.String
 */
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
    return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " + id;
}

2、Controller层

 //====服务熔断
 @GetMapping("/hystrix/circuit/{id}")
 public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
     String result = paymentService.paymentCircuitBreaker(id);
     log.info("****result: " + result);
     return result;
 }

3、测试

正例:http://localhost:8001/payment/hystrix/circuit/1

反例:http://localhost:8001/payment/hystrix/circuit/-1  

先访问正例,发现可以正常访问,然后访问反例,会调用服务熔断方法:paymentCircuitBreakerFallback,

一直访问反例,然后再访问正例,会发现就算是正例,返回的也还是服务熔断方法:paymentCircuitBreakerFallback


 

因为服务调用失败次数达到60%,按照上面的配置,比如十次调用,如果超过了6次调用失败,就会触发服务熔断,除非过了时间窗口期(10S),这时候访问正例,接口才会正常~

4、服务熔断的所有配置

@HystrixCommand(fallbackMethod = "fallbackMethod", 
                groupKey = "strGroupCommand", 
                commandKey = "strCommand", 
                threadPoolKey = "strThreadPool",
                
                commandProperties = {
                    // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
                    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                    // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 配置命令执行的超时时间
                    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
                    // 是否启用超时时间
                    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                    // 执行超时的时候是否中断
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
                    
                    // 执行被取消的时候是否中断
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
                    // 允许回调方法执行的最大并发数
                    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 服务降级是否启用,是否执行回调函数
                    @HystrixProperty(name = "fallback.enabled", value = "true"),
                    // 是否启用断路器
                    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                    // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                    
                    // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。
                    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
                    // 断路器强制打开
                    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
                    // 断路器强制关闭
                    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
                    // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
                    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
                    
                    // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
                    // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
                    // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
                    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
                    // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
                    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
                    // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
                    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
                    // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
                    // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
                    // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
                    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
                    
                    // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
                    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
                    // 是否开启请求缓存
                    @HystrixProperty(name = "requestCache.enabled", value = "true"),
                    // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
                    @HystrixProperty(name = "requestLog.enabled", value = "true"),

                },
                threadPoolProperties = {
                    // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
                    @HystrixProperty(name = "coreSize", value = "10"),
                    // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,否则将使用 LinkedBlockingQueue 实现的队列。
                    @HystrixProperty(name = "maxQueueSize", value = "-1"),
                    // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
                    // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
                }
               )
public String doSomething() {
	...
}

5.1.11、Hystrix之仪表盘Dashboard

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。

Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

1、新建模块:cloud-consumer-hystrix-dashboard-9001

2、pom

<dependencies>
    <!--hytrix 监控平台-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml 

server:
  port: 9001

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

/**
 * @ClassName HystrixDashboardMain9001
 * @Author zhangzhixi
 * @Description Hystrix仪表盘
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardMain9001.class, args);
    }
}

5、将8001添加到监控中:修改cloud-provider-hygtrix-payment-8001主启动

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

/**
 * @ClassName SpringCloudProviderPayment
 * @Author zhangzhixi
 * @Description Hytrix服务提供者
 * @Date 2023-04-03 10:39
 * @Version 1.0
 */
@SpringBootApplication
// 开启Eureka客户端
@EnableEurekaClient
// 开启服务降级
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

    /**
     * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
     * 只要在自己的项目里配置上下面的servlet就可以了
     * 否则,Unable to connect to Command Metric Stream 404
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

6、访问地址

启动服务,访问:http://localhost:9001/hystrix/  

填写监控地址:http://localhost:8001/hystrix.stream

访问接口,查看监控面板变化:

http://localhost:8001/payment/hystrix/circuit/1

http://localhost:8001/payment/hystrix/circuit/-1

图例解释:

 六、服务网关

6.1、Getway

6.1.1、Getway概述

Getway是什么

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;

但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloud Gateway—句话:gateway是原zuul1.x版的替代

  Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和Project Reactor等技术。

Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。

SpringCloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0+Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供—种简单有效的统一的API路由管理方式。

SpringCloud Gateway作为Spring Cloud 生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。

而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流

作用

  • 方向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控

微服务架构中网关的位置

GateWay非阻塞异步模型

  有Zuull了怎么又出来Gateway?我们为什么选择Gateway?

netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。

  1. 方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
  2. Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
  3. 多方面综合考虑Gateway是很理想的网关选择。

SpringCloud Gateway具有如下特性:

  1. 基于Spring Framework 5,Project Reactor和Spring Boot 2.0进行构建;
  2. 动态路由:能够匹配任何请求属性;
  3. 可以对路由指定Predicate (断言)和Filter(过滤器);
  4. 集成Hystrix的断路器功能;
  5. 集成Spring Cloud 服务发现功能;
  6. 易于编写的Predicate (断言)和Filter (过滤器);
  7. 请求限流功能;
  8. 支持路径重写。

SpringCloud Gateway与Zuul的区别

  1. 在SpringCloud Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul。
  2. Zuul 1.x,是一个基于阻塞I/O的API Gateway。
  3. Zuul 1.x基于Servlet 2.5使用阻塞架构它不支持任何长连接(如WebSocket)Zuul的设计模式和Nginx较像,每次I/О操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。
  4. Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul .x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
  5. Spring Cloud Gateway建立在Spring Framework 5、Project Reactor和Spring Boot2之上,使用非阻塞API。
  6. Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验

 

Gateway模型

WebFlux是什么?https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#spring-webflux

传统的Web框架,比如说: Struts2,SpringMVC等都是基于Servlet APl与Servlet容器基础之上运行的。

但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。

相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring 5必须让你使用Java 8)。

Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

Getway工作流程:路由、断言、过滤

  • Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
  • Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
  • Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

Gayway工作流程

官网说明:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-how-it-works

客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。

Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post")执行业务逻辑。

Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑:路由转发 + 执行过滤器链。

6.1.2、Gayway模块搭建

1、新建模块:cloud-gateway-gateway-9527

2、pom

<dependencies>
    <!--gateway-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--eureka-client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</ar
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    loadbalancer:
      ribbon:
        #关闭ribbon的负载均衡,因为gateway已经自带了负载均衡
        enabled: false
eureka:
  instance:
    hostname: cloud-gateway-service
    instance-id: cloud-gateway-service-9527
    # 显示IP地址
    prefer-ip-address: true
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @ClassName GateWayMain9527
 * @Author zhangzhixi
 * @Description 网关启动类
 * @Date 2023-04-13 9:49
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class, args);
    }
}

6.1.3、Gayway配置路由(yml方式)

yml配置

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    loadbalancer:
      ribbon:
        #关闭ribbon的负载均衡,因为gateway已经自带了负载均衡
        enabled: false
    #############################新增网关配置###########################
    gateway:
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由

        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################
eureka:
  instance:
    hostname: cloud-gateway-service
    instance-id: cloud-gateway-service-9527
    # 显示IP地址
    prefer-ip-address: true
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka

2、添加服务提供者web接口:cloud-provider-payment-8001

@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/lb")
public String getPaymentLB() {
    return serverPort;
}

3、测试

启动:cloud-eureka-server-7001、cloud-provider-payment-8001、cloud-gateway-gateway-9527

  • 访问说明

    • 添加网关前 - http://localhost:8001/payment/get/31
    • 添加网关后 - http://localhost:9527/payment/get/31
    • 两者访问成功,返回相同结果

6.1.4、Gayway配置路由(代码方式)

 代码中注入RouteLocator的Bean:com.zhixi.config.GateWayConfig

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * @ClassName GateWayConfig
 * @Author zhangzhixi
 * @Description gateway配置路由
 * @Date 2023-04-13 17:28
 * @Version 1.0
 */
@Configuration
public class GateWayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        // 创建路由
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        // 创建一个新的 Route,其中:参数1是路由的唯一标识,参数2是目标uri
        routes.route("path_route_atguigu",
                r -> r.path("/孙笑川")
                        .uri("https://tieba.baidu.com//f?kw=")).build();

        return routes.build();
    }
}

测试

  浏览器输入http://localhost:9527/孙笑川,返回孙笑川吧相同的页面~

6.1.5、GateWay配置动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能(不写死一个地址)。  

cloud-gateway-gateway-9527修改yml配置

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    loadbalancer:
      ribbon:
        #关闭ribbon的负载均衡,因为gateway已经自带了负载均衡
        enabled: false
    #############################新增网关配置###########################
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由

        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################
eureka:
  instance:
    hostname: cloud-gateway-service
    instance-id: cloud-gateway-service-9527
    # 显示IP地址
    prefer-ip-address: true
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka

测试

启动:cloud-eureka-server-7001、cloud-provider-payment-8001、cloud-provider-payment-8002、cloud-gateway-gateway-9527

浏览器输入 - http://localhost:9527/payment/lb

结果:

  不停刷新页面,8001/8002两个端口切换。

6.1.6、GateWay常用的Predicate(断言)

1、Route Predicate Factories这个是什么

  Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。

Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配,多个RoutePredicate工厂可以进行组合。

Spring Cloud Gateway创建Route 对象时,使用RoutePredicateFactory 创建 Predicate对象,Predicate 对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。

所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。

常用的Route Predicate Factory

  • After Route Predicate Factory(之后路由谓词工厂)
  • Before Route Predicate Factory(之前路由谓词工厂)
  • Between Route Predicate Factory(之间路由谓词工厂)
  • Cookie Route Predicate Factory(Cookie 路由谓词工厂)
  • Header Route Predicate Factory(标头路由谓词工厂)
  • Host Route Predicate Factory(主机路由谓词工厂)
  • Method Route Predicate Factory(方法路由谓词工厂)
  • Path Route Predicate Factory(路径路由谓词工厂)
  • Query Route Predicate Factory(查询路由谓词工厂)
  • RemoteAddr Route Predicate Factory(远程地址路由谓词工厂)
  • Weight Route Predicate Factory(权重路由谓词工厂)

下面演示几个简单例子:

The After Route Predicate Factory(根据请求时间戳来过滤请求的路由)

    #############################新增网关配置###########################
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
            # 这个时间后才能起效
            - After=2023-01-20T17:42:47.789-07:00[America/Denver]
        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################

 

可以通过下述方法获得上述格式的时间戳字符串:

public static void main(String[] args){
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj);
}

The Between Route Predicate Factory(根据请求时间戳在指定时间段内来过滤请求的路由)  

    #############################新增网关配置###########################
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
            # 在两个时间之间才能访问
            - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################

The Cookie Route Predicate Factory(根据请求中的cookie值来过滤请求的路由)  

例如,可以创建一个路由规则,使得只有包含名为"auth_token"的cookie且其值以"Bearer "开头的请求才能匹配该规则。

    #############################新增网关配置###########################
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
            # 包含名为"auth_token"的cookie且其值以"Bearer "开头的请求才能匹配该规则。
            - Cookie=auth_token, Bearer.*
        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################

普通Get命令(503):curl http://localhost:9527/payment/lb

添加了Cokkie的Get命令:curl http://localhost:9527/payment/lb --cookie "auth_token=BearerZhangzhixi"

C:\Users\A1820>curl http://localhost:9527/payment/lb
{"timestamp":"2023-04-24T01:22:22.338+0000","path":"/payment/lb","status":503,"error":"Service Unavailable","message":"Unable to find instance for cloud-payment-service","requestId":"9cdac0a7"}
C:\Users\A1820>curl http://localhost:9527/payment/lb --cookie "auth_token=BearerZhangzhixi"
8001
C:\Users\A1820>curl http://localhost:9527/payment/lb --cookie "auth_token=BearerZhangzhixi"

The Header Route Predicate Factory(请求中的标头值来过滤请求的路由)  

头部路由谓词工厂需要两个参数,一个是标头名称,另一个是一个正则表达式。

该谓词用于匹配具有给定名称的标头,并且该标头的值与正则表达式匹配。

    #############################新增网关配置###########################
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
            # 断言,请求头,相匹配的进行路由(正则)
            - Header=X-Request-Id, \d+
        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################

测试:

# 带指定请求头的参数的CURL命令
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"

6.1.7、GateWay的Filter

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。

Spring Cloud Gateway的Filter:

生命周期:

  • pre
  • post

种类(具体看官方文档):

  • GatewayFilter - 有31种
  • GlobalFilter - 有10种

常用的GatewayFilter:AddRequestParameter GatewayFilter

自定义全局GlobalFilter:

两个主要接口介绍:

  1. GlobalFilter
  2. Ordered

能干什么:

  1. 全局日志记录
  2. 统一网关鉴权

代码示例

  cloud-gateway-gateway-9527添加UserIdVerifyGateWayFilter类进行过滤

import lombok.extern.slf4j.Slf4j;
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 zhangzhixi
 * @version 1.0
 * @description gateway过滤器,验证userId是否为空或者小于0
 * @date 2023-04-13 17:29
 */
@Component
@Slf4j
public class UserIdVerifyGateWayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String userId = exchange.getRequest().getQueryParams().getFirst("userId");
        // 如果userId为空,或者小于0,就拦截
        if (userId == null || userId.isEmpty() || Integer.parseInt(userId) < 0) {
            log.info("userId is empty or less than 0");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

启动:

  • EurekaMain7001
  • PaymentMain8001
  • GateWayMain9527
  • PaymentMain8002

浏览器输入:

  • 反例:http://localhost:9527/payment/lb
  • 正例:http://localhost:9527/payment/lb?userId=1

 七、服务配置

7.1、SpringConfig分布式配置中心

7.1.1、分布式系统面临的配置问题

  微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理.……

 SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

怎么玩

  1. SpringCloud Config分为服务端和客户端两部分。
  2. 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
  3. 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

能干嘛

  1. 集中管理配置文件
  2. 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
  3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
  4. 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  5. 将配置信息以REST接口的形式暴露 - post/crul访问刷新即可…

与Gitee整合配置

  由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式。

官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/

7.1.2、Config配置总控中心搭建

1、在Gitee新建仓库

2、增加三个配置文件,分别是:开发环境(dev)、测试环境(test)、线上/生产环境(prod)

config-dev.yml:
config:
  info: "master branch,springcloud-config/config-dev.yml version=1"

 config-test:

config:
  info: "master branch,springcloud-config/config-test.yml version=1" 

config-prod:

config:
  info: "master branch,springcloud-config/config-prod.yml version=1"

1、新建Cloud的配置中心模块:cloud-config-center-3344 

2、pom

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactI
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--引入actuator监控-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 3344

spring:
  application:
    name: cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          # Git仓库地址
          uri: https://gitee.com/zhang-zhixi/spring-cloud-config.git
          # 配置仓库中的哪个目录下的配置文件被读取
          search-paths:
            - SpringCloud-Config
          # 配置Gitee的用户名和密码
          username: xxx
          password: xxx
      ####读取分支
      label: master
#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
# 日志
logging:
  level:
    com.zhixi.*: debug

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 配置中心
 * @date 2023-04-14 09:10
 */
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain3344.class, args);
    }
}

 5、测试

  测试通过Config微服务是否可以从GitHub上获取配置内容

  • 启动cloud-eureka-server-7001
  • 启动ConfigCenterMain3344

  • 浏览器访问 - http://localhost:3344/master/config-dev.yml

  • 页面返回结果:

6、配置读取规则

  • 官方文档

  • /{label}/{application}-{profile}.yml(推荐)

  • label:分支(branch)
  • name:服务名
  • profiles:环境(dev/test/prod)

7.1.3、Config客户端配置与测试

1、新建模块:cloud-config-client-3355

2、pom

<dependencies>
    <!--SpringCloudConfig客户端-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <!--Eureka客户端-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--引入actuator监控-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、bootstrap.yml

applicaiton.yml:是用户级的资源配置项

bootstrap.yml:是系统级的,优先级更加高

Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的父上下文。

初始化的时候,BootstrapContext负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context配置的分离。

要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 #配置中心地址
      
#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
  # 服务端的服务发现页面和健康检查地址
  instance:
    # 显示IP地址
    prefer-ip-address: true
# 日志
logging:
  level:
    com.zhixi.*: debug

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 配置中心客户端
 * @date 2023-04-14 09:10
 */
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}

5、业务类: com.zhixi.controller.ConfigClientController

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author zhangzhixi
 * @version 1.0
 * @description web接口请求
 * @date 2023-04-14 09:10
 */
@RestController
public class ConfigClientController {

    @Value("${server.port}")
    private String serverPort;

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String configInfo() {
        return "serverPort: " + serverPort + "\t\n\n configInfo: " + configInfo;
    }
}

6、测试  

修改Gitee中config-dev.yml中版本号

 

  • 1、启动Config配置中心3344微服务并自测
    • http://localhost:3344/master/config-dev.yml  
  • 2、启动3355作为Client准备访问
    • http://localhost:3355/configInfo

成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息可题随时而来

7.1.4、分布式配置的动态刷新问题

  • Linux运维修改Gitee上的配置文件内容做调整
  • 刷新3344,发现ConfigServer配置中心立刻响应
  • 刷新3355,发现ConfigClient客户端没有任何响应
  • 3355没有变化除非自己重启或者重新加载
  • 难到每次运维修改配置文件,客户端都需要重启??噩梦

7.1.5、Config动态刷新之手动版

修改cloud-config-client-3355模块

1、修改yml,暴露接口断点配置

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

2、业务类Controller修改

添加注解:@RefreshScope

3、测试

1、启动服务:cloud-config-client-3355

2、修改Gitee配置文件版本号

3、访问:

    http://localhost:3344/master/config-dev.yml

    http://localhost:3355/configInfo

可以发现3344已经将版本号刷新出来了,3355没有将版本号刷新出来

毕竟是手动版本的,还需要一步骤:需要运维人员发送Post请求刷新3355

curl -X POST "http://localhost:3355/actuator/refresh"

然后就可以看到3355服务的配置文件也被刷新了

4、手动刷新的问题

想想还有什么问题?

  • 假如有多个微服务客户端3355/3366/3377
  • 每个微服务都要执行—次post请求,手动刷新?
  • 可否广播,一次通知,处处生效?
  • 我们想大范围的自动刷新,求方法

八、服务总线

8.1、SpringCloud Bus

8.1.1、Bus消息总线是什么

Spring Cloud Bus 配合Spring Cloud Config 使用可以实现配置的动态刷新。

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。Spring Clud Bus目前支持RabbitMQ和Kafka。

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

8.1.2、为何被称为总线

什么是总线

在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理

ConfigClient实例都监听MQ中同一个topic(默认是Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。

8.1.3、Bus之RabbitMQ环境配置

 安装:https://www.cnblogs.com/zhangzhixi/p/17271621.html#_label1_1

 8.1.4、Bus动态刷新全局广播的设计思想和选型(复制3355项目)

1、新建和3355项目一样的项目:cloud-config-client-3366

2、两个项目均添加pom依赖:

<!--添加消息总线RabbitMQ支持-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

3、两个项目均添加RabbitMQ配置:

server:
  port: 3366

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 #配置中心地址

  #rabbitmq相关配置
  rabbitmq:
    host: 182.92.209.212
    port: 5672
    username: guest
    password: guest

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
  # 服务端的服务发现页面和健康检查地址
  instance:
    # 显示IP地址
    prefer-ip-address: true

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
# 日志
logging:
  level:
    com.zhixi.*: debug

利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置  

 8.1.5、Bus动态刷新全局广播配置实现

1、给cloud-config-center-3344添加添加消息总线RabbitMQ支持

<!--添加消息总线RabbitNQ支持-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

2、cloud-config-center-3344的yml配置  

server:
  port: 3344

spring:
  application:
    name: cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          # Git仓库地址
          uri: https://gitee.com/zhang-zhixi/spring-cloud-config.git
          # 配置仓库中的哪个目录下的配置文件被读取
          search-paths:
            - SpringCloud-Config
          # 配置Gitee的用户名和密码
          username: xxx
          password: xxx
      ####读取分支
      label: master
  #rabbitmq相关配置
  rabbitmq:
    host: 182.92.209.212
    port: 5672
    username: guest
    password: guest

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka

# rabbitmq相关配置,暴露bus刷新配置的端点
# curl http://localhost:3344/actuator:暴露bus刷新配置的端点
# curl -X POST http://localhost:3344/actuator/bus-refresh:刷新配置
# curl -X POST http://localhost:3344/actuator/bus-refresh/config-client:3355:刷新3355端口的配置
management:
  endpoints: #暴露bus刷新配置的端点
    web:
      exposure:
        include: "bus-refresh"
# 日志
logging:
  level:
    com.zhixi.*: debug

3、测试  

  • 1、启动

    • EurekaMain7001
    • ConfigcenterMain3344
    • ConfigclientMain3355
    • ConfigclicntMain3366
  • 2、修改Gitee上配置文件内容,增加版本号
  • 3、测试访问
    • http://localhost:3344/master/config-dev.yml
    • http://localhost:3355/configInfo
    • http://localhost:336/configInfo

  

  • 4、发送POST请求(一次请求,处处生效)
    • curl -X POST "http://localhost:3344/actuator/bus-refresh"  

 8.1.6、Bus动态刷新之定点通知

想全部通知,只想定点通知

  • 只通知3355
  • 不通知3366

简单一句话 :指定具体某一个实例生效而不是全部

公式:

  • http://localhost:3344/actuator/bus-refresh/{destination},/bus/refresh请求不再发送到具体的服务实例上,而是发给config server通过destination参数类指定需要更新配置的服务或实例

测试:

  • 1、修改Gitee配置文件
  • 2、只通知3355,不通知3366
  • config-client:表示微服务名称 3355表示微服务端口
  • curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"

九、消息链路

9.1、SpringCloud Stream

9.1.1、SpringCloudStream是什么

  官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。

应用程序通过inputs或者 outputs 来与Spring Cloud Stream中binder对象交互。

通过我们配置来binding(绑定),而Spring Cloud Stream 的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。

Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。

目前仅支持RabbitMQ、 Kafka。

9.1.2、为什么使用SpringCloudStream

  比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区。

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了—种解耦合的方式。

Stream凭什么可以统一底层差异?

  在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。

9.1.3、Stream编码常用注解简介

  • Binder - 很方便的连接中间件,屏蔽差异。
  • Channel - 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。
  • Source和Sink - 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。

编码API和常用注解

案例说明

准备RabbitMQ环境(上文有提及)

工程中新建三个子模块

  • cloud-stream-rabbitmq-provider-8801,作为生产者进行发消息模块
  • cloud-stream-rabbitmq-consumer-8802,作为消息接收模块
  • cloud-stream-rabbitmq-consumer-8803,作为消息接收模块

9.1.4、Stream消息驱动之生产者

 1、新建模块:cloud-stream-rabbitmq-provider-8801 

2、pom

<dependencies>
    <!--Web依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Actuator依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--Eureka客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--Stream整合RabbitMQ-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <!--基础配置-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml  

server:
  port: 8801
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: #配置要绑定的rabbitmq的服务信息
        rabbit: #表示定义的名称,用于bingding的整合
          type: rabbit
          # 设置rabbitmq的连接信息
          environment:
              rabbitmq:
                host: 182.92.209.212
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 表示要绑定的rabbitmq的服务信息
  #rabbitmq相关配置
  rabbitmq:
    host: 182.92.209.212
    port: 5672
    username: guest
    password: guest
eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    prefer-ip-address: true     # 访问的路径变为IP地址

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @ClassName StreamMQMain8801
 * @Author zhangzhixi
 * @Description Stream整合RabbitMQ-消息生产者
 * @Date 2023-04-04 9:49
 * @Version 1.0
 */
@EnableEurekaClient
@SpringBootApplication
public class StreamMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class, args);
    }
}

5、业务类Service:发送消息

 com.zhixi.service.IMessageProvider:

/**
 * 业务发送接口
 * @author zhixi
 */
public interface IMessageProvider {
    public String send();
}

com.zhixi.service.impl.IMessageProviderImpl  

import com.zhixi.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 业务发送接口实现类
 * @description @EnableBinding(Source.class) 定义消息的推送管道
 * @date 2023-04-17 18:07
 */
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {

    /**
     * 消息发送管道
     */
    @Resource
    private MessageChannel output;

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("*****serial: " + serial);
        return null;
    }
}

6、Controller

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 消息发送控制器
 * @date 2023-04-17 18:07
 */
@RestController
public class SendMessageController {
    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage() {
        return messageProvider.send();
    }

}

7、测试

  • 启动 7001eureka
  • 启动8801
  • 访问:http://localhost:8801/sendMessage

9.1.5、Stream消息驱动之消费者

1、新建模块:cloud-stream-rabbitmq-consumer-8802

2、pom  

<dependencies>
    <!--Web依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Actuator依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--Eureka客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--Stream整合RabbitMQ-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <!--基础配置-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml  

server:
  port: 8802
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: #配置要绑定的rabbitmq的服务信息
        rabbit: #表示定义的名称,用于bingding的整合
          type: rabbit
          # 设置rabbitmq的连接信息
          environment:
            rabbitmq:
              host: 182.92.209.212
              port: 5672
              username: guest
              password: guest
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 表示要绑定的rabbitmq的服务信息
  #rabbitmq相关配置
  rabbitmq:
    host: 182.92.209.212
    port: 5672
    username: guest
    password: guest
eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    prefer-ip-address: true     # 访问的路径变为IP地址

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @ClassName StreamMQMain8801
 * @Author zhangzhixi
 * @Description Stream整合RabbitMQ-消息消费者
 * @Date 2023-04-04 9:49
 * @Version 1.0
 */
@EnableEurekaClient
@SpringBootApplication
public class StreamMQMain8802 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8802.class, args);
    }
}

5、业务类:Controller

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Controller;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 消息发送控制器
 * @date 2023-04-17 18:07
 */
@Controller
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
    @Value("${server.port}")
    private String serverPort;

    /**
     * 消息接收
     * @param message 服务端发送的是什么类型的消息,这里接收的就是什么类型的消息
     */
    @StreamListener(Sink.INPUT)
    public void input(Message<String> message) {
        System.out.println("消费者1号,----->接受到的消息: " + message.getPayload() + "\t  port: " + serverPort);
    }
}

6、测试  

  • 启动EurekaMain7001

  • 启动StreamMQMain8801

  • 启动StreamMQMain8802

  • 8801发送8802接收消息

9.1.6、Stream消息消费之:广播模式

依照8802,克隆出来一份运行8803 - cloud-stream-rabbitmq-consumer8803。

启动

  • Eureka
  • 消息生产 - 8801
  • 消息消费 - 8802
  • 消息消费 - 8803

可以发现访问:http://localhost:8801/sendMessage,那么两个消费者8802和8803都能够接收到消息

9.1.7、Stream消息消费之:分组

有时候我们的业务需求,可能想要同一类/组的微服务能够接收到这种消息,这时候需要用到了分组

微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。

不同的组是可以重复消费的,同一个组内会发生竞争关系,只有其中一个可以消费。

下面将8802和8803的分组都设置成一样的,进行测试:

server:
  port: 8802
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: #配置要绑定的rabbitmq的服务信息
        rabbit: #表示定义的名称,用于bingding的整合
          type: rabbit
          # 设置rabbitmq的连接信息
          environment:
            rabbitmq:
              host: 182.92.209.212
              port: 5672
              username: guest
              password: guest
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 表示要绑定的rabbitmq的服务信息
          group: studyExchangeGroup # 设置消费者的组名,如果不设置,那么就是广播模式
  #rabbitmq相关配置
  rabbitmq:
    host: 182.92.209.212
    port: 5672
    username: guest
    password: guest
eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka,http://localhost:7003/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    prefer-ip-address: true     # 访问的路径变为IP地址

访问十次:http://localhost:8801/sendMessage

发现8002、8003是轮询的规则接收到消息

十、SpringCloudAlibaba

为什么会出现SpringCloud alibaba

Spring Cloud Netflix项目进入维护模式:https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now

什么是维护模式?

  将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能。

他们将修复block级别的 bug 以及安全问题,他们也会考虑并审查社区的小型pull request。

SpringCloudAlibaba是什么?

  Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

诞生:2018.10.31,Spring Cloud Alibaba 正式入驻了Spring Cloud官方孵化器,并在Maven 中央库发布了第一个版本。

能干嘛

  • 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

 

生态

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

Spring Cloud Alibaba学习资料获取

官网:https://spring.io/projects/spring-cloud-alibaba#overview

英文:

  https://github.com/alibaba/spring-cloud-alibaba

  https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
中文:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

10.1、Nacos

10.1.1、Nacos简介

为什么叫Nacos

  前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。
是什么

  一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  Nacos: Dynamic Naming and Configuration Service
  Nacos就是注册中心+配置中心的组合 -> Nacos = Eureka+Config+Bus
能干嘛

  替代Eureka做服务注册中心
  替代Config做服务配置中心
去哪下

  https://github.com/alibaba/nacos/releases

官网文档(1.x):https://nacos.io/zh-cn/docs/quick-start.html

官网文档(2.x):https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

10.1.2、Nacos安装

解压根目录下nacos-server-1.1.4.zip安装包,进入到bin目录:startup.cmd

浏览器访问(nacos/nacos):http://localhost:8848/nacos

*后面的Nacos链接我会写我的服务器地址,各位手动替换下即可,Linux下启动Nacos命令:sh startup.sh -m standalone

10.1.3、Nacos-服务提供者注册

1、新建模块:cloud-alibaba-provider-payment-9001

2、pom

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--基础配置-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 9001
spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        #配置Nacos地址
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

4、主启动 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
 * @ClassName PaymentMain9001
 * @Author zhangzhixi
 * @Description Nacos服务注册与发现
 * @Date 2023-04-24 18:49
 * @Version 1.0
 */
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
    public static void main(String[] args) {
            SpringApplication.run(PaymentMain9001.class, args);
    }}

5、Controller

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zhixi
 */
@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @GetMapping(value = "/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id) {
        return "nacos registry, serverPort: " + serverPort + "\t id" + id;
    }
}

6、测试

测试

  • http://localhost:9001/payment/nacos/1
  • 进入nacos控制台
  • 查看到nacos服务注册中心+服务提供者9001都OK了

7、复制9001  

  为了下一章节演示nacos的负载均衡,参照9001新建9002

启动9001、9002

10.1.4、服务消费者注册和负载

1、新建模块:cloud-alibaba-consumer-nacos-order-83

2、pom

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--基础配置-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 83

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName PaymentMain9001
 * @Author zhangzhixi
 * @Description Nacos服务注册与发现-服务消费者
 * @Date 2023-04-24 18:49
 * @Version 1.0
 */
@SuppressWarnings("all")
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain83.class, args);
    }
}

5、负载均衡配置:com.zhixi.config.ApplicationContextConfig

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @ClassName ApplicationContextConfig
 * @Description 负载均衡配置类
 * @Author zhixi
 * @Date 2020/4/24 18:52
 * @Version 1.0
 */
@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

6、controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
 * Nacos服务注册与发现-服务消费者-接口
 *
 * @author zhixi
 */
@RestController
@Slf4j
public class OrderNacosController {

    @Resource
    private RestTemplate restTemplate;

    @Value("${service-url.nacos-user-service}")
    private String serverUrl;

    @GetMapping(value = "/consumer/payment/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Long id) {
        return restTemplate.getForObject(serverUrl + "/payment/nacos/" + id, String.class);
    }
}

7、测试  

  • 启动Nacos
  • 启动服务提供者:cloud-alibaba-provider-payment-9001
  • 启动服务提供者:cloud-alibaba-provider-payment-9002
  • 启动服务消费者:cloud-alibaba-consumer-nacos-order-83

访问:http://localhost:83/consumer/payment/nacos/13

发现9001、9001是负载状态

10.1.5、Nacos之服务配置中心

1、新建配置中心模块:cloud-alibaba-config-nacos-client-3377

2、pom

<dependencies>
    <!--Nacos配置中心-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--基础配置-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、bootstrap.yml和application.yaml配置

Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。

springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application

bootstrap.yml

# nacos配置
server:
  port: 3377
spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: 43.143.195.160:8848 #Nacos服务注册中心地址
      config:
        server-addr: 43.143.195.160:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置

application.yaml

spring:
  profiles:
    active: dev # 表示开发环境
    #active: test # 表示测试环境
    #active: info
    #active: prod # 表示生产环境

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


/**
 * @ClassName NacosConfigClientMain3377
 * @Author zhangzhixi
 * @Description Nacos配置中心
 * @Date 2023-04-25 18:49
 * @Version 1.0
 */
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377 {
    public static void main(String[] args) {
        SpringApplication.run(NacosConfigClientMain3377.class, args);
    }
}

5、controller

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
//支持Nacos的动态刷新功能。
@RefreshScope
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
        return configInfo;
    }
}

6、配置Nacos

在 Nacos Spring Cloud中,dataId的完整格式如下:

${prefix}-${spring-profile.active}.${file-extension}
  • prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.config.prefix来配置。
  • spring.profile.active即为当前环境对应的 profile,详情可以参考 Spring Boot文档。注意:当spring.profile.active为空时,对应的连接符 - 也将不存在,datald 的拼接格式变成${prefix}.${file-extension}
  • file-exetension为配置内容的数据格式,可以通过配置项spring .cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型。
  • 通过Spring Cloud 原生注解@RefreshScope实现配置自动更新。

Nacos增加配置:

config:
    info: hello nacos config - version 1.0

然后发布即可 

7、测试

发现浏览器访问到了配置,且version是1.0

10.1.6、Nacos之多环境多项目管理:NameSpace、Group、DataId

简单来举例吧:

  • namespace:相当于长城网单位,这个单位下有很多项目,可能还有其他单位,一个单位代表一个namespace
  • group:相当于长城网下面的一个项目,比如网站项目、后台项目,一个项目进行一个分组
  • DataId:相当于一个项目的环境,比如网站项目有开发环境dev,测试环境test等

1、创建Namespace

2、创建分组和环境

3、修改项目配置

bootstrap.yml:

# nacos配置
server:
  port: 3377
spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: 43.143.195.160:8848 #Nacos服务注册中心地址
      config:
        server-addr: 43.143.195.160:8848 #Nacos作为配置中心地址
        # 指定命名空间(具体单位)
        namespace: 6a52e4e4-3094-4b3f-890d-48877ca0470c
        # 指定分组(具体项目)
        group: PC_WEBSITE_GROUP
        # 指定配置文件后缀
        file-extension: yaml

application.yaml  

具体要激活Nacos的哪个Data Id配置

spring:
  profiles:
    active: dev # 表示开发环境
    #active: test # 表示测试环境
    #active: info
    #active: prod # 表示生产环境

4、测试 

启动项目:cloud-alibaba-config-nacos-client-3377

访问:http://localhost:3377/config/info

10.1.7、Nacos持久化配置(连接Mysql数据库)

  此处演示的是和老师的Nacos版本一致,是:1.1.4,亲测此版本不支持Mysql8+,然后换了个mysql5.7的数据库,才可以正常启动!

1、执行初始化SQL

新建名为:nacos_config的数据库,然后将Nacos/conf/nacos-mysql.sql执行

2、修改配置文件

vim conf/application.properties,写入以下配置,替换自己数据库的IP和密码

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://IP:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=密码

3、启动即可

bash startup.sh -m standalone

10.1.8、Nacos集群搭建

请确保是在环境中安装使用:

  1. 64 bit OS Linux/Unix/Mac,推荐使用Linux系统。
  2. 64 bit JDK 1.8+;
  3. Maven 3.2.x+;
  4. 3个或3个以上Nacos节点才能构成集群。

Nacos下载Linux版

1、执行初始化SQL

新建名为:nacos_config的数据库,然后将Nacos/conf/nacos-mysql.sql执行

2、修改配置文件

vim conf/application.properties,写入以下配置,替换自己数据库的IP和密码

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://IP:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=密码

3、修改集群配置  

cd $NACOS_HOME/conf/
cp cluster.conf.example cluster.conf

***:注意这个地址一定要写你的内网IP,下面提供两种方式查看

  • 1、hostname -i     
  • 2、ifconfig(找到ech0网卡的地址进行填写)
#填写内网IP
10.0.24.16:3333
10.0.24.16:4444
10.0.24.16:5555

4、复制项目

mv nacos nacos-3333/
cp -r nacos-3333/ nacos-4444
cp -r nacos-3333/ nacos-5555

5、修改端口(为3333、4444、5555)

vim nacos-3333/conf/application.properties
vim nacos-4444/conf/application.properties
vim nacos-5555/conf/application.properties

6、启动

bash nacos-3333/bin/startup.sh
bash nacos-4444/bin/startup.sh
bash nacos-5555/bin/startup.sh

7、修改Nginx配置:实现负载均衡

# 负载均衡配置
upstream cluster{
 # 如果你要开放了3333、4444、5555安全组,这里就可以写IP
 # 如果没有开放3333、4444、5555安全组,这里写IP会无法访问
 # 还是建议这里写localhost,不建议对外开放端口:3333、4444、5555
 server localhost:3333;
 server localhost:4444;
 server localhost:5555;
}

server{
	listen 8848;
	# 写主机IP或者域名
	server_name www.zhangzhixi.cn;
	
	location / {
		proxy_pass http://cluster;
	}
}

8、测试访问  

需对外开放8848安全组端口

 9、测试应用连接集群

application.yml

server:
  port: 9001
spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        #配置Nacos地址(Nginx的server_name配置什么,这里就写什么,域名或者公网IP)
        server-addr: www.zhangzhixi.cn:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

10.2、Sentinel

官网:https://sentinelguard.io/zh-cn/

下载地址:https://github.com/alibaba/Sentinel/releases

Sentinel 是什么?

  随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性:

Hystrix与Sentinel比较:

Hystrix:
  需要我们程序员自己手工搭建监控平台
  没有一套web界面可以给我们进行更加细粒度化得配置流控、速率控制、服务熔断、服务降级
Sentinel:
  单独一个组件,可以独立出来。
  直接界面化的细粒度统一配置。

约定 > 配置 > 编码

都可以写在代码里面,但是我们本次还是大规模的学习使用配置和注解的方式,尽量少写代码

10.2.1、Sentinel安装运行

前提:

  • JDK8+
  • 服务占用8080端口,可通过下面参数指定端口:-Dserver.port=8080

Sentinel 分为两个部分:

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

下载地址:

  • 下载到本地sentinel-dashboard-1.7.0.jar

运行:

  • 直接cmd-->java -jar sentinel-dashboard-1.7.0.jar

访问:localhost:8080,默认用户名密码是sentinel

10.2.2、Sentinel初始化监控

1、新建模块:cloud-alibaba-sentinel-service-8401

2、添加pom  

<dependencies>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- SpringBoot整合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>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>4.6.3</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、写yaml  

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址
        server-addr: www.zhangzhixi.cn:8848
    sentinel:
      transport:
        # Sentinel管理控制台的端口,注意这里的坑:
        # 刚开始这里写的是我服务器的Sentinel地址,报错:fetch metric http://169.254.176.17:8719
        # 原因是由于服务器文法访问我们本地的电脑, 所以会连接失败, 所以要想连接远端sentinel就必须把项目部署到服务器上面。
        # 然后添加参数:client-ip: 上线后的服务器IP地址(你的应用IP)
        dashboard: localhost:8080
        # 微服务与Sentinel组件对应的操作端口
        port: 8719

management:
  endpoints:
    web:
      exposure:
        include: '*'

feign:
  sentinel:
    enabled: true # 激活Sentinel对Feign的支持

4、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName MainApp8401
 * @Author zhangzhixi
 * @Description Sentinel服务注册与发现-服务提供者
 * @Date 2023-05-03 22:50
 * @Version 1.0
 */
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class, args);
    }
}

5、业务类Controller  

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA() {
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB() {
        log.info(Thread.currentThread().getName() + "\t" + "...testB");
        return "------testB";
    }
}

6、测试  

启动项目,因为sentinel1.7版本是懒加载,需要访问一次请求,才能纳入到sentinel监管中去。

访问:

http://localhost:8401/testA

http://localhost:8401/testB

10.2.3、Sentinel流控规则简介

  • 资源名:唯一名称,默认请求路径。
  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。
  • 阈值类型/单机阈值:
    • QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。
    • 线程数:当调用该API的线程数达到阈值的时候,进行限流。  
  • 是否集群:不需要集群。
  • 流控模式:
    • 直接:API达到限流条件时,直接限流。  
    • 关联:当关联的资源达到阈值时,就限流自己。  
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。  
  • 流控效果:
    • 快速失败:直接失败,抛异常。  
    • Warm up:根据Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。  
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效。  

10.2.4、Sentinel流控-QPS直接失败

结果

  返回页面 Blocked by Sentinel (flow limiting)

源码

  com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController

思考

  直接调用默认报错信息,技术方面OK,但是,是否应该有我们自己的后续处理?类似有个fallback的兜底方法?

10.2.4、Sentinel流控-QPS直接失败

线程数:当调用该API的线程数达到阈值的时候,进行限流,下面用Jmeter进行测试

10.2.4、Sentinel流控-关联

是什么?

  • 当自己关联的资源达到阈值时,就限流自己

  • 当与A关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)

下面测试并发访问/testB资源,看/testA资源会怎么样:

可以看到,testB没事,testA请求收到了影响

10.2.5、Sentinel流控-链路

ps:这里需要将CloudAlibaba版本进行升级一下,从之前的2.1.0.RELEASE,升级为:2.2.2.RELEASE,在2.2.2版本,才支持参数:web-context-unify: false

简单来说就是:同一条上的蚂蚱 

可以看到请求testA、testB,都会共同调用一个方法common,链路的控制就是说,可以设置common被谁调用,然后设置不同的流控规则,下面演示方法common只对testA请求进行流控

1、service:com.zhixi.service.TestService

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description
 * @date 2023-05-04 15:48
 */
@Service
public class TestService {
    /**
     * @SentinelResource: 注解中的value值是资源名,可以任意取名,但是要保证唯一性
     * @return 返回值
     */
    @SentinelResource("common")
    public String common() {
        return "common";
    }
}

2、Controller  

import com.zhixi.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class FlowLimitController {
    @Resource
    private TestService testService;

    @GetMapping("/testA")
    public String testA() {
        testService.common();
        log.info(Thread.currentThread().getName() + "\t" + "...testA");
        return "------testA";
    }


    @GetMapping("/testB")
    public String testB() {
        testService.common();
        log.info(Thread.currentThread().getName() + "\t" + "...testB");
        return "------testB";
    }
}

3、yml  

spring:
  cloud:
    sentinel:
      # 关闭Context收敛,这样被监控方法可以进行不同链路的单独控制
      web-context-unify: false

4、Sentinel添加流控链路  

5、测试

10.2.6、Sentinel流控-预热

Warm Up

  Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。

当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。

通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

默认coldFactor为3,即请求QPS 从 阈值/ 3开始,经预热时长逐渐升至设定的QPS阈值。

1、Warm Up 设置

请求QPS:10/3 = 3(次/秒)

也就是说,在一秒钟最多处理3个请求,如果在1秒中超过了三个请求,就会进行预热

比如现在是一秒钟4个请求,会触发预热,过了五秒后,阈值就会增长到每秒10次请求。

2、测试

多次刷新:http://localhost:8401/testB

一秒三次我的手速还是可以实现的,可以看到会出现错误的页面

出现错误页面后,过5秒钟,就会自动设置为10QPS/秒钟,这时候凭借手速是刷不到:Blocked by Sentinel (flow limiting)这个页面了

10.2.6、Sentinel流控-排队等待

匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。

设置:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为1000毫秒。

1、设置排队等待流控规则

2、测试

10.2.7、Sentinel降级简介

熔断降级概述:

  除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。

例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。

然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以

上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。

因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

RT(平均响应时间,秒级)

  • 平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级。
  • 窗口期过后关闭断路器。
  • RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)。

异常比列(秒级)

  • QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 。

异常数(分钟级)

  • 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

Sentinei的断路器是没有类似Hystrix半开状态的。(Sentinei 1.8.0 已有半开状态)

半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。

10.2.8、Sentinel降级-RT

平均响应时间(DEGRADE_GRADE_RT):当1s内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值( count,以ms为单位),那么在接下的时间窗口(DegradeRule中的timeWindow,以s为单位)之内,、

对这个方法的调用都会自动地熔断(抛出DegradeException )。

注意Sentinel 默认统计的RT上限是4900 ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。
注意:Sentinel 1.7.0才有平均响应时间(DEGRADE_GRADE_RT),Sentinel 1.8.0的没有这项,取而代之的是慢调用比例 (SLOW_REQUEST_RATIO)。

慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

1、Controller

@GetMapping("/testD")
public String testD() {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("testD 测试RT");
    return "------testD";
}

2、添加降级-RT配置  

跳闸后,然后过了一秒时间窗口期,如果不同时满足以下两点,就会恢复正常!

比如没有设置sleep为1s,即使每秒发送了100个请求过来,也还是不会跳闸!

结论

  按照上述配置,永远一秒钟打进来10个线程(大于5个了)调用testD,我们希望200毫秒处理完本次任务,如果超过200毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了后续我停止jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK。

10.2.9、Sentinel降级-异常比例

异常比例(DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule中的 count)之后,资源进入降级状态,即在接下的时间窗口( DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0, 1.0],代表0% -100%。

1、Controller

@GetMapping("/testE")
public String testE() {
    log.info("testD 异常比例");
    int age = 10/0;
    return "------testE";
}

2、服务降级-异常比例

按照下面图示规则:在一秒钟异常内的请求数,失败率达到20%,就进行熔断降级。

10.2.10、Sentinel降级-异常数

异常数( DEGRADE_GRADF_EXCEPTION_COUNT ):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后码可能再进入熔断状态。

 1、Controller

@GetMapping("/testF")
public String testF() {
    log.info("testF 测试异常数");
    int age = 10/0;
    return "------testF 测试异常数";
}

2、测试  

在60s内,错误次数大于5,触发熔断.

访问http://localhost:8401/testF,第一次访问绝对报错,因为除数不能为零,我们看到error窗口,但是在一分钟时间内达到5次报错后,进入熔断后降级。

10.2.11、Sentinel热点key

/**
 * 测试热点key
 *
 * @param p1 参数1
 * @param p2 参数2
 * @return 返回值
 */
@GetMapping("/testHotKey/{p1}/{p2}")
@SentinelResource(value = "testHotKey", blockHandler/*兜底方法*/ = "dealTestHotKey")
public String testHotKey(@PathVariable(value = "p1", required = false) String p1,
                         @PathVariable(value = "p2", required = false) String p2) {
    return "------testHotKey";
}
/**
 * 兜底方法
 *
 * @param p1        参数1
 * @param p2        参数2
 * @param exception 异常
 * @return 返回值
 */
public String dealTestHotKey(String p1, String p2, BlockException exception) {
    //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
    return "------deal_testHotKey,o(╥﹏╥)o";
}

1、对指定参数进行限流

http://localhost:8401/testHotKey/1/1

 

2、对指定参数进行限流,并放行指定参数值

 http://localhost:8401/testHotKey/6/1

10.2.12、SentinelResource配置(上):按资源名称限流 + 后续处理

1、Controller

@RestController
public class RateLimitController {

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public CommonResult<Payment> byResource() {
        return new CommonResult<>(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    public CommonResult<Payment> handleException(BlockException exception) {
        return new CommonResult<>(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
    }
}

2、Sentinel配置  

http://localhost:8401/byResource

上面兜底方案面临的问题

  1. 系统默认的,没有体现我们自己的业务要求。
  2. 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
  3. 每个业务方法都添加—个兜底的,那代码膨胀加剧。
  4. 全局统—的处理方法没有体现。

10.2.12、SentinelResource配置(中):全局自定义类

 自定义限流处理类 - 创建CustomerBlockHandler类用于自定义限流处理逻辑

1、全局自定义Handler:com.zhixi.handler.CustomerBlockHandler

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.zhixi.result.CommonResult;


public class CustomerBlockHandler {
    public static CommonResult<Object> handlerException(BlockException exception) {
        return new CommonResult<>(4444, "按客戶自定义,global handlerException----1");
    }

    public static CommonResult<Object> handlerException2(BlockException exception) {
        return new CommonResult<>(4444, "按客戶自定义,global handlerException----2");
    }
}

2、Controller

@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        // 自定义限流处理类
        blockHandlerClass = CustomerBlockHandler.class,
        blockHandler = "handlerException2")
public CommonResult<Payment> customerBlockHandler() {
    return new CommonResult<>(200, "按客戶自定义", new Payment(2020L, "serial003"));
}

3、测试  

连续点击地址:http://localhost:8401/rateLimit/customerBlockHandler,发现使用了我们在Controller中设置的全局兜底方法

10.2.13、Sentinel整合Ribbon-环境说明

Ribbon系列

  • 启动nacos和sentinel
  • 提供者9003/9004
  • 消费者84

10.2.14、Sentinel整合Ribbon-服务提供者(9003、9004)

1、新建模块:cloud-alibaba-provider-payment-9003

 2、pom

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、主启动  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName PaymentMain9003
 * @Author zhangzhixi
 * @Description Sentinel服务注册与发现-服务提供者
 * @Date 2023-05-06 10:15
 * @Version 1.0
 */
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9003.class, args);
    }
}

4、业务类:com.zhixi.controller.PaymentController

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    //模拟数据库
    public static HashMap<Long, Payment> hashMap = new HashMap<>();

    static {
        hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
        hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
        hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSql(@PathVariable("id") Long id) {
        Payment payment = hashMap.get(id);
        return new CommonResult<>(200, "from mysql,serverPort:  " + serverPort, payment);
    }
}

5、复制此项目,改名为9004即可

10.2.15、Sentinel整合Ribbon-服务消费者(84)

1、新建模块:cloud-alibaba-consumer-nacos-order-84

2、pom

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--公共模块-->
    <dependency>
        <groupId>com.zhixi</groupId>
        <artifactId>cloud-api-common</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3、yml

server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 43.143.195.160:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        # 微服务与Sentinel组件对应的操作端口
        port: 8719

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

4、主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
 * @ClassName OrderNacosMain84
 * @Author zhangzhixi
 * @Description Sentinel服务注册与发现-服务消费者
 * @Date 2023-05-06 10:16
 * @Version 1.0
 */
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class, args);
    }
}

5、业务类

负载均衡配置:com.zhixi.config.ApplicationContextConfig

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;


@Configuration
public class ApplicationContextConfig {

    /**
     * 开启负载均衡
     *
     * @return RestTemplate
     */
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

Controller:com.zhixi.controller.CircleBreakerController 

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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;

import javax.annotation.Resource;

@SuppressWarnings("all")
@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    public final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")//没有配置
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
        } else {
            assert result != null;
            if (result.getData() == null) {
                throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
            }
        }
        return result;
    }

}

6、测试  

启动Nacos

启动Sentinel

启动服务提供者9003、9004

启动服务消费者84

访问:http://localhost:84/consumer/fallback/1  

 可以发现,成功访问接口,并且9003、9004是轮询状态。

10.2.16、Sentinel服务熔断无配置

访问上述Controller,这里提供异常的参数进行访问,看下:

http://localhost:84/consumer/fallback/4

http://localhost:84/consumer/fallback/66

结论:没有任何配置 - 给用户error页面,不友好

10.2.17、Sentinel服务熔断-只配置fallback

修改:CircleBreakerController

@SuppressWarnings("all")
@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    public final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//没有配置
    @SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
        } else {
            assert result != null;
            if (result.getData() == null) {
                throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
            }
        }
        return result;
    }

    //本例是fallback
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(444, "兜底异常handlerFallback,exception内容  " + e.getMessage(), payment);
    }
}

测试:发现异常被捕获,然后降级到了自己写的:handlerFallback方法

http://localhost:84/consumer/fallback/1

http://localhost:84/consumer/fallback/4

http://localhost:84/consumer/fallback/66

10.2.18、Sentinel服务熔断-只配置blockHandler

 blockHandler只负责sentinel控制台配置违规

1、Controller

@SuppressWarnings("all")
@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    public final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
    @SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
        } else {
            assert result != null;
            if (result.getData() == null) {
                throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
            }
        }
        return result;
    }

    //本例是fallback
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(444, "兜底异常handlerFallback,exception内容  " + e.getMessage(), payment);
    }

    //本例是blockHandler
    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException  " + blockException.getMessage(), payment);
    }
}

2、设置Sentinel流控  

3、测试

http://localhost:84/consumer/fallback/66

10.2.19、Sentinel服务熔断-配置fallback和blockHandler

先说结论:若blockHandler和fallback 都进行了配置,则被限流降级,而抛出BlockException时只会进入blockHandler处理逻辑。

 1、Controller

@SuppressWarnings("all")
@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    public final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
    //@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
        } else {
            assert result != null;
            if (result.getData() == null) {
                throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
            }
        }
        return result;
    }

    //本例是fallback
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(444, "兜底异常handlerFallback,exception内容  " + e.getMessage(), payment);
    }

    //本例是blockHandler
    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(445, "blockHandler-sentinel限流: blockException  " + blockException.getMessage(), payment);
    }
}

2、测试  

http://localhost:84/consumer/fallback/1

http://localhost:84/consumer/fallback/66

10.2.19、Sentinel服务熔断-忽略指定异常

exceptionsToIgnore,忽略指定异常,即这些异常不用兜底方法处理。

http://localhost:84/consumer/fallback/4

@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback")//没有配置
//@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
//@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
//@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
    CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
    if (id == 4) {
        throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
    } else {
        assert result != null;
        if (result.getData() == null) {
            throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
        }
    }
    return result;
}

10.2.20、Sentinel服务熔断-整合OpenFeign

修改84模块

  • 84消费者调用提供者9003

  • Feign组件一般是消费侧

1、pom

<!--OpenFeign支持-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、主启动添加开启Feign注解支持

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class, args);
    }
}

3、yml

# 开启feign对sentinel的支持
feign:
  sentinel:
    enabled: true

4、业务类:这里是通过OpenFeign进行服务调用

com.zhixi.service.PaymentService

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import com.zhixi.service.impl.PaymentFallbackServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackServiceImpl.class)
public interface PaymentService
{
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSql(@PathVariable("id") Long id);
}

com.zhixi.service.impl.PaymentFallbackServiceImpl  

import com.zhixi.pojo.Payment;
import com.zhixi.result.CommonResult;
import com.zhixi.service.PaymentService;
import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackServiceImpl implements PaymentService {
    @Override
    public CommonResult<Payment> paymentSql(Long id)
    {
        return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
    }
}

5、Controller  

//==================OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
    return paymentService.paymentSql(id);
}

6、关于Feign和Sentinel版本的坑,此处必须

上次因为做:10.2.5、Sentinel流控-链路,将Alibaba的版本修改为了:2.2.2.RELEASE

但是在今天我启动84服务的时候,报错,查阅资料,发现报错原因:

  Sentinel框架SentinelContractHolder类中找不到parseAndValidatateMetadata这个方法,是因为这个方法拼写有错误,在Sentinel和OpenFeign新版本中已经修正parseAndValidateMetadata。

下面提供两种解决办法:

第一种、升级SpringCloud版本

  由Hoxton.SR1升级为Hoxton.SR3

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-dependencies</artifactId>
     <version>Hoxton.SR3</version>
     <type>pom</type>
     <scope>import</scope>
 </dependency>

我测试了Feign的熔断,正常,84也可以正常启动,但是这里还是不太建议,因为不清楚会不会出现其他的问题

第二种、新建一个包:com.alibaba.cloud.sentinel.feign.SentinelContractHolder

import feign.Contract;
import feign.MethodMetadata;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhangzhixi
 * @version 1.0
 * @description 解决Sentinel和Feign的兼容性问题
 * @date 2023-05-06 13:14
 */
public class SentinelContractHolder implements Contract {
    private final Contract delegate;
    public static final Map<String, MethodMetadata> METADATA_MAP = new HashMap();

    public SentinelContractHolder(Contract delegate) {
        this.delegate = delegate;
    }

    @Override
    public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
        List<MethodMetadata> metadatas = this.delegate.parseAndValidatateMetadata(targetType);
        metadatas.forEach((metadata) -> {
            MethodMetadata var10000 = (MethodMetadata) METADATA_MAP.put(targetType.getName() + metadata.configKey(), metadata);
        });
        return metadatas;
    }
}

7、测试 

启动服务:cloud-alibaba-provider-payment-9003、cloud-alibaba-provider-payment-9004、cloud-alibaba-consumer-nacos-order-84 

访问:http://localhost:84/consumer/paymentSQL/1

发现是负载状态

此时故意关闭9003、9004,微服务提供者,可以发现84消费侧自动降级,不会被耗死。

10.2.21、Sentinel-整合Nacos-持久化

本案例拿项目:cloud-alibaba-sentinel-service-8401来说

因为项目已经添加了Sentinel整合Nacos持久化的依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

1、yml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址
        server-addr: www.zhangzhixi.cn:8848
    sentinel:
      # 添加Nacos数据源配置
      datasource:
        ds1:
          nacos:
            server-addr: www.zhangzhixi.cn:8848
            username: nacos
            password: nacos
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
      # 关闭Context收敛,这样被监控方法可以进行不同链路的单独控制
      web-context-unify: false
      transport:
        # Sentinel管理控制台的端口,注意这里的坑:
        # 刚开始这里写的是我服务器的Sentinel地址,报错:fetch metric http://169.254.176.17:8719
        # 原因是由于服务器文法访问我们本地的电脑, 所以会连接失败, 所以要想连接远端sentinel就必须把项目部署到服务器上面。
        # 然后添加参数:client-ip: 上线后的服务器IP地址(你的应用IP)
        dashboard: localhost:8080
        # 微服务与Sentinel组件对应的操作端口
        port: 8719
management:
  endpoints:
    web:
      exposure:
        include: '*'
feign:
  sentinel:
    enabled: true # 激活Sentinel对Feign的支持

 

2、配置Nacos  

  • resource:资源名称;
  • limitApp:来源应用;
  • grade:阈值类型,0表示线程数, 1表示QPS;
  • count:单机阈值;
  • strategy:流控模式,0表示直接,1表示关联,2表示链路;
  • controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
  • clusterMode:是否集群
[{
    "resource": "customerBlockHandler",
    "limitApp": "default",
    "grade": 1,
    "count": 1, 
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
}]

配置多个资源:

[
    {
        "resource": "customerBlockHandler",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    },
    {
        "resource": "byResource",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

然后直接在Sentinel控制台刷新就可以看到配置的流控规则了:

3、测试

重启cloud-alibaba-sentinel-service-8401,发现配置的流控规则还存在

http://localhost:8401/rateLimit/customerBlockHandler

 10.3、Seata

  Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官网https://seata.io/zh-cn/

下载地址https://seata.io/zh-cn/blog/download.html

本例使用的是Centos下:Seata1.4.2,在我的项目源码处就可以找到源码文件和seata服务端压缩包:seata-1.4.2.zip、seata-server-1.4.2.tar.gz

下载地址:

能干嘛

  • 一个典型的分布式事务过程
  • 分布式事务处理过程的一ID+三组件模型:
  • Transaction ID XID 全局唯一的事务ID

三组件概念

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

处理过程:

  • TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
  • XID在微服务调用链路的上下文中传播;
  • RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
  • TM向TC发起针对XID的全局提交或回滚决议;
  • TC调度XID下管辖的全部分支事务完成提交或回滚请求。

10.3.1、Seata-Server安装

以下使用Centos下的Nacos和Seata进行安装部署,Nacos这里我已经搭建好了,不懂的可以看上面搭建Nacos的教程

MySQL版本是:5.7

1、将压缩包放到服务器,并解压

cd /usr/local/
mkdir seata/
tar -zxvf seata-server-1.4.2.tar.gz

2、创建数据库,依照官网给的SQL生成表  

mysql.sql位置:源码\script/server/db/mysql.sql

创建Seata数据库,然后执行上述SQL即可: create database seata 

3、Nacos创建Seata命名空间

此处记下命名空间的ID,后序需要用到Nacos-Namespace的地方,都写这个命名空间ID:500b6131-1f25-4fb5-a8cb-483f2ab8b76d

4、修改Seata配置文件

cd /usr/local/seata/seata/seata-server-1.4.2/conf
vim file.conf

vim registry.conf

5、Nacos添加Seata配置文件信息

配置列表-->添加-->seataServer.properties

配置内容位置:Seata源码/seata-1.4.2/script/config-center/config.txt

复制里面的全部内容到Nacos中,需要修改内容的地方如下:

6、启动Seata

nohup sh ./bin/seata-server.sh -p 8091 -h 182.92.209.212 2>&1 &

参数说明:

Seata在Nacos中注册成功~

 10.3.2、业务数据库准备

以下演示都需要先启动Nacos后启动Seata,保证两个都OK。

分布式事务业务说明

这里我们会创建三个服务:一个订单服务一个库存服务一个账户服务

当用户下单时,会在订单服务中创建一个订单, 然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

一言蔽之,下订单—>扣库存—>减账户(余额)。

创建业务数据库

  • seata_ order:存储订单的数据库;
  • seata_ storage:存储库存的数据库;
  • seata_ account:存储账户信息的数据库。

直接执行下面的SQL即可:

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

CREATE TABLE seata_order.t_order (
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
    `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
    `count` INT(11) DEFAULT NULL COMMENT '数量',
    `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
    `status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


CREATE TABLE seata_storage.t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');


CREATE TABLE seata_account.t_account(
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
	`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
	`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
	`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');

-- SEATA AT模式需要undo_log表,所以在你自己的数据库中创建该表保证事务正常运行
CREATE TABLE seata_order.`undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE seata_storage.`undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE seata_account.`undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


SELECT * FROM seata_order.t_order;
SELECT * FROM seata_storage.t_storage;
SELECT * FROM seata_account.t_account;

 10.3.3、订单模块:seata_order

注意,此处的版本信息可以完全按照我的源码上面进行使用,这里贴一下使用到的Cloud版本

<!--spring cloud Hoxton.SR1-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Hoxton.SR1</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.2.2.RELEASE
    在2.2.2版本,才支持参数:web-context-unify: false
    这里对alibaba版本做了升级
-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.2.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

1、新建模块:seata-order-service-2001

2、pom

<dependencies>
    <!--nacos-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--seata-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <!--排除seata依赖-->
            <exclusion>
                <artifactId>seata-all</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--单独引入Seata依赖-->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.4.2</version>
    </dependency>
    <!--feign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--web-actuator-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--mysql-druid-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.37</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3、配置文件  

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: 43.143.195.160:8848
        username: nacos
        password: nacos
        namespace: 500b6131-1f25-4fb5-a8cb-483f2ab8b76d
        group: SEATA_GROUP
  # 订单数据源配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://182.*****:3306/seata_order
    username: root
    password: xxx

seata:
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  #对应seataServer.properties中的service.vgroupMapping.my_test_tx_group 集群分组
  enable-auto-data-source-proxy: false
  config:
    type: nacos
    nacos:
      server-addr: 43.143.195.160:8848
      group: SEATA_GROUP
      namespace: 500b6131-1f25-4fb5-a8cb-483f2ab8b76d # seata-server的配置文件的命名空间ID
      dataId: seataServer.properties       # seata-server在nacos的配置dataId
      username: nacos
      password: nacos
  registry:
    type: nacos   #注册中心类型:nacos
    nacos:
      server-addr: 43.143.195.160:8848    # nacos的地址端口
      group : SEATA_GROUP            # seata-server在nacos的分组
      namespace: 500b6131-1f25-4fb5-a8cb-483f2ab8b76d           # seata-server在nacos的命名空间ID
      username: nacos                # nacos账号
      password: nacos                # nacos密码

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath*:mapper/*.xml

 4、主启动

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 订单主启动类
 */
@EnableDiscoveryClient
@EnableFeignClients
//取消数据源的自动创建,而是使用自己定义的
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan("com.zhixi.dao")
public class SeataOrderMainApp2001 {

    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

5、配置类: com.zhixi.config.DataSourceProxyConfig

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;

/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

6、实体

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 封装返回结果
 *
 * @param <T> 返回结果类型
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code, String message) {
        this(code, message, null);
    }
}

 

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 订单实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    // 订单id
    private Long id;
    // 用户id
    private Long userId;
    // 产品id
    private Long productId;
    // 产品数量
    private Integer count;
    // 产品单价
    private BigDecimal money;
    // 订单状态:0:创建中;1:已完结
    private Integer status;
}

7、dao层接口 

import com.zhixi.pojo.Order;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderDao {
    //1 新建订单
    void create(Order order);

    //2 修改订单状态,从零改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);

    //3 查询订单
    Order getOrderById(@Param("id") Long id);
}

8、mapper/OrderMapper.xml  

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.zhixi.dao.OrderDao">

    <resultMap id="BaseResultMap" type="com.zhixi.pojo.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="create">
        insert into t_order (id,user_id,product_id,count,money,status)
        values (null,#{userId},#{productId},#{count},#{money},0);
    </insert>


    <update id="update">
        update t_order set status = 1
        where user_id=#{userId} and status = #{status};
    </update>

    <select id="getOrderById" resultType="com.zhixi.pojo.Order">
        select * from t_order where id = #{id}
    </select>

</mapper>

9、Service:通过OpenFeign进行远程接口调用

import com.zhixi.pojo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "seata-account-service")
public interface AccountService {
    /**
     * 扣减账户余额
     *
     * @param userId 用户id
     * @param money  金额
     * @return 结果
     */
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

 

import com.zhixi.pojo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service")
public interface StorageService {
    /**
     * 扣减库存
     *
     * @param productId 产品id
     * @param count     数量
     * @return 结果
     */
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

 

import com.zhixi.pojo.Order;

public interface OrderService {
    void create(Order order);

    Order getOrderById(Long id);
}

com.zhixi.service.impl.OrderServiceImpl

import com.zhixi.dao.OrderDao;
import com.zhixi.pojo.Order;
import com.zhixi.service.AccountService;
import com.zhixi.service.OrderService;
import com.zhixi.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改状态
     * GlobalTransactional:
     * name: 全局事务名字,唯一标识,
     * rollbackFor: 回滚异常类型
     */
    @Override
    //@GlobalTransactional(name = "create-order-tm", rollbackFor = Exception.class)
    public void create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        orderDao.create(order);

        //2 扣减库存
        log.info("----->订单微服务开始调用库存,做扣减Count");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->订单微服务开始调用库存,做扣减end");

        //3 扣减账户
        log.info("----->订单微服务开始调用账户,做扣减Money");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->订单微服务开始调用账户,做扣减end");

        //4 修改订单状态,从0到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(), 0);
        log.info("----->修改订单状态结束");

        log.info("----->下订单结束了,O(∩_∩)O哈哈~");

    }

    @Override
    public Order getOrderById(Long id) {
        return orderDao.getOrderById(id);
    }
}

10、Controller 

import com.zhixi.pojo.CommonResult;
import com.zhixi.pojo.Order;
import com.zhixi.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult<Order> create(Order order) {
        orderService.create(order);
        return new CommonResult<>(200, "订单创建成功");
    }
    // 根据id查询订单
    @GetMapping("/order/getOrderById")
    public CommonResult<Order> getOrderById(Long id) {
        Order order = orderService.getOrderById(id);
        return new CommonResult<>(200, "订单查询成功", order);
    }
}

11、自测

1、启动项目,看是否有报错信息

2、看项目是否加入到Nacos的Seata命名空间中

3、接口访问测试:http://localhost:2001//order/getOrderById?id=1

10.3.4、库存模块:seata_storage

1、新建模块:seata-storage-service-2002

2、直接拿GitHub上面源码

只需要修改yml文件中的:Nacos、库存数据库连接信息即可

10.3.5、账户模块:seata_account

1、新建模块:seata-storage-service-2003

2、直接拿GitHub上面源码

只需要修改yml文件中的:Nacos、库存数据库连接信息即可

10.3.6、Seata分布式事务测试

启动微服务:seata-order-service-2001、seata-storage-service-2002、seata-account-service-2003

下订单 -> 减库存 -> 扣余额 -> 改(订单)状态

数据库初始情况:

正常下单

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 

超时异常,没加@GlobalTransactional

模拟AccountServiceImpl添加超时

import com.zhixi.dao.AccountDao;
import com.zhixi.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * 账户业务实现类
 * Created by zzyy on 2019/11/11.
 */
@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);


    @Resource
    AccountDao accountDao;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service中扣减账户余额开始");
        //模拟超时异常,全局事务回滚
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        accountDao.decrease(userId, money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }
}

修改完后,重启项目:seata-account-service-2003

继续访问接口:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

出现了超时异常

看下数据库:

故障情况

  • 当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1

  • 而且由于feign的重试机制,账户余额还有可能被多次扣减

超时异常,加了@GlobalTransactional

用@GlobalTransactional标注OrderServiceImpl的create()方法。

访问接口(出错):http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

可以看到事务数据已经回滚了

取消AccountServiceImpl超时异常,重启seata-account-service-2003再访问接口进行测试

数据库信息:

 

posted @ 2023-04-18 14:47  Java小白的搬砖路  阅读(166)  评论(0编辑  收藏  举报