Spring Cloud从入门到谈笑风生-上

2020-07-27  本文已影响0人  西海岸虎皮猫大人

实时更新中...

代码仓库:

https://gitee.com/iacs/scdemo.git

参考资料:

尚硅谷2020最新版SpringCloud(H版&alibaba)框架开发教程全套完整版从入门

1 微服务概述

概念

单一服务分为一组小的服务,每个服务运行中独立线程中,服务之间相互协调

维度

库存\订单\支付...

spring cloud是微服务架构的一站式解决方案
eureka已停更,注册发现推荐nacos

spring cloud技术栈

注册发现 eureka
负载调用 ribbon
熔断降级 hystrix
网关 zuul
配置 spring cloud config

版本

本教程使用spring boot 2.2.2和spring cloud H版SR1
java 8+
maven 3.5+

组件停更及替换

被动修复bugs\不再合并请求\不发布新版本
eureka停更->zk | consul(不推荐) | nacos(完美替换)
ribbon进入维护状态 -> loadbalancer
feign停更 -> openfeign(推荐)
hystrix停更 -> resilience4j(国外) | sentinel(国内推荐)
zuul -> gateway
config -> apolo(携程) | nacos(推荐)
bus -> nacos

参考资料

官方文档

2.项目搭建

2.1 建立父项目

新建maven项目scdemo

a.字符集

setting->editor->file encoding
全部选择utf-8

b.注解

settings->build->compiler->annotation processer
enable annotation proccessing

c.编译版本

settings->build->compiler->java compiler
target bytecode version: 8

d.隐藏文件

settings->editor->file types
ignore files and folders: 添加.iml;.idea;

e.依赖

pom.xml

...
    <packaging>pom</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>12</maven.compiler.source>
        <maven.compiler.target>12</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.10</lombok.version>
        <log4j.version>1.2.17</log4j.version>
        <mysql.version>8.0.18</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>2.1.1</mybatis.spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
                <version>3.0.0</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 阿里巴巴-->
            <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>
            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
                <scope>runtime</scope>
            </dependency>
            <!-- druid-->
            <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>
            <!--junit-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <!--log4j-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

dependencyManagement只声明,不引入实现

跳过maven单元测试

右侧maven projects点击闪电图标

2.2 服务提供者-支付模块

新建模块 scdemo-payment

a.依赖

pom.xml

<dependencies>
        <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.18</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>
b.配置

resources/application.yml

server:
  port: 8001

spring:
  application:
    name: scdemo-payment
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/scdemo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.dfun.demo.scdemo.payment.entity

c.启动类 cn.dfun.demo.scdemo.payment.PaymentApplication
@SpringBootApplication
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}
d.建表语句
CREATE TABLE `payment`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`serial` VARCHAR(20) DEFAULT '',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
e.实体类

订单实体类
cn.dfun.demo.scdemo.payment.entity.Payment

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    private Long id;
    private String serial;
}

统一结果类
cn.dfun.demo.scdemo.payment.entity.CommonResult

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;


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

    public CommonResult(ResultEnum resultEnum, T data) {
        this(resultEnum.getCode(), resultEnum.getMsg(), data);
    }

    public static CommonResult success() {
        return new CommonResult(ResultEnum.SUCCESS, null);
    }

    public static<T> CommonResult success(T data) {
        return new CommonResult(ResultEnum.SUCCESS, data);
    }

    public static<T> CommonResult success(String msg, T data) {
        return new CommonResult(ResultEnum.SUCCESS.getCode(), msg, data);
    }

    public static CommonResult fail() {
        return new CommonResult(ResultEnum.FAIL, null);
    }
}

结果枚举
cn.dfun.demo.scdemo.payment.entity.ResultEnum

@Getter
@AllArgsConstructor
public enum ResultEnum {
    SUCCESS(200, "操作成功"),
    FAIL(-1, "操作失败");

    private int code;
    private String msg;
}
f.mapper
<mapper namespace="cn.dfun.demo.scdemo.payment.dao.PaymentDao">
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values(#{serial})
    </insert>
    <resultMap id="BaseResultMap" type="cn.dfun.demo.scdemo.payment.entity.Payment">
        <id column="id" property="id" jdbcType="BIGINT" />
        <id column="serial" property="serial" jdbcType="VARCHAR" />
    </resultMap>
    <select id="getById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id=#{id}
    </select>
</mapper>
g.dao

cn.dfun.demo.scdemo.payment.dao.PaymentDao

@Mapper
public interface PaymentDao {
    int create(Payment payment);

    Payment getById(@Param("id") Long id);
}
h.service

cn.dfun.demo.scdemo.payment.service.PaymentService

public interface PaymentService {
    int create(Payment payment);

    Payment getById(Long id);
}
i.service impl

cn.dfun.demo.scdemo.payment.service.impl.PaymentServiceImpl

@Service
public class PaymentServiceImpl implements PaymentService {
    @Resource
    private PaymentDao paymentDao;

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

    @Override
    public Payment getById(Long id) {
        return paymentDao.getById(id);
    }
}
j.controller

cn.dfun.demo.scdemo.payment.controller.PaymentController

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @PostMapping(value="/payment/service")
    public CommonResult create(@RequestBody Payment payment) {
        int result = paymentService.create(payment);
        return result > 0 ? CommonResult.success(result) : CommonResult.fail();
    }

    @GetMapping(value="/payment/get/{id}")
    public CommonResult getById(@PathVariable Long id) {
        Payment payment = paymentService.getById(id);
        return payment != null ? CommonResult.success(payment) : CommonResult.fail();
    }
}
k.启动

