Spring Cloud认知学习(一)--Eureka使用、Ribbon使用
- Spring Cloud是一个微服务架构,他有多种组件来管理微服务的方方面面。Spring Cloud是用于构建微服务开发和治理的框架的集合。
- Spring Cloud是最热门的Java技术毋庸置疑。
- 官网
微服务的介绍
- 微服务是什么这里就不细化介绍了吧,应用服务化已经成为了趋势,简单的说就是把以前ALL-IN-ONE的一体应用的内部功能进行拆分,比如把短信功能单独出来作为一个可以提供给外部调用的服务,这样既提供了短信服务的复用性(其他的应用也能够复用这个功能),也使得对某个功能进行单独的负载能力提升称为可能(All In One 的如果想提升抢购功能的负载能力的话,采用部署多个服务端来提升抢购功能的负载能力的时候也会顺带提升了用户注册等的负载能力,这就额外浪费了资源)。
- 在微服务的理论中,为了解耦,每个微服务使用单独的数据库(当然了,可能有些人会觉得是同名服务使用同一个数据库,微服务这东西概念其实还挺多争论的。)。
- 马丁.福勒谈微服务
Spring Cloud出现的原因:
- 当你把原来的应用服务化了之后,那么就会遇到这些服务的管理问题了,比如说检测服务的可用性、查看现在有什么服务、多个同名(同功能)的服务怎么做到负载均衡之类的问题。
- Spring Cloud,基于Spring Boot提供了一套微服务解决方案,包括服务注册与发现,配置中心,全链路监控,服务网关,负载均衡,熔断器等组件。这些组件也不全是Spring 自己开发的,有一些是开源的组件,Spring进行了封装了而已(Spring Cloud Netflix主要来自Netflix OSS的开源组件,Spring Cloud Alibaba由阿里提供)。Spring Cloud像Spirng Boot 的starter一样屏蔽了复杂的配置,让我们能够通过简单的配置来进行微服务开发。
常见场景:
Spring Cloud可以解决以下的常见几个场景(暂时只列举几个常见场景,其实微服务的方方面面基本都有解决方案)
- 服务的开发:使用Spring Boot开发服务方便快速(Spring Boot其实不算Spring Cloud的内部组件,只能算一家人吧)
- 服务的注册与发现:主要是Eureka提供,用于把微服务注册到Eureka中和让服务消费者从Eureka中获取可用微服务列表。(当然现在也有很多采用别的组件来做服务的注册与发现)
- 负载均衡:主要由Ribbon提供,用于在服务消费者端进行负载均衡,从而把请求均衡到各个同名服务上。
- API网关:主要由Zuul提供,提供统一的服务调用入口,所有的服务调用都通过Zuul来调用,提供请求转发、请求过滤等功能。
- 服务的容错的处理--断路器:主要有Hystrix提供,用于解决微服务调用时发生服务熔断的问题。
- 分布式服务配置:主要由Spring Cloud Config提供,用于解决多个微服务的统一配置和分发配置问题。(一个服务的配置可以从Config配置中心中拉取)
- 数据监控、消息总线。。。。。。。
微服务的优劣势:
优势:
- 微服务化之后,代码也偏向简单模块化,会比较容易理解,就好比你搞一个正经的商城难,你搞一个注册功能还不轻松吗?😀而且代码模块化之后也方便管理,比如可以专门让某个部门负责某个服务的开发;而且因为代码模块化解耦之后,新的需求可以仅仅针对某个服务来开发。
- 一个服务就是一个独立的服务端,可以独立部署,与其他服务的耦合度低。(All In One中你改个注册功能都要整个服务端重启)
- 服务可以动态扩容,由于是一个服务就是一个独立的服务端,所以可以很自然的水平扩容,部署同名服务的多个服务端。
- 。。。
劣势:
- 运维的难度提升了,以前的ALL IN ONE是一个服务端,现在由于微服务化了,导致了N多个服务端的产生,这些服务端的部署、监控都是问题。(所以这又引起了自动部署和监控的需求。)
- 以一个之前我听说过的事故来说一下:某个证券公司准备更新某个代码,结果漏更了一台机上的服务端,然后这个服务端因为与其他服务的配合问题,导致了不断得“自动收单”,可以理解成游戏中商人对于拍卖行中的商品来自动低价扫货。
- 测试的难度提高了,如果你手动测试过自己的BUG的话,那么你应该知道假如你的某一个方法修改了,那么你应该检测一下调用了这个方法的地方是否可能会发生BUG。服务的调用也是这样的,如果某个服务修改了,安全起见的话你还是应该测试所有相关的服务的。(所以这又引起了自动化测试的需求)
- 如果单单对一个端的测试来说,测试难度是降低了,但对于整体业务流程的测试难度就是加大了。
- 当然了,因为分布式而导致的分布式事务问题也让人头疼。
- (但如此热门的技术,相信大家都心里很清楚他的优点是远大于缺点的。)
Spring Cloud版本问题
版本介绍
- Spring Cloud 的版本名称并不是像其他的项目那样使用1.0之类的数字的,他使用了伦敦的地铁名来作为版本的名称,据说这是考虑到了Spring Cloud与子项目的依赖关系,为了避免版本名称的冲突和误解的。
- 在版本的后面跟上SR(Service Release)代表这是一个稳定的版本,后面会跟一个数字,代表第几次迭代,例如
Hoxton.SR3
与Spring Boot版本对应关系
- Spring Cloud的版本与Spring Boot要考虑版本的兼容性,以下是Spring Cloud与Spring Boot版本的对应关系。
- 请注意:Dalston和Edgware是对应1.5版本的Spring Boot,他不能使用在2.0上面。
Release版本(地铁名) | 对应的Spring Boot版本 |
---|---|
Hoxton | 2.2.x |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
目前最新版本是Hoxton.SR3
,但国内主流的应该还是Finchley
或者Greenwich
,所以下面的示例都将以Finchley
版本为例,注意此版下的组件基本都是2.0.0版本的。
---分割线---学习的前提---分割线---
下面将对于Spring Cloud的常用组件来学习。如果你继续向下学习,请确保你已经掌握了Spring Boot知识
注意:
💡下面的学习只会贴出部分代码,其余的代码将在github上存储,会根据组件的学习来逐步commit代码,所以可以根据代码的差异来比较每一版本代码的区别,了解增加新组件需要改动哪些代码,(从可以从历史记录中查看每一次commit的代码更新,来了解新增的组件修改了哪些代码),从而加深印象。当然,自己动手也很重要。
【PS:下面的代码,我后期才发现我写错了一个单词Service,有些地方写对,有些地方写错了,但并不影响代码运行。😓主要是因为大写的时候没检查好】
基础项目搭建
- Spring Cloud 主要侧重服务的治理,微服务主要由Spring Boot开发,所以我们首先基于Spring Boot构建一个简单的微服务项目,后面通过逐步增加功能来学习Spring Cloud。
下面的示例代码请参考:微服务项目基础搭建
1.创建一个Maven父工程:
父工程的创建方法在IDEA中和Eclipse中有区别,这里给出IDEA的,Eclipse的可以自查(搜索Eclipse创建父工程即可)
父工程的目录结构如下: ![20200408145332](https://progor.oss-cn-shenzhen.aliyuncs.com/img/20200408145332.png)
在父工程的POM.XML中增加如下代码,锁定后面的依赖版本:
<!--使用dependencyManagement锁定依赖的版本 start-->
<dependencyManagement>
<dependencies>
<!--由于此时没有了sping boot starter 作为parent工程,需要使用spring-boot-dependencies来达到相似效果-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.6.RELEASE</version>
<!--但要注意此处版本可能与spring cloud冲突,由于我选择了Finchley,所以这里用了2.0.6-->
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</dependencyManagement>
<!--使用dependencyManagement锁定依赖的版本 end-->
注:当你在父工程下创建了新的module,那么此时父工程的POM.xml就会增加内容:
在IDEA中,父工程下添加module的时候,父工程自动变packaing为pom。
2.创建一个共有依赖包:
如果你学过maven的分模块开发,你应该知道,一些被多个模块依赖的东西会被抽离到一个单独模块中,然后其他模块依赖这个模块即可。下面创建的就是包含了User实体(与数据表对应)的共有依赖包。
在父工程上面右键`New`->`Module`来在父工程下新建模块`spring-cloud-common-data`,选择模块为Maven方式,命名模块后,一路next(也可以在最后一步重新定义模块的存储路径): ![20200408152755](https://progor.oss-cn-shenzhen.aliyuncs.com/img/20200408152755.png)
创建一个User类:
package com.progor.study.entity;
// 请注意类放在哪个包里面。
public class User {
private Integer id;
private String username;
private String fullName;
public User() {
}
public User(Integer id, String username, String fullName) {
this.id = id;
this.username = username;
this.fullName = fullName;
}
// 篇幅考虑,省略setter,getter代码
}
执行一段SQL,我们的后面的测试创建数据:
DROP DATABASE IF EXISTS cloud01;
CREATE DATABASE cloud01 CHARACTER SET UTF8;
USE cloud01;
CREATE TABLE user
(
id int PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255),
fullName VARCHAR(255)
);
INSERT INTO user(username,fullName) VALUES('zhangsan','张三');
INSERT INTO user(username,fullName) VALUES('lisi','李四');
INSERT INTO user(username,fullName) VALUES('wangwu','王五');
INSERT INTO user(username,fullName) VALUES('zhaoliu','赵六');
INSERT INTO user(username,fullName) VALUES('lidazhuang','李大壮');
SELECT * FROM user;
3.创建一个服务提供者:
3.1 在父工程上面右键`New`->`Module`来在父工程下新建模块`spring-cloud-user-service-8001`。 ![20200408145419](https://progor.oss-cn-shenzhen.aliyuncs.com/img/20200408145419.png)
![20200408145450](https://progor.oss-cn-shenzhen.aliyuncs.com/img/20200408145450.png)
3.2 引入web开发相关依赖包:
<dependencies>
<!--引入公共依赖包 start-->
<dependency>
<groupId>com.progor.study</groupId>
<artifactId>spring-cloud-common-data</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--引入公共依赖包 end-->
<!--引入web开发相关包 start-->
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--使用jettey作为默认的服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--引入web开发相关包 end-->
</dependencies>
3.3,基于spring boot创建两个接口(以及Service,Mapper之类的,前面说了需要Spring Boot基础,那么这些默认你都会了,就不解释了):
Controller的核心代码如下:
// 由于返回json数据,懒得加注解@ResponseBody了,加个RestController
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User getUser(@PathVariable Integer id) {
User user = userService.getUser(id);
if (user == null) {
throw new RuntimeException("该ID:" + id + "没有对应的用户信息");
}
return user;
}
@GetMapping("/user/list")
public List<User> listUser() {
List<User> users = userService.listUser();
return users;
}
}
Mapper代码:
@Mapper
public interface UserMapper {
List<User> listUser();
User getUser(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.progor.study.dao.UserMapper">
<select id="listUser" resultType="com.progor.study.entity.User">
SELECT * FROM user
</select>
<select id="getUser" parameterType="Integer" resultType="com.progor.study.entity.User">
SELECT * FROM user WHERE id =#{id}
</select>
</mapper>
application.yml:
server:
port: 8001
spring:
datasource:
# 配置数据源
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud01
mybatis:
# 全局配置文件位置:
config-location: classpath:mybatis/mybatis-config.xml
# 映射文件位置:
mapper-locations: classpath:mybatis/mapper/*.xml
注意,在上面有执行SQL,这里的mybatis要查询的数据从cloud01数据库中获取。
访问http://localhost:8001/user/list
,测试一下是否能调用到接口。
4.创建一个服务消费者
4.1:创建模块spring-cloud-user-consumer-80
4.2:导入依赖:
<dependencies>
<!--引入公共依赖包 start-->
<dependency>
<groupId>com.progor.study</groupId>
<artifactId>spring-cloud-common-data</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--引入公共依赖包 end-->
<!--引入web开发相关包 start-->
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jettey作为默认的服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
4.3:创建代码:
application.yml代码:
server:
port: 80
访问http://localhost:80/user/list
,测试一下是否能调用到8001服务的接口(80是没有任何业务内容的,他调用的是8001的业务)。
小节总结
上面基础项目搭建应该成功实现了服务消费者通过Http请求来调用服务提供者的服务了。
下面将使用Spring Cloud对这个简单的微服务项目来增加功能,来讲解各种组件在微服务中的作用。
如果你不了解上面的例子,那你最好再学习一下,然后再看下面的内容。
Eureka服务注册与发现
介绍
- Eureka,读音尤里卡。
- Eureka用于服务的注册与发现。Eureka是Netflix开源的一个服务注册与发现的组件,也被Spring Cloud整合到Spring Cloud Netflix模块中。
服务的注册与发现
解决的问题:- 服务注册:服务注册使得多个同一服务的多个服务端使用同一个服务端名字注册在了服务注册中心中(比如短信功能有A1,A2,A3三个服务端,但他们在注册中心的名字都是短信功能,那么我需要短信功能的时候,我一查注册中心就发现有三个服务端我可以调用),这样使得可以在服务注册中心中统一管理服务,查看服务的各种状态。
- 服务发现:服务的发现首先要基于服务的注册。在上面的简单的调用服务的例子中,你是需要指定服务提供者的URL路径的,这是非常耦合的行为,一个比较恰当的做法是让他能够变起来,而不是一个固定的值。怎么变呢?当有了服务注册之后,你可以从注册中心中拉取到某个服务的多个服务实例信息,然后获取其中一个服务实例解析出你需要的服务提供者的URL,而实际上我们每一次使用的服务实例可以是不同的,这样就让服务提供者的URL成为了可变的,也使得后面的对同名服务的负载均衡也成为可能。
- 类似Eureka的服务注册与发现组件:consul,etcd,zookeeper。
- 原理:Eureka由服务端Eureka Server和客户端Eureka Client,服务端Eureka Server用于维护服务的列表(注册中心),服务提供者通过客户端Eureka Client把自己的服务信息注册到服务端Eureka Server中;服务消费者通过客户端Eureka Client从服务端Eureka Server中拉取到服务端中注册的服务。获取到服务列表后,服务消费者就知道了服务的IP地址等信息,就可以通过http来调用服务了。
有人说,好多人都开始放弃eureka了,为什么这里还要讲?
虽然老旧,但作为曾经火过的,还是有一定的参考价值,而且你不知道你进的那家公司的技术是不是与时俱进的。或者说万一让你接手改造一个eureka的项目呢?
当然了,要不断学习新的技术,consul目前来看应该是不错的替代方案,我后面也会写这个的。
简单使用步骤
下面的代码可以参考:Eureka简单使用步骤
1.父工程导入依赖:
1.1 修改父工程的依赖:
现在开始spring cloud学习了,我们首先在父工程的pom.xml下面加入spring cloud的依赖锁定,来锁定我们组件的版本:
<!--锁定spring cloud版本 start-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--锁定spring cloud版本 end-->
1.2修改spring-cloud-eureka-server-7001模块依赖:
然后再配置spring-cloud-eureka-server-7001模块的pom.xml,由于前面父工程导入了spring-cloud-dependencies
,所以你这里的eureka虽然没指定版本,但继承了之前锁定的版本。
<dependencies>
<!--这里贴一下旧版本的eureka-server依赖包,注意新版本的eureka位置变了-->
<!--<dependency>-->
<!--<groupId>org.springframework.cloud</groupId>-->
<!--<artifactId>spring-cloud-starter-eureka-server</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
2.新建spring-cloud-eureka-server-7001模块:
2.1新建模块spring-cloud-eureka-server-7001
上面说了,Eureka是有服务端和客户端的,客户端集成在服务消费者和服务提供者上,服务端需要单独创建,我们单独创建一个Eureka Server出来。
2.2:创建主启动类代码:
package com.progor.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer //使用EnableEurekaServer来把当前服务端作为一个Eureka服务端
public class EurekaServer7001Application {
public static void main(String[] args) {
SpringApplication.run(EurekaServer7001Application.class, args);
}
}
2.3修改application.yml:
# 配置服务端口
server:
port: 7001
# 配置eureka相关
eureka:
instance:
hostname: localhost # eureka实例的名字
client:
register-with-eureka: false # 这个选项是“是否把自己注册到eureka服务端”,由于它自己就是服务端,选false
fetch-registry: false # 是否从注册中心拉取服务,由于它自己就是服务端,选false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置Eureka Server的交互地址(注册地址),用于服务检索和服务注册
2.4.测试访问Eureka Server:
运行主程序类之后,访问一下localhost:7001
,如果有eureka界面的显示就说明eureka服务端配置成功了。
3.修改服务提供者
在服务提供者spring-cloud-user-service-8001
中配置eureka,把服务注册到eureka中:
我们修改原来的spring-cloud-user-service-8001
模块:
3.1修改pom.xml:
<!--增加eureka 客户端依赖 start-->
<!--旧版本的依赖:-->
<!--<dependency>-->
<!--<groupId>org.springframework.cloud</groupId>-->
<!--<artifactId>spring-cloud-starter-eureka</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--增加eureka 客户端依赖 end-->
3.2修改主程序类UserService8001Application:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient // 启用Eureka Client
public class UserService8001Application {
public static void main(String[] args) {
SpringApplication.run(UserService8001Application.class, args);
}
}
3.3修改application.yml:
server:
port: 8001
spring:
datasource:
# 配置数据源
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud01
application:
name: UserSerive # 多个同功能的服务使用应用名application.name来注册,这个应用名你可以在eureka 中看到,它变成了服务名
mybatis:
# 全局配置文件位置:
config-location: classpath:mybatis/mybatis-config.xml
# 映射文件位置:
mapper-locations: classpath:mybatis/mapper/*.xml
# eureka配置:
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka # 指定eureka 服务端交互地址
instance:
instance-id: UserService8001 # 当前服务实例名称
prefer-ip-address: true # 是否使用IP地址作为当前服务的标识,有些是会使用主机号,你可以尝试注释看看效果
# 由于拉取服务和是否把自己注册到eureka的都是默认true的,所以不需要配置
3.4运行主程序类,查看http://localhost:7001
,看是否有如下图的信息:
4.修改服务消费者
在服务消费者中配置eureka,使得能从eureka中获取注册的服务并且调用:
修改模块spring-cloud-user-consumer-80
:
4.1修改pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
4.2修改主程序类:
package com.progor.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class UserConsumer80Application {
public static void main(String[] args) {
SpringApplication.run(UserConsumer80Application.class, args);
}
}
4.3 修改application.yml:
server:
port: 80
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: false # 由于它不是一个服务提供者,不注册到eureka
4.4修改AppConfig
修改Bean--RestTemplate,增加@LoadBalanced
,让restTemplate能够把请求地址解析成服务名称:
package com.progor.study.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class AppConfig {
@Bean
@LoadBalanced // eureka与这个配合,要使用LoadBalanced才会调用eureka中注册的服务
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
4.5修改controller:
package com.progor.study.Controller;
import com.progor.study.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
public class UserController {
// 注意这个restTemplate需要自己生成Bean,参考com.progor.study.config.AppConfig
@Autowired
private RestTemplate restTemplate;
// 指定远程访问的URL,也就是服务提供者的URL
// private static final String REST_URL_PREFIX = "http://localhost:8001";
// 1.注释直接使用URL来调用服务的代码,
// 2.下面使用eureka来调用,下面的"http://USERSERIVE"的USERSERIVE是服务的名字,Eureka页面中你看过的
// 3.这样就从eureka中拉取到名为USERSERIVE的服务的列表,并从中选择一个服务实例调用
private static final String REST_URL_PREFIX = "http://USERSERIVE";
@GetMapping("/user/{id}")
public User getUser(@PathVariable Integer id) {
return restTemplate.getForObject(REST_URL_PREFIX + "/user/" + id, User.class);
}
@GetMapping("/user/list")
public List<User> listUser() {
return restTemplate.getForObject(REST_URL_PREFIX + "/user/list", List.class);
}
}
4.6运行主程序类,访问接口http://localhost/user/list
,查看是否能访问。
🟠如果你代码正确了,那么应该是整个正常访问的,那么注意了,我们上面并没有写固定的服务消费者的URL,那么他是怎么访问的呢?他通过拉取eureka中的服务列表来解析出的。【由于有时候可能存在拉取的数据延迟问题,如果不相等的话,最好按顺序启动7001,8001,80】
💡这里提醒一个东西:上面都配了defaultZone,其实在单Eureka Server情况下,Eureka Server的defaultZone是可以不配的,因为没有意义,(但消费者和生产者需要配),对于服务消费者和生产者来说,只要运行了起来,都可以根据IP来获取(上面的就算不配,也可以通过http://localhost:7001/eureka来访问),消费者和生产者并不关心Eureka Server的名字,他只关心地址。但在集群中,defaultZone有独特的意义。下面讲。
Eureka集群
Eureka里面可能会注册了很多服务,而服务消费者都从Eureka Server上拉取服务列表,这个负载压力对于Eureka可能是很大的,而且由于服务列表都从Eureka Server中拉取,所以Eureka Server也是非常重要的。为了保证Eureka Server的健壮性,我们通常都会搭建Eureka集群。
搭建步骤
下面的代码可以参考:Eureka简单集群实验
1.新建三个eureka-server模块,
spring-cloud-eureka-cluster-server-7002
spring-cloud-eureka-cluster-server-7003
spring-cloud-eureka-cluster-server-7004
2.都导入依赖包:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
3.都对应修改主启动类:
// spring-cloud-eureka-cluster-server-7002:
@SpringBootApplication
@EnableEurekaServer //使用EnableEurekaServer来把当前服务端作为一个Eureka服务端
public class EurekaClusterServer7002Application {
public static void main(String[] args) {
SpringApplication.run(EurekaClusterServer7002Application.class, args);
}
}
// spring-cloud-eureka-cluster-server-7003:
@SpringBootApplication
@EnableEurekaServer //使用EnableEurekaServer来把当前服务端作为一个Eureka服务端
public class EurekaClusterServer7003Application {
public static void main(String[] args) {
SpringApplication.run(EurekaClusterServer7003Application.class, args);
}
}
// spring-cloud-eureka-cluster-server-7004:
@SpringBootApplication
@EnableEurekaServer //使用EnableEurekaServer来把当前服务端作为一个Eureka服务端
public class EurekaClusterServer7004Application {
public static void main(String[] args) {
SpringApplication.run(EurekaClusterServer7004Application.class, args);
}
}
4.修改host
❓这是因为eureka默认使用eureka.instance.name作为在eureka集群中的标识名字。那么不修改host的时候,会有一个问题:
- 如果你使用host作为三个server的eureka.instance.name,那么此时eureka怎么区分这三个server呢?对于eureka是访问不了的
- 而且在配置defaultZone的时候也不可以配置多个同名的。你可以尝试一下在defaultZone中写下多个localhost但端口不一样的URL。
- 所以,在本地搭建集群的时候,需要配置host。
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7003.com
127.0.0.1 eureka7004.com
5.修改application.yml:
7002:
# 配置服务端口
server:
port: 7002
# 配置eureka相关
eureka:
instance:
hostname: eureka7002 # eureka实例的名字
client:
register-with-eureka: false # 这个选项是是否把自己注册到eureka服务端,由于它自己就是服务端,选false
fetch-registry: false # 是否从注册中心拉取服务,由于它自己就是服务端,选false
service-url:
defaultZone: http://eureka7003.com:7003/eureka/,http://eureka7004.com:7004/eureka/ # 设置Eureka Server的交互地址(注册地址),用于服务检索和服务注册
7003:
# 配置服务端口
server:
port: 7003
# 配置eureka相关
eureka:
instance:
hostname: eureka7003 # eureka实例的名字
client:
register-with-eureka: false # 这个选项是是否把自己注册到eureka服务端,由于它自己就是服务端,选false
fetch-registry: false # 是否从注册中心拉取服务,由于它自己就是服务端,选false
service-url:
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7004.com:7004/eureka/ # 设置Eureka Server的交互地址(注册地址),用于服务检索和服务注册
7004:
# 配置服务端口
server:
port: 7004
# 配置eureka相关
eureka:
instance:
hostname: eureka7004 # eureka实例的名字
client:
register-with-eureka: false # 这个选项是是否把自己注册到eureka服务端,由于它自己就是服务端,选false
fetch-registry: false # 是否从注册中心拉取服务,由于它自己就是服务端,选false
service-url:
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ # 设置Eureka Server的交互地址(注册地址),用于服务检索和服务注册
❓为什么这里是配另外的集群节点的地址,不需要配自己的地址?
首先上面说了,其实自己配不配是不重要的,就算你不配,你的服务地址还是在的,消费者和生产者还是能够通过端口来访问eureka server。这里配置的是与其他集群节点的交互地址。
6.启动三个server,查看效果:
可以看到DS Replicas中有另外两个节点的列表,下图是7001的。
7.对消费者和生产者的处理
如果此时要注册服务或拉取服务,那么defaultZone要注意改成集群的:
参考代码spring-cloud-user-service-8002-eureka-cluster
7.1.然后你就会在三个eureka中都可以看到你注册的服务了:
当配置了集群服务,结果某个节点挂掉的时候,会报错,但并不影响服务。
知识补充:
- 服务续约:每隔30s,eureka就会检测服务的可用性。
- 自我保护:你可以看到,如果当你把服务注册到eureka之后,如果你停止这个服务,这个服务很长时间都不会把这个服务从eureka中移除,这是eureka的自我保护机制,乐观的认为这个服务不久之后就会重新可用。
- 服务剔除:如果Eureka Client(而且要是个服务提供者) 90s没有向Eureka Server发送心跳,那么Eureka Server就会认为这个服务实例已经不可用了,把它从服务列表中删除。【但基于自我保护后并不会删除。】
Ribbon负载均衡
客户端负载均衡的意思是,让客户端来进行负载均衡,而不是服务端来进行负载均衡,是什么意思呢?比如说你要去排队买东西,有三条队,你自然而然地会选择队伍短的队去排咯😀,这是由你去进行的负载均衡,而不是服务员帮你去安排你该排哪条队。
💡为什么要采用客户端负载均衡呢?
- 主要是,客户端的负载均衡是会减少服务端的资源消耗的,就好像如果有很多消费者的话,你要么就雇佣很多服务员,要么就让消费者等服务员有空。
- 其次呢,这也是从架构上考虑的,因为客户端会从eureka中拉取服务可用列表,那么如果此时顺便拉取了此时各种服务的负载状态的话,那么也就顺手地可以使用这些信息来进行客户端负载均衡了。
简单使用步骤:
下面的代码可以参考:Ribbon负载均衡简单实验
1.新建模块,用于负载均衡
新建模块spring-cloud-user-service-8002
,spring-cloud-user-service-8003
2.修改模块代码:
- 给模块
spring-cloud-user-service-8002
,spring-cloud-user-service-8003
导入pom.xml、修改application.yml和修改主启动类。 - 代码基本与模块
spring-cloud-user-service-8001
一样的。 - pom.xml与
spring-cloud-user-service-8001
一样的。 - 控制器,mapper,都跟
spring-cloud-user-service-8001
一样的。只是主程序类的名字问题 - 主要是修改服务端口和数据库。
由于新建了服务提供者,所以为了让不同的服务使用不同的数据库,所以要执行下面的SQL来创建额外的数据库:
sql:
-- db2
DROP DATABASE IF EXISTS cloud02;
CREATE DATABASE cloud02 CHARACTER SET UTF8;
USE cloud03;
CREATE TABLE user
(
id int PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255),
fullName VARCHAR(255)
);
INSERT INTO user(username,fullName) VALUES('zhangsan','张三2');
INSERT INTO user(username,fullName) VALUES('lisi','李四2');
INSERT INTO user(username,fullName) VALUES('wangwu','王五2');
INSERT INTO user(username,fullName) VALUES('zhaoliu','赵六2');
INSERT INTO user(username,fullName) VALUES('lidazhuang','李大壮2');
SELECT * FROM user;
--- db3
DROP DATABASE IF EXISTS cloud03;
CREATE DATABASE cloud03 CHARACTER SET UTF8;
USE cloud03;
CREATE TABLE user
(
id int PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255),
fullName VARCHAR(255)
);
INSERT INTO user(username,fullName) VALUES('zhangsan','张三3');
INSERT INTO user(username,fullName) VALUES('lisi','李四3');
INSERT INTO user(username,fullName) VALUES('wangwu','王五3');
INSERT INTO user(username,fullName) VALUES('zhaoliu','赵六3');
INSERT INTO user(username,fullName) VALUES('lidazhuang','李大壮3');
SELECT * FROM user;
3.启动模块
3.1启动服务生产者:spring-cloud-eureka-server-7001
,spring-cloud-user-service-8001
,spring-cloud-user-service-8002
,spring-cloud-user-service-8003
查看eureka内部注册的服务实例有多少个,你可以看到我们启动的三个服务实例都显示出来了。
3.2启动服务消费者:
现在对于USERSERIVE服务有三个服务实例了,然后我们启动服务消费者模块,看多次调用下他是怎么调用的吧。
由于上面对不同服务的数据库有了小修改(为了有区分,所以我修改了一下数据,但实际业务中他们应该是相同的),所以可以根据数据来判断当前调用了哪个数据库。
多次调用http://localhost/user/list
,你应该能看到数据在变化,说明默认是有负载均衡的。
⚪你可能有点疑惑,默认的负载均衡是什么时候配置的呢?还记得我们之前在配置消费者从eureka中获取服务列表时配置了什么吗?我们给我们的restTemplate加了一个注解@LoadBalanced
,而LoadBalanced就是负载均衡的意思。
当你调用服务之后,你会看到消费者拉取服务列表的时候会拉取到一些服务的健康信息。
默认情况下,从eureka中拉取的服务会使用轮询调用(加入有ABC三个服务实例,会顺序的逐一的调用,比如说可能会是不断按BCA的顺序来调用;),但ribbon能帮我们做更多。
下面使用Ribbon来进行客户端的负载均衡。
4.修改消费者模块
由于Ribbon是客户端的负载均衡,所以要修改消费者模块spring-cloud-user-consumer-80
:
4.1修改pom.xml,添加ribbon依赖:
<!--增加ribbon依赖 start-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<!--旧版的需要去掉-netflix-->
</dependency>
<!--增加ribbon依赖 end-->
4.2.修改负载均衡策略:
⚪当导入ribbon的时候,如果你不做其他操作,默认的负载均衡还是轮询。下面我们修改一下负载均衡策略:
@Configuration
public class AppConfig {
@Bean
@LoadBalanced // eureka与这个配合,要使用LoadBalanced才会调用eureka中注册的服务
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public IRule myRule() {
// return new RoundRobinRule();
return new RandomRule();
// return new RetryRule();
}
}
然后你重新调用几次http://localhost/user/list
,你应该能看到负载均衡策略变了。
上面的例子中已经演示了修改负载均衡策略。下面来讲负责均衡相关的东西。
负载均衡算法:
Ribbon中负责负载均衡策略的就是IRule,所以我们上面的代码就新创建了一个IRule的bean。
下面讲一下这个IRule的几个常见的实现类。
- RandomRule:随机调用服务
- RoundRobinRule:轮询调用服务
- WeightedResponseTimeRule:是加权策略,某个服务权重高的话调用的次数就会多。内部会维护一个权重表,每隔一段时间就依据服务的响应时间来更新权重,响应时间短的权重高。(刚启动的时候由于没有服务调用的相关信息,会先使用轮询策略。)
- RetryRule:默认内部是轮询调用(注意这个策略可以更改),不过是带重试机制的轮询,ABC三个服务实例,如果B服务实例突然挂了,那么默认的轮询策略轮询到B的时候应该会调用失败,而如果采用了RetryRule,那么B调用不了的时候,就会尝试调用C,以保证此次调用是成功的。
@Configuration
public class AppConfig {
@Bean
@LoadBalanced // eureka与这个配合,要使用LoadBalanced才会调用eureka中注册的服务
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public IRule myRule(){
// return new RandomRule(); // 随机调用服务
// return new RoundRobinRule();// 轮询策略
return new RetryRule();// 带重试的轮询
}
}
自定义负载规则:
除了以上的规则,你还可以自定义负载均衡的规则。
你可能会想,那么我应该可以参考一下RandomRule或RoundRobinRule的实现重写一下,然后像上面指定IRule这个Bean的实现对象就行了。
💡 但要注意一点,如果你是在主程序类的同级目录或下级目录下(也就是能被主程序类的ComponentScan扫描到的目录),那么这个规则会对这个消费者的所有调用的服务生效。
1.写一个负载均衡规则:只调用8003的服务。
package com.progor.study.myrule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
public class MyLoadBalancedRule extends AbstractLoadBalancerRule {
// 这个choose方法就是选择哪个服务来进行调用
public Server choose(ILoadBalancer lb, Object key) {
// ILoadBalancer是服务注册列表
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();//可用服务
List<Server> allList = lb.getAllServers();// 所有服务
int serverCount = allList.size();
if (serverCount == 0) {
// 服务数为0
return null;
}
// 看一下下面的代码(此段代码来自RandomRule),应该能判断出来,就是这里指定了返回的server,
// 所以我们也在这里修改策略,假如说我们指定只调用8003的
// int index = rand.nextInt(serverCount);
// server = upList.get(index);
// 修改 start
if (upList.size()==0){
return null;
}
for (int i = 0; i < upList.size() ; i++) {
Server item = upList.get(i);
int httpPort = item.getPort();
// 你可以通过Server和ILoadBalancer的各种参数来自定义你的规则()
if (httpPort == 8003){
server = item; // 这里由于只是示例,所以就随便写了,所以安全逻辑并没有做完全。
}
}
// 修改 end
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// TODO Auto-generated method stub
}
}
2.在主程序类的外部目录创建一个Configuration:
package com.progor.config;
import com.netflix.loadbalancer.IRule;
import com.progor.study.myrule.MyLoadBalancedRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public IRule myRule(){
return new MyLoadBalancedRule();// 使用我们自定义的规则
}
}
3.修改主程序类,增加@RibbonClient
如果你的Configuration放在了主程序类外部的时候就要加上这个才能扫描到外部的Configuration:
💡这个注解用来指定某个服务的负载均衡规则,如果不使用这个注解来给对应服务配置负载均衡策略,并且你在内层目录的配置类中指定了IRule的实例为我们创建的实现类实例,那么所有的服务都会采用这个策略。
package com.progor.study;
import com.progor.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="USERSERIVE",configuration= MyConfig.class)
public class UserConsumer80Application {
public static void main(String[] args) {
SpringApplication.run(UserConsumer80Application.class, args);
}
}
4.调用http://localhost/user/list
,你会发现现在只会有8003的数据返回了,这说明我们的负载均衡生效了。
🐶对于不同服务的负载均衡规则的问题,也可以参考代码中的MessageService。这里面我写了一个Message服务的两个服务实例8004和8005,并且让消费者来调用了这个服务,效果如下:
- 当不使用@RibbonClient时:由于那个Configuration并没有被扫描到,所以不会生效,此时应该所有的服务都使用内部配置的负载均衡规则
如果在内部配置中采用内部的定义规则:
@Bean
public IRule myRule(){s
// return new RandomRule(); // 随机调用服务
// return new RoundRobinRule();// 轮询策略
return new RetryRule();// 带重试的轮询
}
当在内部配置规则的额时候,如果使用我们定义的规则:由于我们之前为USERSERVICE定义了只使用8003服务的规则,此时MESSAGESERVICE也会使用这个规则,那么这时候MESSAGESERVICE会一直请求不到。
@Bean
public IRule myRule() {
// return new RoundRobinRule();
// return new RandomRule();
// return new RetryRule();
return new MyLoadBalancedRule();
}
- 当使用@RibbonClient时:对于指定的服务,会使用指定的负载均衡规则。【从其他资料(不确定😓)中了解到以前的@RibbonClient的name应该是可以为空的,从这个注解的name有default默认值,我觉得这个说法应该是对的。但现在不允许了,应该是为了避免专属的负载均衡规则覆盖全局规则的问题】
// 自定义负载均衡的代码
package com.progor.config;
import com.netflix.loadbalancer.IRule;
import com.progor.study.myrule.MyLoadBalancedRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public IRule myRule() {
return new MyLoadBalancedRule();// 使用我们自定义的规则
}
}
// 主程序类代码
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "USERSERIVE", configuration = MyConfig.class)
public class UserConsumer80Application {
public static void main(String[] args) {
SpringApplication.run(UserConsumer80Application.class, args);
}
}