SpringCloud

认识微服务

单体架构

在认识微服务之前,我先来说说单体架构,我们之前所学的SpringBoot做的最多项目就是单体架构项目。而单体架构是将业务的所有功能集中一个项目中开发,打成一个包部署。所以单体架构的优缺点就十分的明显,如下图表示。

微服务架构

而微服务就是把单体架构中某些功能或者某些模块给拆分出来,拆分单个项目或者单个模块,成为一个微服务集群

SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架,它集成了各种微服务功能组件,并且基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱。

SpringCloud基于SpringBoot实现了微服务组件的自动装配,从而提供了良好的开箱即用体验。对于SpringBoot的版本也有要求所以使用的版本

准备工作

熟悉黑马商城

黑马商城包括:

  • 用户模块
  • 商品模块
  • 购物车模块
  • 订单模块
  • 支付模块

一会我们进行纵向拆分

服务拆分原则

什么时候拆分

在企业里面使用微服务拆分一般分为两种

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

怎么拆分

拆分目标来说,要做到以下两个方面

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

拆分方式来说,包含以下两个方面

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

拆分服务

将一个项目拆成多个模块,把hm-service先拆出来item-service和cart-service,我们首先拆分itme-service

新建一个model

然后将hm-service里面的item相关代码复制粘贴到item-service里面

添加Maven依赖

<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>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-rsa</artifactId>
    <version>1.0.9.RELEASE</version>
  </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>
  <!--redis-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
</dependencies>
<build>
  <finalName>${project.artifactId}</finalName>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

修改yml文件

另外一个模块同理

不过注意一下,itemService注释掉,以及UserContext如下图,拆分成所以我们拿不到一会我们使用远程调用来解决

远程调用

如我刚刚说的,因为我们把单一项目拆分成了模块,模块与模块直接请求需要用到远程调用,而Spring给我们提供了一个RestTemplate工具,可以方便实现Http请求调用。

  1. 注入RestTemplate到Spring容器当中
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
  1. 发起调用
public <T> ResponseEntity<T> exchange(
    String url, // 请求路径
    HttpMethod method, // 请求方式
    @Nullable HttpEntity<?> requestEntity, // 请求实体不为空
    Class<T> responseType, // 返回值类型
    Map<String, ?> uriVariables // 请求参数
)

下面我们直接使用RestTemplate来进行远程调用,先在Card模块注入

然后在到service层注入RestTemplate

再注入的同时我们发现@autowired有黄色的下黄线,我们查看发现idea提示我们不推荐这样注入,要求我们直接写一个构造方法。

但是加入我们的成员变量很多,那么我们使用构造函数的列表就会很长,这样我们后续去阅读代码就会很麻烦,所以我们使用Lombok的@RequiredArgsConstructor的注解并且给RestTemplate加上final这样就变成了必备成员变量的构造函数。

接下来我们直接远程调用

    private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
//        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                "http://localhost:8081/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<>() {
                },
                Map.of("ids", CollUtils.join(itemIds, ","))
        );
        if(!response.getStatusCode().is2xxSuccessful()) {
            return;
        }
        List<ItemDTO> items = response.getBody();
        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());
        }
    }

服务治理

刚刚我们已经使用远程调用,但是刚刚远程调用我们后续会遇到一个问题,那就是发送请求的路劲是写死的。如果某一天我们写死的路径发生了变化或者它崩掉了,我们应该怎么办。所以这个时候我们就要用到注册中心。

注册中心

注册中心原理

注册中心主要是帮助服务提供者,提供给服务调用者,比如说我们有服务三个接口,注册中心会把这个三个接口统计下来,等到服务调用者来调用时,注册中心把统计好的服务给调用者,调用者拿到服务通过负载均衡去调用相应的接口。

如果提供者的某个服务挂掉注册中心也会知道,并把挂掉的服务告诉调用者,调用者就不用去请求到挂掉的服务。

总结:

  1. 服务治理中的三个角色分别是什么?
  • 服务提供者:暴露服务接口,供其它服务调用
  • 服务消费者:调用其它服务提供的接口•注册中心:记录并监控微服务各实例状态,
  1. 推送服务变更信息消费者如何知道提供者的地址?
  • 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
  1. 消费者如何得知服务状态变更?
  • 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
  1. 当提供者有多个实例时,消费者该选择哪一个?
  • 消费者可以通过负载均衡算法,从多个实例中选择一个

Nacos注册中心

安装nacos

GitHub下载地址:https://github.com/alibaba/nacos/releases

Nacos官网:https://nacos.io/zh-cn/docs/quick-start.html

目前最稳定版本是2.2.3,下载2.2.3版本即可

启动nacos

解压到某盘中,进入到nacos/bin下,双击startup.cmd,或者在cmd输入 <font style="color:#DF2A3F;">startup.cmd -m standalone

导入nacos数据源

nacos本事有数据库的,我们直接导入到MySQL中就好

-- --------------------------------------------------------
-- 主机:                           192.168.150.101
-- 服务器版本:                        8.0.27 - MySQL Community Server - GPL
-- 服务器操作系统:                      Linux
-- HeidiSQL 版本:                  12.2.0.6576
-- --------------------------------------------------------

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


-- 导出 nacos 的数据库结构
DROP DATABASE IF EXISTS `nacos`;
CREATE DATABASE IF NOT EXISTS `nacos` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `nacos`;

-- 导出  表 nacos.config_info 结构
DROP TABLE IF EXISTS `config_info`;
CREATE TABLE IF NOT EXISTS `config_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) COLLATE utf8_bin DEFAULT NULL,
  `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
  `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COLLATE utf8_bin COMMENT 'source user',
  `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL,
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL,
  `c_use` varchar(64) COLLATE utf8_bin DEFAULT NULL,
  `effect` varchar(64) COLLATE utf8_bin DEFAULT NULL,
  `type` varchar(64) COLLATE utf8_bin DEFAULT NULL,
  `c_schema` text COLLATE utf8_bin,
  `encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info';

-- 正在导出表  nacos.config_info 的数据:~0 rows (大约)
DELETE FROM `config_info`;

-- 导出  表 nacos.config_info_aggr 结构
DROP TABLE IF EXISTS `config_info_aggr`;
CREATE TABLE IF NOT EXISTS `config_info_aggr` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'datum_id',
  `content` longtext COLLATE utf8_bin NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL,
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='增加租户字段';

-- 正在导出表  nacos.config_info_aggr 的数据:~0 rows (大约)
DELETE FROM `config_info_aggr`;

-- 导出  表 nacos.config_info_beta 结构
DROP TABLE IF EXISTS `config_info_beta`;
CREATE TABLE IF NOT EXISTS `config_info_beta` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
  `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) COLLATE utf8_bin DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COLLATE utf8_bin COMMENT 'source user',
  `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
  `encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info_beta';

-- 正在导出表  nacos.config_info_beta 的数据:~0 rows (大约)
DELETE FROM `config_info_beta`;

-- 导出  表 nacos.config_info_tag 结构
DROP TABLE IF EXISTS `config_info_tag`;
CREATE TABLE IF NOT EXISTS `config_info_tag` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
  `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
  `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COLLATE utf8_bin COMMENT 'source user',
  `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info_tag';

-- 正在导出表  nacos.config_info_tag 的数据:~0 rows (大约)
DELETE FROM `config_info_tag`;

-- 导出  表 nacos.config_tags_relation 结构
DROP TABLE IF EXISTS `config_tags_relation`;
CREATE TABLE IF NOT EXISTS `config_tags_relation` (
  `id` bigint NOT NULL COMMENT 'id',
  `tag_name` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_tag_relation';

-- 正在导出表  nacos.config_tags_relation 的数据:~0 rows (大约)
DELETE FROM `config_tags_relation`;

-- 导出  表 nacos.group_capacity 结构
DROP TABLE IF EXISTS `group_capacity`;
CREATE TABLE IF NOT EXISTS `group_capacity` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

-- 正在导出表  nacos.group_capacity 的数据:~0 rows (大约)
DELETE FROM `group_capacity`;

-- 导出  表 nacos.his_config_info 结构
DROP TABLE IF EXISTS `his_config_info`;
CREATE TABLE IF NOT EXISTS `his_config_info` (
  `id` bigint unsigned NOT NULL,
  `nid` bigint unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) COLLATE utf8_bin NOT NULL,
  `group_id` varchar(128) COLLATE utf8_bin NOT NULL,
  `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
  `content` longtext COLLATE utf8_bin NOT NULL,
  `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `src_user` text COLLATE utf8_bin,
  `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL,
  `op_type` char(10) COLLATE utf8_bin DEFAULT NULL,
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
  `encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='多租户改造';

-- 正在导出表  nacos.his_config_info 的数据:~0 rows (大约)
DELETE FROM `his_config_info`;

-- 导出  表 nacos.permissions 结构
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE IF NOT EXISTS `permissions` (
  `role` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `resource` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `action` varchar(8) COLLATE utf8mb4_general_ci NOT NULL,
  UNIQUE KEY `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- 正在导出表  nacos.permissions 的数据:~0 rows (大约)
DELETE FROM `permissions`;