启动项目,浏览器访问:
http://localhost:8001/payment/get/1

l.开启热部署

i.父模块pom添加插件

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
    </build>

ii.子模块添加依赖

        <!--热部署插件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

iii.设置
settings->build->compiler
勾选Display...和Build Project...
iv.ctrl + alt + shift + /,点击1.Registry...
勾选compiler.automake.allow.when.app.running
v.生产环境要关闭热部署

2.3 服务消费者-订单模块
a.新建模块

scdemo-order
pom复制scdemo-payment的pom,修改artifactId

b.配置

配置端口及数据源
虽然该模块目前没有用到数据源,但不配置会报错...

c.拷贝scdemo-payment的实体类
d.配置类

cn.dfun.demo.scdemo.order.config.ApplicationContextConfig

@Configuration
public class ApplicationContextConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
e.controller

cn.dfun.demo.scdemo.order.controller.OrderController

@RestController
@Slf4j
public class OrderController {
    public static final String PAYMENT_URL = "http://localhost:8001";
    @Resource
    private RestTemplate restTemplate;

    @PostMapping("/consumer/payment/create")
    public CommonResult create(@RequestBody Payment payment) {
        // 使用restTemplate调用服务提供者接口
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
    }

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

}
f.启动,测试

使用idea的run dashboard启动两个模块
使用postman测试
url: http://localhost/consumer/payment/create
headers:
Content-Type: application/json
body: raw

{
    "id": 1,
    "serial": "111"
}
2.4 抽取公用模块-common

a.新建模块scdemo-common

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>

b.copy实体类包到该模块下
c.其他两个模块依赖该模块

        <dependency>
            <groupId>cn.dfun</groupId>
            <artifactId>scdemo-common</artifactId>
            <version>${project.version}</version>
        </dependency>

d.修改mybatis实体类前缀
scdemo-payment模块application.yml

mybatis:
...
  type-aliases-package: cn.dfun.demo.scdemo.common.entity

3.注册中心

3.1 eureka注册中心

服务注册

注册中心配多个避免单点故障
服务与注册中心维持心跳

3.1.1 建立eureka server
a.新建模块

scdemo-eureka-server
依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
b.配置
server:
  port: 7001

eureka:
  instance:
    # eureka服务端实例名称
    hostname: localhost
  client:
    # 不向注册中心注册自己
    register-with-eureka: false
    # 不去检索服务
    fetch-registry: false
    # 服务查询地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
c.启动类

cn.dfun.demo.scdemo.eurekaserver.EurekaServerApplication

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
3.1.2 支付服务注册到eureka server
a.依赖

scdemo-payment pom添加

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
b.配置

添加

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: false
c.注解

启动类添加@EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient
public class PaymentApplication {
...

浏览器访问:
http://localhost:7001/
可以看到scdemo-payment服务

d 订单服务注册到eureka server

同支付服务类似
配置

    register-with-eureka: false
    fetch-registry: true
3.1.3 搭建eureka server集群

避免注册中心单点故障
原理是多个节点相互注册

a.配置
# 无参数启动
spring:
  application:
    name: eureka-server

server:
  port: 7001

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/

---
# 启动参数 --spring.profiles.active=server1
spring:
  profiles: server1
server:
  port: 7001
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7002/eureka/
---
# 启动参数 --spring.profiles.active=server2
spring:
  profiles: server2
server:
  port: 7002
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
---
b.启动

Run->Edit Configurations...
分别配置Program argumenents
--spring.profiles.active=server1
--spring.profiles.active=server2

c.服务注册到eureka server集群

修改payment和order的配置

#      defaultZone: http://localhost:7001/eureka
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
3.1.4 多实例注册到eureka server
a.多个端口启动

payment服务
VMOptions: -Dserver.port=8002
VMOptions: -Dserver.port=8001

b.@LoadBalanced注解

order模块restTemplate添加@LoadBalanced注解

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
c.payment模块controller注入端口号并打印
    @Value("${server.port}")
    private String serverPort;
 ...
    @GetMapping(value="/payment/get/{id}")
    public CommonResult getById(@PathVariable Long id) {
        Payment payment = paymentService.getById(id);
        String msg = "端口号: " + serverPort;
        return payment != null ? CommonResult.success(msg, payment) : CommonResult.fail();
    }
d.order模块修改controller payment服务地址
public static final String PAYMENT_URL = "http://SCDEMO-PAYMENT";

多次访问:
http://localhost/consumer/payment/get/1
可见访问了不同的端口号,默认轮询

e.修改服务在注册中心的显示
eureka:
  instance:
    # 服务名:ip:端口
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
f.actuator微服务信息

修改PaymentController
注入discoveryClient

