一、概念
1、Spring Cloud 简介
Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。
Spring 并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过 Spring Boot 风格进行再封装、屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
2、Spring Cloud 五大组件
- 服务发现——————Netflix Eureka
- 客服端负载均衡———Netflix Ribbon
- 断路器———————Netflix Hystrix
- 服务网关——————Spring Cloud Gateway
- 分布式配置—————Spring Cloud Config
3、微服务
微服务架构是一种架构模式或者说一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行在其独立的进程中,服务之间互相协调、互相配合,为用户提供最终的价值。同时,服务之间采用轻量级的通信机制(通常是基于 HTTP 的 RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境等。
微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底去掉耦合,每一个微服务提供单个业务功能,一个服务只做一件事。
从技术角度讲就是一种小而独立的处理过程,类似与进程的概念,能够自行单独启动或销毁,可以拥有自己独立的数据库。
① 微服务优点
- 每个服务足够内聚,足够小,代码容易理解。这样能聚焦一个业务功能或业务需求。
- 开发简单、开发效率提高,一个服务可能就是专业的只干一件事,微服务能够被小团队单独开发,这个小团队可以是 2 到 5 人的开发人员组成。
- 微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。
- 微服务能使用不同的语言开发。
- 易于和第三方集成,微服务运行容易且灵活的方式集成自动部署。
- 微服务易于被一个开发人员理解、修改和维护,这样小团队能够更关注自己的工作成果,无需通过合作才能体现价值。
- 微服务允许你利用融合最新技术。微服务只是业务逻辑的代码,不会和 HTML/CSS 或其他界面组件混合,即前后端分离。
- 每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一数据库。
② 微服务的缺点
开发人员要处理分布式系统的复杂性。
4、微服务、Spring Boot、Spring Cloud 之间的关系
- 微服务是一种架构的理念,提出了微服务的设计原则,从理论为具体的技术落地提供了指导思想。
- Spring Boot 是一套快速配置脚手架,可以基于 Spring Boot 快速开发单个微服务。
- Spring Cloud 是一个基于 Spring Boot 实现的服务治理工具包;
- Spring Boot 专注于快速、方便集成的单个微服务个体;
- Spring Cloud 关注全局的服务治理框架。
5、服务调用方式
RPC 和 HTTP 区别
RPC(Remote Procedure Call Protocol):远程过程调用协议
它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议
① 序列化协议
HTTP 是基于 HTTP 协议
RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输。
② 使用方式
HTTP 是服务端把方法写好,客户端并不知道具体方法。客户端只想获取资源,所以发起 HTTP 请求,而服务端接收到请求后根据 URI 经过一系列的路由才定位到方法上面去,因此,HTTP 接口只关注服务提供方,接口只要保证有客户端调用时,返回对应的数据就行了。
RPC 是服务端提供好方法给客户端调用,客户端需要知道服务端的具体类,具体方法,然后像调用本地方法一样直接调用它。所以 RPC 要求客户端接口保持和服务端的一致。
③ 面向对象
从设计上来看,RPC,所谓的远程过程调用 ,是面向方法的 ,HTTP :是面向资源的
④ 性能
由于 HTTP 本身提供了丰富的状态功能与扩展功能,但也正由于 HTTP 提供的功能过多,导致在网络传输时,需要携带的信息更多,从性能角度上讲,较为低效。
RPC 服务网络传输上仅传输与业务内容相关的数据,传输数据更小,性能更高。
6、Spring 的 RestTemplate
RestTemplate 介绍
- RestTemplate 是 Rest 的 HTTP 客户端模板工具类
- 对基于 Http 的客户端进行封装
- 实现对象与 JSON 的序列化与反序列化(JSON<——->JavaBean)
- 不限定客户端类型,目前常用的 3 种客户端都支持:HttpClient、OKHttp、JDK 原生 URLConnection(默认方式)
二、入门案例
1、搭建注册中心
Eureka 架构中的三个核心角色
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址,提供服务注册发现功能
服务提供者:启动后向 Eureka 注册自己的信息(地址,提供什么服务),要求统一对外提供 Rest 风格服务
服务消费者:向 Eureka 订阅服务,Eureka 会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
Eurka 基本工作流程
- Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息
- Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务
- Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常
- 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例
- 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端
- 当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式
- Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地
- 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存
- Eureka Client 获取到目标服务器信息,发起服务调用
- Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除
心跳(续约):提供者定期通过 http 方式向 Eureka 刷新自己的状态
① 新建工程
创建 Maven 父工程 springcloud-parent
导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> </parent>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
|
在父工程 springcloud-parent
下创建注册中心项目 eureka-server
pom.xml
依赖导入
1 2 3 4 5 6 7 8
| <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
|
配置application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| server: port: 7001 spring: application: name: eureka-server eureka: client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://localhost:7001/eureka
|
② 创建启动类
在 src 下创建com.atguigu.EurekaServerApplication
在类上需要添加一个注解@EnableEurekaServer
,用于开启 Eureka 服务
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.atguigu;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
|
运行启动类的主方法,测试访问:http://127.0.0.1:7001/
2、服务提供者-注册服务
引入 eureka 客户端依赖包
在 application.yml 中配置 Eureka 服务地址
在启动类上添加@EnableDiscoveryClient 或者@EnableEurekaClient
在父工程 springcloud-parent
下新建 user-provider
工程
引入 pom.xml 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
|
application.yml
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| server: port: 18081
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root url: jdbc:mysql://192.168.88.88:3306/springcloud?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC application: name: user-provider
eureka: client: service-url: defaultZone: http://localhost:7001/eureka
|
①User
创建com.atguigu.domain.User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.atguigu.domain;
import javax.persistence.*; import java.util.Date;
@Entity @Table(name = "tb_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String username; private String password; private String name; private Integer age; private Integer sex; private Date birthday; private Date created; private Date updated; private String note;
}
|
对应的数据库表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| USE springcloud;
CREATE TABLE `tb_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(100) DEFAULT NULL COMMENT '用户名', `password` varchar(100) DEFAULT NULL COMMENT '密码', `name` varchar(100) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `sex` int(11) DEFAULT NULL COMMENT '性别,1男,2女', `birthday` date DEFAULT NULL COMMENT '出生日期', `created` date DEFAULT NULL COMMENT '创建时间', `updated` date DEFAULT NULL COMMENT '更新时间', `note` varchar(1000) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户信息表';
INSERT INTO `tb_user` VALUES ('1', 'zhangsan', '123456', '张三', '13', '1', '2006-08-01', '2021-05-16', '2021-05-16', '张三'); INSERT INTO `tb_user` VALUES ('2', 'lisi', '123456', '李四', '13', '1', '2006-08-01', '2021-05-16', '2021-05-16', '李四');
|
②UserDao
创建com.atguigu.dao.UserDao
1 2 3 4 5 6 7
| package com.atguigu.dao;
import com.atguigu.domain.User; import org.springframework.data.jpa.repository.JpaRepository;
public interface UserDao extends JpaRepository<User, Integer> { }
|
③Service
创建com.atguigu.service.UserService
接口
1 2 3 4 5 6 7 8 9 10 11 12
| package com.atguigu.service;
import com.atguigu.domain.User;
public interface UserService {
User findByUserId(Integer id); }
|
④UserServiceImpl
创建com.atguigu.service.impl.UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.atguigu.service.impl;
import com.atguigu.dao.UserDao; import com.atguigu.domain.User; import com.atguigu.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
@Service public class UserServiceImpl implements UserService {
@Autowired private UserDao userDao;
@Override public User findByUserId(Integer id) { return userDao.findById(id).get(); } }
|
⑤UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.atguigu.controller;
import com.atguigu.domain.User; import com.atguigu.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping(value = "/user") public class UserController {
@Autowired private UserService userService;
@RequestMapping(value = "/find/{id}") public User findById(@PathVariable(value = "id") Integer id) { return userService.findByUserId(id); } }
|
⑥ 启动类创建
创建com.atguigu.UserProviderApplication
启动类
@EnableEurekaClient
或者 @EnableDiscoveryClient
,都是用于开启客户端发现功能
区别在于@EnableEurekaClient
的注册中心只能是 Eureka。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.atguigu;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication @EnableDiscoveryClient public class UserProviderApplication { public static void main(String[] args) { SpringApplication.run(UserProviderApplication.class, args); } }
|
运行启动类
访问 Eureka 地址:http://127.0.0.1:7001/
3、服务消费者-注册服务
引入 eureka 客户端依赖包
在 application.yml 中配置 Eureka 服务地址
在启动类上添加@EnableDiscoveryClient 或者@EnableEurekaClient
在父工程 springcloud-parent
下新建 user-consumer
工程
引入 pom.xml 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
|
application.yml
配置
1 2 3 4 5 6 7 8 9 10 11
| server: port: 18082 spring: application: name: user-consumer
eureka: client: service-url: defaultZone: http://localhost:7001/eureka
|
①User
创建com.atguigu.domain.User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package com.atguigu.domain;
import java.io.Serializable; import java.util.Date;
public class User implements Serializable { private Integer id; private String username; private String password; private String name; private Integer age; private Integer sex; private Date birthday; private Date created; private Date updated; private String note;
}
|
②UserController
创建com.atguigu.controller.UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.atguigu.controller;
import com.atguigu.domain.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController @RequestMapping(value = "/consumer") public class UserController {
@Autowired private RestTemplate restTemplate;
@Autowired private DiscoveryClient discoveryClient;
@GetMapping(value = "/{id}") public User queryById(@PathVariable(value = "id") Integer id) { List<ServiceInstance> instances = discoveryClient.getInstances("user-provider"); ServiceInstance serviceInstance = instances.get(0); String instanceUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/find/" + id; return restTemplate.getForObject(instanceUrl, User.class); } }
|
③ 启动引导类
创建com.atguigu.UserConsumerApplication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.atguigu;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate;
@SpringBootApplication @EnableDiscoveryClient public class UserConsumerApplication { public static void main(String[] args) { SpringApplication.run(UserConsumerApplication.class, args); }
@Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
|
使用 IP 访问配置
可以通过配置文件,将请求地址换成 IP,修改 application.yml 配置文件
1 2 3 4 5 6 7 8 9 10
| eureka: client: service-url: defaultZone: http://localhost:7001/eureka instance: ip-address: 127.0.0.1 prefer-ip-address: true
|
4、总结
①Eureka 客户端
服务提供者要向 EurekaServer 注册服务,并完成服务续约等工作
服务注册:
- 当我们开启了客户端发现注解
@EnableDiscoveryClient
。同时导入了 eureka-client 依赖坐标 - 同时配置 Eureka 服务注册中心地址在配置文件中
- 服务在启动时,检测是否有
@EnableDiscoveryClient
注解和配置信息 - 如果有,则会向注册中心发起注册请求,携带服务元数据信息(IP、端口等)
- Eureka 注册中心会把服务的信息保存在 Map 中。
② 服务续约
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。
默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| eureka: client: service-url: defaultZone: http://localhost:7001/eureka instance: ip-address: 127.0.0.1 prefer-ip-address: true lease-expiration-duration-in-seconds: 150 lease-renewal-interval-in-seconds: 30
|
③ 获取服务列表
Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每 30 秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。
如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。
1 2 3 4 5 6 7
| eureka: client: service-url: defaultZone: http://localhost:7001/eureka registry-fetch-interval-seconds: 30
|
说明
服务消费者启动时,会检测是否获取服务注册信息配置
如果是,则会从 EurekaServer 服务列表获取只读备份,缓存到本地
每隔 30 秒,会重新获取并更新数据
每隔 30 秒的时间可以通过配置 registry-fetch-interval-seconds 修改
④ 失效剔除和自我保护
当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。
服务下线
当服务正常关闭操作时,会发送服务下线的 REST 请求给 EurekaServer。服务中心接受到请求后,将该服务置为下线状态
失效剔除
服务中心每隔一段时间(默认 60 秒)将清单中没有续约的服务剔除。
1 2 3 4 5 6 7 8 9 10
| eureka: client: service-url: defaultZone: http://localhost:7001/eureka registry-fetch-interval-seconds: 30 service: eviction-interval-timer-in-ms: 5000
|
⑤ 自我保护
默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。
固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 即会进入自我保护机制。
自我保护模式下,不会剔除任何服务实例
自我保护模式保证了大多数服务依然可用
通过 enable-self-preservation 配置可用关停自我保护,默认值是打开
1 2 3 4 5 6 7 8 9 10 11 12
| eureka: client: service-url: defaultZone: http://localhost:7001/eureka registry-fetch-interval-seconds: 30 service: enable-self-preservation: false eviction-interval-timer-in-ms: 5000
|