第一章 微服务介绍
1.1 系统架构演变
随着互联网的发展,网站应用规模扩大,系统架构也随之变化。
系统架构经历以下过程:单体应用架构->垂直应用架构->分布式架构->SOA架构->微服务架构。
以下将介绍每种系统架构的特点和优缺点。
1.1.1 单体应用架构
在互联网早期,网站应用流量通常较小。一个应用将所有功能代码部署在一起就足够了,这种方式可以降低开发、部署和维护成本。
以电商系统为例,它包含诸多模块,如用户管理、商品管理、订单管理和物流管理等。
我们会将这些模块整合成一个Web项目,然后部署到单个Tomcat服务器上。
优点 | 缺点 |
---|---|
架构简单,适合小型项目,开发成本低。 | 所有功能集中在一个工程中,大型项目难以开发和维护。 |
单节点部署,维护便捷。 | 模块间紧密耦合,单点故障风险高。 |
难以针对不同模块进行优化和横向扩展。 |
1.1.2 垂直应用架构
为了应对访问量增加,单一应用架构通常只能增加节点。然而,并非所有模块都需要同等扩展。以电商为例,用户和订单模块往往受访问量影响较大,而消息模块影响相对较小。因此,我们可以采用垂直应用架构,将一个应用拆分为几个相对独立的系统,如电商系统、后台系统和CMS系统,以提高整体效率。这种方式下,当用户访问量激增时,我们只需增加电商系统的节点,而无需扩展后台和CMS系统。
优点 | 缺点 |
---|---|
系统拆分实现了流量分担,解决了并发问题,且可针对不同模块进行优化和水平扩展 | 系统间相互独立,无法相互调用 |
单一系统的问题不会影响其他系统,提高了整体容错率 | 系统间相互独立,可能导致重复开发 |
1.1.3 分布式架构
随着垂直应用的增多,业务代码的重复性也随之上升。
那么,如何解决这个问题呢?
我们可以将重复的代码抽取出来,作为独立的服务,然后由前端控制层调用不同的业务层服务。
这就是新的分布式系统架构——将工程拆分为表现层和服务层。服务层包含业务逻辑,而表现层仅需处理页面交互。
优点 | 缺点 |
---|---|
将公共功能抽取为服务层,提高代码复用性 | 系统间耦合度增高,调用关系变得错综复杂,难以维护 |
1.1.4 SOA架构
随着分布式架构的发展,服务数量激增,导致容量评估困难和小服务资源浪费等问题日益凸显。为解决这些挑战,需要引入一个调度中心来实时管理服务集群。
在这种情况下,SOA(Service Oriented Architecture,面向服务的架构)成为关键,它作为资源调度和治理中心发挥重要作用。
优点 | 缺点 |
---|---|
通过注册中心实现服务间调用关系的自动调节 | 服务间存在依赖关系,一旦某个环节出错可能引发"服务雪崩" |
服务关系复杂,增加了运维、测试和部署的难度 |
1.1.5 微服务架构
微服务架构是面向服务的架构(SOA)的进一步发展,它更加强调服务的"彻底拆分"。
优点 | 缺点 |
---|---|
服务原子化拆分,独立打包、部署和升级,确保每个微服务有清晰的任务划分,有利于扩展 | 分布式系统开发的技术成本高(如容错、分布式事务等) |
微服务之间采用HTTP等轻量级协议相互调用 |
1.2 微服务架构介绍
微服务架构, 简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。
1.2.1 微服务架构的常见挑战
采用微服务系统架构inevitably会带来以下几个关键问题:
-
如何有效管理众多小服务?(服务治理:注册中心[服务注册、发现、剔除])—— 最核心
-
这些小服务之间如何高效通信?(HTTP协议)—— 核心
-
客户端如何便捷访问这些小服务?(网关)—— 核心
-
当某个小服务出现问题时,如何实现自我修复?(容错机制)—— 次要但不可忽视
这些挑战是每个微服务架构设计者都必须面对的。因此,大多数微服务产品都针对性地提供了相应组件来解决这些问题。
1.2.2 微服务架构的常见概念
1.2 .2 .1 服务治理
服务治理是指对服务进行自动化管理,其核心在于服务的自动注册与发现。
服务注册:服务实例将自身信息注册到注册中心。
服务发现:服务实例通过注册中心获取其他已注册服务的信息,并使用这些信息请求所需服务。
服务剔除:注册中心自动将故障服务从可用列表中移除,防止其被调用。
1.2.2.2 服务调用
微服务架构中,多个服务之间的远程调用是常见需求。目前主流的远程调用技术包括基于 HTTP 的 RESTful 接口和基于 TCP 的 RPC 协议。
REST(Representational State Transfer)重点
REST 是一种基于 HTTP 的调用格式,具有标准化和通用性特点。它支持各种编程语言,因为所有语言都能与 HTTP 协议兼容。
RPC(Remote Procedure Call)了解
RPC 是一种进程间通信方式,使远程服务调用如同本地服务调用一样简单。RPC 框架的核心目标是简化和透明化远程服务调用过程。它负责处理底层的传输方式、序列化和通信细节,使开发人员只需关注服务接口的位置和功能,而无需深入了解底层通信过程。
浏览器上浏览商品 ⇒ 网关 ⇒ 商品服务 ⇒ 后台商品服务(公网ip1); 后台商品服务(公网ip2);
浏览器上浏览订单 ⇒ 网关 ⇒ 商品服务 ⇒ 后台商品服务(公网ip1); 后台商品服务(公网ip2);
前端ajax访问 localhost:8080/goods/add1.2.2.3 服务网关
随着微服务数量的增加,不同微服务通常拥有不同的网络地址。外部客户端可能需要调用多个服务接口来完成一个业务需求。
如果让客户端直接与各个微服务通信,可能面临以下问题:
- 客户端需要调用不同的 URL 地址,增加开发难度。
- 某些场景下可能出现跨域请求问题。
- 每个微服务都需要单独进行身份认证。
为解决这些问题,API 网关应运而生。API 网关提供以下基本功能:统一接入、安全防护、协议适配、流量管控、长短链接支持和容错能力。有了网关后,各 API 服务团队可以专注于业务逻辑处理,而 API 网关则负责处理安全、流量和路由等问题。
1.2.2.4 服务容错
在微服务架构中,一个请求通常涉及多个服务的调用。如果其中某个服务不可用且未实施服务容错,极可能引发一连串的服务不可用,这就是所谓的"雪崩效应"。
虽然我们无法完全预防雪崩效应的发生,但可以通过做好容错来最大限度地减少其影响。服务容错的三个核心原则是:
- 不被外界环境影响
- 防止上游请求压垮服务
- 避免被下游响应拖垮
1.2 .2 .5 链路追踪
随着微服务架构的流行,服务按不同维度进行拆分,一次请求通常涉及多个服务。互联网应用构建在各种软件模块之上,这些模块可能由不同团队开发,使用不同的编程语言实现,并分布在数千台服务器上,横跨多个数据中心。因此,需要对一次请求涉及的多个服务链路进行日志记录和性能监控,即链路追踪。
1.2.3 微服务架构的常见解决方案
1.2 .3 .1 ServiceComb
Apache ServiceComb,原为华为云的微服务引擎 CSE (Cloud Service Engine) 云服务。
作为全球首个Apache微服务顶级项目,ServiceComb提供了一站式微服务开源解决方案。它旨在帮助企业、用户和开发者轻松实现企业应用的微服务化,并高效管理微服务应用。
1.2 .3 .2 SpringCloud
Spring Cloud是一系列框架的集合。它巧妙地利用了Spring Boot的开发便利性,简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等。这些功能都可以用Spring Boot的开发风格实现一键启动和部署。
Spring Cloud并没有重复造轮子,而是将各大公司已经开发的比较成熟、经得起实际考验的服务框架组合起来。通过Spring Boot风格进行再封装,屏蔽了复杂的配置和实现原理。最终,开发者可以使用一套简单易懂、易部署和易维护的分布式系统开发工具包。
早期spring cloud 由netflix 网飞公司维护
alibaba后来接收, 继续维护各个代码, 但是用的是阿里内部的各组件 nacos
1.2 .3 .3 SpringCloud Alibaba
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。该项目包含开发分布式应用微服务的必需组件,使开发者能够通过 Spring Cloud 编程模型轻松使用这些组件来构建分布式应用服务。
1.3 SpringCloud Alibaba介绍
Spring Cloud Alibaba 旨在提供微服务开发的一站式解决方案。该项目包含开发分布式应用微服务所需的关键组件,使开发者能够通过 Spring Cloud 编程模型轻松使用这些组件来构建分布式应用服务。
借助 Spring Cloud Alibaba,您只需添加少量注解和配置,即可将 Spring Cloud 应用无缝接入阿里微服务解决方案,利用阿里中间件快速构建强大的分布式应用系统。
1.3.1 主要功能
**服务限流降级:**默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 的限流降级功能。可在运行时通过控制台实时修改限流降级规则,并支持查看限流降级 Metrics 监控。
**服务注册与发现:**完全适配 Spring Cloud 服务注册与发现标准,并默认集成 Ribbon 支持。
**分布式配置管理:**支持分布式系统中的外部化配置,配置更改时自动刷新。
**分布式事务:**通过 @GlobalTransactional 注解,高效且对业务零侵入地解决分布式事务问题。
1.3.2 核心组件
Nacos、Feign、Gateway
**Sentinel:**以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保障服务的稳定性。(注:作者认为其实用性有限)
**Nacos:**一个专为云原生应用设计的动态服务发现、配置管理和服务管理平台。
**Feign:**简化远程服务调用的强大工具。
**Seata:**阿里巴巴开源的高性能微服务分布式事务解决方案。(注:作者认为某些分布式事务问题可通过优化代码顺序解决,减少对 Seata 的依赖)
第二章 微服务环境搭建
我们本次是使用的电商项目中的商品、订单为案例进行讲解。
2.1 案例准备
2.1.1 技术选型
SpringBoot 技术栈
2.1.2 模块设计
springcloud-alibaba 父工程
shop-common 公共模块【实体类】
shop-product 商品微服务 服务提供方 【端口: 808x】
shop-order 订单微服务 服务调用方【端口: 809x】
2.1.3 微服务调用
在微服务架构中,最常见的场景是微服务之间的相互调用。以电商系统中的"用户下单"为例:客户向订单微服务发起下单请求,在保存订单之前,需要调用商品微服务查询商品信息。
我们通常将主动调用服务的一方称为"服务消费者",被调用的一方称为"服务提供者"。
在这个场景中,订单微服务是服务消费者,而商品微服务是服务提供者。
2.2 创建父工程
创建一个maven工程,然后在pom.xml文件中添加下面内容
可以通过springboot快速创建
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-alibaba-dependencies</artifactId>-->
<!-- <version>${spring-cloud-alibaba.version}</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
版本对应:
2.3 创建基础模块
1 创建shop-common模块,在pom.xml中添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.itszt22</groupId>
<artifactId>springcloud22</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-common</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
2 创建实体类
package com.itszt22.springcloud22.shopcommon.entity;
import lombok.Data;
//商品
@Data
public class ShopProduct {
private Integer pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
}
package com.itszt22.springcloud22.shopcommon.entity;
import lombok.Data;
//订单
@Data
public class ShopOrder {
private Long oid;//订单id
private Integer pid;//商品id
private String pname;//商品名称
private Double pprice;//商品单价
private Integer number;//购买数量
}
2.4 创建商品微服务
1. 创建一个名为shop-product的模块,并添加springboot依赖
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.itszt22</groupId>
<artifactId>springcloud22</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-product</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.itszt22</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
2. 创建工程的主类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
@SpringBootApplication
public class ShopProductApplication {
public static void main(String[] args) {
SpringApplication.run(ShopProductApplication.class);
}
}
3. 创建配置文件application.yml
server:
port: 8082
4. 创建Controller
5. 启动工程
6. 通过浏览器访问服务
package com.itszt22.springcloud22.shopproduct.controller;
import com.itszt22.springcloud22.shopcommon.entity.ShopProduct;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
//商品controller
@RestController
public class ProductController {
/**
* 根据商品id 查询到对应的商品详细信息 localhost:8081/product/8
*/
@GetMapping("/product/{ids}")
public ShopProduct getProduct(@PathVariable("ids") Integer id){
//todo 调用service service调用dao 查数据库的商品表 查到商品数据
ShopProduct product = new ShopProduct();
product.setPid(id);
product.setPprice(800.0);
product.setStock(2000);
product.setPname("高配二手机");
return product;
}
}
2.5 创建订单微服务
1. 创建一个名为shop-order的模块,并添加springboot依赖
依赖同 shop-product模块
2. 创建工程的主类
3. 创建配置文件application.yml
server:
port: 8092
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class);
}
}
4. 创建Controller
package com.itszt22.springcloud22.shoporder.controller;
import com.itszt22.springcloud22.shopcommon.entity.ShopOrder;
import com.itszt22.springcloud22.shopcommon.entity.ShopProduct;
import org.springframework.http.ResponseEntity;
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;
//下订单的controller
@RestController
public class OrderController {
/**
* 用户选择某一个商品, 我们需要根据这个商品id, 下订单
*/
@GetMapping("/order/{productid}")
public ShopOrder addOrder(@PathVariable("productid") Integer pid){
//创建一个订单
ShopOrder order = new ShopOrder();
order.setPid(pid);
//根据商品id, 调用shop-product服务, 得到商品详细信息
//shop-product 仅仅提供了一个controller 给外界访问
//怎么样才能调用controller 通过http请求就可以调用
RestTemplate restTemplate = new RestTemplate();
//http请求三部分 1. 请求行 1.1url 1.2请求方法
// url: http://localhost:8080/product/233
// 方法: get
ResponseEntity<ShopProduct> productResponseEntity = restTemplate.getForEntity("http://localhost:8081/product/" + pid, ShopProduct.class);
ShopProduct product = productResponseEntity.getBody();
System.out.println("我是服务调用方, 我查询到了服务提共方返回给我的商品所有数据" + product);
order.setPprice(product.getPprice());
order.setPname(product.getPname());
return order;
}
}
5. 启动工程,通过浏览器访问服务进行测试
浏览器访问 localhost:8091/order/9
第三章 Nacos Discovery–服务治理
服务调用方 (product) ⇒ 中介(product, 192.168.8.127; device, 192.168.8.128) 服务提供方 (192.168.8.127) 服务提供方(192.168.8.128)
3.1 服务治理介绍
先来思考一个问题
通过上一章的操作,我们已经可以实现微服务之间的调用。
但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:
一旦服务提供者地址变化,就需要手工修改代码
一旦是多个服务提供者,无法实现负载均衡功能
那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现 服务治理 。
什么是服务治理
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的 自动化注册与发现 。
服务注册: 在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服
务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中
的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
服务发现: 服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实
例的访问。
通过上面的调用图会发现,除了微服务,还有一个组件是 服务注册中心 ,它是微服务架构非常重要
的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:
1. 服务发现:
服务注册:保存服务提供者和服务调用者的信息
服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
2. 服务配置:
配置订阅:服务提供者和服务调用者订阅微服务相关的配置
配置下发:主动将配置推送给服务提供者和服务调用者
3. 服务健康检测
检测服务提供者的健康情况,如果发现异常,执行服务剔除
常见的注册中心
Zookeeper
zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式
应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用
配置项的管理等。
Eureka
Eureka是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源
Consul
Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现
和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value
存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以
安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring
Cloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
3.2 nacos简介
Nacos 致力于帮助您发现、配置和管理微服务。它提供了一组简单易用的特性,使您能够快速实现动态服务发现、服务配置、服务元数据及流量管理。
从这个介绍可以看出,Nacos 的核心功能是作为一个注册中心,用于管理各个注册到其中的微服务。
3.3 nacos实战入门
接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去。
3.3.1 搭建nacos环境
第 1 步: 安装nacos
第 2 步: 启动nacos
进入nacos的bin目录中, 双击点击startup.cmd即可启动
注意: 黑窗口跑一会儿后多按几次回车
注意: 需要环境变量中配置JAVA_HOME
第 3 步: 访问nacos
打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos
下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作
3.3.2 将商品微服务注册到nacos
接下来开始修改shop-product模块的代码, 将其注册到nacos服务上
1 在pom.xml中添加nacos的依赖
<!--这个是添加在商品模块的pom.xml中的 nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
==========================================
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
dependencyManagementd的dependencies
<!-- 注意解开 父pom中的这个管理alibaba cloud 组件版本的管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2 在主类上添加 @EnableDiscoveryClient 注解
3 在application.yml中添加nacos服务的地址
4 启动服务, 观察nacos的控制面板中是否有注册上来的商品微服务
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication
# nacos将会把application.name作为注册中心的代表该服务的键
spring:
application:
name: shop-product
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
3.3.3 将订单微服务注册到nacos
接下来开始修改shop_order模块的代码, 将其注册到nacos服务上
1 在pom.xml中添加nacos的依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2 在主类上添加 @EnableDiscoveryClient 注解
@SpringBootApplication
@EnableDiscoveryClient
public class OrderApplication
3 在application.yml中添加nacos服务的地址
spring:
application:
name: shop-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
4 修改OrderController, 实现微服务调用
/**
* 2.0版本, 通过discoveryClient, 动态的获取服务提供方(商品)的ip+port
* @param pid
* @return
*/
@GetMapping("/order/{productid}")
public ShopOrder addOrder(@PathVariable("productid") Integer pid){
//创建一个订单
ShopOrder order = new ShopOrder();
order.setPid(pid);
RestTemplate restTemplate = new RestTemplate();
//根据服务名称, 想nacos注册中心索要对应的ip+port信息
List<ServiceInstance> shopProductInfoList = discoveryClient.getInstances("shop-product");
ServiceInstance serviceInstance = shopProductInfoList.get(0);
String ip = serviceInstance.getHost();
int port = serviceInstance.getPort();
System.out.println("ip = " + ip);
System.out.println("port = " + port);
ResponseEntity<ShopProduct> productResponseEntity = restTemplate.getForEntity("http://"+ip + ":" + port+"/product/" + pid, ShopProduct.class);
ShopProduct product = productResponseEntity.getBody();
System.out.println("我是服务调用方, 我查询到了服务提共方返回给我的商品所有数据" + product);
order.setPprice(product.getPprice());
order.setPname(product.getPname());
return order;
}
DiscoveryClient是专门负责服务注册和发现的,我们可以通过它获取到注册到注册中心的所有服务
5 启动服务, 观察nacos的控制面板中是否有注册上来的订单微服务,然后通过访问消费者服务验证调
用是否成功
Nacos帮助我们完成了两个主要功能:
- 服务自动注册与发现
- 集群管理
负载均衡是我们自己手动实现的。
注意:负载均衡是在服务调用方进行的。
关于服务的集群与分布式:
广义上,集群和分布式是相似的,都是使用多台机器完成一个系统的功能。
狭义上,集群指的是多台机器完成同一个任务,而分布式是多台机器各自完成不同的功能,这些功能合起来构成一个完整的系统。
3.4 实现服务调用的负载均衡
3.4.1 什么是负载均衡
通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上
进行执行。
根据负载均衡发生位置的不同,一般分为 服务端负载均衡 和 客户端负载均衡 。
服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡
而客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请
求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供
者执行。
3.4.2 自定义实现负载均衡 [了解]
1 通过idea再启动一个shop-product微服务,设置其端口为 8082
2 通过nacos查看微服务的启动情况
3 修改shop-order的代码,实现负载均衡
import com.itszt19.springcloud.shopcommon.entity.Order;
import com.itszt19.springcloud.shopcommon.entity.Product;
import com.itszt19.springcloud.shoporder.service.IOrderService;
import lombok.extern.slf4j.Slf4j;
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.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
@Slf4j
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private IOrderService orderService;
@Autowired
private DiscoveryClient discoveryClient;
private AtomicInteger currentCount = new AtomicInteger(0);
//准备买 1 件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//从nacos中获取服务地址
//两种常用的负载策略 -- 轮询 随机
List<ServiceInstance> instances = discoveryClient.getInstances("shop-product");
// 随机获取实例
// ServiceInstance serviceInstance = instances.get(ThreadLocalRandom.current().nextInt(instances.size());
//轮询 -- 100% 典型的线程不安全代码 --- 理论上的效果 --- 单机的情况
//如果考虑到多节点轮询 可以将状态字段存储到redis中
int index = currentCount.incrementAndGet();
ServiceInstance serviceInstance = instances.get(index % instances.size());
if (index == Integer.MAX_VALUE) {
currentCount.compareAndSet(Integer.MAX_VALUE, 0);
}
String url = serviceInstance.getHost() + ":" +
serviceInstance.getPort();
log.info(">>从nacos中获取到的微服务地址为:" + url);
//通过restTemplate调用商品微服务
Product product = restTemplate.getForObject(
"http://" + url + "/product/" + pid, Product.class);
log.info(">>商品信息,查询结果:" + product);
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
return order;
}
}
第 3 步:启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果
3.4.3 基于Ribbon实现负载均衡 [重点]
Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡
第 1 步:在RestTemplate 的生成方法上添加@LoadBalanced注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
第 2 步:修改服务调用的方法
package com.itszt20.springclouddemo.shoporder.controller;
import com.itszt20.springclouddemo.shopcommon.entity.Order;
import com.itszt20.springclouddemo.shopcommon.entity.Product;
import com.itszt20.springclouddemo.shoporder.service.IShopOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
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 javax.annotation.Resource;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@RestController
public class OrderController {
@Resource
private IShopOrderService shopOrderService;
@Resource
private RestTemplate restTemplate;
// @Resource
// private DiscoveryClient discoveryClient;
// private static AtomicInteger i = new AtomicInteger(0);
// private static final Object lock = new Object();
@GetMapping("/order/product/{id}")
public ResponseEntity addOrder(@PathVariable("id") Integer id){
Order order = new Order();
order.setPid(id);
//当前操作用户的id从session当中取到
order.setUid(123);
//本次订单中该商品买了多少个 默认1个
order.setNumber(1);
//从代表服务的集合中挑选出来一个 就是所谓的当前环境下的负载均衡
// 1. 随机 2. 轮询
//1. 随机
// Random random = new Random();
// int serviceIndex = random.nextInt(serviceInstanceList.size());
// ServiceInstance serviceInstance = serviceInstanceList.get(serviceIndex);
//
// String host = serviceInstance.getHost();
// int port = serviceInstance.getPort();
//
// List<ServiceInstance> serviceInstanceList = listArray[0];
// ServiceInstance serviceInstance = serviceInstanceList.get(newIndex);
//
// String host = serviceInstance.getHost();
// int port = serviceInstance.getPort();
//
// log.info("调用服务的ip[{}],端口[{}]", host, port);
// todo 商品的名称和价格 需要通过http的方式调用商品的根据id查找商品这个服务
Product goods = restTemplate.getForObject(String.format("http://shop-product/product/%d", id), Product.class);
order.setPname(goods.getPname());
order.setPprice(goods.getPprice());
shopOrderService.saveOrder(order);
return ResponseEntity.ok().build();
}
}
Ribbon支持的负载均衡策略
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为
com.netflix.loadbalancer.IRule, 具体的负载策略如下图所示:
策略名 策略描述 实现说明
我们可以通过修改配置来调整Ribbon的负载均衡策略,具体代码如下
#写在服务调用方的配置
shop-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ribbon:
ConnectTimeout: 2000 #超时时间
ReadTimeout: 5000 #请求处理的超时时间
3.5 基于Feign实现服务调用 [最终方案]
3.5.1 什么是Feign
Feign是Spring Cloud提供的一个声明式的伪HTTP客户端。它让调用远程服务变得像调用本地服务
一样简单,只需创建一个接口并添加一个注解即可。
Feign与Nacos兼容性良好,并且默认集成了Ribbon。因此,在Nacos环境下使用Feign时,
已经自动实现了负载均衡。
3.5.2 Feign的使用 [重要]
注意: feign是http服务调用, 自然是配置在http的发起方
1 加入Fegin的依赖
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--加在父pom的dependencymanagement中-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2 在主类上添加Fegin的注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients//开启对Fegin注解的扫描
public class OrderApplication {}
3 创建一个service, 并使用Fegin实现微服务调用
4 修改controller代码,并启动验证
import com.itszt19.springcloud.shopcommon.entity.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("shop-product")//声明调用的提供者的name
public interface ProductService {
//指定调用提供者的哪个方法
//@FeignClient+@GetMapping 就是一个完整的请求路径 http://shop-product/product/{pid}
@GetMapping(value = "/product/{pid}")
Product findByPid(@PathVariable("pid") Integer pid);
}
import com.itszt19.springcloud.shopcommon.entity.Order;
import com.itszt19.springcloud.shopcommon.entity.Product;
import com.itszt19.springcloud.shoporder.service.IOrderService;
import com.itszt19.springcloud.shoporder.service.IProductService;
import lombok.extern.slf4j.Slf4j;
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;
@RestController
@Slf4j
public class OrderController {
@Autowired
private IOrderService orderService;
@Autowired
private IProductService productService;
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//通过fegin调用商品微服务
Product product = productService.findByPid(pid);
log.info(">>商品信息,查询结果:" + product);
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
return order;
}
}
5 重启order微服务,查看效果
第四章 Gateway–服务网关
4.1 网关简介
在微服务架构中,一个系统通常被拆分为多个微服务。这就引发了一个问题:客户端如何调用这些微服务?没有网关的情况下,客户端需要记录每个微服务的地址并分别调用,这种架构存在以下问题:
- 客户端需要多次请求不同的微服务,增加了代码和配置的复杂性。
- 认证变得复杂,每个服务都需要独立认证。
- 跨域请求的处理在某些场景下较为复杂。
API网关可以解决上述问题。API网关是系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务。在网关层面可以实现一些与业务功能无关的公共逻辑,如认证、鉴权、监控和路由转发等。
添加上API网关之后,系统的架构图变成了如下所示:
我们也可以观察下,我们现在的整体架构图:
在业界中,有几种流行的网关解决方案:
Nginx+Lua
Nginx的反向代理和负载均衡功能可实现API服务器的负载均衡和高可用。Lua是一种轻量级脚本语言,能够在Nginx中编写简单逻辑,增强其功能。
Kong
基于Nginx和Lua开发,Kong以高性能和稳定性著称。它提供多个即插即用的插件,如限流和鉴权。然而,Kong也存在一些局限性:仅支持HTTP协议,二次开发和自由扩展较为困难,且虽然提供管理API,但缺乏更直观的管控和配置方式。
Zuul
Netflix开源的网关服务,功能丰富且使用Java开发,便于二次开发。不过,Zuul也面临一些挑战:缺乏动态配置能力,依赖组件较多,且由于依赖Web容器处理HTTP请求,性能不如Nginx。
Spring Cloud Gateway
Spring公司专门开发用来替代Zuul的网关服务。我们将在后续章节详细介绍这一解决方案。
注意:SpringCloud Alibaba技术栈并未提供自己的网关服务,因此我们可以采用Spring Cloud Gateway作为网关解决方案。
4.2 Gateway简介
Spring Cloud Gateway 是 Spring 公司基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发的网关。它为微服务架构提供了一种简单有效的统一 API 路由管理方式。Gateway 旨在替代 Netflix Zuul,不仅提供统一的路由方式,还基于 Filter 链的方式提供了网关的基本功能,如安全、监控和限流。
优点:
- 性能强劲:比第一代网关 Zuul 快 1.6 倍
- 功能强大:内置多种实用功能,如转发、监控、限流等
- 设计优雅,易于扩展
缺点:
- 实现依赖 Netty 与 WebFlux,不同于传统的 Servlet 编程模型,学习成本较高
- 只能打包成 jar 文件执行,无法部署在 Tomcat、Jetty 等 Servlet 容器中
- 仅支持 Spring Boot 2.0 及以上版本
4.3 Gateway快速入门
要求: 通过浏览器访问api网关,然后通过网关将请求转发到商品微服务
第 1 步:创建一个api-gateway的maven模块,导入相关依赖
注意: spring cloud gateway 不兼容 tomcat
<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
第 2 步: 创建主类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
第 3 步: 添加配置文件
server:
port: 7000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
第 4 步: 启动项目, 并通过网关去访问微服务
4.4 网关限流 [了解]
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前
面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进
行限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
1 导入依赖
2 编写配置类
下面的操作都是在gateway模块中进行的
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的
SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
注意: 导入了sentinel依赖后, 需要在gateway得springboot配置中, 添加对于sentinel服务得ip地址得配置
spring:
application:
name: gateway
cloud:
sentinel:
transport:
port: 18081 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: 127.0.0.1:9999 # 指定控制台服务的地址
3 测试
在一秒钟内多次访问http://localhost:7000/product-serv/product/1就可以看到限流启作用了。
4 自定义API分组
自定义API分组是一种更细粒度的限流规则定义
package com.itszt21.apigateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
//配置限流的异常处理
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
//配置初始化的限流参数
//定义被保护资源的保护规则
@PostConstruct
public void initGatewayRules(){
Set<GatewayFlowRule> rules = new HashSet<>();
//为分组进行限流 一秒一次 这里名字是自定义的 与下面映射
rules.add(new GatewayFlowRule("provider_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("provider_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
//初始化限流过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//自定义限流异常页面
@PostConstruct
public void initBlockHandlers(){
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code",0);
map.put("msg","被限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
//自定义API分组
//定义sentinel资源
@PostConstruct
private void initCustomizedApis(){
Set<ApiDefinition> definitions = new HashSet<>();
//为上面自定义组名字匹配是映射哪些请求
ApiDefinition api1 = new ApiDefinition("provider_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem().setPattern("/shop-product/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("provider_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem().setPattern("/shop-order/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}