基于商城项目了解SpringCloud

认识微服务

单体架构

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

优点:

  • 架构简单
  • 部署成本低

缺点:

  • 团队写作成本高
  • 系统发布效率低
  • 系统可用性差

 

总结:

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

微服务 

微服务是一种软件架构风格,它是以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。

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

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

SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud

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

SpringCloud和SpringBoot的版本对应:

SpringCloud版本

SpringBoot版本

2022.0.x aka Kilburn

3.0.x

2021.0.x aka Jubilee

2.6.x, 2.7.x (Starting with 2021.0.3)

2020.0.x aka Ilford

2.4.x, 2.5.x (Starting with 2020.0.3)

Hoxton

2.2.x, 2.3.x (Starting with SR5)

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

拆分微服务

服务拆分原则

什么时候拆分?

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

怎么拆分?

从拆分目标来说,要做到:

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

从拆分方式来说,一般包含两种方式:

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

☆拆分服务

工程结构有两种:

  • 独立Project
  • Maven聚合

☆远程调用(复习苍穹外卖的HttpClient)

拆分后碰到的第一个问题是什么,如何解决?

  • 拆分后,某些数据在不同服务,无法直接调用本地方法查询数据
  • 利用RestTemplate发送Http请求,实现远程调用

☆服务治理

注册中心原理

服务治理中的三个角色分别是什么?

  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息。
  • 服务提供者:暴露服务接口,供其他服务调用。
  • 服务调用者:调用其他服务提供的接口

消费者如何指导提供者的地址?

  • 服务提供者会在启动时注册自己的信息到注册中心,消费者可以从注册中心订阅和拉取服务信息。

消费者如何得知服务状态变更?

  • 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者。

当提供者有多个实例,消费者该选哪一个?

  • 消费者可以通过负载均衡算法,从多个实例中选择一个。

什么是负载均衡?

Nacos注册中心

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

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

官网:Nacos官网| Nacos 配置中心 | Nacos 下载| Nacos 官方社区 | Nacos

服务注册

提供者需要连接nacos以暴露服务

在pom.xml文件中导入Nacos依赖

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

在bootstrop.yaml中配置Nacos地址

spring:
    application:
        name: item-service # 服务名称
    cloud:
        nacos:
            server-addr: 192.168.100.103:8848 # nacos地址

服务发现

消费者需要连接nacos以拉取和订阅服务,前两步与服务注册一样,后面要加上服务调用

服务调用:


// 加上类注解@RequiredArgsConstructor
private final DiscoveryClient discoveryClient; 

private void handleCartItems(List<CartVO> vos) {
        // 1根据服务名称获取服务的实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
        if (CollUtils.isEmpty(instances)) {
            return;
        }
        // 2手写负载均衡,从实例列表中挑选一个实例
        ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
        // 3利用RestTemplate发起http请求,得到http的响应
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                instance.getUri() + "/items/ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<ItemDTO>>() {
                },
                Map.of("ids", CollUtils.join(itemIds, ","))
        );
        // 4 解析响应
        if (!response.getStatusCode().is2xxSuccessful()) {
            return;
        }
        List<ItemDTO> items = response.getBody();
    }

         @RequiredArgsConstructor是Lombok库中的一个注解,用于自动生成一个包含所有final和@NonNull字段的构造函数。这个注解可以帮助简化Java类的编写,避免手动编写繁琐的构造函数代码。

OpenFeign

        OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。官网:GitHub - OpenFeign/feign: Feign makes writing java http clients easier

        其作用是基于SpringMVC的常见注解,帮我们优雅的实现http请求发送。

快速入门

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

①.引入依赖

cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖:

  <!--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

cart-serviceCartApplication启动类上添加注解,启动OpenFeign功能:

@MapperScan("com.itcam.cart.mapper")
@SpringBootApplication
//@EnableFeignClients(basePackages = "com.itcam.api.client") // 指定FeignClient所在包
@EnableFeignClients(clients = {ItemClient.class},defaultConfiguration = DefaultFeignConfig.class) // 指定FeignClient字节码
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

③.编写FeignClient

cart-service中,定义一个新的接口,编写Feign客户端:

@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
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> :返回值类型

④.使用FiengClient实现远程调用 

cart-servicecom.hmall.cart.service.impl.CartServiceImpl中改造代码,直接调用ItemClient的方法:

