1.微服务简介
1.1 背景分析
讲微服务之前,我们先分析以下单体应用。所谓单体应用一般是基于idea/eclipse,maven等建一个工程,然后基于SpringBoot,spring,mybatis框架进行整合,接下来再写一堆dao、mapper、service、controller,再加上一些的配置文件,有可能还会引入redis、elasticsearch、mq等其它项目的依赖,开发好之后再将项目打包成一个jar包/war包。然后再将包扔到类似tomcat这样的web服务中,最后部署到公司提供给你的linux服务器上。 接下来,你针对服务提供的访问端口(例如8080端口)发起http请求,请求会由tomcat直接转交给你的spring web组件,进行一层一层的代码调用。对于这样的设计一般适合企业的内部应用,访问量不大,体积比较小,5人以内的团队即可开发和维护。但对于一些大型互联网项目,假如需要10人以上的开发和维护团队,仅频繁的创建代码分支,编写业务功能,然后合并分支,就会出现很多代码冲突。每次解决这些大量的代码冲突,可能就会耗费好几天的时间。基于这样的背景微服务诞生了.
在微服务架构设计中,建议超出需要10人开发和维护的项目要进行系统拆分,就是把大系统拆分为很多小系统,几个人负责一个服务这样每个服务独立的开发、测试和上线,代码冲突少了,每次上线就回归测试自己的一个服务即可,测试速度快了,上线是独立的,只要向后兼容接口就行了,不需要跟别人等待和协调,技术架构和技术版本的升级,几个人ok就行,成本降低,更加灵活了。
1.2 什么是微服务
微服务架构(MSA)的基础是将单个应用程序开发为一组小型独立服务,这些独立服务在自己的进程中运行,独立开发和部署。如图所示:
这些服务使用轻量级 API 通过明确定义的接口进行通信。这些服务是围绕业务功能构建的,每项服务执行一项功能。由于它们是独立运行的,因此可以针对各项服务进行更新、部署和扩展,以满足对应用程序特定功能的需求。
生活中的微服务,如图所示:右图的螺丝刀每一个都可以看作一个单体应用,只要坏掉,就无法继续使用,左图的螺丝刀就可以看作微服务架构,其可以自由进行组配使用。
程序中的微服务,就是将各个业务系统的共性再进行抽取,做成独立的服务,如图所示:
总之,微服务是分布式系统中的一种流行的架构模型,它并不是银弹,所以,也不要寄希望于微服务构架能够解决所有的问题。微服务架构主要解决的是如何快速地开发和部署我们的服务,这对于一个能够适应快速开发和成长的公司是非常必要的。同时,微服务设计中有很多很不错的想法和理念,通过学习微服务架构我们可以更快的迈向卓越。
2.SpringCloud Alibaba微服务解决方案
2.1 概述
Spring Cloud Alibaba 是Spring Cloud的一个子项目,致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
2.2 核心组件分析
Spring Cloud Alibaba 默认提供了如下核心功能(先了解):
-
服务限流降级:
默认支持 WebServlet、OpenFeign、RestTemplate、Spring Cloud Gateway, RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。 -
服务注册与发现:
基于Spring Cloud 服务注册与发现标准,借助Nacos进行实现,默认还集成了 Ribbon 的支持。 -
分布式配置管理:
基于Nacos支持分布式系统中的外部化配置,配置更改时自动刷新。 -
消息驱动能力:
基于Spring Cloud Stream 为微服务应用构建消息驱动能力。 -
分布式事务:
使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。 -
分布式任务调度:
提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker上执行。
2.3 解决方案架构设计
基于Spring Cloud Alibaba实现的微服务,解决方案设计架构如图所示:
3.构建SpringCloud 聚合项目并进行环境初始化
3.1 工程结构
微服务项目一般都会采用聚合工程结构,可通过聚合工程结构实现共享资源的复用,简化项目的管理方式。本小节以一个聚合工程结构为案例,讲解微服务架构方案中的maven聚合工程的基本结构,例如:
chapter_five (工作区/空项目)
├── 01-sca //(微服务父工程)
├── sca-provider //服务提供方法
├── sca-consumer //服务消费方法
├── sca-gateway //网关服务
3.2 创建空项目
打开Idea,创建一个空项目(Empty Project),项目名为GitCGB2108IVProjects,例如:
其中,这个空项目就类似磁盘中的一个空的文件夹,可以将此文件夹作为一个代码工作区。
3.3 项目初始化配置
第一步:配置maven环境(只要是新的工作区,都要重新配置)
建议本地库选择新的位置不要与其它项目共用本地库,因为多个项目所需要依赖的版本不同时,可能会有一些依赖版本的冲突。
说明,这里的本地库名字最要不要选择中文,单词之间也不要有空格。
第二步:配置JDK编译环境
聚合工程在编译时,需要对相关依赖的工程进行一起编译,所以需要做一些配置,例如:
指定一下当前工作区的jdk编译版本,例如:
第三步:配置工作区中项目编码方式
3.4 创建聚合父工程
我们后续在创建微服务工程进行学习时,相关服务依赖版本的管理,公共依赖,项目编译,打包设计等都可以放在此工程下,进行统一的配置,然后实现共享。
第一步:创建01-sca(spring cloud alibaba)父工程模块,例如:
第二步:删除工程中的src目录(父工程不需要这个目录),例如:
因为我们并不会在父工程中完成代码,其只作为相关服务依赖版本的管理,公共依赖,项目编译,打包设计等功能,所以并不需要src目录;
第三步:修改项目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>
<!--当前工程的坐标-->
<groupId>com.jt</groupId>
<artifactId>sca</artifactId>
<version>1.0-SNAPSHOT</version>
<!--有的同学在创建maven工程时,可能会有如下有两句话,这两句话用于指定
当前项目的jdk编译版本以及运行版本,也可以不指定,后续我们自己通过maven插件方式进行配置-->
<!--
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
-->
<!--maven父工程的pom文件中一般要定义子模块,
子工程中所需依赖版本的管理,公共依赖并且父工程的
打包方式一般为pom方式-->
<!--第一步: 定义子工程中核心依赖的版本管理(注意,只是版本管理)-->
<dependencyManagement>
<dependencies>
<!--spring boot 核心依赖版本定义(spring官方定义)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Cloud 微服务规范(由spring官方定义)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type><!--假如scope是import,type必须为pom-->
<scope>import</scope><!--引入三方依赖的版本设计-->
</dependency>
<!--Spring Cloud alibaba 依赖版本管理 (参考官方说明)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--第二步: 添加子工程的所需要的公共依赖-->
<dependencies>
<!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
</dependency>
<!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope><!--test表示只能在test目录下使用此依赖-->
<exclusions>
<exclusion><!--排除一些不需要的依赖-->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--其它依赖...-->
</dependencies>
<!--第三步: 定义当前工程模块及子工程的的统一编译和运行版本-->
<build><!--项目构建配置,我们基于maven完成项目的编译,测试,打包等操作,
都是基于pom.xml完成这一列的操作,但是编译和打包的配置都是要写到build元素
内的,而具体的编译和打包配置,又需要plugin去实现,plugin元素不是必须的,maven
有默认的plugin配置,常用插件可去本地库进行查看-->
<plugins>
<!--通过maven-compiler-plugin插件设置项目
的统一的jdk编译和运行版本-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!--假如本地库没有这个版本,这里会出现红色字体错误-->
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
其中,服务核心依赖版本可参考如下网址(涉及到一个兼容性问题,不能随意指定其版本):
https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
上图中定义子工程中核心依赖的版本管理我们使用了import关键字,我们使用import关键字引入一个工程依赖时,其类型必须是pom,此前我们一直使用`<parent></parent>标签进行父级依赖引入,但是这种继承方式只能引入一个,所以我们这里使用引入的方式进行操作。
并且父工程的打包方式也必须为pom,只有打包方式为pom时,我们在其他工程才能对夫功能进行import引入;
并且,虽然此次我们项目是springCloud项目,但是springCloud依赖于springboot框架,所以也要引入springBoot依赖;
springCloud框架只是一种规范,springCloud Alibaba才是其具体的实现,所以还要需要引入springCloud Alibaba;
3.5 创建服务提供方模块
创建服务提供方工程,继承sca,例如:sca-provider
3.6 创建服务消费方模块
创建服务消费方工程,继承sca,例如:sca-consumer
3.7 创建API网关服务模块
创建网关工程(这个工程后续会作为API服务访问入口),继承01-sca,例如: sca-gateway
3.8 服务关系以及调用关系设计
创建完成后父工程的pom文件中会增加此内容,其表明所有子工程都会以module方式添加到父工程:
但是如果我们要删除module,但是pom文件中的此处文件并不会自动删除,此时再次创建会显示工程已存在,无法再次创建,我们可以将此处的内容删除后再创建。
基于前面章节创建的项目,后续我们会讲解服务的注册,服务的配置,服务之间的调用,负载均衡,限流,熔断,网关等相关知识,现在先了解一个简易结构,例如:
我们最终会基于这个结构的设计,实现一个从网关到服务消费方,再从服务消费方到服务提供方的一个调用链路的业务及代码实践过程。
4.聚合工程中的资源引用问题
4.1 方法的引用
假设我们现在新创建了另一个工程sca-common,此前创建的三个子工程需要引用sca-common工程的部分内容,这时我们应该怎么操作呢?
我们可以在此前创建的三个子工程中以依赖的方式将sca-common工程添加进来。
首先,我们需要在sca-common工程中创建一个类, 并完成一个静态方法,我们这里写一个判断传入字符串是否为空的静态方法:
然后我们需要在sca-provider工程中,以依赖的方式添加sca-common工程:
然后,我们需要在sca-provider工程中添加一个启动类,因为父级工程只做版本管理,所以我们需要在子工程中添加相关依赖:
<!--添加springboot web依赖(此依赖中包含了springboot工程需要的很多基础依赖)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
同时,创建一个测试类,用来测试sca-common工程的方法是否可以正常使用,同时也可以测试是否可以引用sca-common工程的方法:
package com.jt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class StringTest {
@Test
public void testStringEmpty(){
String str = "";
boolean isEmpty = StringUtils.isEmpty(str);
System.out.println(isEmpty);
}
}
最后我们启动测试方法,便能得到运行后的结果:
4.2 对象属性的引用
我们在sca-common工程的cache目录下,创建一个DefaultCache类,将其作为一个对象交给spring去管理,并在sca-provider工程中引用这个对象,并在单元测试中输出这个对象的toString方法的返回值,我们应该怎么去完成呢?
我们还是将需求拆解开来,首先,我们要准备一个DefaultCache类,我们可以通过@Component
注解将其交给springboot管理,可以我们尝试之后发现无法添加,这是因为没有添加依赖的原因,我们可以在sca-common工程中添加spring-boot-starter
依赖来解决此问题。
但是,上一个案例中,我们在sca-provider工程中引入了sca-common工程,并且sca-provider工程已经有了spring-boot-starter-web
依赖已经包含了spring-boot-starter
依赖,虽然这么写没有关系,但还是会有冗余的情况出现,这时我们就可以通过添加·provider`标签,表明其只为其他工程提供服务或只为当前工程编译时提供服务,当项目打包时并不会打包其依赖,减少了项目依赖的冗余:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
添加依赖后,我们就可以为DefaultCache类添加@Component
注解将其交给springboot管理了:
package com.jt.cache;
import org.springframework.stereotype.Component;
@Component
public class DefaultCache {
}
然后我们在sca-provider工程中创建单元测试类,并通过依赖注入引入DefaultCache类,然后输出DefaultCache 类的toString返回值类型:
package com.jt;
import com.jt.cache.DefaultCache;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DefaultCacheTest {
@Autowired
private DefaultCache defaultCache;
@Test
public void testDefaultCache(){
System.out.println(defaultCache);
}
}
运行后我们可以得到其返回值类型:
4.3 @SpringBootTest
注解
我们俩个测试类中都使用的 @SpringBootTest
注解,我们首先去掉StringTest类中的 @SpringBootTest
注解,再次运行发现运行结果是一样的。但是在DefaultCacheTest类中,返回结果却是null,这是为什么呢?
这是因为在DefaultCacheTest类中,我们使用了依赖注入,但是去掉 @SpringBootTest
注解后,此测试类没有交给springboot去管理,所以当前的依赖注入便不会生效,springboot不会帮助进行属性值的注入。
而StringTest类中是通过依赖注入调用的方法,所以并不依赖于springboot进行属性的管理,运行结果不会受到影响。
也就是说,我们只写方法,没有属性时,可以省略 @SpringBootTest
注解,涉及到属性的使用,就一定要使用 @SpringBootTest
注解,但是在日常开发中,我们建议一定要规范开发,约定大于规则,不要省略 @SpringBootTest
注解。
4.4 引用第三方类
假设sca-common工程是一个第三方工程,并且已经打为jar包,其并没有在DefaultCache类中添加@Component
注解,我们怎么在sca-provider工程的单元测试类中引用呢?
我们可以在sca-provider工程的启动类中,通过@Bean
方法注入DefaultCache类,之后便可以继续正常引用,这是对其它第三方资源常用的一种引用方式。
甚至我们可以通过@Import
注解进行类的导入,然后引用使用也可以,但是这种方式只推荐配置类去进行使用。
5 总结
总之,微服务是一个架构设计方式,此架构中的每个服务(service)都是针对一组功能而设计的,并专注于解决特定的问题。如果开发人员逐渐将更多代码增加到一项服务中并且这项服务变得复杂,那么可以将其拆分成多项更小的服务(软件即服务,所有软件对外的表现形式就诗提供一种或多种业务服务)。接下来进行独立的开发、测试、部署、运行、维护。进而更好,更灵活的处理客户端的请求并提高系统的可靠性,可扩展性。