改动代码实时编译出来就可以运行,及时看到修改内容,自动化重启
如何使用呢?
第一步,把依赖粘贴进子工程pom
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
第二部,把插件加入父工程pom
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
第三步,idea开启自动编译权限
第四步,配置自动编译
不同版本idea之间或许会有些许差异,找到名字勾上就行
注意:这个功能仅限在开发阶段使用,实际上线部署后这个功能必须关闭
1.建module
2.改pom
3.写yml
4.主启动
5.业务类
新建module
建完之后的样子
子工程的pom文件
子工程的yml文件
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
# 当前数据源操作类型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驱动类
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3307/db2019?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
mybatis:
mapper-locations: classpath*:mapper/*.xml
#实体类
type-aliases-package: com.eiletxie.springcloud.entities
主启动类
package com.springcloud.cc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment {
private Long id;
private String serial;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String msg;
private T data;
//介于上面已经有了全参和空参的构造方法,这里再加一个两个参数只有code和msg的构造方法
public CommonResult(Integer code, String msg) {
this(code, msg, null);
}
}
dao层以及对应mapper文件
@Mapper
public interface PaymentDao {
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springcloud.cc.dao.PaymentDao">
<insert id="create" parameterType="com.springcloud.cc.eneties.Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment (serial)
Values (#{serial});
insert>
<resultMap id="BaseResultMap" type="com.springcloud.cc.eneties.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 payment
where id = #{id};
select>
mapper>
业务类
public interface PaymentService {
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}
业务实现类
@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);
}
}
controller层
注意有个@RequestBody注解,把地址栏里的内容匹配成对象
@RestController
@Slf4j
@RequestMapping("payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping("/create")
//注意有个@RequestBody注解,把地址栏里的内容匹配成对象
public CommonResult create(@RequestBody Payment payment){
int result = paymentService.create(payment);
log.info("插入结果"+result);
if (result > 0) {
return new CommonResult(200, "插入OK",result);
} else {
return new CommonResult(400, "error",null);
}
}
@GetMapping("/getPaymentById/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id){
Payment payment=paymentService.getPaymentById(id);
log.info("查询结果"+payment.toString());
if (payment!=null){
return new CommonResult(200, "查询成功", payment);
}else {
return new CommonResult(400, "查询失败", payment);
}
}
}
这个模块的整体分层结构
通过APIFox测试controller接口
工程结构和上面的差不多
先说一下RestTemplate
使用的话要加入restTemplate配置类
@Configuration
public class ApplicationContextConfig {
//这里记得用@Bean把这个方法注册到容器,不然在Controller里面无法自动注入
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller层
@RestController
@Slf4j
@RequestMapping("consumer/payment")
public class OrderController {
public static final String Primart_Url = "http://localhost:8001";
@Resource
private RestTemplate restTemplate;
@GetMapping("create")
public CommonResult<Payment> commonResult(Payment payment){
//这里通过拼接就可以访问到支付服务端url:http://localhost:8001/payment/create
return restTemplate.postForObject(Primart_Url + "/payment/create", payment, CommonResult.class);
}
@GetMapping("get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
//这里通过拼接就可以访问到支付服务端url:http://localhost:8001/payment/getPaymentById/{id}
return restTemplate.getForObject(Primart_Url + "/getPaymentById/" + id, CommonResult.class);
}
}
测试controller接口
观察问题:两个微服务的实体类部分是通用的,这就涉及到了新的内容,工程重构
观察cloud-consumer-order80与cloud-provider-payment8001两工程有重复代码(entities包下的实体)(坏味道),重构。
1.新建 - cloud-api-commons
2.POM
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springCloudartifactId>
<groupId>com.springcloud.ccgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-api-commonsartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.1.0version>
dependency>
dependencies>
project>
3.entities
将cloud-consumer-order80与cloud-provider-payment8001两工程的公有entities包移至cloud-api-commons工程下。
4.maven clean、install cloud-api-commons工程,以供给cloud-consumer-order80与cloud-provider-payment8001两工程调用。
5.订单80和支付8001分别改造
将cloud-consumer-order80与cloud-provider-payment8001两工程的公有entities包移除
引入cloud-api-commons依赖
<dependency>
<groupId>com.lun.springcloudgroupId>//安照你自己工程内的内容对应修改,不能一模一样
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
Eureka主管服务注册
就像上面的生产者和消费者的模式,两个还好,如果一多了,就会出现混乱难以管理的情况。这个时候就需要服务治理的情况。
Eureka Server提供服务注册服务
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
EurekaClient通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
注:“心跳”指的是一段定时发送的自定义信息,让对方知道自己“存活”,以确保连接的有效性。大部分 CS 架构的应用程序都采用了心跳机制,服务端和客户端都可以发心跳。通常情况下是客户端向服务器端发送心跳包,服务端用于判断客户端是否在线。
默认端口号7001
改pom引入Eureka Server端依赖,新老版本的依赖对比
以前的老版本(当前使用2018),没有标示,无法分辨是服务端还是客户端
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
现在新版本(当前使用2020.2),新版本加入了server的标示,能够对服务端进行区分
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
yml文件配置内容
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己。
register-with-eureka: false
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
编写完毕后,要在主启动类上加入注解,来标示这个服务是Eureka的注册中心
@EnableEurekaServer
浏览器访问7001端口:http://localhost:7001/
得到如下页面即为注册成功
把之前的8001端口的支付服务入驻进Eureka,这个时候8001的身份就变成了Eureka Client,那要做哪些工作呢?
首当其冲就是加入**@EnableEurekaClient
**注解,如果要加入这个注解,就一定要改POM文件加依赖
改pom引入Eureka Server端依赖,新老版本的依赖对比
以前的老版本(当前使用2018),没有标示,无法分辨是服务端还是客户端
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
现在新版本(当前使用2020.2),新版本加入了server的标示,能够对客户端进行区分
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
加完了这些之后,就得改yml了,你得让Eureka 认出客户端
启动服务再次访问
配置yml的名字和实例相对应
过程和上面差不多,加依赖,改yml,加注解,启动服务,测试
yml文件
加注解
访问测试
测试访问成功
多个Eureka服务之间互相注册,相互守望
问题:微服务RPC远程服务调用最核心的是什么
高可用,试想你的注册中心只有一个only one, 它出故障了那就呵呵( ̄▽ ̄)"了,会导致整个为服务环境不可用,所以
解决办法:搭建Eureka注册中心集群 ,实现负载均衡+故障容错
集群就是不再是单一的7001,要有多个注册中心,包括7002,7003…多个注册中心组集群
先添加映射
新建时和7001的新建过程一样,一定要把Eureka的服务改名,都叫一个名字,Client端就傻了
由于之前的7001里没改名,还叫localhost,所以这边要先改7001的名字
编写7002的yml文件
7002主启动类
两个注册中心同时启动
访问7001:http://localhost:7001/
访问7002:http://localhost:7002/
到这里集群搭建完毕,就可以把服务注册进去了
注册主要是改yml文件
注册8001服务
注册80服务
示意图
启动所有服务
测试7001
测试7002
测试端口成功
将8001的服务复制一份出来变成8002.也是client端
注意看yml
二者之间的区别也就是在两个端口号不同,具体走哪个服务就看端口号
很有可能一个名字下有n多台机器来执行这个服务
这个时候可以在业务代码里进行修改
修改8001的业务代码。在controller层加入端口号的读取
8002也是一样的改法,来区分出端口号
启动服务测试
8001测试
8002测试
访问Eureka 注册中心,这里只看一个7001的注册中心,7002不过多赘述
之前订单服务里的内容
以后只认服务名称,不认端口号是多少,因为单机下端口号写死了可能没有问题,但是一组集群了就有可能出现端口号冲突的问题,所以以后只认服务名称不认端口号
更正后按照Eureka的微服务名称走,但是这样是不够的,因为我只有一个微服务的名字是没有办法准确定位到某个服务器上的
直接访问会报错
那么怎么去配置自动定位到某个端口上呢?答案是负载均衡,自动把找到这个微服务的人引导到负载低的服务器上
这个任务就交给RestTemplate来做,加一个负载均衡的注解就可以了
在RestTemplate的配置类里加入注解@LoadBalance
赋予RestTemplate负载均衡的能力
这样就不需要再去关心端口号了,自动帮你负载均衡
上面说到,到了真正部署的阶段可能会有n多台机器,那么这个时候单靠主机名称去定位哪个机子上出问题了是不现实的,这个时候有个ip地址显示一下就很舒服,到时候排错直接定位到哪个ip哪个端口哪个微服务速度很快
修改方法在yml的Eureka下加入一个
instance:
instance-id: payment8001 #名字自己定
instance:
instance-id: payment8001 #自定义的名称
prefer-ip-address: true #启动ip显示
加入了新的注解@EnableDiscoveryClient
类似于“关于我们”的功能,介绍注册到我Eureka的服务。通过服务发现来获得服务的信息。
相当于写一个接口
注意这里注入的时候不要导错包了,导错包就麻烦了
import org.springframework.cloud.client.discovery.DiscoveryClient;
controller层代码,用于输出ip,端口,服务名称等信息
@GetMapping(value = "/discovery")
public Object discovery(){
List<String> services = discoveryClient.getServices();
for (String element : services) {
System.out.println(element);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance element : instances) {
System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t"+ element.getUri());
}
return this.discoveryClient;
}
最后的最后在主启动类加入注解@EnableDiscoveryClient
,这个注解以后会常用
启动测试访问8001端口下的服务发现接口,此时Eureka等微服务应该已经启动
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,
Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE (网页上那一大串红字)
一句话概括:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存
打个比方,公司欠科技园物业费了,科技园不会立马把他清走,会给他缓两天。交上来继续,长时间交不上来才走人
为什么会产生Eureka自我保护机制?
为了防止EurekaClient可以正常运行,但是 与 EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
什么是自我保护模式?
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
自我保护一关闭就代表了只要心跳包一停,马上就删掉服务。
相当于一欠物业费了马上就把公司清出去
关闭步骤
yml的Eureka7001服务端配置
注意,这个是在Eureka服务端设置的,不是在Client端设置的
server:
#关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
#心跳包间隔时长:2000ms
eviction-interval-timer-in-ms: 2000
更改之后
进行访问测试可以发现保护模式已经关闭
yml的Eureka8001客户端配置
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka 这是之前的单机版
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: payment8001 #自定义的名称
prefer-ip-address: true #启动ip显示
#心跳检测与续约时间
#开发时设置小些,保证服务关闭后注册中心能即使剔除服务
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
启动测试,这里只启动7001和8001两个服务
访问7001
此时停掉8001服务,模拟8001宕机状态
重新访问7001端口可以看到服务已经清掉了
由于Eureka已经停更了,一部分注册的服务被Zookeeper代替
zookeeper是一个分布式协调工具,可以实现注册中心功能,代替之前的Eureka作为新的注册中心
安装教程地址
配置文件里的Zookeeper端口号为2181,具体要根据实际配置的来
docker配置要自行搜索,这里只提供命令,docker真香,一拉就好用,香香了家人们
#拉取Zookeeper镜像
docker pull zookeeper
#启动Zookeeper
docker run --name zk01 -p 2181:2181 --restart always -d zookeeper
新建工程8004
pom文件中新引入的Zookeeper依赖
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
配置yml文件,注意端口号要匹配好
主启动类
一定要加服务暴露接口的注解
Controller层
@RestController
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/zk")
public String paymentzk()
{
return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString();
}
}
zookeeper也是有心跳机制的
服务节点是临时节点还是持久节点?
答案:临时性的,每次重启同一个服务后节点的UUID编号都不一样。所以是临时的。
pom,yml内容基本一致
controller层,注意,这里使用了RestTemplate
记得加入配置类
主启动类
启动服务并测试
查看Zookeeper中注册的服务(没有web端),可以看到两个服务都入驻了
由于我们Zookeeper是Docker拉来的,不好找文件夹,这里提供一个Big佬的方法
Docker版本查看Zookeeper的入驻服务方法
什么是Consul?
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。
它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
和之前的内容差不多,先上新依赖,这个是在pom文件中通用的
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
yml文件配置
###consul服务端口号
server:
port: 8006
spring:
application:
name: consul-provider-payment
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
controller
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/consul")
public String paymentInfo(){
return "springcloud with consul: "+serverPort+"\t\t"+ UUID.randomUUID().toString();
}
}
主启动类不写了,和上面一样,别问,问就是懒(乐)
启动测试,访问http://localhost:8500
测试controller接口
pom和上面的一样
yml文件
###consul服务端口号
server:
port: 80
spring:
application:
name: cloud-consumer-order
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
主启动类这里省略
Controller层和配置类
@RestController
public class OrderConsulController
{
public static final String INVOKE_URL = "http://cloud-provider-payment"; //consul-provider-payment
@Autowired
private RestTemplate restTemplate;
@GetMapping(value = "/consumer/payment/consul")
public String paymentInfo()
{//这里用了
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul", String.class);
System.out.println("消费者调用支付服务(consule)--->result:" + result);
return result;
}
}
@Configuration
public class ApplicationContextBean
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
经典图片,CAP三者不可兼得,只能兼顾两者
AP架构,保证高可用性、容错性
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。
也就是不太考虑C,数据差不多就可以了类似点赞数这种,能用就行,数据精确度不高
结论:违背了一致性C的要求,只满足可用性和分区容错,即AP
CP架构,保证高一致性、容错性
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性
保证数据完全一致,安全,转账这种用的最多,有一个不对就报错
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
说白了就是一个管宏观的一个管微观的。以牙疼要去看医生为例,Nginx就是决定把你导向哪个医院,Ribbon就是把你导向医院内部的科室内让哪个医生来为你服务。
一句话概括:Ribbon就是负载均衡+RestTemplate调用
Ribbon在工作时分成两步
第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
先看依赖这是原来的老版本依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon,因为Eureka-client依赖自带了ribbon
之前的getForObject方法不够详细,操作对象返回json串比较单一,没有拓展,返回类似请求头等等参数都没有
返回对象为响应体中数据转化成的对象,基本上可以理解为Json
getForEntity方法,返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。引入了一些新的方法,对于返回的状态码可以进行判断
启动测试一样可以查询到内容。
负载均衡方法关系图
IRule:根据特定算法中从服务列表中选取一个要访问的服务
IRule自带方法有哪些
注意!官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了
也就是说,@ComponentScan能扫描到的地方,都不能放自定义配置类
@ComponentScan在@SpringApplication注解内,点进去就能看到,所以主启动类在的包就不能放了
改造现有的包名
改造后,新建规则配置类
配置类,将出厂的轮询访问微服务改成了随机访问微服务端口
最后在主启动类上加入@RibbonClient
注解
访问测试,已经改写为随机访问,访问目标一共有8001,8002两个接口,访问时随机接入一个端口
这里我出Bug了,没时间搞,详细解决办法
算法公式:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标
8001和8002进行改造,两个Controller里加入同一个方法
@GetMapping(value = "/payment/lb")
public String getPaymentLB()
{
return serverPort;
}
因为之前自己用RestTemplate的方法里有@LoadBalance注解,含有负载均衡的功能
这个时候我要用自己的负载均衡就得把他去掉
面向接口编程
public interface LoadBalance {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
@Component
public class MyLb implements LoadBalance {
/**
* AtomicInteger原子操作类,避免高并发时不安全
* 对于Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。
* num++解析为num=num+1,明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题。
* 本次是累计访问次数
*/
private AtomicInteger atomicInteger = new AtomicInteger();
public final int getAndIncrement(){
int current;
int next;
do {
//拿到当前的次数
current = this.atomicInteger.get();
//如果超了Integer的上限就清零,没超就把当前访问数+1
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
//compareAndSet就是CAS自旋锁,比较并交换值。达到期望值就更新,没达到保持原样
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("第几次访问,次数next: "+next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
//rest接口第几次请求数 求余 服务器集群总数量
int index = getAndIncrement() % serviceInstances.size();
//按照接口游标位置来取出要用的实例
return serviceInstances.get(index);
}
}
controller层应用负载均衡(这里是80订单的controller,使用restTemplate进行服务跳转)
/**
*载入可发现的服务
*/
@Resource
private DiscoveryClient discoveryClient;
/**
* 载入自定义的负载均衡规则
*/
@Resource
private MyLb myLb;
@GetMapping("/consumer/payment/lb")
public String getPaymentLB()
{
//从已经注册的服务实例中,拿到服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
//服务实例不满足条件的,直接返回null
if(instances == null || instances.size()<=0) {
return null;
}
//以自定义负载均衡的方式启动服务,自动导向微服务
ServiceInstance serviceInstance = myLb.instances(instances);
//拿到uri前缀
URI uri = serviceInstance.getUri();
//跳转访问
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
服务测试,访问上面的controllerhttp://localhost/consumer/payment/lb
由于这个接口最终导向出来的是返回接口号,再加上是轮询算法,所以两个接口交替
控制台输出
OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。
Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解
。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。
Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加一个与Feign相关的注解即可(也可以叫做服务接口绑定器+注解)
引入依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
主启动类引入新的注解@EnableFeignClients
@SpringBootApplication
@EnableFeignClients
public class OrderMainFegin {
public static void main(String[] args) {
SpringApplication.run(OrderMainFegin.class, args);
}
}
Service接口(OpenFeign)
加入新注解@FeignClient
controller层
测试结果,访问同一个地址可以看到端口号不同,来回切换(也就实现了负载均衡)
在8001上添加一个超时接口
/**
* 模拟Feign超时接口
*/
@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeOut(){
//延时3s
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("*****paymentFeignTimeOut from port: "+serverPort);
return serverPort;
}
回到有OpenFeign服务的80端
service接口
controller接口
启动测试http://localhost/consumer/payment/feign/timeout
因为OpenFeign的等待时间默认为1s,刚刚在样例中延时为3s,超时了自然报错
报错页面
默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出
等级 | 功能 |
---|---|
默认的 | 不显示任何日志 |
BASIC | 仅记录请求方法、URL、响应状态码及执行时间 |
HEADERS | 除了 BASIC 中定义的信息之外,还有请求和响应的头信息 |
FULL | 除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据 |