文章目录
- 微服务01
- 1 认识微服务
- 1.1 单体架构
- 1.2 微服务
- 1.3 SpringCloud
- 2 微服务拆分
- 2.1 熟悉黑马商城
- 2.2 服务拆分原则
- 2.2.1.什么时候拆
- 2.2.2.怎么拆
- 2.3 拆分服务
- 2.3.1 拆分商品管理功能模块
- 2.3.2 拆分购物车功能模块
- 2.4 远程调用
- 2.4.1 RestTemplate
- 2.4.2.远程调用
- 2.5 总结
- 3 服务注册与发现
- 3.1.注册中心原理
- 3.2 Nacos注册中心
- 3.3 服务注册
- 3.4 服务发现
- 4 OpenFeign
- 4.1 快速入门
- 4.2 连接池
- 4.3 最佳实践
- 4.3.1.思路分析
- 4.3.2 扫描包
- 4.4 日志
- 4.5 总结
微服务01
1 认识微服务
1.1 单体架构
单体架构: 将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:
- 架构简单
- 部署成本低
缺点:
- 团队协作成本高
- 系统发布效率低
- 系统可用性差(当某一个服务负载过大,比如访问量太大,导致此服务崩溃,由于所有服务公用一个资源空间,其它服务也会受到影响,崩溃)
总结:单体架构适合开发功能相对简单,规模较小的项目。
1.2 微服务
微服务架构: 是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。
优点:
- 粒度小
- 团队自治
- 服务自治
1.3 SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud.
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的我开箱即用体验:
对于SpringBoot的版本也有要求
2 微服务拆分
2.1 熟悉黑马商城
2.2 服务拆分原则
2.2.1.什么时候拆
- 创业型项目: 先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
- 确定的大型项目: 资金充足,项目明确,可以直接选择微服务架构,避免后续拆分的麻烦。
2.2.2.怎么拆
之前我们说过,微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。
一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。
当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。
明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:
- 纵向拆分: 按照业务模块来拆分
- 横向拆分: 抽取公共服务,提高复用性
所谓纵向拆分,就是按照项目的功能模块来拆分。例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。
而横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
当然,由于黑马商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务:
- 用户服务
- 商品服务
- 订单服务
- 购物车服务
- 支付服务
2.3 拆分服务
工程结构有两种:
-
独立Project(适用淘宝等大型复杂工程)
-
Maven聚合(中小型企业 相对简单的工程)
2.3.1 拆分商品管理功能模块
将hm-service中与商品管理相关功能拆分到一个微服务module中,命名为item-service
2.3.2 拆分购物车功能模块
将hm-service中与购物车有关的功能拆分到一个微服务module中,命名为cart-service
2.4 远程调用
在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service
服务,导致我们无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。
因此,现在查询购物车列表的流程变成了这样:
代码中需要变化的就是这一步:
那么问题来了:我们该如何跨服务调用,准确的说,如何在cart-service
中获取item-service
服务中的提供的商品数据呢?
前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items
这个接口发起的请求:而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
2.4.1 RestTemplate
Spring给我们提供了一个ResTemplate工具,可以方便的实现Http请求的发送。其中提供了大量的方法,方便我们发送Http请求,例如:
可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。
使用步骤如下:
- 注入RestTemplate到Spring容器
- 发起远程调用
2.4.2.远程调用
接下来,我们修改cart-service
中的com.hmall.cart.service.impl.``CartServiceImpl
的handleCartItems
方法,发送http请求到item-service
:
可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:
- ① 请求方式
- ② 请求路径
- ③ 请求参数
- ④ 返回值类型
2.5 总结
3 服务注册与发现
在上一章我们实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个item-service
的实例其IP或端口不同,问题来了:
- item-service这么多实例,cart-service如何知道每一个实例的地址?
- http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? - 如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? - 如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了,接下来我们就一起来分析下注册中心的原理。
3.1.注册中心原理
流程如下:
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
- 调用者自己对实例列表负载均衡,挑选一个实例
- 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
3.2 Nacos注册中心
Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴产品,目前已经加入SpringCloudAlibaba中。
3.3 服务注册
服务注册步骤如下:
- 引入nacos discovery依赖:
- 配置Nacos地址
3.4 服务发现
消费者 需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样,后面再加上服务调用即可:
-
引入nacos discovery
-
配置Nacos地址
-
服务发现
// 2.查询商品
// 2.1 根据服务名称获取服务的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if(CollUtils.isEmpty(instances)){
return;
}
// 2.2 手写负载均衡 从实例列表中挑选一个实例
ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
// 2.3 利用ResTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
serviceInstance.getUri() + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
//2.4 解析响应
if(!response.getStatusCode().is2xxSuccessful()){
//查询失败 直接结束
return;
}
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
return;
}
4 OpenFeign
在上一章,我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:
而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。
其实远程调用的关键点就在于四个:
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
4.1 快速入门
OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。其作用就是基于SpringMVC的常见注解,帮我们优雅地实现http请求的发送。
OpenFeign已经被SpringCloud自动装配,实现起来非常简单
-
引入依赖,包括OpenFeign和负载均衡组件SpringCloudLoadBalancer
-
通过@EnableFeignClients注解,启用OpenFeign功能
-
编写FeignClient,这段代码就是代替了之前写的一大坨代码(发现实例,负载均衡,发送请求)
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
-
@FeignClient("item-service")
:声明服务名称 localhost:8081 localhost8083 -
@GetMapping
:声明请求方式 Get -
@GetMapping("/items")
:声明请求路径 /items -
@RequestParam("ids") Collection<Long> ids
:声明请求参数 /{ids}以上拼起来路径就是 Get方式的 https://localhost:8081/items?ids=1,2,3
-
List<ItemDTO>
:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向
http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。我们只需要直接调用这个方法,即可实现远程调用了
-
-
使用FeignClient,实现远程调用
4.2 连接池
OpenFeign对http请求做了优雅的封装,不过其底层发起http请求,依赖于其它的框架。这些框架可以自己选择,包括一下三种:
- HttpURLConnection: 默认实现。不支持连接池
- Apache HttpClient: 支持连接池
- OKHttp: 支持连接池
这里使用的是okhttp
-
引入依赖
<!--OK http 的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
-
开启连接池
feign: okhttp: enabled: true # 开启OKHttp功能
4.3 最佳实践
如果拆分了交易微服务(trade-service
),它也需要远程调用item-service
中的根据id批量查询商品功能。这个需求与cart-service
中是一样的。
因此,我们就需要在trade-service
中再次定义ItemClient
接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?
4.3.1.思路分析
避免重复编码的办法就是抽取。不过这里有两种抽取思路:
-
思路1:抽取到微服务之外的公共module
-
思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
那么该选择方案:
我们知道拆分服务分为两种结构,一种是project独立结构,一种是Maven聚合结构。对于Project独立结构,本身每一个模块就是一个工程,耦合度低,也更方便采用方案2的方法;对于Maven聚合结构,由于耦合度稍微高一点,且本身就带有common通用模块,再多一个通用模块也无所谓,因此建议选择方案一。不过其实如果不嫌麻烦的话,还是建议方案二。
对于本使用了Maven结构的项目来说,选择方案1。
4.3.2 扫描包
当定义的FeignClient不在SpringBootApplicaiton的扫描包范围时,这些FeignClient无法使用,有两种方式解决
方式一:指定FeignClient所在包
方式二:指定FeignClient字节码
4.4 日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE: 不记录任何日志信息,这是默认值
- BASIC: 仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS: 在BASUC的基础上,额外记录了请求和响应的头信息
- FULL: 记录所有请求和响应的明细,包括头信息、请求体、元数据
由于Feign默认的日志级别是NONE,所以默认我们看不到请求日志
要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:
但此时这个Bean并未生效,要想配置某个FeignClient的日志,可以在@FeignClient注解中声明:
如果想要全局配置,让所有FeignClient都按照这个日志配置,则需要在@EnableFeignClients注解中声明: