SpringCloud微服务

微服务

认识微服务

单体架构

把业务的所有功能集中在一个项目中开发 打成一个包部署
优点

  • 架构简单
  • 部署成本低

缺点

  • 团队协作成本高
  • 系统发布效率低 代码量过大 编译时间长
  • 系统可用性差

只适合开发简单 规模较小的项目

微服务

把单体项目中的功能模块拆分成多个独立的项目

SpringCloud

微服务拆分

服务拆分原则

拆分之后做到高内聚低耦合

  • 高内聚 指责单一 完整度高
  • 低耦合 相对独立 减少对其他业务依赖

拆分方式分两种

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

远程调用

有些拆分完的模块需要别的service 但由于各个模块都是独立开的 而网络是没有独立的 因此可以通过网络来远程调用
但是远程调用存在很多问题 比如请求路径如果写死 将来多台服务启动会出现问题

这类问题就叫做服务治理
可以使用注册中心来解决该问题

服务治理

注册中心

注册中心所提供的功能是协调调用者和提供者 提供服务的是服务提供者 还有服务调用者 提供者先给注册中心注册服务信息 然后调用者就可以订阅信息 来通过注册中心远程调用接口 如果提供者有多个实例 采取负载均衡的方式来 就可以调用了 并且提供者都会持续给注册中心一个心跳续约 来告诉注册中心自己是否正常 然后如果有异常 服务炸掉或者新增服务 注册中心就会推送变更给调用者 从而达到持续服务

Nacos

配置nacos 在微服务飞书云存档

多实例部署 换端口

先在服务里复制一份配置 然后修改选项-添加VM选项 然后写-D server-port=8080即可

服务发现

消费者需要连接nacos来拉取和订阅服务 因此前两步都是一样的 就是引入依赖 配置地址 最后配置代码服务发现即可

OpenFeign

快速入门

用来发送http请求

  1. 引依赖 早期负载均衡组件用的是Ribbon 现在用的是loadbalancer
  2. 在启动类打开开关 @EnableFeignClients
  3. 编写接口 跟controller类似
@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
  1. 直接调用 List<ItemDTO> items = itemClient.queryItemByIds(itemIds);

连接池

OpenFeign对Http请求做了封装 底层发起的Http请求默认是HttpURLConnection实现 不支持连接池
为了性能的选择 可以使用Apache HttpClient实现和OKHttp实现 这两种都是支持连接池的

最佳实践

当前模块需要item-service的远程调用 编写OpenFeign接口 其他模块以后可能也需要 如果每个模块各自写OpenFeign接口 就会重复难以修改
因此最佳实践就是不独立实现远程调用的api

  • 第一种就是让item-service只作为独立的maven模块 然后下面分为dto api 和biz模块 之后哪一个服务要用 就可以引入item-service的maven坐标即可

  • 第二种就是保留原有结构不变 然后再开发一个新的模块来放统一的api 但是维护麻烦 耦合度高 不过方便

日志

OpenFeign只会在FeignClient所在包的日志级别是DEBUG时 才会输出日志 日志分为四个级别

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

定义日志级别分为局部全局
局部
需要先声明一个类型为Logger.level的Bean 定义日志级别

此时Bean并未生效 需要在Client注解中声明

全局
直接在启动类的@EnableFeignClient开启

网关路由

就是网络端口 负责请求 转发 身份校验

就是对每个微服务都加一层 然后前端请求是到网关 网关进行身份校验之后判断是哪个微服务 再去做路由的转发 以及根据多实例进行负载均衡 网关是直接从注册中心来拉取微服务端口的
网关的实现有两个

  • Spring Cloud Gateway spring官方出品 基于WebFlux响应式变成 无需调优就能获得优异性能
  • Netflix Zuul Netflix出品 基于Servlet阻塞式编程 需要调优才能跟Gateway一样

快速入门

网关也是一个微服务 主要分为四步 创建模块 引入网关依赖 编写启动类 配置路由规则
主要就是依赖和配置路由规则

  • 依赖
<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>
  • 路由配置规则

路由属性

网关路由对应的Java类型是RouteDefinition 常见属性是

  • id 路由唯一标识

  • uri 路由目标地址

  • predicates 路由断言 判断是否符合需求 Spring提供了12种断言规则

  • filters 路由过滤器 Spring提供了33种规则

网关登录校验

登录授权是在user-service微服务中实现 但是校验jwt令牌在其他微服务也有 因此校验jwt令牌可以交给网关 把信息向后传递 因此要确保在网关转发之前进行jwt校验

网关请求处理流程

