前进之路 发表于 2025-4-9 13:57:39

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

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

认识单体架构

单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时必要对所有模块一起编译、打包;项目的架构计划、开发模式都非常简朴。
https://i-blog.csdnimg.cn/direct/aed2e23ebef04bb2bfc0ab3694eb1fe3.png
   像我们之前写过的苍穹外卖,黑马点评,他们虽然被拆分成了差别的模块,但是还是一个单体项目,通过Maven的聚合,让所有模块接洽在一起,这种单体项目架构开发起来非常方便,例如我们简朴写一个背景管理系统,或者是访问量较小的个人博客的背景系统的时候,单体项目是再简朴不外的,但是如果我们用微服务来写,属实是大材小用。
    但随着项目的业务规模越来越大,团队开发人员也不绝增加,单体架构就出现出越来越多的标题:


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

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

[*]单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
[*]团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
[*]服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
https://i-blog.csdnimg.cn/direct/dcb6ed05c102472f9921cb5a3dbbae36.png
那么,单体架构存在的标题有没有解决呢?
   

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

微服务拆分以后碰到的各种标题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java范畴最全面的微服务组件的聚集了。
https://i-blog.csdnimg.cn/direct/4838438d6114422d8bf30a661323c7e9.png
而且SpringCloud依托于SpringBoot的自动装配能力,大大降低了其项目搭建、组件使用的成本。对于没有自研微服务组件能力的中小型企业,使用SpringCloud百口桶来实现微服务开发可以说是最符合的选择了!
   SpringCloud官方网址
拆分微服务

拆分原则

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

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

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

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

这里我们以商品服务为例子,点击新建,选择新建模块
https://i-blog.csdnimg.cn/direct/b0d53ad56b2d402399289a4c8b68be95.png
这里我们选择Java的Maven项目,JDK选择项目的JDK,父工程选择项目父工程
https://i-blog.csdnimg.cn/direct/af2244805cdc40b6a03f9783567ba889.png
引入依赖
<?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">
    <parent>
      <artifactId>hmall</artifactId>
      <groupId>com.heima</groupId>
      <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>item-service</artifactId>

    <properties>
      <maven.compiler.source>11</maven.compiler.source>
      <maven.compiler.target>11</maven.compiler.target>
    </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>
      </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>
编写启动类
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);
    }
}
接下来就是拷贝与商品管理有关的代码到该微服务项目当中,然后写配置
server:
port: 8081
spring:
application:
    name: item-service
profiles:
    active: dev
datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${hm.db.pw}
mybatis-plus:
configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
    db-config:
      update-strategy: not_null
      id-type: auto
logging:
level:
    com.hmall: debug
pattern:
    dateformat: HH:mm:ss:SSS
file:
    path: "logs/${spring.application.name}"
knife4j:
enable: true
openapi:
    title: 商品服务接口文档
    description: "信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
      group-name: default
      api-rule: package
      api-rule-resources:
          - com.hmall.item.controller
   注意在这里所有获取用户id的代码我们必要写死,背面我们会讲到如何获取。
服务调用

在微服务拆分的时候我们会发现,当一个微服务必要调用另一个微服务里的功能的时候,并不能直接注入Service,最终结果就是查询到的购物车数据不完备,因此要想解决这个标题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。最终就变成了这样
https://i-blog.csdnimg.cn/direct/1a57f6e7383d41609d52702bc36ee3e6.png
那么标题来了:我们该如何跨服务调用,正确的说,如何在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方法来构造请求。
https://i-blog.csdnimg.cn/direct/27bbde51f62b422cb98e12edab76d3fc.png
我们先将其注入为一个Bean:
package com.hmall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RemoteCallConfig {

    @Bean
    public RestTemplate restTemplate() {
      return new RestTemplate();
    }
}
远程调用

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


