SpringCloud

发布于:2025-07-14 ⋅ 阅读:(14) ⋅ 点赞:(0)

一.Nacos(管理服务)

1.服务注册

①引入 nacos discovery 依赖
<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
②配置Nacos地址
spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址

2.服务发现

①引入 nacos discovery 依赖和配置Nacos地址

这两步和上面注册一摸一样,这里略.....

③服务发现(后面有OpenFeign,这个了解一下)
private final DiscoveryClient discoveryClient;

private void handleCartItems(List<CartVO> vos) {
    // 1.根据服务名称,拉取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    // 2.负载均衡,挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    // 3.获取实例的IP和端口
    URI uri = instance.getUri();
    // ... 略
}

二.OpenFeign(服务通信)

OpenFeign是一个声明式的http客户端

1.快速入门

①引入依赖
  <!--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>
②通过@EnableFeignClients注解,启动OpenFeign功能
@EnableFeignClients
@SpringBootApplication
public class CartApplication { // ... 略 }
③编写FeignClient客户端
// 这个接口不用实现,OpenFeign会动态代理帮我们实现
@FeignClient("item-service")  //这个是从nacos的item-service服务中获取实例列表,然后通过负载均衡算法获取一个实例
public interface ItemClient {
    @GetMapping("/items") //告诉OpenFeign去服务实例中调用这个接口,这个接口是get请求,路径是/items
//返回值默认是json格式,转换为java对象       请求参数是ids                  请求数据
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    
}
  • @FeignClient("item-service") :声明服务名称

  • @GetMapping :声明请求方式

  • @GetMapping("/items") :声明请求路径

  • @RequestParam("ids") Collection<Long> ids :声明请求参数

  • List<ItemDTO> :返回值类型

④使用FeignClient,实现远程调用
// 要先依赖注入 ItemClient这个接口,然后在调用
List<ItemDTO> items = itemClient.queryItemByIds(List.of(1,2,3));

2.连接池

前面讲了底层原理,动态代理听不懂。

每次发送请求的时候都是创建一个连接,效率比较低下,所以创建一个连接池效率高一些。

OpenFeign整合OKHttp开始连接池:

①引入依赖
<!--ok-http-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
②开启连接池功能
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持

3.最佳实践 

两种最佳实践的项目格式:

①每个微服务分为三个模块

好处:当前微服务写好自己给别人调用的接口,别人引入依赖直接使用即可。

弊端:项目结构变复杂了 

②储存在一个公共的模块下

弊端:代码耦合度增加了。

4. 日志输出

OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时,才会输出日志。而且其日志级别有 4 级:

  • NONE:不记录任何日志信息,这是默认值,所以默认我们看不到日志。
  • BASIC:仅记录请求的方法,URL 以及响应状态码和执行时间
  • HEADERS:在 BASIC 的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

使用:

①自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }
}

②配置全局,让所有FeignClient 都按照这个日志配置,要在@EnableFeignClient注解中声明:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

一般情况下不用配置,调试的时候在配置。

这两个红色框的没看懂

三.Gateway(路由)

作用:请求的路由,转发,身份校验

Ⅰ.网关路由 

1. 快速入门
①创建新模块
②引入网关依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
③编写启动类
④配置路由规则(重要)

2.路由属性

四个属性含义如下:

  • id:路由的唯一标示

  • predicates:路由断言,其实就是匹配条件

  • filters:路由过滤条件,后面讲

  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

①路由断言 predicates:

名称

说明

示例

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+

Host

请求必须是访问某个host(域名)

- Host=**.somehost.org,**.anotherhost.org

Method

请求方式必须是指定方式

- Method=GET,POST

Path

请求路径必须符合指定规则

