【SpringCloud】从入门到精通【上】

打印 上一主题 下一主题

主题 1536|帖子 1536|积分 4608

本日主播我把黑马新版微服务课程MQ高级之前的内容都看完了,虽然在看视频的时候也记了笔记,但是看完之后还是忘得差不多了,以是打算写一篇博客再温习一下内容。
   课程坐标:黑马程序员SpringCloud微服务开发与实战
  微服务

认识单体架构

单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时必要对所有模块一起编译、打包;项目的架构计划、开发模式都非常简朴。

   像我们之前写过的苍穹外卖,黑马点评,他们虽然被拆分成了差别的模块,但是还是一个单体项目,通过Maven的聚合,让所有模块接洽在一起,这种单体项目架构开发起来非常方便,例如我们简朴写一个背景管理系统,或者是访问量较小的个人博客的背景系统的时候,单体项目是再简朴不外的,但是如果我们用微服务来写,属实是大材小用。
    但随着项目的业务规模越来越大,团队开发人员也不绝增加,单体架构就出现出越来越多的标题:
  

  • 团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,差别模块的代码之间物理边界越来越模糊。最终要把功能归并到一个分支,你绝对会陷入到解决辩论的泥潭之中。在公司当中一般都是用git来管理代码,你想象下,你开发一个模块,别人开发另一个模块,但是有一天,你们都对公共代码进行了修改,向git提交的时候是不是就会出现归并辩论。
  • 系统发布效率低:任何模块变更都必要发布整个系统,而系统发布过程中必要多个模块之间制约较多,必要对比各种文件,任何一处出现标题都会导致发布失败,往往一次发布必要数十分钟乃至数小时。
  • 系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会相互影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
    关于系统可用性差,我们可以想象下,如果我们单体项目有两个服务,一个是不太经常被访问的接口A,而一个是经常被访问的热点接口B,如果我们使用的是单体项目架构,那么热点接口B在被频繁访问的时候就会影响A的访问速度和性能,这就是单体项目的缺点,功能之间的相互影响比较大。而要想解决这些标题,就必要使用微服务架构了。
  认识微服务

微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:
   

  • 单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
  • 团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
  • 服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
  

那么,单体架构存在的标题有没有解决呢?
   

  • 团队协作成本高?
  • 由于服务拆分,每个服务代码量大大淘汰,参与开发的背景人员在1~3名,协作成本大大降低
  • 系统发布效率低?
  • 每个服务都是独立部署,当有某个服务有代码变更时,只必要打包部署该服务即可
  • 系统可用性差?
  • 每个服务独立部署,并且做好服务隔离,使用自己的服务器资源,不会影响到其它服务。
  SpringCloud

微服务拆分以后碰到的各种标题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java范畴最全面的微服务组件的聚集了。

而且SpringCloud依托于SpringBoot的自动装配能力,大大降低了其项目搭建、组件使用的成本。对于没有自研微服务组件能力的中小型企业,使用SpringCloud百口桶来实现微服务开发可以说是最符合的选择了!
   SpringCloud官方网址
  拆分微服务

拆分原则

服务拆分肯定要考虑几个标题:什么时候拆? 如何拆?
什么时候拆

一般环境下,对于一个初创的项目,首先要做的是验证项目的可行性。因此这一阶段的首要使命是敏捷开发,快速产出生产可用的产品,投入市场做验证。为了达成这一目的,该阶段项目架构往往会比较简朴,许多环境下会直接采用单体架构,这样开发成本比较低,可以快速产出结果,一旦发现项目不符合市场,丧失较小。
如果这一阶段采用复杂的微服务架构,投入大量的人力和时间成本用于架构计划,最终发现产品不符合市场需求,等于全部做了无用功。
以是,对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再渐渐拆分为微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的标题就在于后期做服务拆分时,可能会遇到许多代码耦合带来的标题,拆分比较困难(前易后难)。
而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构计划时就直接选择微服务架构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。
怎么拆

之前我们说过,微服务拆分时粒度要小,这实在是拆分的目标。具体可以从两个角度来分析:
   

  • 高内聚:每个微服务的职责要尽量单一,包罗的业务相互关联度高、完备度高。
  • 低耦合:每个微服务的功能要相对独立,尽量淘汰对其它微服务的依赖,或者依赖接口的稳固性要强。
  高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完备性为条件。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。
