微服务上(黑马)

微服务01

1 认识微服务

1.1 单体架构

单体架构: 将业务的所有功能集中在一个项目中开发,打成一个包部署。

优点:

  • 架构简单
  • 部署成本低

缺点:

  • 团队协作成本高
  • 系统发布效率低
  • 系统可用性差(当某一个服务负载过大,比如访问量太大,导致此服务崩溃,由于所有服务公用一个资源空间,其它服务也会受到影响,崩溃)
    在这里插入图片描述

总结:单体架构适合开发功能相对简单,规模较小的项目。

1.2 微服务

微服务架构: 是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。

优点:

  • 粒度小
  • 团队自治
  • 服务自治

1.3 SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud.

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的我开箱即用体验:

在这里插入图片描述

对于SpringBoot的版本也有要求
在这里插入图片描述

2 微服务拆分

2.1 熟悉黑马商城

在这里插入图片描述

2.2 服务拆分原则

2.2.1.什么时候拆
  • 创业型项目: 先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
  • 确定的大型项目: 资金充足,项目明确,可以直接选择微服务架构,避免后续拆分的麻烦。
2.2.2.怎么拆

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

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
  • 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。

一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。

当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。

明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:

  • 纵向拆分: 按照业务模块来拆分
  • 横向拆分: 抽取公共服务,提高复用性

所谓纵向拆分,就是按照项目的功能模块来拆分。例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。

横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。

当然,由于黑马商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务:

  • 用户服务
  • 商品服务
  • 订单服务
  • 购物车服务
  • 支付服务

2.3 拆分服务

工程结构有两种:

  • 独立Project(适用淘宝等大型复杂工程)
    在这里插入图片描述

  • Maven聚合(中小型企业 相对简单的工程)
    在这里插入图片描述

2.3.1 拆分商品管理功能模块

将hm-service中与商品管理相关功能拆分到一个微服务module中,命名为item-service

2.3.2 拆分购物车功能模块

将hm-service中与购物车有关的功能拆分到一个微服务module中,命名为cart-service

2.4 远程调用

在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service服务,导致我们无法查询。

最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。

因此,现在查询购物车列表的流程变成了这样:

在这里插入图片描述

代码中需要变化的就是这一步:

在这里插入图片描述

那么问题来了:我们该如何跨服务调用,准确的说,如何在cart-service中获取item-service服务中的提供的商品数据呢?

image-20240724092022546

前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items这个接口发起的请求:而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。

2.4.1 RestTemplate

Spring给我们提供了一个ResTemplate工具,可以方便的实现Http请求的发送。其中提供了大量的方法,方便我们发送Http请求,例如:

可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。

使用步骤如下:

  • 注入RestTemplate到Spring容器

在这里插入图片描述

  • 发起远程调用
2.4.2.远程调用

接下来,我们修改cart-service中的com.hmall.cart.service.impl.``CartServiceImplhandleCartItems方法,发送http请求到item-service

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

  • ① 请求方式
  • ② 请求路径
  • ③ 请求参数
  • ④ 返回值类型

2.5 总结

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.注册中心原理

流程如下:

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用

当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?

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

在这里插入图片描述

3.2 Nacos注册中心

Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴产品,目前已经加入SpringCloudAlibaba中。

3.3 服务注册

服务注册步骤如下:

  • 引入nacos discovery依赖:

  • 配置Nacos地址

3.4 服务发现

消费者 需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样,后面再加上服务调用即可:

  • 引入nacos discovery

  • 配置Nacos地址

  • 服务发现

// 2.查询商品
        // 2.1 根据服务名称获取服务的实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
        if(CollUtils.isEmpty(instances)){
            return;
        }
        // 2.2 手写负载均衡 从实例列表中挑选一个实例
        ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
        // 2.3 利用ResTemplate发起http请求,得到http的响应
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                serviceInstance.getUri() + "/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<ItemDTO>>() {
                },
                Map.of("ids", CollUtil.join(itemIds, ","))
        );
        //2.4 解析响应
        if(!response.getStatusCode().is2xxSuccessful()){
            //查询失败 直接结束
            return;
        }