会先经过HandlerMapping来做路由映射和WebHandler来做请求处理 然后经过一系列过滤器的pre逻辑 也就是过滤器链 最后通过Netty路由过滤器完成路由转发 转发之后拿到结果封装 再一步步执行过滤器的post逻辑返回给用户
因此完成登录校验就是通过一个过滤器 并放在过滤器的pre阶段即可
网关通过jwt校验 然后保存到请求头即可完成 但是有些微服务需要调用其他微服务 微服务之间也需要传递用户信息 也可以保存信息到请求头

自定义过滤器

自定义过滤器分两种

  • GatewayFilter 路由过滤器 作用于任意指定的路由 默认不生效 配置后才行
  • GlobalFilter 全局过滤器 作用范围是所有路由 声明后自动生效

网关传递用户

大致流程就是

  • 在网关的过滤器中 把用户的id放到请求头里
    实现流程就是用ServerWebExchange提供的Api
  • 用MVC拦截器 在每个微服务业务执行前 获取请求头 放到ThreadLocal中

注意 这里有个问题 就是在common包下配置了MVC拦截器 然后每个微服务模块都引入了Common包 因此每个微服务调用之前都会拦截 并放到各自微服务的ThreadLocal中 但是MVC用的@Configuration想要生效 需要被spring扫描到 因此需要用到自动装配 需要放在META-INF下进行自动装配 把MVCConfig让spring自动装配 这时候还有个问题 就是如果启动网关会报错 因为网关是基于WebFlux响应式编程 并没有引入MVC 因此需要用条件注解来自动装配 有DispathcherServlet的类 也就是有MVC 才进行自动装配

OpenFeign传递用户

OpenFeign提供了一个拦截器接口 所有由OpenFeign发起的请求都会调用拦截器请求

然后把拦截器定义在hm-api中 因为是调用的openFeign 因此 哪个微服务调用openFeign 该服务的UserContext是由userId的 所以在拦截器里能直接从ThreadLocal里拿到userId
注意 在hm-api中配完之后需要生效 需要在启动类上把配置类加上@EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class) 这样才会生效 否则是不会生效的

配置管理

微服务重复配置过多 且业务配置经常变动 每次修改都需要重启微服务 网关路由配置写死的话 每次变更还要重启网关
因此需要进行配置管理 nacos就集成了配置管理 可以进行热更新 在服务变更时推送给相关微服务 不需要重启

配置共享

nacos可以实现共享配置 只需要在nacos中点击配置管理-加号

拉取微服务

之前springboot就是通过yaml文件去进行初始化 而springcloud是先拉取nacos进行初始化之后再阅读springboot中的yaml文件进行初始化 因为springboot的yaml配置了nacos配置文件中的详细信息 这样会导致前后顺序不一致 因此可以通过bootstrap.yaml把springboot中配置拿过来 然后进行配置拉取 最后进行配置合并

  • 引入依赖
<!--nacos配置管理-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--读取bootstrap文件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
  • 新建bootstrap.yaml配置

配置热更新

当修改配置文件中配置时 微服务无需重启即可使配置生效
前提条件有两个

  • 一个是nacos中要有一个于微服务名有关的配置文件

这个配置在bootstrap中已经配置过了 配置过就不用再配置了 分别是服务名称 profile 和后缀名 启动时会自动拉取这个配置

  • 然后相关配置要通过注解特定方式读取配置中属性

第一种最好 就是获取配置的信息 不需要额外的注解

动态路由

实现动态路由就是先把路由配置到nacos中 当nacos的路由变更时 推送新配置到网关 更新网关的路由信息 因此有两件事

  • 监听nacos配置变更信息
  • 配置变更时 把最新的路由信息更新到网关路由

首先就是先拉取一次配置 然后配置监听器 然后无论第一次获取还是监听器变更 都需要更新路由表 整个过程都需要在初始化之后立即执行 因此把该方法放在后置处理器中

而更新路由表就需要利用RouterDefinitonWriter来更新
为了方便从nacos读到的配置 因此上传nacos时采用json来上传 这样拉取下来容易解析
需要注意的是更新路由表的时候先删除旧的路由 然后清空id集合 再把新的路由全保存下来 并且保存id集合

微服务保护

雪崩问题

原因

微服务调用链路中的某个服务故障 然后导致调用该服务也故障 然后如果并发数量很高 导致所有这个服务的请求都卡住 占用了tomcat资源 如果tomcat资源被占满 就会导致其他服务也故障 从而多个服务宕机 这就是雪崩
原因

  • 微服务相互调用 服务提供者出现故障或阻塞
  • 服务调用这没有做好异常处理 导致自身故障
  • 调用链中的所有服务级联失败 导致整个集群故障