一旦微服务做到了高内聚,那么服务之间的耦合度天然就降低了。
当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时必要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且肯定要保证微服务对外接口的稳固性(即:尽量保证接口表面不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。
明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:纵向拆分 横向拆分
所谓纵向拆分,就是按照项目的功能模块来拆分。例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能进步服务的内聚性。
而横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是必要发送消息关照,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中央服务、风控管理服务。这样可以进步业务的复用性,避免重复开发。同时通用业务一般接口稳固性较强,也不会使服务之间太过耦合。
拆分实操

这里我们以商品服务为例子,点击新建,选择新建模块

这里我们选择Java的Maven项目,JDK选择项目的JDK,父工程选择项目父工程

引入依赖
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5.     <parent>
  6.         <artifactId>hmall</artifactId>
  7.         <groupId>com.heima</groupId>
  8.         <version>1.0.0</version>
  9.     </parent>
  10.     <modelVersion>4.0.0</modelVersion>
  11.     <artifactId>item-service</artifactId>
  12.     <properties>
  13.         <maven.compiler.source>11</maven.compiler.source>
  14.         <maven.compiler.target>11</maven.compiler.target>
  15.     </properties>
  16.     <dependencies>
  17.         <!--common-->
  18.         <dependency>
  19.             <groupId>com.heima</groupId>
  20.             <artifactId>hm-common</artifactId>
  21.             <version>1.0.0</version>
  22.         </dependency>
  23.         <!--web-->
  24.         <dependency>
  25.             <groupId>org.springframework.boot</groupId>
  26.             <artifactId>spring-boot-starter-web</artifactId>
  27.         </dependency>
  28.         <!--数据库-->
  29.         <dependency>
  30.             <groupId>mysql</groupId>
  31.             <artifactId>mysql-connector-java</artifactId>
  32.         </dependency>
  33.         <!--mybatis-->
  34.         <dependency>
  35.             <groupId>com.baomidou</groupId>
  36.             <artifactId>mybatis-plus-boot-starter</artifactId>
  37.         </dependency>
  38.         <!--单元测试-->
  39.         <dependency>
  40.             <groupId>org.springframework.boot</groupId>
  41.             <artifactId>spring-boot-starter-test</artifactId>
  42.         </dependency>
  43.     </dependencies>
  44.     <build>
  45.         <finalName>${project.artifactId}</finalName>
  46.         <plugins>
  47.             <plugin>
  48.                 <groupId>org.springframework.boot</groupId>
  49.                 <artifactId>spring-boot-maven-plugin</artifactId>
  50.             </plugin>
  51.         </plugins>
  52.     </build>
  53. </project>
复制代码
编写启动类
  1. package com.hmall.item;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. @MapperScan("com.hmall.item.mapper")
  6. @SpringBootApplication
  7. public class ItemApplication {
  8.     public static void main(String[] args) {
  9.         SpringApplication.run(ItemApplication.class, args);
  10.     }
  11. }
复制代码
接下来就是拷贝与商品管理有关的代码到该微服务项目当中,然后写配置
  1. server:
  2.   port: 8081
  3. spring:
  4.   application:
  5.     name: item-service
  6.   profiles:
  7.     active: dev
  8.   datasource:
  9.     url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
  10.     driver-class-name: com.mysql.cj.jdbc.Driver
  11.     username: root
  12.     password: ${hm.db.pw}
  13. mybatis-plus:
  14.   configuration:
  15.     default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  16.   global-config:
  17.     db-config:
  18.       update-strategy: not_null
  19.       id-type: auto
  20. logging:
  21.   level:
  22.     com.hmall: debug
  23.   pattern:
  24.     dateformat: HH:mm:ss:SSS
  25.   file:
  26.     path: "logs/${spring.application.name}"
  27. knife4j:
  28.   enable: true
  29.   openapi:
  30.     title: 商品服务接口文档
  31.     description: "信息"
  32.     email: zhanghuyi@itcast.cn
  33.     concat: 虎哥
  34.     url: https://www.itcast.cn
  35.     version: v1.0.0
  36.     group:
  37.       default:
  38.         group-name: default
  39.         api-rule: package
  40.         api-rule-resources:
  41.           - com.hmall.item.controller
复制代码
  注意在这里所有获取用户id的代码我们必要写死,背面我们会讲到如何获取。
  服务调用

在微服务拆分的时候我们会发现,当一个微服务必要调用另一个微服务里的功能的时候,并不能直接注入Service,最终结果就是查询到的购物车数据不完备,因此要想解决这个标题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。最终就变成了这样

那么标题来了:我们该如何跨服务调用,正确的说,如何在cart-service中获取item-service服务中的提供的商品数据呢?
   大家思考一下,我们以前有没有实现过类似的远程查询的功能呢?
有的兄弟,有的,我们前端向服务端查询数据,实在就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items这个接口发起的请求:
  而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
   假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢?
  那么:我们该如何用Java代码发送Http的请求呢?
RestTemplate

Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。其中提供了大量的方法,方便我们发送http请求。可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。

我们先将其注入为一个Bean:
  1. package com.hmall.cart.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.client.RestTemplate;
  5. @Configuration
  6. public class RemoteCallConfig {
  7.     @Bean
  8.     public RestTemplate restTemplate() {
  9.         return new RestTemplate();
  10.     }
  11. }
复制代码
远程调用

可以看到,使用RestTemplate发送http请求与前端ajax发送请求非常相似,都包罗四部分信息:


  • ① 请求方式
  • ② 请求路径
  • ③ 请求参数
  • ④ 返回值范例
  1. ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
  2.         "http://localhost:8081/items?ids={ids}",
  3.         HttpMethod.GET,
  4.         null,
  5.         new ParameterizedTypeReference<List<ItemDTO>>() {
  6.         },
  7.         Map.of("ids", CollUtil.join(itemIds, ","))
  8. );
  9. // 解析响应
  10. if(!response.getStatusCode().is2xxSuccessful()){
  11.     // 查询失败,直接结束
  12.     return;
  13. }
