Spring Cloud从入门到谈笑风生-上
实时更新中...
代码仓库:
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 {
...
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