6 服务注册和发现
6.1 问题引出
-
在上一章实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题。试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
-
此时,每个
item-service
的实例其IP或端口不同,问题来了:item-service
这么多实例,cart-service
如何知道每一个实例的地址?- http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? - 如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? - 如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
-
为了解决上述问题,就必须引入注册中心的概念了。
6.2 注册中心原理
-
在微服务远程调用的过程中,包括两个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service
- 服务消费者:调用其它微服务提供的接口,比如
cart-service
- 服务提供者:提供接口供其它微服务访问,比如
-
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
-
流程如下:
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心;
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署);
- 调用者自己对实例列表负载均衡,挑选一个实例;
- 调用者向该实例发起远程调用;
-
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求);
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除;
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表;
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表。
6.3 Nacos注册中心
-
目前开源的注册中心框架有很多,国内比较常见的有:
- Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用;
- Nacos:Alibaba公司出品,目前被集成在SpringCloud Alibaba中,一般用于Java应用;
- Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言;
-
以上几种注册中心都遵循SpringCloud中的API规范,因此在业务开发使用上没有太大差异。由于Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能,因此在国内使用较多,此处以Nacos为例来学习,官网:Nacos官网| Nacos 配置中心 | Nacos 下载| Nacos 官方社区 | Nacos 官网;
-
由于是基于Docker来部署Nacos的注册中心,首先要准备MySQL数据库表,用来存储Nacos的数据。由于是Docker部署,所以需要将下面的SQL文件导入到Docker中的MySQL容器中:
create table if not exists config_info ( id bigint auto_increment comment 'id' primary key, data_id varchar(255) not null comment 'data_id', group_id varchar(128) null, content longtext not null comment 'content', md5 varchar(32) null comment 'md5', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', src_user text null comment 'source user', src_ip varchar(50) null comment 'source ip', app_name varchar(128) null, tenant_id varchar(128) default '' null comment '租户字段', c_desc varchar(256) null, c_use varchar(64) null, effect varchar(64) null, type varchar(64) null, c_schema text null, encrypted_data_key text not null comment '秘钥', constraint uk_configinfo_datagrouptenant unique (data_id, group_id, tenant_id) ) comment 'config_info' collate = utf8mb3_bin; create table if not exists config_info_aggr ( id bigint auto_increment comment 'id' primary key, data_id varchar(255) not null comment 'data_id', group_id varchar(128) not null comment 'group_id', datum_id varchar(255) not null comment 'datum_id', content longtext not null comment '内容', gmt_modified datetime not null comment '修改时间', app_name varchar(128) null, tenant_id varchar(128) default '' null comment '租户字段', constraint uk_configinfoaggr_datagrouptenantdatum unique (data_id, group_id, tenant_id, datum_id) ) comment '增加租户字段' collate = utf8mb3_bin; create table if not exists config_info_beta ( id bigint auto_increment comment 'id' primary key, data_id varchar(255) not null comment 'data_id', group_id varchar(128) not null comment 'group_id', app_name varchar(128) null comment 'app_name', content longtext not null comment 'content', beta_ips varchar(1024) null comment 'betaIps', md5 varchar(32) null comment 'md5', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', src_user text null comment 'source user', src_ip varchar(50) null comment 'source ip', tenant_id varchar(128) default '' null comment '租户字段', encrypted_data_key text not null comment '秘钥', constraint uk_configinfobeta_datagrouptenant unique (data_id, group_id, tenant_id) ) comment 'config_info_beta' collate = utf8mb3_bin; create table if not exists config_info_tag ( id bigint auto_increment comment 'id' primary key, data_id varchar(255) not null comment 'data_id', group_id varchar(128) not null comment 'group_id', tenant_id varchar(128) default '' null comment 'tenant_id', tag_id varchar(128) not null comment 'tag_id', app_name varchar(128) null comment 'app_name', content longtext not null comment 'content', md5 varchar(32) null comment 'md5', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', src_user text null comment 'source user', src_ip varchar(50) null comment 'source ip', constraint uk_configinfotag_datagrouptenanttag unique (data_id, group_id, tenant_id, tag_id) ) comment 'config_info_tag' collate = utf8mb3_bin; create table if not exists config_tags_relation ( id bigint not null comment 'id', tag_name varchar(128) not null comment 'tag_name', tag_type varchar(64) null comment 'tag_type', data_id varchar(255) not null comment 'data_id', group_id varchar(128) not null comment 'group_id', tenant_id varchar(128) default '' null comment 'tenant_id', nid bigint auto_increment primary key, constraint uk_configtagrelation_configidtag unique (id, tag_name, tag_type) ) comment 'config_tag_relation' collate = utf8mb3_bin; create index idx_tenant_id on config_tags_relation (tenant_id); create table if not exists group_capacity ( id bigint unsigned auto_increment comment '主键ID' primary key, group_id varchar(128) default '' not null comment 'Group ID,空字符表示整个集群', quota int unsigned default '0' not null comment '配额,0表示使用默认值', `usage` int unsigned default '0' not null comment '使用量', max_size int unsigned default '0' not null comment '单个配置大小上限,单位为字节,0表示使用默认值', max_aggr_count int unsigned default '0' not null comment '聚合子配置最大个数,,0表示使用默认值', max_aggr_size int unsigned default '0' not null comment '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', max_history_count int unsigned default '0' not null comment '最大变更历史数量', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', constraint uk_group_id unique (group_id) ) comment '集群、各Group容量信息表' collate = utf8mb3_bin; create table if not exists his_config_info ( id bigint unsigned not null, nid bigint unsigned auto_increment primary key, data_id varchar(255) not null, group_id varchar(128) not null, app_name varchar(128) null comment 'app_name', content longtext not null, md5 varchar(32) null, gmt_create datetime default CURRENT_TIMESTAMP not null, gmt_modified datetime default CURRENT_TIMESTAMP not null, src_user text null, src_ip varchar(50) null, op_type char(10) null, tenant_id varchar(128) default '' null comment '租户字段', encrypted_data_key text not null comment '秘钥' ) comment '多租户改造' collate = utf8mb3_bin; create index idx_did on his_config_info (data_id); create index idx_gmt_create on his_config_info (gmt_create); create index idx_gmt_modified on his_config_info (gmt_modified); create table if not exists permissions ( role varchar(50) not null, resource varchar(255) not null, action varchar(8) not null, constraint uk_role_permission unique (role, resource, action) ); create table if not exists roles ( username varchar(50) not null, role varchar(50) not null, constraint idx_user_role unique (username, role) ); create table if not exists tenant_capacity ( id bigint unsigned auto_increment comment '主键ID' primary key, tenant_id varchar(128) default '' not null comment 'Tenant ID', quota int unsigned default '0' not null comment '配额,0表示使用默认值', `usage` int unsigned default '0' not null comment '使用量', max_size int unsigned default '0' not null comment '单个配置大小上限,单位为字节,0表示使用默认值', max_aggr_count int unsigned default '0' not null comment '聚合子配置最大个数', max_aggr_size int unsigned default '0' not null comment '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', max_history_count int unsigned default '0' not null comment '最大变更历史数量', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', constraint uk_tenant_id unique (tenant_id) ) comment '租户容量信息表' collate = utf8mb3_bin; create table if not exists tenant_info ( id bigint auto_increment comment 'id' primary key, kp varchar(128) not null comment 'kp', tenant_id varchar(128) default '' null comment 'tenant_id', tenant_name varchar(128) default '' null comment 'tenant_name', tenant_desc varchar(256) null comment 'tenant_desc', create_source varchar(32) null comment 'create_source', gmt_create bigint not null comment '创建时间', gmt_modified bigint not null comment '修改时间', constraint uk_tenant_info_kptenantid unique (kp, tenant_id) ) comment 'tenant_info' collate = utf8mb3_bin; create index idx_tenant_id on tenant_info (tenant_id); create table if not exists users ( username varchar(50) not null primary key, password varchar(500) not null, enabled tinyint(1) not null );
-
最终表结构如下:
-
新建一个
nacos
文件,创建custom.env
文件,改文件主要用于 Nacos 与数据库交互:-
SPRING_DATASOURCE_PLATFORM
指定数据库为:mysql; -
MYSQL_SERVICE_HOST
是虚拟机地址; -
MYSQL_SERVICE_DB_NAME
指定要连接的数据库名为:nacos; -
MYSQL_SERVICE_PORT
指定访问数据库的端口为:3306; -
MYSQL_SERVICE_USER
和MYSQL_SERVICE_PASSWORD
为连接数据库的账号密码;
PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql MYSQL_SERVICE_HOST=192.168.184.138 MYSQL_SERVICE_DB_NAME=nacos MYSQL_SERVICE_PORT=3306 MYSQL_SERVICE_USER=root MYSQL_SERVICE_PASSWORD=1234 MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
-
-
将
nacos
整个文件夹上传至虚拟机的/root
目录:- 可以自己去官网下载压缩包;
-
进入root目录,然后执行下面的docker命令:
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
-
然后访问:http://192.168.184.138:8848/nacos/,会跳转到下面的页面。首次访问会跳转到登录页,账号密码都是nacos:
6.4 服务注册
- 接下来,把
item-service
注册到Nacos,步骤如下:- 引入依赖
- 配置Nacos地址
- 重启
6.4.1 添加依赖
-
在的
pom.xml(item-service)
中添加依赖:<!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
6.4.2 配置Nacos
-
在
item-service
的application.yml
中添加nacos地址配置:spring: application: name: item-service cloud: nacos: server-addr: 192.168.184.129:8848 # nacos地址
6.4.3 启动服务实例
-
为了测试一个服务多个实例的情况,再配置一个
item-service
的部署实例: -
然后配置启动项,注意重命名并且配置新的端口,避免冲突:
-Dserver.port=8083
-
重启
item-service
的两个实例: -
访问nacos控制台,可以发现服务注册成功:
-
点击详情,可以查看到
item-service
服务的两个实例信息:
6.5 服务发现
- 服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
- 引入依赖
- 配置Nacos地址
- 发现并调用服务
6.5.1 引入依赖
-
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖;
-
在
cart-service
中的pom.xml
中添加下面的依赖:<!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
可以发现,这里Nacos的依赖与服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者;
-
因此,等一会儿
cart-service
启动,同样会注册到Nacos。
6.5.2 配置Nacos地址
-
在
cart-service
的application.yml
中添加nacos地址配置:spring: application: name: cart-service cloud: nacos: server-addr: 192.168.184.138:8848
6.5.3 发现并调用服务
-
接下来,服务调用者
cart-service
就可以去订阅item-service
服务了。不过item-service
有多个实例,而真正发起调用时只需要知道一个实例的地址。因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:- 随机
- 轮询
- IP的hash
- 最近最少访问
- …
-
此处选择最简单的随机负载均衡;
-
另外,服务发现需要用到一个
DiscoveryClient
的工具,SpringCloud已经帮我们自动装配,可以直接注入使用。在item-server
的com/hmall/cart/service/impl/CartServiceImpl.java
中:private final DiscoveryClient discoveryClient;
-
接下来,就可以对原来的远程调用做修改了,之前调用时我们是写死服务提供者的IP和端口。但现在不需要了,可以通过
DiscoveryClient
发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:private void handleCartItems(List<CartVO> vos) { // 1.获取商品id Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 // 2.1.发现item-service服务的实例列表 List<ServiceInstance> instances = discoveryClient.getInstances("item-service"); // 2.2.负载均衡,挑选一个实例 ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size())); // 2.3.利用RestTemplate发起http请求,得到http的响应 ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( instance.getUri() + "/items?ids={ids}", //请求路径 HttpMethod.GET, //请求方式。HttpMethod的包是 org.springframework.http.HttpMethod; null, //请求实体 new ParameterizedTypeReference<List<ItemDTO>>() {}, //返回值类型 Map.of("ids", CollUtil.join(itemIds, ",")) //请求参数 ); // 2.2.解析响应 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()); } }
-
测试:启动
CartApplicattion
,访问http://localhost:8082/doc.html -
在注册中心也可以看见
cart-service
: