拆分原则

什么时候做拆分

  • 创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分
  • 确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦

怎么拆分

  1. 从拆分目标来说:
    • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联程度高、完整度高
    • 低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖
  2. 从拆分方式来说:
    • 纵向拆分:按照业务模块拆分
    • 横向拆分:抽取公共服务,提高复用性

服务治理

RestTemplate

spring中自带的http请求封装,利用它可以向服务提供者发起http请求获取想要的数据

注册中心原理

  1. 服务提供者去注册中心注册服务信息,并通过心跳续约保证连接
  2. 服务调用者去注册中心订阅所需服务,通过负载均衡算法选择所需提供者远程调用,当服务提供者变化时,提供推送变更

    负载均衡算法主要有以下集中:随机、轮询、加权轮询、哈希

Nacos注册中心

(Nacos 快速开始 | Nacos 官网)
部署(docker)

  1. 准备数据库表,用来存储Nacos的数据
  2. 配置nacos的custom.env:选数据库,写地址端口、账号密码
  3. /root目录下执行以下命令
    1
    2
    3
    4
    5
    6
    7
    8
    docker run -d\
    --name nacos \
    --env-file ./nacos/custom.env \
    -p 8848:8848 \
    -p 9848:9848 \
    -p 9849:9849 \
    --restart=always \
    nacos/nacos-server:v2.1.0-slim

可以去网上先下载好镜像,通过docker load -i nacos.tar加载

  1. 可以通过ip:端口/nacos访问

服务注册

  1. 引入nacos discovery依赖
    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 配置Nacos地址
    1
    2
    3
    4
    5
    6
    7
    spring:
    application:
    name: oyuo # 服务器名称
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址

服务发现

  1. 同样需要引入依赖和配置nacos地址
  2. 服务发现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private final DiscoveryClient disccoveryclient;

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

OpenFeign

一个声明式的http客户端,其作用就是基于SpringMVC的常见注解,帮助开发者优雅的实现http请求的发送

使用

  1. 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <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>
  2. 通过@EnableFeignClients注解,启动OpenFeign功能
    1
    2
    3
    4
    5
    @EnableFeignClients
    @SpringBootApplication
    public class CartApplication{
    //....略
    }
  3. 编写FeignClient
    1
    2
    3
    4
    5
    6
    @FeignClient(value = "oyuo")
    public interface ItemClient{

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    }
  4. 使用FeignClient,实现远程调用
    1
    List<ItemDTO> items  =  itemClient.queryItemByIds(List.of(1,2,3));

连接池

OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其他的框架。这些框架可以自己选择,包括以下三种:

  • HttpURLConnection:默认实现,不支持连接池(故而性能不佳)
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池
    具体源码可以参考FeignBlockingLoadBalancerClient类中的delegate成员变量

调优(OKHttp为例)

  1. 引入依赖
    1
    2
    3
    4
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    </dependency>
  2. 开启连接池功能
    1
    2
    3
    feign:
    okhttp:
    enable: true # 开启OKHttp连接池支持

最佳实践

服务提供者编写独立的moudle,将FeignClient及DTO抽取,就是OpenFeign的最佳实践,当然也会遇到不少的问题

当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。解决办法如下两种:

  1. 指定FeignClient所在包
    1
    @EnableFeignClients(basePackages  = "com.app.api.clients")
  2. 指定FeignClient字节码
    1
    @EnableFeignClients(clients = {UserClient.class})

日志输出

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

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

自定义日志级别

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

1
2
3
4
5
6
public class DefaultFeignConfig{
@Bean
public Logger.Level feignLogLevel(){
return Logger.level.FULL;
}
}

此时这个Bean并未生效,有两种办法:

  1. 局部配置某个FeignClient的日志,可以在@FeignClient注解声明:
    1
    @FeignClient(value = "item-service", configuration = DefaultFeignClient.class)
  2. 如果想要全局配置,则需要在@EnableFeignClients中声明:
    1
    @EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

网关

网关:就是网络的关口,负责请求的路由、转发、身份校验
在SpringCloud中网关的实现包括两种:
Spring Cloud Gateway

  • Spring官方出品
  • 基于WebFlux响应式编程
  • 无需调优即可获得优异性能
    Netfilx Zuul:
  • Netflix出品
  • 基于Servlet的阻塞式编程
  • 需要调优才能获得与Spring Cloud Gateway类似的性能