-- 导出  表 nacos.roles 结构
DROP TABLE IF EXISTS `roles`;
CREATE TABLE IF NOT EXISTS `roles` (
  `username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `role` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  UNIQUE KEY `idx_user_role` (`username`,`role`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- 正在导出表  nacos.roles 的数据:~1 rows (大约)
DELETE FROM `roles`;
INSERT INTO `roles` (`username`, `role`) VALUES
	('nacos', 'ROLE_ADMIN');

-- 导出  表 nacos.tenant_capacity 结构
DROP TABLE IF EXISTS `tenant_capacity`;
CREATE TABLE IF NOT EXISTS `tenant_capacity` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='租户容量信息表';

-- 正在导出表  nacos.tenant_capacity 的数据:~0 rows (大约)
DELETE FROM `tenant_capacity`;

-- 导出  表 nacos.tenant_info 结构
DROP TABLE IF EXISTS `tenant_info`;
CREATE TABLE IF NOT EXISTS `tenant_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='tenant_info';

-- 正在导出表  nacos.tenant_info 的数据:~0 rows (大约)
DELETE FROM `tenant_info`;

-- 导出  表 nacos.users 结构
DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
  `username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `password` varchar(500) COLLATE utf8mb4_general_ci NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- 正在导出表  nacos.users 的数据:~1 rows (大约)
DELETE FROM `users`;
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES
	('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', 1);

/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;

服务注册

服务注册步骤如下:

  1. 引入nacos discovery服务:
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 配置nacos
spring:
  application:
  	name: item-service # 服务名称
  cloud:
    nacos:
    	server-addr: 192.168.179.1:8848 # nacos地址

配置好两个文件我们直接重启端口去nacos就会发现注册中心有服务了

如果我们想多服务接口的话需要在idea里面在添加一个,点击鼠标右键或者Ctrl+D

点击add VM options

在配置好端口号即可

启动好就会在nacos服务中看到两个端口

服务发现

我直接

private final DiscoveryClient discoveryClient;

private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
//        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        // 2.1 根据服务名称获取服务列表
        List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
        if (CollUtils.isEmpty(instances)) {
            return;
        }
        // 2.2 负载均衡
        ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));

        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                serviceInstance.getUri() + "/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<>() {
                },
                Map.of("ids", CollUtils.join(itemIds, ","))
        );
        if(!response.getStatusCode().is2xxSuccessful()) {
            return;
        }
        List<ItemDTO> items = response.getBody();
        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());
        }
    }

Openfeign

快速入门

为什么要使用openfeign呢?首先我们来分析之前写的代码

通过图我们会发现这样子太繁琐了,什么都要自己写,能不能直接自己获取呢?所以我们可以使用到Openfeign

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

  1. 引入依赖
<!--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>
  1. 通过@EnableFeignCilents注解,启动Openfeign功能
@SpringBootApplication
@EnableFeignClients
public class CardApplication {
    public static void main(String[] args) {
        SpringApplication.run(CardApplication.class, args);
    }
  1. <font style="color:#DF2A3F;">card-service</font>模块中创建一个Client包编写一个FeignClient接口
@FeignClient("item-service") // 服务名称
public interface ItemClient {
    @GetMapping("/items") // 请求路径
    List<ItemDTO> queryItemByIds(@RequestParam("ids")Collection<Long> ids); // 请求
}
  1. 在服务层去注入ItemClient这个接口
private final ItemClient itemClient;
private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 调用
        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());
        }
    }

连接池

为什么使用连接池呢?回答这个问题就不得不去看Openfeign的源码了,看源码很痛苦但只有看过了才能懂的

我们可以打断点调试一下,我们会发现我们之前创建的ItemClient接口并没有去实现它,我们直接能拿到这个对象说明它是一个代理对象,如图我们都知道代理对象都是由InvocationHandler实现的我们去FeignInvocationHandler实现类去看看

我们都知道代理对象最终实现都是走invoke方法,所以我们直接跟入这个invoke方法。

跟入进来之后,我们发现如图所示,这些东西都是我们之前ItemClient接口里面获取来得到这个请求,但是这个请求路径还差一点,差目标服务器的IP地址以及端口,所以我们还要接着往下看我进入到executeAndDecode这个方法里面

如图进入方法里面之后,我们发现路径请求是服务器的名称,而这个名称是无法发送的继续往下看,跟入到client.execute(request, options);

我们跟入进来之后,我们会发现下面这段代码跟我们之前写的有点熟系,我们可以来分析一下这段代码了

我们往下走会发现把路劲已经拼接好了,但是还是服务名称我们继续往下走

走到ServiceInstance instance = this.loadBalancerClient.choose(serviceId, lbRequest);这一步我们会发现它已经获取到我们服务的端口了,负载均衡成功了。

所以接下来这段代码,把获取到的IP和端口来替换之前的服务url,交给delegate去发送

而这个delegate是一个Client,而这个Clint是一个接口专门用来发送Http请求的接口

而这个底层发送请求的底层逻辑采用了HttpURLConnection

而这个是jdk自带的请求,而这个请求每一次都要手动的去建立链接,然后利用stream流的方式去建立读写,所以它的效率是不高的。

所以接下来我们要使用连接池来替换,而连接池包括以下三种。

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

而我们使用OKHttp,使用步骤如下在card-service模块中

  1. 引入依赖
<!--ok-http-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
  1. 开启连接池功能
feign:
  okhttp:
    enabled: true # 开启连接池

最佳实践

我们之前在cart-service中创建了一个client创建了一个itemclient,发现这样并解耦。所以我们要在进行拆分,有两种拆分模式

  1. 在模块下面再次拆分,拆成几个子模块,这样子就是会麻烦,但是很解耦。

  1. 在创建一个公共模块,但这种就是代码耦合度增加了

我们这个项目比较聚合,所以我们使用第二种

  1. 创建一个hm-api模块

  1. 将card-service模块的依赖剪切到hm-api中
    <dependencies>
        <!--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>
        <!--ok-http-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
  1. 更改目录

  1. 去card-service中导入api依赖
<!--导入api依赖-->
<dependency>
    <groupId>com.heima</groupId>
    <artifactId>hm-api</artifactId>
    <version>1.0.0</version>
</dependency>
  1. 修改card-service中业务,现在业务中是无法扫描到的,因为包路径不一致,当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:
// 指定所在包
@EnableFeignClients(basePackages = "com.hmall.api.client")

// 指定字节码
@EnableFeignClients(client = {ItemClient.class})

日志

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

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

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

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

/**
 * Openfeign日志类 配置
 */
public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.FULL;
    }
}

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

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

全局配置

@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
posted @   ゐ叶う枫ゆ  阅读(21)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示