认识微服务
单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:
- 架构简单
- 部署成本低
缺点:
- 团队写作成本高
- 系统发布效率低
- 系统可用性差
总结:
单体架构适合开发功能相对简单,规模较小的项目。
微服务
微服务是一种软件架构风格,它是以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。
微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个单独项目。
- 粒度小
- 团队自治
- 服务自治
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) |
2.2.x, 2.3.x (Starting with SR5) |
|
2.1.x |
|
2.0.x |
|
1.5.x |
|
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-service
的CartApplication
启动类上添加注解,启动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-service
的com.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+ |