搭建网关

  1. 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    	<!--spring cloud gateway-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--nacos 服务发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--客户端负载均衡loadbalancer-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
  2. 路由配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server:
    port: 8081 # 网关端口
    spring:
    application:
    name: gateway # 服务名称
    cloud:
    nacos:
    server-addr: 127.0.0.1:8848 # nacos地址
    gateway:
    routes: # 网关路由配置
    - id: baidu # 路由id, 自定义,唯一即可
    # uri: 127.0.0.1:/order # - 路由目的地,支持lb和http两种
    uri: lb://orderService # 路由的目的地,lb是负载均衡,后面跟服务名称
    predicates: # 路由断言,也就是判断是否符合路由规则的条件
    - Path=/order-service/** # path 按照路径进行匹配,只要以/order-service/开头就符合规则
    filters:
    - StripPrefix=1 # 过滤器,去除请求的前缀路径,StripPrefix=1 表示去除请求路径的第一个路径片段

路由属性

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:

  • id:路由唯一标示
  • uri:路由目标地址
  • predicates:路由断言,判断请求是否符合当前路由
  • filters:路由过滤器,对请求或响应做特殊处理

路由断言

Spring提供了12种基本的RoutePredicateFactory实现:

路由过滤器

网关提供了33种路由过滤器,不赘述,可以查官网
如果过滤器想对所有的路由生效,就在routes:同级下配置default-filters:

1
2
3
routes: <? items>
default-filters:
- AddRequestHeader=nihao hello world

网关登录校验

思路:JWT过滤器验证、将用户信息保存在请求头中,通过http请求发送到各个微服务中,各个微服务的传递同理

自定义过滤器

网关过滤器分为两种:

实现登录校验

JWT的实现

  1. 配置yaml文件
  2. AuthProperties、JwtProperties读取配置、MvcConfig拦截、SecurityConfig生成密钥
  3. JwtTool(生成,解析Token)
    登录校验:
  4. 创建AuthGlobalFilter
    • 获取requestServerHttpRequest request = exchange.getRequest();
    • 判断是否需要登录拦截
    • 获取tokenList<String> headers = request.getHeaders().get("authorization");
    • 校验并解析token
    • 传递用户信息
    • 放行return chain.filter(swe)
      网关传递用户->把用户信息写在请求头,用springmvc拦截器做拦截保存到threadlocal中
1
2
3
4
5
6
7
8
9
//在过滤器中0v0
//传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
//这里传递的名称要约定好,没有固定
.request(builder -> builder.header("user-info",userInfo))
build();
//放行
return chain.filter(swe)
  • threadlocal与拦截器的书写,不在这里演示,就是注意要String userInfo = request.getHeader("user-Info")而不是authorization
  • META-INFspring.factories中添加mvc不然不生效
  • 通过条件注解@ConditionalOnclass(DispatcherServlet.class)使得网关中mvc不会生效(因为网关没有springmvc的servlet服务器这个类)

OpenFeign传递用户信息(微服务之间)

  • OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求
  • 其中RequestTemplate类中提供了一些方法可以让我们修改请求头

配置管理

配置问题:

  • 微服务重复配置过多,维护成本高
  • 业务配置经常变动,每次修改都要重启服务
  • 网关路由配置写死,如果变更要重启网关

配置共享

  1. 添加配置到Nacos
    • 添加一些共享配置到Nacos中,包括:Jdbc、MybatisPlus、日志、Swagger、OpenFeign等配置
    • 可以直接在Nacos的可视化控制台中配置
  2. 拉取共享配置
    • 基于NacosConfig拉去共享配置代替微服务的本地配置
    1. 引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!--nacos配置管理-->
      <dependency>
      <groupId>com.alibaba.could</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>
      <!--读取bootstrap文件-->
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
      </dependency>
    2. 新建bootstrap.yaml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      spring:
      application:
      name: service-name # 服务名称
      profiles:
      active: dev
      cloud:
      nacos:
      server-addr: 192.168.150.101 # nacos地址
      config::
      file-extension: yaml # 文件后缀名
      shared-configs: # 共享配置,下面id都是自己取的那个id
      - dataId: shared-jdbc.yaml # 共享mybatis配置
      - dataId: shared-log.yaml # 共享日志配置
      - dataId: shared-swagger.yaml # 共享日志配置

配置热更新

当我们修改配置文件中的配置时,微服务无需重启即可使配置生效
前提条件

  1. nacos中要有一个与微服务名有关的配置文件
  2. 微服务中要以特定方式读取需要热更新的配置属性(一种是@ConfigurationProperties(predix = "sof.service",另一种是@Value+@RefreshScope的形式)

动态路由

要实现动态路由首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息
需要完成两件事:

  1. 监听Nacos配置变更的信息,此事在监控手册 | Nacos 官网中的用户指南的java的SDK中已有记载
  2. 当配置变更时,将最新的路由信息更新到网关的路由表
    • 可以利用RouteDefinitionWriter来更新路由表

路由配置语法:

  • 为了方便解析从Nacos读取到路由配置,推荐使用Json格式的路由配置,模板如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "id": "item",
    "uri": "lb://item-service",
    "predicates": [{
    "name": "Path",
    "args": {
    "_genkey_0": "/items/**"
    "_genkey_1": "/search/**"
    }
    }],
    "filters": []

    }