微服务
为什么要学习微服务?
维度 | 单体架构 | 微服务架构 |
---|---|---|
开发效率 | 简单,适合小项目 | 适合大型项目,团队协作更高效 |
部署 | 整体部署,耗时长 | 独立部署,灵活快捷 |
可用性 | 低(服务相互影响) | 高(服务隔离) |
扩展性 | 差(整体扩展) | 强(按需扩展) |
适用场景 | 小型项目 | 中大型互联网项目 |
- 单体架构: 适合业务简单、团队小的项目。
- 微服务架构: 更适合复杂、高并发的互联网项目,但需要解决分布式系统带来的新问题。
RestTemplate
定义一个配置类
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();
}
}
发起跨服务请求获取商品数据
private void getCartItems(List<CartVO> vos) {
...
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
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, ",")) // 请求参数
);
...
}
服务注册与发现
假如商品微服务被调用较多,为了应对更高的并发,我们需要进行多实例部署
此时,每个 item-service
的实例其 IP
或端口不同,问题来了:
item-service
这么多实例,cart-service
如何知道每一个实例的地址?http
请求要写url
地址,cart-service
服务到底该调用哪个实例呢?- 如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? - 如果并发太高,
item-service
临时多部署了N
台实例,cart-service
如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了
注册中心
在微服务远程调用的过程中,有两个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service
- 服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了 注册中心 的概念。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
- 调用者自己对实例列表负载均衡,挑选一个实例
- 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
服务注册
接下来我们把 item-service
注册到 Nacos
,步骤如下:
引入依赖
配置 Nacos 地址
重启
添加依赖
在 item-service
的 pom.xml
中添加依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置 Nacos
在 item-service
的 application.yml
中添加 nacos
地址配置:
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
服务发现
服务的消费者要去 nacos
订阅服务,这个过程就是服务发现,步骤如下:
引入依赖
配置Nacos地址
发现并调用服务
引入依赖
我们在服务调用者 cart-service
中的添加依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
可以发现,这里 Nacos
的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。
因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
配置 Nacos
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
发现并调用服务
接下来,服务调用者 cart-service
就可以去订阅 item-service
服务了。不过 item-service
有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
- 随机
- 轮询
- IP 的 hash
- 最近最少访问
- ...
这里我们可以选择最简单的随机负载均衡。
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient;
private void getCartItems(List<CartVO> vos) {
...
// 通过服务发现调用商品服务
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
// 随机负载均衡
ServiceInstance instance = instances.get(ThreadLocalRandom.current().nextInt(instances.size()));
// 构造请求URL和参数
String url = instance.getUri() + "/items?ids={ids}";
String idsParam = String.join(",", itemIds.stream().map(String::valueOf).collect(Collectors.toList()));
// 发送HTTP请求
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
idsParam
);
...
}
}
OpenFeign
虽然 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
在启动类加上这个注解:@EnableFeignClients
@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}
编写客户端
@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<ItemDTO>
。
我们只需要直接调用这个方法,即可实现远程调用了。
使用 FeignClient
改造之前远程调用的复杂代码
@Resource
private ItemClient itemClient;
private void handleCartItems(List<CartVO> vos) {
...
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
...
}
feign
替我们完成了服务拉取、负载均衡、发送 http
请求的所有工作,是不是看起来优雅多了。
而且,这里我们不再需要 RestTemplate
了,还省去了 RestTemplate
的注册。
连接池
连接池的作用:
- 性能优化:避免每次请求都建立新的
TCP
连接 - 资源复用:重用已存在的连接,减少系统资源消耗
- 连接管理:控制并发连接数,防止系统过载
简单来说就是: 复用 HTTP
连接提升性能的机制,避免频繁创建/销毁 TCP
连接。
我们可以选择 OKHttp
连接池
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
feign:
okhttp:
enabled: true
日志配置
OpenFeign
只会在 FeignClient
所在包的日志级别为 DEBUG 时,才会输出日志。下面是日志的四个级别:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign
默认的日志级别就是 NONE
,所以默认我们看不到请求日志。
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
接下来,要让日志级别生效,还需要配置这个类
- 局部生效:在某个
FeignClient
中配置,只对当前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
- 全局生效:在
@EnableFeignClients
中配置,针对所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
日志格式:
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body)
17:35:32:278 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms)
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 26 May 2023 09:35:32 GMT
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds]
17:35:32:280 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":100000006163,"name":"巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉裤","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}]
17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (369-byte body)