//        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        List<ItemDTO> items = response.getBody();
        if (CollUtils.isEmpty(items)) {
            return;
        }

4 OpenFeign

在上一章,我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:

而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。

因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。

其实远程调用的关键点就在于四个:

  • 请求方式
  • 请求路径
  • 请求参数
  • 返回值类型

所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。

4.1 快速入门

OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。其作用就是基于SpringMVC的常见注解,帮我们优雅地实现http请求的发送。

OpenFeign已经被SpringCloud自动装配,实现起来非常简单

  • 引入依赖,包括OpenFeign和负载均衡组件SpringCloudLoadBalancer

  • 通过@EnableFeignClients注解,启用OpenFeign功能

  • 编写FeignClient,这段代码就是代替了之前写的一大坨代码(发现实例,负载均衡,发送请求)

    这里只需要声明接口,无需实现方法。接口中的几个关键信息:

    • @FeignClient("item-service") :声明服务名称 localhost:8081 localhost8083

    • @GetMapping :声明请求方式 Get

    • @GetMapping("/items") :声明请求路径 /items

    • @RequestParam("ids") Collection<Long> ids :声明请求参数 /{ids}

      以上拼起来路径就是 Get方式的 https://localhost:8081/items?ids=1,2,3

    • List<ItemDTO> :返回值类型

    有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>

    我们只需要直接调用这个方法,即可实现远程调用了

  • 使用FeignClient,实现远程调用

4.2 连接池

OpenFeign对http请求做了优雅的封装,不过其底层发起http请求,依赖于其它的框架。这些框架可以自己选择,包括一下三种:

  • HttpURLConnection: 默认实现。不支持连接池
  • Apache HttpClient: 支持连接池
  • OKHttp: 支持连接池

这里使用的是okhttp

  • 引入依赖

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

    feign:
      okhttp:
        enabled: true # 开启OKHttp功能
    

4.3 最佳实践

如果拆分了交易微服务(trade-service),它也需要远程调用item-service中的根据id批量查询商品功能。这个需求与cart-service中是一样的。

因此,我们就需要在trade-service中再次定义ItemClient接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?

4.3.1.思路分析

避免重复编码的办法就是抽取。不过这里有两种抽取思路:

  • 思路1:抽取到微服务之外的公共module

  • 思路2:每个微服务自己抽取一个module
    在这里插入图片描述

方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。

方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

那么该选择方案:

我们知道拆分服务分为两种结构,一种是project独立结构,一种是Maven聚合结构。对于Project独立结构,本身每一个模块就是一个工程,耦合度低,也更方便采用方案2的方法;对于Maven聚合结构,由于耦合度稍微高一点,且本身就带有common通用模块,再多一个通用模块也无所谓,因此建议选择方案一。不过其实如果不嫌麻烦的话,还是建议方案二。

对于本使用了Maven结构的项目来说,选择方案1。

4.3.2 扫描包

当定义的FeignClient不在SpringBootApplicaiton的扫描包范围时,这些FeignClient无法使用,有两种方式解决

方式一:指定FeignClient所在包

方式二:指定FeignClient字节码

4.4 日志

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE: 不记录任何日志信息,这是默认值
  • BASIC: 仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS: 在BASUC的基础上,额外记录了请求和响应的头信息
  • FULL: 记录所有请求和响应的明细,包括头信息、请求体、元数据

由于Feign默认的日志级别是NONE,所以默认我们看不到请求日志

要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:

但此时这个Bean并未生效,要想配置某个FeignClient的日志,可以在@FeignClient注解中声明:

如果想要全局配置,让所有FeignClient都按照这个日志配置,则需要在@EnableFeignClients注解中声明:

4.5 总结

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值