    @Resource
    private DiscoveryClient discoveryClient;

打印相关信息


    @GetMapping(value="/payment/discovery")
    public Object discovery() {
        // 获取服务清单列表
        List<String> services = discoveryClient.getServices();
        for(String element: services) {
            log.info("############element:" + element);
        }
        // 获取实例信息
        List<ServiceInstance> instances = discoveryClient.getInstances("scdemo-payment");
        for(ServiceInstance instance : instances) {
            // 服务名 主机名 端口 uri
            log.info("##########" + instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
        }
        return this.discoveryClient;
    }
g.eureka保护机制

eureka控制台红字

EMERGENCY! EUREKA MAY BE...

当服务不可用时,eureka不会立即将服务剔除
有可能微服务健康,但eureka server网络不通
设计哲学是宁可保留错误的微服务,也不盲目清理

i.禁用自我保护
eureka server

eureka:
  server:
    # 禁用自我保护
    enable-self-preservation: false
    # 2秒踢除
    eviction-interval-timer-in-ms: 2000

eureka client

eureka:
  instance:
    # Eureka客户端向服务端发送心跳的时间间隔,默认是30秒
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳后等待时间上限,默认是90秒
    lease-expiration-duration-in-seconds: 2

3.2 zookeeper注册中心

3.2.1 centos7搭建zk
a.zk环境搭建

** 注意:
a.zk使用低版本会报错
b.要下载*bin.tar.gz版本

# 解压
tar -zxvf apache-zookeeper-3.5.8-bin.tar.gz -C /usr/local/
cd /usr/local
mv apache-zookeeper-3.5.8-bin/ zookeeper
cd zookeeper
# 创建数据文件目录
mkdir data

# 配置
cp conf/zoo_sample.cfg conf/zoo.cfg
vi conf/zoo.cfg
# 修改dataDir
dataDir=/usr/local/zookeeper/data

# 启动
./bin/zkServer.sh start
# 查看启动状态
./bin/zkServer.sh status
3.2.2 服务注册

payment模块

a.依赖

pom添加

        <!--SpringBoot整合Zookeeper客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        </dependency>
b.配置
server:
  port: 8004

spring:
  profiles: zk
...
  cloud:
    zookeeper:
      connect-string: 192.168.68.101:2181
c.启动类注解
@EnableDiscoveryClient
public class PaymentApplication {

启动
--spring.profiles.active=zk

d.验证
bin/zkCli.sh
ls /
# 显示服务目录
ls /services
# 显示服务注册信息
get /services/scdemo-payment/b603f451-7d50-42f0-b22d-9ef5bb3588fa

浏览器访问:
http://localhost:8004/payment/get/1
补充说明:
zk节点分临时和持久
关闭8004服务后一段时间,zk节点被删除,是临时节点

3.2.3 服务调用

order模块

a.依赖/配置/注解

参考payment模块

b.配置类

RestTemplate与Euraka配置方式相同
注意:
需要加@LoadBalanced注解,否则会报异常

c.启动

--spring.profiles.active=zk
浏览器访问:
http://localhost/consumer/payment/get/1

3.3 consul注册中心

3.3.1 consul概述及环境搭建
a.概述

go语言实现,服务发现和配置管理,支持多数据中心
官网: https://www.consul.io/docs
中文文档: https://www.springcloud.cc/spring-cloud-consul.html

b.环境搭建

下载: https://releases.hashicorp.com/consul/1.6.1/
consul_1.6.1_windows_amd64.zip
下载后直接解压
目录加入到环境变量

# 验证
consul --version
# 启动
consul agent -dev

浏览器访问: http://localhost:8500/

3.3.2 服务注册

payment模块

依赖

注掉zk依赖,添加consul依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
配置 application-consul.yml
server:
  port: 8006

spring:
  profiles: consul
  application:
    name: scdemo-payment
...
  # consul注册中心配置
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
...
启动

--spring.profiles.active=consul
访问控制台,可以看到注册的服务
测试调用:
http://localhost:8006/payment/get/1

3.3.3 服务消费

依赖\配置参考3.3.2,配置类和controller和zk部分一样

3.3.4 三个注册中心异同点

eureka是AP,主要保证高可用
consul和zk是CP,主要保证数据一致
分区容错性P必须保证
场景:
双十一主要保证可用,使用AP,允许部分节点返回旧值
CP数据同步失败时拒绝请求

4.ribbon-负载均衡

项目代码恢复到eureka注册中心(集群)

4.1 概述

客户端负载均衡组件,已进入维护模式
未来使用loadbalancer替换
ribbon是进程内LB,注册中心获取服务列表缓存到本地
nginx是集中式LB
算法: 轮询\随机\根据访问时间加权
新版eureka-client集成了ribbon,不需单独引入

4.2 getForObject和getForEntity区别

getForObject和getForEntity区别:
Object基本可以理解为json,Entity包含了响应头\状态码\响应体等,推荐使用Object

order模块controller添加方法

    @GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult getPaymentEntity(@PathVariable Long id) {
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        return entity.getStatusCode().is2xxSuccessful() ? entity.getBody() : CommonResult.fail();
    }
4.3 替换负载规则

自带算法七种:
轮询\随机\根据时间加权\过滤故障节点等
ribbon配置不能放到@Component组件能扫描到的当前包和子包下,不然会被所有ribbon客户端共享

配置类

order模块
cn.dfun.demo.scdemo.myrule.MyRule

@Configuration
public class MyRule {
    @Bean
    public IRule iRule() {
        // 随机
        return new RandomRule();
    }
}
启动类注解

** 注意这里name要配置服务提供者的名称

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="scdemo-payment", configuration = MyRule.class)
public class OrderApplication {
...
4.4 手写负载算法

恢复默认轮询算法,注掉@RibbonClient注解
轮询算法:
接口第几次请求数%集群总数=实际调用服务器位置下标
服务重启后rest接口计数从1开始

order模块注掉restTemplate配置类的@LoadBalancer注解
定义接口
cn.dfun.demo.scdemo.order.lb.LoadBalancer

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

实现类
cn.dfun.demo.scdemo.order.lb.MyLB
** 注意Component注解

@Component
public class MyLB implements LoadBalancer{
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            // 整数越界
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        } while(!this.atomicInteger.compareAndSet(current, next));
        System.out.println("***next: " + next);
        return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        // 当前计数对集群数取余
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

OrderController添加loadBalancer和discoveryClient注入,并添加实现接口

    @Resource
    private LoadBalancer loadBalancer;
    @Resource
    private DiscoveryClient discoveryClient;

...

    @GetMapping("/consumer/payment/lb")
    public String getPaymentLB() {
        List<ServiceInstance> instances = discoveryClient.getInstances("scdemo-payment");
        if(instances == null || instances.size() <= 0) {
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();
        return restTemplate.getForObject(uri + "/payment/lb", String.class);
    }

PaymentController添加相关接口

    @GetMapping(value="/payment/lb")
    public String lb() {
        return this.serverPort;
    }

启动,测试

5.Feign/OpenFeign-服务调用

5.1 概述

feign是一个声明式WebService客户端
feign已停止维护,推荐open feign
使用方法是定义一个服务接口然后在上面添加注解,支持可插拔式的编码器和解码器,spring cloud对feign进行了封装,使其支持spring mvc标准注解和HttpMessageConverter,Feign可与Ribbon结合

5.2 服务调用

新建模块: scdemo-order-feign
可从scdemo-order拷贝,删除冗余代码,注意修改pom中的artifactId和配置中的应用名,并将子模块添加到父模块pom

依赖

在原有基础上添加

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

配置文件无修改,删除配置文件类(RestTemplate配置)

启动类注解

添加@EnableFeignClients

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderFeignApplication {
...
定义feign接口

cn.dfun.demo.scdemo.order.service.PaymentFeignService
注意@Component注解和@PathVariable("id")

@Component
@FeignClient(value="scdemo-payment")
public interface PaymentFeignService {
    @GetMapping(value="/payment/get/{id}")
    CommonResult getById(@PathVariable("id") Long id);
}
controller
@RestController
@Slf4j
public class OrderController {
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult getPayment(@PathVariable Long id) {
        return paymentFeignService.getById(id);
    }
}

启动,测试可见自带ribbon负载均衡

5.3 超时控制

提供者和消费者要约定好超时时间

a.模拟超时异常

payment模块controller添加接口

    @GetMapping(value="/payment/timeout")
    public String timeout() {
        try {
            // 睡眠3秒,模拟超时
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return serverPort;
    }

order模块feign接口添加

    @GetMapping(value="/payment/timeout")
    String timeout();

order模块controller添加

    @GetMapping(value="/consumer/payment/timeout")
    public String timeout() {
        return paymentFeignService.timeout();
    }

启动,访问: http://localhost/consumer/payment/timeout
报错:

feign.RetryableException: Read timed out executing GET http://scdemo-payment/payment/timeout
...
b.超时时间设置

order模块application.yml添加

ribbon:
  # 建立连接所用的时间
  ReadTimeout: 5000
  # 建立连接后服务读取到可用资源所用的时间
  ConnectTimeout: 5000

再次访问a中接口,报错解除

5.4 日志增强
日志级别

NONE 默认,不显示日志
BASIC 仅记录请求方法\url\状态码及执行时间
HEADERS BASIC+请求和响应的头信息
FULL HEADERS+请求和响应的正文及元数据

配置类
cn.dfun.demo.scdemo.order.config.FeignConfig

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

配置

logging:
  level:
    # feign日志级别及监控的端口
    cn.dfun.demo.scdemo.order.service.PaymentFeignService: debug

访问: http://localhost/consumer/payment/get/1
可见控制台打印出了详细日志

6.断路器-Hystrix

6.1 概述

处理延迟和容错
停更进维,但设计优秀,重要,推荐resilience4j(头条在用其他大厂很少)和sentinel
服务链路复杂,单个服务不可用或响应时间长造成服务雪崩
hystrix类似保险丝,通过故障监控,返回备选响应,避免雪崩
hystrix提供服务降级\熔断和接近实时的监控

降级

对方系统不可用,给友好提示
程序异常\超时\服务熔断\线程池|信号量打满等情况

熔断

达到最大访问号,拒绝访问,调用服务降级给友好提示
降级->熔断->恢复

限流

秒杀高并发等,严禁拥挤,排队处理

6.2 提供者高延迟高并发场景模拟
a.新建scdemo-payment-hystrix模块

可从scdemo-payment模块copy
依赖在scdemo-payment基础上添加

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
b.service

简单起见只写实现类
cn.dfun.demo.scdemo.payment.service.PaymentService
paymentInfoOK为立即返回的接口
paymentInfoTimeout模拟返回时间较长的方法

    public String paymentInfoOK(Integer id) {
        return "线程池: " + Thread.currentThread().getName() + " paymentInfoOK, id: " + id;
    }

    public String paymentInfoTimeout(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: " + Thread.currentThread().getName() + " paymentInfoTimeout, id: " + id + ", 耗时: " + timeNumber;
    }
c.controller
@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

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

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeout(@PathVariable Integer id) {
        String result = paymentService.paymentInfoTimeout(id);
        log.info("*****result: " + result);
        return result;
    }
}
d.启动项目,压测

启动jmeter
i.Test Plan右键->Add->Threads->Thread Group
Number of Threads: 200
Loop Count: 100
ii.HTTP Request右键->Add->Sampler->HTTP Request
Server Name or IP: localhost
Port Number: 8001
Path: /payment/hystrix/timeout/1
点击工具栏绿色三角按钮启动
iii.访问: http://localhost:8001/payment/hystrix/ok/1
可见timeout接口高延迟请求堆积导致系统资源占用,其他接口响应也变慢

6.3 消费者高延迟场景

a.新建模块scdemo-order-hystrix

在scdemo-order-feign基础上拷贝,修改pom artifactId,配置文件应用名,父模块引入该子模块
** 注意:
如果未修改artifactId就mvn import会报错,恢复比较麻烦,建议删除idea工程文件后重新导入maven项目,并重新进行项目的各项配置
这里使用eureka server单实例

  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
#      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka

添加依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
b.feign接口
@Component
@FeignClient(value="scdemo-payment-hystrix")
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfoOK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeout(@PathVariable("id") Integer id);
}
c.controller
@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

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

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

}
d.压测提供者接口导致消费者接口高延迟

参考6.2.d使用jmeter对提供者/hystrix/timeout接口压测,访问消费者接口:
http://localhost/consumer/payment/hystrix/ok/1
可见明显延迟甚至报错

6.4 降级

超时不等待,出错友好提示
解决:
i.提供者超时,消费者不能一直卡死等待,须服务ii.降级;
iii.提供者down机,消费者不能一直等待,须服务降级;
提供者服务ok,消费者故障或者有自我要求,自处理降级

a.提供者降级

service超时方法添加@HystrixCommand注解,并实现handler方法

    @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
            // 超时时间
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="3000")
    })
    public String paymentInfoTimeout(Integer id) {
...
    public String paymentInfoTimeoutHandler(Integer id) {
        // 友好提示系统繁忙等
        return "线程池: " + Thread.currentThread().getName() + " paymentInfoTimeoutHandler, id: " + id;
    }

启动类添加@EnableCircuitBreaker注解

...
@EnableCircuitBreaker
public class PaymentHystrixApplication {
...

消费者调用:http://localhost/consumer/payment/hystrix/timeout/1
可见触发降级,方法抛异常和超时都会降级

b.消费者降级

降级处理可以加在提供着,也可加在消费者
一般加在消费端
对hystrix注解修改热部署不生效建议重启服务
消费者controller添加@HystrixCommand,并实现handler方法

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="1500")
    })
    public String paymentInfoTimeout(@PathVariable("id") Integer id) {
...

    public String paymentInfoTimeoutHandler(Integer id) {
        return "消费者降级: 线程池: " + Thread.currentThread().getName() + " paymentInfoTimeoutHandler, id: " + id;
    }

启动类添加@EnableCircuitBreaker

...
@EnableCircuitBreaker
public class OrderHystrixApplication {
...

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

由于消费者超时时间设为1.5秒,小于提供者的超时时间,所以直接触发消费者降级.

c.降级方法膨胀-通用降级方法

如果,每个方法都配置降级方法会导致方法膨胀,可配置一个全局通用的默认降级方法
消费者controller加@DefaultProperties,并实现通用降级方法

...
@DefaultProperties(defaultFallback = "paymentInfoGlobalHandler")
public class OrderHystrixController {
...
    public String paymentInfoGlobalHandler() {
        return "Global异常处理...";
    }
d.降级方法与业务逻辑耦合-feignclient fallback

消费者添加配置配置

# feign开启降级处理
feign:
  hystrix:
    enabled: true

继承feign接口,实现fallback类
cn.dfun.demo.scdemo.order.service.PaymentHystrixService

@Component
@FeignClient(value="scdemo-payment-hystrix", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfoOK(@PathVariable("id") Integer id);

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

@FeignClient指定fallback属性

@FeignClient(value="scdemo-payment-hystrix", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
...

验证,关闭提供者,然后访问消费者接口:
http://localhost/consumer/payment/hystrix/ok/1
可见直接进入了fallback类的方法

6.5 熔断

当链路某个微服务出错或者响应时间长,进行服务降级,熔断该服务的调用
服务可用后自动恢复
熔断源于Martin Fowler,相关资料(慢):
https://martinfowler.com/bliki/CircuitBreaker.html
半开状态尝试请求响应,如果可用则恢复

a.提供者添加service方法配置@HystrixCommand属性,并实现降级方法

cn.dfun.demo.scdemo.payment.service.PaymentService

@Service
public class PaymentService {
...
    /**
     *  服务熔断
     */
    @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")
            // 以上参数表示10秒内失败超过6次跳闸
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
        if(id < 0) {
            throw new RuntimeException("*****id不能为负");
        }
        // hutool,国产工具类包,https://www.hutool.cn/docs/#/
        String serialNumber = IdUtil.simpleUUID();
        return Thread.currentThread().getName() + "\t" + "调用成功, 流水号: " + serialNumber;
    }

    public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
        return "paymentCircuitBreakerFallback: id不能负数,请稍后再试, id: " + id;
    }

}
b.提供者controller
    /**
     * 服务熔断
     */
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable Integer id) {
        String result = paymentService.paymentCircuitBreaker(id);
        return result;
    }

测试,访问:
http://localhost:8001/payment/circuit/1
id为整数,服务可用
当短时间连续访问
http://localhost:8001/payment/circuit/-1
触发熔断,再次访问:
http://localhost:8001/payment/circuit/1
发现服务不可用被降级处理
经过一段时间后服务自动恢复

c.其他参数说明:

请求总数阈值,默认20,即请求时间内,必须满足总数阈值才会熔断
默认阈值10秒超过20个请求
默认失败率10秒超过50%的请求失败
一段时间(默认5秒)后,断路器是半开状态,会让一个请求进行转发,如果成功断路器会关闭

6.6 限流

略,参见Sentinel章节

6.7 仪表盘

提供准实时监控,报表形式
新建模块scdemo-payment-hystrix-dashboard
添加依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>

配置

server:
  port: 9001

spring:
  application:
    name: scdemo-payment-hystrix-dashboard

启动类注解

@SpringBootApplication
@EnableHystrixDashboard
public class PaymentHystrixDashboardApplication {
...

http://localhost:9001/hystrix

6.8 仪表盘监控服务

对scdemo-payment-hystrix模块进行修改
需要保证服务依赖了
spring-boot-starter-actuator
消费者启动类注入Bean

...
public class PaymentHystrixApplication {
main方法...

    /**
     * Hystrix升级的坑,此配置不必深究
     */
    @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;
    }
}

hystrix仪表盘填入url和名称
http://localhost:8001/hystrix.stream
Title随便填
访问payment /payment/circuit/接口(发起多次失败请求),并实时查看仪表盘状态
仪表盘:
7色一圈一线

7.网关Gateway

7.1 概述

zuul过时,zuul2跳票,弃用
gateway,新一代网关,spring社区自研
基于spring boot2.0\webflux\project reactor
网关可以做反向代理\鉴权\监控等
webflux基于netty(Reactor模式)

gateway特性

动态路由\断言\过滤器\支持websocket
非阻塞io(zuul1为阻塞io)

7.2 三大核心概念

路由-Route

构建网关的基本模块
由ID\目标uri\一系列断言和过滤器组成,如果断言为true则匹配该路由

断言-Predicte

参考java8的java.util.function.Predicte
匹配HTTP请求所有内容(请求头和参数),请求与断言相匹配则进行路由

过滤-Filter

在请求被路由之前或者之后进行处理

7.3 入门配置

新建模块,scdemo-gateway
依赖
** 注意不能依赖web和acurator

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <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-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--热部署插件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

配置

server:
  port: 9527
spring:
  application:
    name: scdemo-gateway
  cloud:
    gateway:
      routes:
        # 路由id,没有固定规则但要求唯一,建议配合服务名
        - id: payment_route
          # 匹配后提供服务的路由地址
          uri: http://localhost:8001
          predicates:
            # 断言,路径相匹配的进行路由
            - Path=/payment/get/**
        - id: payment_route2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/lb/**

eureka:
  instance:
    hostname: scdemo-gateway
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
          defaultZone: http://localhost:7001/eureka

启动相关项目,访问:
http://localhost:9527/payment/get/1
http://localhost:9527/payment/lb
可见通过网关路由到了微服务

7.4 编码方式实现路由转发

cn.dfun.demo.scdemo.gateway.config.GatewayConfig

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        // 映射地址转发
        routes.route("path_route_dfun", r -> r.path(
                "/guonei").uri("http://news.baidu.com/guonei")).build();
        return routes.build();
    }
}

浏览: http://localhost:9527/guonei
可见路由被转发到了相应地址

7.5 通过微服务名动态路由

问题: 路由写死无法实现负载均衡
scdemo-payment模块启动两个实例8001和8002
VM Options: -Dserver.port=8002

修改网关配置

i 开启动态路由
ii 替换uri

spring:
  application:
    name: scdemo-gateway
  cloud:
    gateway:
      discovery:
        locator:
          # 开启从注册中心动态创建路由功能,根据微服务名路由
          enabled: true
      routes:
        # 路由id,没有固定规则但要求唯一,建议配合服务名
        - id: payment_route
          # 匹配后提供服务的路由地址
          uri: lb://scdemo-payment
#          uri: http://localhost:8001
          predicates:
            # 断言,路径相匹配的进行路由
            - Path=/payment/get/**
        - id: payment_route2
          uri: lb://scdemo-payment
#          uri: http://localhost:8001
          predicates:
            - Path=/payment/lb/**

验证:
多次访问:
http://localhost:9527/payment/lb
可见端口切换

7.6 断言-Predicate

配置

          predicates:
            - Path=/payment/lb/**
            # 在该时间之后访问才有效 Before和Between的使用类似
#            - After=2020-07-27T18:25:25.078+08:00[Asia/Shanghai]
            # 需要携带此cookie才能访问
#            - Cookie=username,vincent
            # 请求头要有X-Request-Id且属性值为整数的正则表达式
            - Header=X-Request-Id, \d+
            # 此外还有Host\Method\Path等

获取上述时间格式的测试类

public class T2 {
    public static void main(String[] args) {
        ZonedDateTime zbj = ZonedDateTime.now();
        System.out.println(zbj);
    }
}

配置文件predicates注释逐行放开,使用curl测试

# 测试cookie
curl http://localhost:9527/payment/lb --cookie "username=vincent"
# 测试header
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"

7.7 过滤器-Filter

官方提供了一系列的GatewayFilter

生命周期: Pre\Post
类型: 单一\全局

Filter作用:
全局日志记录\统一网关鉴权等

自定义过滤器

cn.dfun.demo.scdemo.gateway.filter.MyLogGatewayFilter
实现GlobalFilter, Ordered接口

@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
    /**
     * 过滤方法
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("come in MyLogGatewayFilter: " + new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if(uname == null) {
            log.info("******非法用户*******");
            // 不被接受状态码
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    /**
     * 加载过滤器顺序,数值越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

浏览器访问:
http://localhost:9527/payment/lb?uname=vincent
可以正常访问,去掉uname参数无法访问

8.配置中心-Spring Cloud Config

8.1 概述

cloud config\cloud bus没有停更进维,但渐渐被nacos渠道
nacos,注册中心\配置\bus->稳了

问题

多模块引用同一配置(如数据库配置)以及多环境配置,修改繁琐,重复配置
spring cloud config提供中心化的外部配置
服务端是独立微服务应用,为客户端提供配置信息,以及加密\解密等
客户端启动时从配置中心加载配置,默认通过git存储(有助于版本控制,可通过git客户端方便的管理)

config功能

集中管理配置
多环境配置,动态更新
运行期间动态调整
REST接口暴露配置信息

8.2 服务端获取配置信息

a.新建仓库

gitee(网速考虑)新建仓库,scdemo-config

# 克隆
git clone https://gitee.com/iacs/scdemo-config.git
b.仓库添加三个配置文件

config-dev.yml

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

config-test.yml

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

config-prod.yml

config:
  info: master branch,scdemo-config/config-prod.yml version=1
c.新建模块

scdemo-config-server模块
依赖

<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</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>
        <!--热部署插件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

配置

server:
  port: 3344

spring:
  application:
    name: scdemo-config-server
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/iacs/scdemo-config.git
          username: wu_peizhen@126.com
          password: ww458141458141
          search-paths:
            - scdemo-config
      # 读取分支
      label: master


# 服务注册到Eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

启动类
cn.dfun.demo.scdemo.configserver.ConfigServerApplication

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

启动eureka和config模块,访问:

格式: /{label}/{application}-{profile}.yml

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

省略分支,默认master

http://localhost:3344/config-dev.yml
可见配置内容
访问:
http://localhost:3344/config/dev/master
可返回json串

8.3 客户端获取配置信息

新建模块

scdemo-config-client
依赖与server相同,只需把spring-cloud-config-server修改为

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
配置

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

server:
  port: 3355

spring:
  application:
    name: scdemo-config-client
  # config客户端配置
  cloud:
    config:
      # 分支名称
      label: master
      # 配置文件名称
      name: config
      # 读取后缀名称
      profile: dev
      # 配置中心地址
      uri: http://localhost:3344

# 服务注册到Eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

c.启动类

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientApplication.class, args);
    }
}

启动项目,访问: http://localhost:3355/configInfo
可以获取到配置信息
修改bootstrap.yml的分支名称和profile即可获取其他分支其他环境的配置

8.4 配置实时刷新

问题1:

修改git上配置文件的内容,服务端获取的配置实时更新但客户端接口获取的配置没有实时更新
客户端重启后可以获取,但生产环境重启繁琐

curl -X POST "http://localhost:3355/actuator/refresh"
客户端配置实时更新

i. 确保依赖了spring-boot-starter-actuator
ii. 配置

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

iii. controller添加@RefreshScope注解

@RefreshScope
public class ConfigClientController {
...

此时配置仍无法实时刷新,需要发送post请求刷新
iv.post请求刷新配置

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

如果有多台客户端,需要写多个脚本刷新多个客户端配置,繁琐;
使用消息总线解决.

9.消息总线-Spring Cloud Bus

9.1 概述

可实现配置广播刷新,并对个别实例差异化处理
支持两种消息代理: rabbitmq和kafka
使用消息代理构建共用消息主题,消息被所有实例监听和消费

基本原理

实例监听mq中同一个topic,一个服务刷新数据时,把信息放入topic中,其他实例得到通知更新自身配置

9.2 rabbitmq环境搭建

centos7安装rabbitmq
参考: RabbitMQ 2.5节

9.3 Bus动态刷新全局广播配置

两种设计思想

i.消息总线触发一个客户端refresh,从而刷新所有客户端配置
ii.消息总线触发一个服务端refresh,从而刷新所有客户端

选型

i打破了微服务唯一职责性,破坏各节点对等性
i具有局限性,如微服务迁移时,网络地址发生变化,要做到自动刷新,就会增加更多的修改
因此选择ii

项目配置

i. scdemo-config-server模块
添加依赖

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

添加配置
** 注意:
rabbitmq是spring的子项,否则项目不会报错,但执行刷新请求会报错,项目会连接localhost:5672的mq地址

spring:
  # rabbitmq相关配置
  rabbitmq.:
    host: 192.168.68.101
    port: 5672
    username: guest
    password: guest

# rabbitmq相关配置,暴露bus刷新配置端点
management:
  endpoints:
    web:
      exposure:
        include: "bus-refresh"

ii. scdemo-config-client模块
添加依赖

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

添加配置

spring:
  # rabbitmq相关配置
  rabbitmq.:
    host: 192.168.68.101
    port: 5672
    username: guest
    password: guest

scdemo-config-client启动3355和3366两个实例
此时修改git配置,config-server实时更新,两个客户端实例不更新
iii.执行脚本

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

可见两个客户端配置同时被刷新
rabbitmq管控台可以看到springCloudBus exchange

9.4 Bus动态刷新定点通知

# 通过路径指定实例: scdemo-config-client:3355
curl -X POST "http://localhost:3344/actuator/bus-refresh/scdemo-config-client:3355"

附: 异常

Failed to configure a DataSource: 'url' attribute...

1.mvn install后问题解决
2.项目依赖了mybatis的druid的starter但没有配置数据库,去掉依赖后正常启动

Caused by: org.eclipse.jgit.api.errors.TransportException: https://gitee.com/iacs/scdemo-config.git: Authentication is required but no CredentialsProvider has been registered
上一篇下一篇

猜你喜欢

热点阅读