[*]① 请求方式
[*]② 请求路径
[*]③ 请求参数
[*]④ 返回值范例
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
      "http://localhost:8081/items?ids={ids}",
      HttpMethod.GET,
      null,
      new ParameterizedTypeReference<List<ItemDTO>>() {
      },
      Map.of("ids", CollUtil.join(itemIds, ","))
);
// 解析响应
if(!response.getStatusCode().is2xxSuccessful()){
    // 查询失败,直接结束
    return;
}
微服务的注册与发现

在上一章我们实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不外这种手动发送Http请求的方式存在一些标题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
https://i-blog.csdnimg.cn/direct/b53f36ce3ad84751964318d617e1f4f8.png
   此时,每个item-service的实例其IP或端口差别,标题来了:


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

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


[*]服务提供者:提供接供词其它微服务访问,比如item-service
[*]服务消费者:调用其它微服务提供的接口,比如cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中央的概念。注册中央、服务提供者、服务消费者三者间关系如下:
https://i-blog.csdnimg.cn/direct/75526757a1eb43799c66b6b06b8b7521.png
流程如下
   

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

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

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

[*]Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
[*]Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
[*]Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限定微服务语言
以上几种注册中央都遵照SpringCloud中的API规范,因此在业务开发使用上没有太大差别。但是Nacos是阿里巴巴公司开源的,有中文API,方便我们使用。
   Nacos官网
我们部署Nacos是基于Docker进行的,以是先要准备Nacos的相关表,然后我们必要修改Nacos的配置文件,在运行的时候根据官方配置挂载指定目录
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
https://i-blog.csdnimg.cn/direct/857eba2a596c4d98bcb38f5b85e9428b.png
   如果mysql和nacos在同一个网段下,这里直接写mysql的容器名字就可以,启动完成之后我们访问网址http://虚拟机IP:8848/nacos/,账号密码都是nacos
https://i-blog.csdnimg.cn/direct/24df090a91cc4cb8a561df0c1210136c.png
服务注册

引入依赖

<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置nacos

spring:
application:
    name: item-service # 服务名称
cloud:
    nacos:
      server-addr: 虚拟机IP:8848 # nacos地址
   在Nacos注册的时候,就会根据微服务的名字来注册,以是每个微服务的名字要唯一不重复
启动项目之后,我们在网站上可以看到该服务已经被注册
https://i-blog.csdnimg.cn/direct/65b179351a564da1913392360efc4c80.png
服务发现

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

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

spring:
cloud:
    nacos:
      server-addr: IP:8848
发现并调用服务

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


[*]随机
[*]轮询
[*]IP的hash
[*]最近最少访问
[*]…
[*]
这里我们可以选择最简朴的随机负载均衡。
   服务的发现必要一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用
https://i-blog.csdnimg.cn/direct/0629e0dd4df74df6bf3350172300c242.png
我们先通过这个工具,获取到所有定名为item-service的实例聚集,然后随机获取一个,获取它的URI,然后调用。
OpenFegin

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

引入依赖

<!--openFeign-->
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
启用OpenFeign

https://i-blog.csdnimg.cn/direct/b91f226df5224a8088d452f5eaacca06.png
编写OpenFeign客户端

package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只必要声明接口,无需实现方法。接口中的几个关键信息:
   

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

https://i-blog.csdnimg.cn/direct/b0747399f4ad47ec8b15e6fdc1ff5cc2.png
先注入,后使用
连接池

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


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

<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
配置开启连接池

feign:
okhttp:
    enabled: true # 开启OKHttp功能
抽取Feign客户端

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


[*]思绪1:抽取到微服务之外的公共module
[*]思绪2:每个微服务自己抽取一个module
https://i-blog.csdnimg.cn/direct/75d2d1a91f8944a2aa2233ed681163bf.png
方案1抽取更加简朴,工程布局也比较清楚,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程布局相对更复杂,但服务之间耦合度降低。
实战

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


[*]第一种是生命扫描包
https://i-blog.csdnimg.cn/direct/839c480878844229b017180eabd3c472.png
[*]第二种是声明要用的FeignClient,这里面是一个数组,可以声明多个
https://i-blog.csdnimg.cn/direct/68bbd2c57ffe41f2ac700ddea9a6013c.png

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【SpringCloud】从入门到精通【上】