目录
1.认识微服务
1.1单体架构
1.2微服务
1.3SpringCloud
2.微服务拆分
2.1服务拆分原则
2.1.1什么时候拆?
2.1.2怎么拆?
2.2微服务项目结构:
2.3服务拆分示例:
2.4远程调用
2.4.1RestTemplate
2.4.2远程调用示例
2.5总结
3.服务治理
3.1注册中心原理
3.2Nacos注册中心
3.3服务注册
3.3.1.添加依赖
3.3.2.配置Nacos
3.3.3.启动服务实例
3.4服务发现
3.4.1引入依赖
3.4.2.配置Nacos地址
3.4.3.发现并调用服务
1.认识微服务
1.1单体架构
将业务的所有功能集中在一个项目中开发,打成一个包部署。
当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。
单体架构的优缺点:
优点:
架构简单
部署成本低
缺点:
团队协作成本高:你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
1.2微服务
微服务架构:首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:
-单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
-团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人
-服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
于是,单体架构存在的问题就得到了解决。
- 团队协作成本高:
- 由于服务拆分,每个服务代码量大大减少,参与开发的后台人员在1~3名,协作成本大大降低
- 系统发布效率低:
- 每个服务都是独立部署,当有某个服务有代码变更时,只需要打包部署该服务即可
- 系统可用性差:
- 每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。
微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。大家以前可能听说过分布式架构,分布式就是服务拆分的过程,其实微服务架构正式分布式架构的一种最佳实践的方案。
1.3SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架,其官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
其中常见的组件包括:
另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,如下:
2.微服务拆分
2.1服务拆分原则
服务拆分要考虑的两个问题:什么时候拆?怎么拆?
2.1.1什么时候拆?
创业型项目:一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分为微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。
确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。(前难后易)
2.1.2怎么拆?
拆分项目要做到:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。
一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。
当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
拆分方式一般有两种:
纵向拆分:按照业务模块来拆分。例如一个项目中,可能有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。
横向拆分:抽取公共服务,提高复用性。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
2.2微服务项目结构:
一般微服务项目有两种不同的工程结构:
- 独立Project:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。
- 优点:服务之间耦合度低
- 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
- Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
- 优点:项目代码集中,管理和运维方便(授课也方便)
- 缺点:服务之间耦合,编译时间较长
2.3服务拆分示例:
下面这个是一个示例项目,项目结构如下。
我们的要求是拆分商品服务功能。
1、在hamll中新建Module功能
2、填写信息---模块名称---选择Maven项目---选择JDK版本---点击Create完成创建
3、引入依赖:可以直接复制hm-service的依赖,然后删除一些不需要的依赖就可以了。
<?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.heima</groupId><artifactId>hmall</artifactId><version>1.0.0</version></parent><artifactId>item-services</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--数据库--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--mybatis--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><!--单元测试--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
4、编写包和启动类---新建com.hmall.item包---在包下新建启动类(可以直接复制hm-service的启动类,然后改启动类的名称就好了)
package com.hmall.item;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.hmall.item.mapper")
@SpringBootApplication
public class ItemApplication {public static void main(String[] args) {SpringApplication.run(ItemApplication.class, args);}
}
5、 创建以后会用到的各种包--controller--domain--mapper--service等
6、配置文件:可以直接从hm-service中拷贝。
7、然后更改application.yaml的信息,其他两个不用改。
server:port: 8081 #端口需要改,不然会和hm-service的冲突
spring:application:name: item-services #微服务名称也需要改profiles:active: local#数据库连接需要改:微服务的数据库需要独立,不能访问其他微服务的数据库,所以我们需要在docker中重新创建一个独立的mysql,但这里我们就只是为了模拟,#所以就在当前数据库新建一个database。把hamll数据库换了就行,这里换成了hm-item。datasource:url: jdbc:mysql://${hm.db.host}:3307/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: ${hm.db.pw}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: "logs/${spring.application.name}"
knife4j:enable: trueopenapi:title: 黑马商城商品管理接口文档 #修改文档名称为模块对应的功能。description: "黑马商城商品管理接口文档"email: zhanghuyi@itcast.cnconcat: 虎哥url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.hmall.item.controller #改为item-services下的包
8、从hm-service拷贝与商品管理有关的代码到item-service
导入顺序:domain---mapper---service---controller。因为是依赖关系。
因为引入的import需要更改,可以设置自动导入import,避免一直需要按Alt+Enter
File->Settings->Editor->General->Auto Import->勾选右边两个有关import的选项
9、这里有一个地方的代码需要改动,就是ItemServiceImpl
中的deductStock
方法:
这也是因为ItemMapper的所在包发生了变化,因此这里代码必须修改包路径。
最后,还要导入数据库表。默认的数据库连接的是虚拟机,在你docker数据库执行课前资料提供的SQL文件:
最终,会在数据库创建一个名为hm-item的database,将来的每一个微服务都会有自己的一个database:
11、配置完所有东西后,启动item-services,然后访问商品微服务的swagger接口文档:
http://localhost:8081/doc.html
然后测试其中的根据id批量查询商品这个接口:
测试参数:100002672302,100002624500,100002533430,结果如下:
说明商品微服务抽取成功了。
2.4远程调用
在业务拆分的过程中,有时候我们会遇到比如这样的问题:购物车业务需要查询商品信息,但是商品信息查询的功能已经拆分开了,两者的数据库又不是同一个,导致我们无法查询商品信息。
虽然代码不是连通的,但是网络是连通的。
要想解决这样的问题,我们要使用跨微服务的远程调用的方法。
我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如通过Swagger测试商品查询接口,就是向http://localhost:8081/items
这个接口发起的请求:
而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
于是我们也可以通过Java代码发送http请求来实现远程调用
2.4.1RestTemplate
Spring给我们提供了一个RestTemplate工具,可以方便的实现Http请求的发送。使用步骤如下;
(1)注入RestTemplate到Spring容器
(2)发起远程调用
RestTemplate给我们提供了很多的方法,方便发送Http请求
2.4.2远程调用示例
先将RestTemplate注册为一个Bean:
注入RestTemplate
修改相应代码,发送http请求到另一个模块
然后开始接口调试----记住两个业务功能都要启动,item-services和cart-service都要启动。
查询成功
2.5总结
什么时候需要拆分微服务?
- 如果是创业型公司,最好先用单体架构快速迭代开发,验证市场运作模型,快速试错。当业务跑通以后,随着业务规模扩大、人员规模增加,再考虑拆分微服务。
- 如果是大型企业,有充足的资源,可以在项目开始之初就搭建微服务架构。
如何拆分?
- 首先要做到高内聚、低耦合
- 从拆分方式来说,有横向拆分和纵向拆分两种。纵向就是按照业务功能模块,横向则是拆分通用性业务,提高复用性
服务拆分之后,不可避免的会出现跨微服务的业务,此时微服务之间就需要进行远程调用。微服务之间的远程调用被称为RPC,即远程过程调用。RPC的实现方式有很多,比如:
- 基于Http协议
- 基于Dubbo协议
我们课堂中使用的是Http方式,这种方式不关心服务提供者的具体技术实现,只要对外暴露Http接口即可,更符合微服务的需要。
Java发送http请求可以使用Spring提供的RestTemplate,使用的基本步骤如下:
- 注册RestTemplate到Spring容器
- 调用RestTemplate的API发送请求,常见方法有:
- getForObject:发送Get请求并返回指定类型对象
- PostForObject:发送Post请求并返回指定类型对象
- put:发送PUT请求
- delete:发送Delete请求
- exchange:发送任意类型请求,返回ResponseEntity
3.服务治理
前面我们实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个item-service
的实例其IP或端口不同,问题来了:
item-service这么多实例,cart-service如何知道每一个实例的地址?
http请求要写url地址,cart-service服务到底该调用哪个实例呢?
如果在运行过程中,某一个item-service实例宕机,cart-service依然在调用该怎么办?
如果并发太高,item-service临时多部署了N台实例,cart-service如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了,接下来我们就一起来分析下注册中心的原理。
3.1注册中心原理
在微服务远程调用的过程中,包括两个角色:
-
服务提供者:提供接口供其它微服务访问,比如
item-service
-
服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
3.2Nacos注册中心
目前开源的注册框架有很多,我们用Nacos来实现。
我们基于Docker来部署Nacos的注册中心,首先我们要准备MySQL数据库表,用来存储Nacos的数据。由于是Docker部署,所以大家需要将资料中的SQL文件导入到你Docker中的MySQL容器中:
最终表结构如下:
然后,找到课前资料下的nacos文件夹:
其中的nacos/custom.env
文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,需要修改为你自己的虚拟机IP地址,还有端口,密码等:
然后,将课前资料中的nacos
目录上传至虚拟机的/root
目录。
进入root目录,然后执行下面的docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
容器部署完成后可以通过下面命令来查看Nacos的运行日志
docker logs -f nacos
启动完成后,访问下面地址:http://192.168.52.135:8848/nacos,注意将192.168.52.135
替换为你自己的虚拟机IP地址。
首次访问会跳转到登录页,账号密码都是nacos
3.3服务注册
比如将item-service注册到Nacos中,有以下步骤:
3.3.1.添加依赖
在item-service
的pom.xml
中添加依赖:
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.3.2.配置Nacos
在item-service
的application.yml
中添加nacos地址配置:
spring:application:name: item-service # 服务名称cloud:nacos:server-addr: 192.168.52.135:8848 # nacos地址
3.3.3.启动服务实例
为了测试一个服务多个实例的情况,我们再配置一个item-service
的部署实例:
右击Modify options
添加VM options
设置不同的启动类名称和端口号。然后点击Apply--Ok
启动Item-services的多个启动类。
访问nacos控制台,可以发现服务注册成功:
点击详情,可以查看到item-service
服务的两个实例信息:
3.4服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
-
引入依赖
-
配置Nacos地址
-
发现并调用服务
3.4.1引入依赖
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
我们在cart-service
中的pom.xml
中添加下面的依赖:
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
可以发现,这里Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
因此,等一会儿cart-service
启动,同样会注册到Nacos。
3.4.2.配置Nacos地址
在cart-service
的application.yml
中添加nacos地址配置:
spring:cloud:nacos:server-addr: 192.168.150.101:8848 #nacos地址
3.4.3.发现并调用服务
服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
然后通过Swagger调试,成功。