复制代码
微服务的注册与发现

在上一章我们实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不外这种手动发送Http请求的方式存在一些标题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:

   此时,每个item-service的实例其IP或端口差别,标题来了:
  

  • item-service这么多实例,cart-service如何知道每一个实例的地点?
  • http请求要写url地点,cart-service服务到底该调用哪个实例呢?
  • 如果在运行过程中,某一个item-service实例宕机,cart-service依然在调用该怎么办?
  • 如果并发太高,item-service临时多部署了N台实例,cart-service如何知道新实例的地点?
  为相识决上面的标题,就必须引入注册中央的概念了
注册中央

在微服务远程调用的过程中,包括两个角色:


  • 服务提供者:提供接供词其它微服务访问,比如item-service
  • 服务消费者:调用其它微服务提供的接口,比如cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中央的概念。注册中央、服务提供者、服务消费者三者间关系如下:

流程如下
   

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中央
  • 调用者可以从注册中央订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用
  那么当提供服务的宕机或者开启了新的服务了,服务调用者该怎么知道呢
   

  • 心跳机制:服务提供者会定期向注册中央发送请求,报告自己的康健状态,当注册中央长时间收不到提供者的心跳时,会以为该实例宕机,将其从服务的实例列表中剔除
  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中央的服务实例列表
  • 当注册中央服务列表变更时,会主动关照微服务,更新本地服务列表
  Nacos注册中央

注册中央框架许多,目前国内盛行的有三个
   

  • Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
  • Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
  • Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限定微服务语言
  以上几种注册中央都遵照SpringCloud中的API规范,因此在业务开发使用上没有太大差别。但是Nacos是阿里巴巴公司开源的,有中文API,方便我们使用。
   Nacos官网
  我们部署Nacos是基于Docker进行的,以是先要准备Nacos的相关表,然后我们必要修改Nacos的配置文件,在运行的时候根据官方配置挂载指定目录
  1. docker run -d \
  2. --name nacos \
  3. --env-file ./nacos/custom.env \
  4. -p 8848:8848 \
  5. -p 9848:9848 \
  6. -p 9849:9849 \
  7. --restart=always \
  8. nacos/nacos-server:v2.1.0-slim
复制代码

   如果mysql和nacos在同一个网段下,这里直接写mysql的容器名字就可以,启动完成之后我们访问网址http://虚拟机IP:8848/nacos/,账号密码都是nacos
  

服务注册

引入依赖

  1. <!--nacos 服务注册发现-->
  2. <dependency>
  3.     <groupId>com.alibaba.cloud</groupId>
  4.     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