- Path=/red/{segment},/blue/**

Query

请求参数必须包含指定参数

- Query=name, Jack或者- Query=name

RemoteAddr

请求者的ip必须是指定范围

- RemoteAddr=192.168.1.1/24

weight

权重处理

②路由过滤器 filters:
名称 说明 示例
AddRequestHeader 给当前请求添加一个请求头 AddrequestHeader=headerName,headerValue
RemoveRequestHeader 移除请求中的一个请求头 RemoveRequestHeader=headerName
AddResponseHeader 给响应结果中添加一个响应头 AddResponseHeader=headerName,headerValue
RemoveResponseHeader 从响应结果中移除有一个响应头 RemoveResponseHeader=headerName
RewritePath 请求路径重写 RewritePath=/red/(?<segment>.*), /${segment}
StripPrefix 去除请求路径中的 N 段前缀 StripPrefix=1,则路径 /a/b 转发时只保留 /b
...总共33种 ... ...

Ⅱ.网关登录校验

1.自定义拦截器
①GlobalFilter(常用这个)

@Component
public class MyGlobalFilter implements GlobalFilter , Ordered {

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 登录校验逻辑
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();

        // 放行
        return chain.filter(exchange);
    }

    public int getOrder() {  // 过滤的顺序,数字越大越靠后
        return 0;
    }
}

需要实现GlobalFilter和Ordered接口

ServerWebExchange:网关内部的上下文对象,保存网关共享对象如:request,respone,session,或者一些自定的共享属性。

GatewayFilterChain:当前过滤器执行完后,要调用过滤器链中的下一个过滤器。

Ordered接口作用:是我们自定义的过滤器要在 NettyRoutingFilter 的前面执行,因为NettyRoutingFilter 是用于路由转发到微服务的。

②GatewayFilter(这个写的麻烦一点)

相比GlobalFilter可以指定作用范围,还可以配置自定义参数如下

无参数:图片是代码模板

代码实现

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new  OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 登录校验逻辑
                System.out.println("print any filter running");
                // 放行
                return chain.filter(exchange);
            }
        },1);
    }
}
spring:
  cloud:
    gateway:
      default-filters:
        - PrintAny

该类的名称一定要以GatewayFilterFactory为后缀!

有参数:这里只给代码模板,就不给具体代码实现了,因为太多了。

2.实现登录校验
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private final JwtTool jwtTool;

    private final AuthProperties authProperties;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取Request
        ServerHttpRequest request = exchange.getRequest();
        // 2.判断是否不需要拦截,
        if(isExclude(request.getPath().toString())){
            // 无需拦截,直接放行
            return chain.filter(exchange);
        }
        // 3.获取请求头中的token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 4.校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();  // 拦截
        }

        // TODO 5.如果有效,传递用户信息
        System.out.println("userId = " + userId);
        // 6.放行
        return chain.filter(exchange);
    }

    private boolean isExclude(String antPath) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, antPath)){
                return true;    // 如果这个地址在排除拦击的路径里面,直接放即可
            }
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
3.网关传递用户

实现思路:

网关过滤器储存用户信息到请求头:

// 5.如果有效,传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
        .request(builder -> builder.header("user-info", userInfo))
        .build();

拦截器获取请求头,并储存到ThreadLocal中:

public class UserInfoInterceptor implements HandlerInterceptor {
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        String userInfo = request.getHeader("user-info");
        if(StrUtil.isNotBlank(userInfo)) {
            UserContext.setUser(Long.valueOf(userInfo));
        }
        return true;
    }
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

 然后接下来就是注册拦截器,但是注册完之后还是存在问题,因为代码添加到了common这个模块中,网关也 也引入了common的依赖,但是gateway不是有SpringMVC实现的,所以不能运行拦截器的代码,我们需要条件装配,解决方案:在添加注册拦截器的类上添加

@ConditionalOnClass(DispatcherServlet.class) 注解即可

4.OpenFeign传递用户

使用OpenFegin拦截器。

public class DefaultFeignConfig {
    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            public void apply(RequestTemplate template) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    template.header("user-info", userId.toString());
                }
            }
        };
    }
}

DefaultFeignConfig 这个配置类要在启动类上声明:

@EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class)

basePackages参数:

  • 作用:指定扫描哪些包下的 Feign 客户端接口。Spring 会扫描basePackages指定包及其子包中的所有被@FeignClient注解标注的接口,然后创建这些接口的代理实例,以便在其他组件中可以通过依赖注入的方式使用这些 Feign 客户端来调用远程服务。

defaultConfiguration参数:

  • 作用:指定 Feign 客户端的默认配置类。这个配置类可以用来配置 Feign 客户端的一些通用属性,例如日志级别、解码器、编码器、负载均衡策略等。通过设置defaultConfiguration,可以为所有 Feign 客户端应用相同的配置。

整体代码思路流程图:

Ⅲ.配置管理(Nacos)

1.共享配置(更好的管理配置文件)

在nacos里面配置 配置文件,这个配置文件是提供给每个微服务的。

在springcloud环境下配置文件加载流程:

使用步骤:

①引入依赖 
  <!--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的配置,根据上面的流程图,我们必须写nacos的配置不写就无法找到nacos。

2.配置热更新

微服务无需重启即可使配置生效。

3.动态路由

四.Sentinel(微服务保护)

1.微服务整合Sentinel

①引入依赖
<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
②配置控制台
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
      http-method-specify: true # 是否设置请求方式作为资源名称

2.请求限流

3.线程隔离

4.FallBack

触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。

FeignClient作为Sentinel的簇点资源
feign:
  okhttp:
    enabled: true # 开启OKHttp功能
  sentinel:
    enabled: true # 开启sentinel对OpenFeign的监控
自定义类,实现FallbackFactory,编写对某个FeignClientfallback逻辑
// 对不同的方法写失败后的逻辑
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
    @Override
    public ItemClient create(Throwable cause) {
        return new ItemClient() {
            @Override
            public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
                log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
                // 查询购物车允许失败,查询失败,返回空集合
                return CollUtils.emptyList();
            }

            @Override
            public void deductStock(List<OrderDetailDTO> items) {
                // 库存扣减业务需要触发事务回滚,查询失败,抛出异常
                throw new RuntimeException(cause);
            }
        };
    }
}
③将刚刚定义的UserClientFallbackFactory注册为一个Bean
public class DefaultFeignConfig {
    @Bean
    public ItemClientFallbackFactory itemClientFallbackFactory() {
        return new ItemClientFallbackFactory();
    }
}
④在UserClient接口中使用UserClientFallbackFactory
// 添加到FeignClient 注解上
@FeignClient(value = "item-service",fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);

    @PutMapping("/items/stock/deduct")
    void deductStock(@RequestBody List<OrderDetailDTO> items);
}

value参数:

  • 作用:指定要调用的远程服务的名称。这个名称通常对应于服务注册中心(如 Eureka、Consul、Nacos 等)中注册的服务名。Feign 会根据这个服务名,从服务注册中心获取服务的实例信息,并通过负载均衡选择一个实例进行调用。

fallbackFactory参数:

  • 作用:指定一个降级工厂类,用于在远程服务调用失败时提供降级逻辑。降级工厂类需要实现FallbackFactory<T>接口,其中T@FeignClient注解标注的接口类型。通过降级工厂,可以为不同的异常情况提供不同的降级处理逻辑。

5.服务熔断

三个状态

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态

  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态

  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。

    • 请求成功:则切换到closed状态

    • 请求失败:则切换到open状态

五.Seata(分布式事务)

在Seata的事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

1.微服务整合Seata

引入依赖

<!--统一配置管理-->
  <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>
  <!--seata-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  </dependency>

添加配置文件

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: "default"

2.XA模式

①工作原理图:

RM一阶段的工作:

  1. 注册分支事务到TC

  2. 执行分支业务sql但不提交

  3. 报告执行状态到TC

TC二阶段的工作:

  1. TC检测各分支事务执行状态

    1. 如果都成功,通知所有RM提交事务

    2. 如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

②优缺点:

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则

  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差

  • 依赖关系型数据库实现事务

③使用

修改 application.yml 文件(每个参与事务的微服务),开启 XA 模式:

seata:
  data-source-proxy-mode: XA

给发起全局事务的入口方法添加 @GlobalTransactional 注解:

@GlobalTransactional
public Long createOrder(OrderFormDTO order) {
    // 创建订单 ... 略
    // 清理购物车 ...略
    // 扣减库存 ...略
    return order.getId();
}

3.AT模式

①工作原理图:

阶段一RM的工作:

  • 注册分支事务

  • 记录undo-log(数据快照)

  • 执行业务sql并提交

  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

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

②优缺点:

优:性能比XA模式高。

缺:不是强一致性,中间会有短暂的不一致,称为最终一致。 

③使用

1.给每个微服务创建一个 undo_log 表sql语句如下

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT         NOT NULL COMMENT '分支事务id',
    `xid`           VARCHAR(128)   NOT NULL COMMENT '全局事务id',
    `context`       VARCHAR(128)   NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB       NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)        NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)    NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)    NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

2.改配置文件

seata:
  data-source-proxy-mode: AT

3.使用和上面一样的分布式注解 @GlobalTransactional


网站公告

今日签到

点亮在社区的每一天
去签到