解决方案

  • 请求限流 限制访问微服务的请求的并发量 避免服务因流量激增出现故障

也称之为流量整形

  • 线程隔离 也叫舱壁模式 就是模拟船舱的木板 通过限定每个业务使用的线程数量而将业务隔离 避免故障扩散

就是限制调用C的最大线程 这样最多只有4个请求卡住 其他请求并不会卡住 达到隔离的效果

  • 服务熔断断路器统计请求异常比例或者慢调用比例 如果超出阈值 则熔断该业务 即使该服务的所有请求
    熔断期间 所有请求快速失败 全部走fallback逻辑 服务降级

Sentinel

初识Sentinel

Sentinel是阿里开源的一个流量控制组件 实现很简单 安装Web控制台 所以只需要引入依赖 通过控制台配置即可

簇点链路
就是单机调用链路 一次请求进入服务后经过的每一个被Sentinel监控的资源链 默认会监控SpringMVC的每一个http接口 就是controller接口 这就叫一个簇点 限流熔断就是对簇点链路中的资源进行的设置
而Restful风格中 请求路径有的会相同 是用请求方式分隔开的 因此要修改配置 把簇点改为请求方式+请求路径作为簇点资源名

启动Sentinel

java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
然后访问localhost:8090

请求限流

启动Sentinel 点击流控 选QPS 点两下就好

线程隔离

启动Sentinel 点击流控 选并发线程数 点两下就好

Fallback

首先就是让FeignClient作为Sentinel的簇点资源
需要开启配置feign.sentinel.enable=true
FeignClient实现fallback有两种方式

  • FallbackClass 无法对远程调用做异常处理
  • FallbackFactory 可以对远程调用做异常处理 选这个

整体实现流程

  1. 开启配置 让每一个接口调用成为feignClient 不对入口阻挡 这样即使失败了也不耽误整体流程
  2. 然后定义FallbackFactory类 实现FallbackFactory接口 泛型就是feign客户端接口类型
  3. 实现之后编写fallback逻辑
  4. 去在配置类里把FallbackFactory注册为Bean
  5. 然后在ItemClient声明fallbackFacktory是编写的自定义FallbackFactory类

服务熔断

熔断是处理雪崩的重要手段 思路就是由断路器统计服务调用的异常比例 慢请求比例 超出阈值就会熔断该服务 恢复正常就会放行

分布式事务

分布式系统中 如果一个业务需要多个服务完成 每一个服务都有一个事务 那么必须多个事务要么都成功 要么都失败 这就是分布式事务
其中单个事务叫分支事务 所有事务叫全局事务

Seata

Seata的架构和原理

Seata中有三个部分

  • TC(Transaction Coordinator) 事务协调者 维护全局和分支事务的状态 协调全局事务提交或者回滚
  • TM(Transaction Manager) 事务管理者 定义全局事务的范围 开始全局事务 提交或回滚全局事务
  • RM(Resource Manager) 资源管理器 管理分支事务 和TC交谈以注册分支事务 并报告分支事务状态

部署TC服务

docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.88.130 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hmall \
-d \
seataio/seata-server:1.5.2

访问localhost:7099

微服务继承Seata

事务组就可以理解成集群

XA模式

分为两阶段的工作
一阶段

  1. 开启全局业务并让RM注册业务到TC中
  2. RM执行但是不提交sql 会占用锁然后一直等待全部事务执行完
  3. RM报告执行状态到TC

二阶段

  1. TC检查各服务执行状态 如果都成功 通知RM提交 反之
  2. RM接收TC消息并执行

优点

  • 事务的强一致性 满足ACID特性
  • 常用数据库都支持 实现简单 没有代码侵入

缺点

  • 一阶段需要锁定数据库 等待二阶段结束才释放 性能较差
  • 依赖关系型数据库实现事务

AT模式

Seata主推AT模式 因为AT模式弥补了XA中资源锁定时间过长的问题 不过同样是分阶段提交事务

还是两阶段的工作
一阶段

  1. 注册分支事务
  2. 记录undo log(数据快照)
  3. 报告事务状态

二阶段提交动作

  • 删除undo-log即可

二阶段回滚动作

  • 根据undo-log恢复数据到更新前

AT和XA的最大区别

  • XA模式一阶段不提交事务 锁定资源 而AT模式一阶段直接提交 不锁定资源
  • XA模式一俩数据库机制实现回滚 AT模式利用数据快照实现数据回滚
  • XA模式强一致性 AT模式最终一致

MQ入门

初始MQ

同步调用

优点

  • 时效性强 等待结果后才返回

缺点

  • 拓展性差
  • 性能下降
  • 级联失败

异步调用

异步调用是用消息通知的方式 包含三个角色

  • 消息发送者 投递消息的人 就是之前的调用者
  • 消息接收者 接收和处理消息的人 就是原来的服务调用者
  • 消息代理 暂存 管理 转发消息

优势

  • 耦合度低 拓展性强
  • 性能好 无需等待
  • 故障隔离 下游服务故障不影响上游服务
  • 缓存消息 流量削峰填谷

问题

  • 不能立即得到调用结果 时效性差
  • 不确定下游业务是否成功
  • 业务安全依赖于Broker(消息代理)的可靠性

MQ技术选型

MQ(MessageQueue) 就是消息队列 也就是异步调用中的broker

RabbitMQ

安装部署

docker run \
 -e RABBITMQ_DEFAULT_USER=root\
 -e RABBITMQ_DEFAULT_PASS=123456\
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 --network hmall\
 -d \
 rabbitmq:3.8-management

基本介绍

  • viutual-host 虚拟主机 类似于数据库的database 起到数据隔离的作用
  • publisher 消息发送者
  • consumer 消息的消费者
  • queue 队列 存储消息
  • exchange 交换机 负责路由信息

快速入门

交换机只能路由消息 不能存储消息
交换机只会把消息给到其绑定的队列

Java客户端

快速入门

AMQP Advanced Message Queuing Protocol 先进消息队列协议 消息队列的一套标准 和语言无关
Spring AMQP 是基于AMQP定义的一套API规范 提供了模板来发送和接收消息 包含两部分 spring-amqp是基础抽象 spring-rabbit是底层的默认实现

  1. 引入依赖
        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
  1. 配置信息
  2. 发送消息
 @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testSimpleQueue() {
        String queueName = "simple.queue";
        String message = "Hello World!";

        rabbitTemplate.convertAndSend(queueName,message);
    }
  1. 接收消息
@RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String message) {
        log.info("监听到消息,{}",message);
    }

Work Queues

任务模型 就是让多个消费者绑定到一个队列 共同消费队列中的消息
默认情况下 RabbitMQ是轮询发送给消费者的 但是并没有考虑到消费者处理速度 有可能出现消息堆积
因此可以设置配置的spring.rabbitmq.listener.simple.prefetch=1为1 确保同一时刻最多投递给消费者一条消息

work queue模型特征

  • 多个消费者绑定一个队列 可以加速消息处理速度
  • 同一条消息只会被一个消费者获取
  • 通过设置fetch来控制消费者获取的消息数量 处理完一条再处理下一条 可以做到以性能来处理消息 而不是轮询

Fanout交换机

交换机主要就是接收发送者发送的消息 并将消息路由到与其绑定的队列
常见交换机类型有

  • Fanout 广播
  • Direct 定向
  • Topic 话题

Fanout交换机会把收到的消息路由给每一个绑定的队列
使用场景就是一对多 比如处理完订单之后 需要做加积分业务 发短信业务 因此 只需要通过交换机 发送一条消息 可以让两个队列同时做两件事

Direct交换机

Direct Exchange 会将接收到的消息根据规则路由到指定的Queue 因此称为定向路由

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时 只当消息的Routing Key
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

Direct交换机适用场景还是支付 当支付完 可以给所有的队列发送 也就是支付成功 所有队列的RoutingKey一致 然后发送消息 但是不成功 用户取消支付 那就是只单独给取消业务的队列发送消息即可

Fanout和Direct差异

  • Fanout将消息路由给每一个与之绑定的队列
  • Direct根据BandingKey来判断路由给哪个队列
  • 如果多个队列的BandingKey相同 则与Fanout交换机类似

Topic交换机

Topic交换机也有RoutingKey 但是RoutingKey是多个单词的组合 用"."分隔开 可以使用通配符

  • '#'代表0或多个单词
  • '*'代表一个单词

基于Bean声明队列和交换机

SpringAMQP提供了几个类 用来声明队列 交换机及其绑定关系

  • Queue 用于声明队列 可以用工厂类QueueBuilder创建
  • Exchange 用于声明交换机 可以用工厂类ExchangeBuilder构建
  • Binding 声明队列和交换机的绑定关系 用BindingBuilder构建

基于注解声明队列和交换机

SpringAMQP提供了基于@RabbitListener注解实现的创建队列交换机

消息转换器

Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的 而默认实现是SimpleMessageConverter 基于JDK的ObjectOutputStream完成序列化

  • JDK序列化有风险
  • JDK序列化占用字节太大
  • 可读性差

因此需要用JSON序列化替代默认的JDK序列化

业务改造

posted @ 2025-08-02 22:06  big4mart  阅读(12)  评论(0)    收藏  举报