复制代码
配置nacos

  1. spring:
  2.   application:
  3.     name: item-service # 服务名称
  4.   cloud:
  5.     nacos:
  6.       server-addr: 虚拟机IP:8848 # nacos地址
复制代码
  在Nacos注册的时候,就会根据微服务的名字来注册,以是每个微服务的名字要唯一不重复
  启动项目之后,我们在网站上可以看到该服务已经被注册

服务发现

服务调用者想要调用其他微服务就要,引入依赖 配置Nacos地点 发现并调用服务 走这三步
引入依赖

服务发现除了要引入nacos依赖以外,由于还必要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
  1. <!--nacos 服务注册发现-->
  2. <dependency>
  3.     <groupId>com.alibaba.cloud</groupId>
  4.     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
复制代码
可以发现,这里Nacos的依赖于服务注册时同等,这个依赖中同时包罗了服务注册和发现的功能。由于任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
因此,等一会儿cart-service启动,同样会注册到Nacos
配置Nacos

  1. spring:
  2.   cloud:
  3.     nacos:
  4.       server-addr: IP:8848
复制代码
发现并调用服务

接下来,服务调用者cart-service就可以去订阅item-service服务了。不外item-service有多个实例,而真正发起调用时只必要知道一个实例的地点。
因此,服务调用者必须使用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:


  • 随机
  • 轮询
  • IP的hash
  • 最近最少访问


这里我们可以选择最简朴的随机负载均衡。
   服务的发现必要一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用
  

我们先通过这个工具,获取到所有定名为item-service的实例聚集,然后随机获取一个,获取它的URI,然后调用。
OpenFegin

在上一章,我们使用Nacos实现了服务的治理,使用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了,而且这种调用方式,与原本的本地方法调用差别太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简朴。而这就要用到OpenFeign组件了。
快速入门

引入依赖

  1.   <!--openFeign-->
  2.   <dependency>
  3.       <groupId>org.springframework.cloud</groupId>
  4.       <artifactId>spring-cloud-starter-openfeign</artifactId>
  5.   </dependency>
  6.   <!--负载均衡器-->
  7.   <dependency>
  8.       <groupId>org.springframework.cloud</groupId>
  9.       <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  10.   </dependency>
复制代码
启用OpenFeign


编写OpenFeign客户端

  1. package com.hmall.cart.client;
  2. import com.hmall.cart.domain.dto.ItemDTO;
  3. import org.springframework.cloud.openfeign.FeignClient;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. import java.util.List;
  7. @FeignClient("item-service")
  8. public interface ItemClient {
  9.     @GetMapping("/items")
  10.     List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
  11. }
复制代码
这里只必要声明接口,无需实现方法。接口中的几个关键信息:
   

  • @FeignClient("item-service") :声明服务名称
  • @GetMapping :声明请求方式
  • @GetMapping("/items") :声明请求路径
  • @RequestParam("ids") Collection<Long> ids :声明请求参数
  • List<ItemDTO> :返回值范例
  有了上述信息,OpenFeign就可以使用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List。
我们只必要直接调用这个方法,即可实现远程调用了。
使用FeignClient


先注入,后使用
连接池

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:


  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池
引入依赖

  1. <!--OK http 的依赖 -->
  2. <dependency>
  3.   <groupId>io.github.openfeign</groupId>
  4.   <artifactId>feign-okhttp</artifactId>
  5. </dependency>
复制代码
配置开启连接池

  1. feign:
  2.   okhttp:
  3.     enabled: true # 开启OKHttp功能
复制代码
抽取Feign客户端

我们在里微服务调同一个接口的时候,如果没有抽取出来,那么每个微服务是不是都必要重新编写一下,那么有什么办法能解决这种重复编码的标题吗
这里有两种解决办法


  • 思绪1:抽取到微服务之外的公共module
  • 思绪2:每个微服务自己抽取一个module

方案1抽取更加简朴,工程布局也比较清楚,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程布局相对更复杂,但服务之间耦合度降低。
实战

这里我们选择方案1,只必要再创建一个模块,名为hm-all引入必要的依赖,在里面编写接口就可以。但是这里我们必要注意一个包扫描的标题,我们每个微服务都在独立的包中,包括这个API模块也在独立的保重,boot项目默认扫描的是当前包及其子包 。这里我们有两种解决方案。


  • 第一种是生命扫描包

  • 第二种是声明要用的FeignClient,这里面是一个数组,可以声明多个


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

前进之路

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表