@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    private final ItemClient itemClient;

    @Override
    public void addItem2Cart(CartFormDTO cartFormDTO) {
        ...
    }

    @Override
    public List<CartVO> queryMyCarts() {
        ...
    }

    private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 4.写入vo
        for (CartVO v : vos) {
            ItemDTO item = itemMap.get(v.getItemId());
            if (item == null) {
                continue;
            }
            v.setNewPrice(item.getPrice());
            v.setStatus(item.getStatus());
            v.setStock(item.getStock());
        }
    }

    @Override
    @Transactional
    public void removeByItemIds(Collection<Long> itemIds) {
        ...
    }
}
连接池

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

  • HttpURLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

①.引入依赖

在cart-service的pom.xml中引入依赖:

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

②.开启连接池

在cart-service的application.yml配置文件中开启Feign的连接池功能:

feign:
  okhttp:
    enabled: true # 开启OKHttp功能
最佳实践

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

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

不难看出:

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

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

抽取Feign客户端

创建hm-api模块

pom.xml的依赖如下:

    <dependencies>
        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- load balancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- swagger 注解依赖 -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

扫描包

在其他模块导入hm-api模块的依赖:

  <!--feign模块-->
  <dependency>
      <groupId>com.heima</groupId>
      <artifactId>hm-api</artifactId>
      <version>1.0.0</version>
  </dependency>
案例

在hm-api的client包下定义一个接口(比如CartClient):

@FeignClient("cart-service")
public interface CartClient {
    @DeleteMapping("/carts")
    void deleteCartItemByIds(@RequestParam("ids") Collection<Long> ids);
}

 在其他模块(比如cart-service)的启动类上添加@EableFeignClients注解:

@MapperScan("com.itcam.cart.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages = "com.itcam.api.client") // 指定FeignClient所在包
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

}
日志配置

日志级别有4级:

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

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

在hm-api模块下新建一个配置类,定义Feign的日志级别:

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

局部生效:在某个FeignClient中配置,只对当前FeignClient生效

@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)

比如:

@FeignClient(value = "cart-service",configuration = DefaultFeignConfig.class)
public interface CartClient {
    @DeleteMapping("/carts")
    void deleteCartItemByIds(@RequestParam("ids") Collection<Long> ids);
}

全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

比如: 

@MapperScan("com.itcam.cart.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages = "com.itcam.api.client",defaultConfiguration = DefaultFeignConfig.class) // 指定FeignClient所在包
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

}

☆网关路由

快速入门

1.创建新模块

2.引入网关依赖

     <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>

3.编写启动类

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

4.配置路由规则

hm-gateway模块的resources目录新建一个application.yaml文件,内容如下:

server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.100.103:8848
    gateway:
      routes:
        - id: item # 路由规则id,自定义,唯一
          uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
          predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
        - id: cart
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**
        - id: trade
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: pay
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**

路由属性(bootstrap.yaml)

spring:
  cloud:
    gateway:
      routes:
        - id: item
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**

 其中ctrl+鼠标左键进入routes对应的源码如下:

public void setRoutes(List<RouteDefinition> routes) {
        this.routes = routes;
        if (routes != null && routes.size() > 0 && this.logger.isDebugEnabled()) {
            this.logger.debug("Routes supplied from Gateway Properties: " + routes);
        }
    }

 routes是一个List<RouteDefinition>类型的集合,其中RouteDefinition的源码如下:

@Validated
public class RouteDefinition {
    private String id;
    private @NotEmpty @Valid List<PredicateDefinition> predicates = new ArrayList();
    private @Valid List<FilterDefinition> filters = new ArrayList();
    private @NotNull URI uri;
    private Map<String, Object> metadata = new HashMap();
    private int order = 0;

    略...
}

四个属性含义如下:

  • id:路由的唯一标示

  • predicates:路由断言,其实就是匹配条件

  • filters:路由过滤条件,后面讲

  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

路由断言,也就是predicates,SpringCloudGateway中支持的断言类型有12种。

名称

说明

示例

After

是某个时间点后的请求

- After=2037-01-20T17:42:47.789-07:00[America/Denver]

Before

是某个时间点之前的请求

- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]

Between

是某两个时间点之前的请求

- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]

Cookie

请求必须包含某些cookie

- Cookie=chocolate, ch.p

Header

请求必须包含某些header

- Header=X-Request-Id, \d+

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值